Skip to content

Phase/9b subscription state entitlements - #12

Merged
Mujhtech merged 131 commits into
developmentfrom
phase/9b-subscription-state-entitlements
Jul 31, 2026
Merged

Phase/9b subscription state entitlements#12
Mujhtech merged 131 commits into
developmentfrom
phase/9b-subscription-state-entitlements

Conversation

@Mujhtech

Copy link
Copy Markdown
Owner

No description provided.

@gitguardian

gitguardian Bot commented Jul 31, 2026

Copy link
Copy Markdown

⚠️ GitGuardian has uncovered 5 secrets following the scan of your pull request.

Please consider investigating the findings and remediating the incidents. Failure to do so may lead to compromising the associated services or software components.

🔎 Detected hardcoded secrets in your pull request
GitGuardian id GitGuardian status Secret Commit Filename
35590552 Triggered JSON Web Token 360618f protocol/fixtures/customer-access-token/v1/invalid/issuer-carries-signed-payload-value.json View secret
35590554 Triggered JSON Web Token 0010ad2 protocol/fixtures/billing-state-webhook/v1/invalid/raw-purchase-token-in-event.json View secret
35590555 Triggered JSON Web Token 40d9b0d protocol/fixtures/authoritative-entitlement/v1/invalid/snapshot-carries-signed-payload-value.json View secret
35590557 Triggered Generic High Entropy Secret 360618f protocol/fixtures/customer-access-token/v1/tokens/issuance-result.json View secret
35590559 Triggered JSON Web Token 360618f protocol/tools/customer-access-token-validation-v1.test.mjs View secret
🛠 Guidelines to remediate hardcoded secrets
  1. Understand the implications of revoking this secret by investigating where it is used in your code.
  2. Replace and store your secrets safely. Learn here the best practices.
  3. Revoke and rotate these secrets.
  4. If possible, rewrite git history. Rewriting git history is not a trivial act. You might completely break other contributing developers' workflow and you risk accidentally deleting legitimate data.

To avoid such incidents in the future consider


🦉 GitGuardian detects secrets in your source code to help developers and security teams secure the modern development process. You are seeing this because you or someone else with access to this repository has authorized GitGuardian to scan your pull request.

@Mujhtech
Mujhtech force-pushed the phase/9b-subscription-state-entitlements branch from 93ab60b to 055b82b Compare July 31, 2026 09:35
@Mujhtech
Mujhtech force-pushed the phase/9b-subscription-state-entitlements branch from 055b82b to 1aba739 Compare July 31, 2026 09:36
@Mujhtech
Mujhtech force-pushed the phase/9b-subscription-state-entitlements branch from 1aba739 to 9e40039 Compare July 31, 2026 09:36
@Mujhtech
Mujhtech force-pushed the phase/9b-subscription-state-entitlements branch from 9e40039 to cef527a Compare July 31, 2026 09:37
@Mujhtech
Mujhtech force-pushed the phase/9b-subscription-state-entitlements branch from cef527a to 0ac9a0d Compare July 31, 2026 09:37
@Mujhtech
Mujhtech force-pushed the phase/9b-subscription-state-entitlements branch from 0ac9a0d to 702728a Compare July 31, 2026 09:37
@Mujhtech
Mujhtech force-pushed the phase/9b-subscription-state-entitlements branch from 702728a to d9da93b Compare July 31, 2026 09:38
@Mujhtech
Mujhtech force-pushed the phase/9b-subscription-state-entitlements branch from d9da93b to 4d8eb5f Compare July 31, 2026 09:39
Mujhtech added 13 commits July 31, 2026 10:39
… linkedPurchaseToken

The Google linkedPurchaseToken is a persistent attribute of the successor
subscription, so overwriting fact_kind with purchase_superseded hid every
later expiration, cancellation, and grace fact for the lineage. The state
kind is now kept, supersession is recorded via supersedes_chain_digest on
every fact, and the supersession edge is emitted as its own fact built from
lineage-constant fields so the fact-identity constraint records it exactly
once per newly observed link.
…a refund fact

A voided (refunded) one-time purchase re-queries with purchaseState != 0,
and 9A recorded no fact for it, so refunded non-consumables stayed entitled
forever. The voided-purchase notification is now decoded with its refund
type and provider event time, the SKU is recovered through orders.get when
absent, and the validation produces a refund fact carrying refunded_at and
revoked_at at the provider-stated void time. Voided subscriptions take the
same override. A void with no provider timestamp quarantines instead of
being dated with worker wall-clock.
occurred_at participates in FactDigest, so the wall-clock fallbacks on both
provider paths made every replay of a timestamp-less input a new fact,
defeating replay idempotency. Both paths now quarantine such inputs under a
new missing_provider_timestamp reason (schema CHECK extended in migration
00029). Also corrects the Apple revocation mapping: both revocation reasons
are refunds, so revoked_at is always accompanied by refunded_at rather than
only for revocationReason 1.
…on 2

Additive columns on billing_transaction_facts for provider statements that
validator 1 parsed but dropped: Apple gracePeriodExpiresDate,
isInBillingRetryPeriod, autoRenewProductId, isUpgraded, revocationReason,
inAppOwnershipType, subscriptionGroupIdentifier; a refund type from Google
voided refundType; and a recovered provider event time for Google facts so
canonical ordering does not tie on the lineage-constant startTime. All new
fields participate in FactDigest and the validator version is now 2.
Migration 00029 also extends the quarantine reason CHECK with
missing_provider_timestamp (B7).
…tlements, tokens, webhooks

Migrations 00030-00036 per plan section 5 and 16:
- billing customers (Project-scoped), digest-only aliases with partial-unique
  active resolution and no FK into analytics identity tables, append-only
  association evidence, one open identity conflict per lineage
- Environment-scoped purchase lineages with the environment-mode alignment
  CHECK and an explicit superseded_by edge, plus subscription and one-time
  purchase instances
- immutable subscription snapshots with the five state axes and derivation
  invariants, snapshot fact links, append-only timeline entries reusing the
  ledger safe-detail guard, rebuildable checkpoints, seeded rule version 1,
  and a projection job queue whose scope key coalesces duplicate triggers
- immutable product entitlement grant versions with the OD-8(ii) backfill and
  a trigger blocking hard DELETE on the legacy grant table
- entitlement sources keyed by (lineage, product, grant version),
  Environment-scoped customer entitlement snapshots, entries, and current
  pointers via the two-step circular FK
- opaque customer access tokens with monotonic revocation and a 24h ceiling
- webhook destinations, sealed signing secrets, immutable events, and
  append-only delivery attempts

All foreign keys are ON DELETE RESTRICT. Verified up, down to 28, and up
again against a clean 9A schema.
New internal/billingprojection package holding the deterministic cores:
- canonical ordering version 1, one implementation, with per-fact-kind
  effective time, kind precedence, checkpoint positions, out-of-order
  detection, and the Google event-time tie-break the fact-shape pass enabled
- subscription engine folding ordered facts into the five state axes in the
  plan's derivation order; cancellation moves renewal intent only, grace
  grants access, billing retry and pause do not, a scheduled pause keeps
  access, a prorated refund preserves the period, and a later purchase or
  renewal reinstates a terminal lineage
- one-time projection for non-consumables with no time-based expiry
- grant version selection by period effective time with the earliest-version
  rule for purchases predating the backfill, plus additive-superset validation
  for retroactive changes
- entitlement aggregation keyed by (lineage, product, grant version) with
  multi-source support, permanent sources that never report a false expiry,
  unknown-preserving evidence handling, and checksum-based no-change detection

No package member performs I/O.
Mujhtech added 25 commits July 31, 2026 10:39
… the customer's lineages

Committing a fact on one of a customer's lineages wrote a projection job naming
both the customer and that lineage. loadLineages filtered to the lineage while
Compute still minted a full customer aggregate, so every other Entitlement
Source vanished from the authoritative snapshot and any Entitlement depending on
one flipped to inactive — no refund, no revocation, just sources that were never
loaded. A customer holding a subscription and a lifetime purchase lost the
lifetime purchase on the subscription's next fact.

The two scopes are now disjoint at every enqueue site and in Job.Scope(), which
is also what restores the coalescing index's meaning. A lineage-scoped command
never mints a customer snapshot; when its lineage has acquired a customer it
escalates by enqueueing customer scope instead.
billingcustomer.LineageKey hashed under its own domain while every fact carries
a purchase_chain_digest computed by billing.AppleTransactionKey or
billing.TokenDigest, and every fact-to-lineage join compares those two values. A
lineage keyed with the package's own helper could never join to a fact, so the
whole 9B read model was reachable only by bypassing the function. The fact's
domain is canonical because the facts are the append-only record.

This is the design gap the D-1 writeup requires resolving before the 9A-to-9B
seam can be wired. The seam itself is not landed here.
The driver's D-3 substitution (a second operator mux), its D-4 workaround (a
direct customer-scoped reprojection after every queued one), and its D-5 and D-2
defect transcripts are deleted; the surviving bridge substitution is D-1, which
is not fixed.

Demonstration 13's retried delivery was an artefact of D-4: revoking one of the
customer's several granting lineages changes no Entitlement state, so with the
fix Mosaic correctly mints no snapshot and emits no event. It now re-queues a
committed delivery through the operator replay API, which keeps every property
the demonstration is about and reaches them through a surface a person uses.

The operator repository's integration router registers the Environment-scoped
half through the shared subrouter, mirroring the production composition.
… note

The original finding record in the demo evidence document is untouched; a
'Fixes verified' section is appended with the disposition of each defect, the
re-run results, and an explicit statement that D-1's seam is not wired plus the
blocking question found while designing it.

The backend doc gains an operational note about D-0's side effect: an
hour-bucketed quarantine row makes a systematic intake failure hard to size from
the quarantine surface alone.
A validated Transaction Fact now reaches a Purchase Lineage, a projection
instance, a Billing Customer, and a projection. Before this, LocateLineage,
ResolveLineageCustomer, RecordSupersession, and EvidenceForReference had zero
production callers and the entire 9B read model was unreachable from a purchase.

The seam is two halves. The structural half — the lineage row keyed on the
chain root, and the subscription or one-time instance it owns — is written in
the same transaction that records the fact, because it is a deterministic
function of the fact and must be exactly as durable as the projection trigger
written beside it. The identity half runs after the commit through a port, and
is the full OD-2 evidence ladder: submission-context evidence from a
token-bound observation, then provider correlators (Apple appAccountToken,
Google obfuscatedExternalAccountId, hashed in the validator and stored as
digests only), then a prior association, then lazy creation of a
purchase-anchored Billing Customer per plan 5a rules 1 and 2.

Migration 00049 adds the purchase_anchor evidence type. An association that
establishes an owner now also enqueues the customer projection that can mint an
entitlement snapshot.

Observations accept an optional Customer Access Token in Mosaic-Customer-Token;
that submission is the only thing in a deployed system that can attach a first
purchase to an identified customer, because a store notification names nobody.
…red path

Two integration tests, because the defect was that four application services had
no production caller: each passed its own unit tests while a purchase reached
nothing. Both drive production wiring only from the fact-commit transaction
onward.

The first is the brief's end-to-end regression: submission-context evidence, a
validated fact, and a committed customer entitlement snapshot with an active
entitlement, through the real fact-commit transaction, the real seam, the real
identity service, and the real projection worker job.

The second is plan 5a rules 1-3: a purchase nothing identifies anchors to a new
customer, the reason is recorded as purchase_anchor evidence, and identifying
the person afterwards attaches the alias to that same customer instead of
minting the duplicate the whole model exists to avoid.

The driver's bridge() substitution and its instance materialization are deleted.
It now reports purchases the way an SDK does — a token-bound observation through
the public endpoint — so the demonstration exercises only production wiring.
The backend doc gains a section on the seam: the two halves and why they sit in
different places, the three rules that make the lineage write correct, the full
OD-2 evidence ladder with what each rung can and cannot do, why the Customer
Access Token travels as a header rather than a body member, and why a purchase
correlator never reaches a fact column or a log line.

The demo evidence document records D-1 as fixed and verified, with the two
re-run observations that are the fix working rather than regressions: the
identity conflict now resolves as two equal claims rather than a reassignment,
and snapshot versions are lower because one correct projection replaced the
wrong-then-corrected pair D-4 used to produce.
… token header

The conditional GET branch was removed in the Stage 4 defect fixes (D-5), so
the documentation no longer offers it as a server-side option. The GET read is
unconditional and always answers 200 with the full customerEntitlementSnapshot;
POST with entitlementSyncRequest is the only conditional mechanism and
snapshotUnchanged the only unchanged response. A 304 has no body, so it could
confirm a snapshot without stating how long the confirmation held, and no
freshness header exists in the frozen schemas to say it instead.

Documents the optional Mosaic-Customer-Token request header on observation
submission, which lets the server record submission-context association
evidence. This is transport documentation under the established
transport-is-not-contract rule: no Billing Ingestion record schema changes and
the frozen draft stays frozen. The credential travels as a header and never in
the body because observation bodies are persisted as raw validation inputs,
replayed, and digested into fact identity, so a credential inside one would be
written to an append-only ledger, re-presented after expiry, and would make two
submissions of the same purchase look like two purchases.
Observation submissions carry the held Customer Access Token in a
Mosaic-Customer-Token header so an identified user's purchase anchors to their
Billing Customer. Read at send time rather than enqueue time, never persisted
with the queue, never logged; absence leaves the submission anonymous and valid.
… 3, and the delivery-attempt authority note

- L1: replace the raw NUL and DEL bytes in the non-ASCII assertions with \x
  escapes, in the authoritative-entitlement and billing-ingestion test files.
- L2: ADR-0024 names the scheme version v1, matching the contract and vectors.
- L3: document that product_unresolved is reachable by an SDK reader, clears
  only through operator action, and never resolves to inactive.
- L4: record on tokenValue and in the token document that the mcat_ value in
  the issuance-result fixture is fabricated and grants nothing.
- L5: the canonical signature vector signs the fixture bytes with the trailing
  newline removed, not the exact file bytes.
- Product finding 3: the three freshness settings are deployment-wide in 9B;
  per-Environment configuration is a tracked follow-up.
- H2: the webhookDeliveryAttempt schema is authoritative for the
  delivery-attempt surface and the 9B API conforms to it.
…known

unavailable describes Mosaic's ability to answer a read. An event is not a
read: it exists only because a projection committed a new snapshot, so the
projection did answer. The worst an event can say about an axis is unknown,
carrying the uncertainty that explains why, which the if/then already required
and now narrows with the enumeration.

Draft narrowing, no version bump, recorded in the changelog. No fixture carried
the value. A contract guard fails the build if the enumeration is re-widened.
@Mujhtech
Mujhtech force-pushed the phase/9b-subscription-state-entitlements branch from 4d8eb5f to 3315cb3 Compare July 31, 2026 09:39
Base automatically changed from phase/9a-transaction-ingestion-validation to development July 31, 2026 09:40
@Mujhtech
Mujhtech marked this pull request as ready for review July 31, 2026 09:40
@Mujhtech
Mujhtech merged commit e636a9f into development Jul 31, 2026
11 of 25 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant