Phase/9b subscription state entitlements - #12
Merged
Mujhtech merged 131 commits intoJul 31, 2026
Conversation
|
| 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
- Understand the implications of revoking this secret by investigating where it is used in your code.
- Replace and store your secrets safely. Learn here the best practices.
- Revoke and rotate these secrets.
- 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
- following these best practices for managing and storing secrets including API keys and other credentials
- install secret detection on pre-commit to catch secret before it leaves your machine and ease remediation.
🦉 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
force-pushed
the
phase/9b-subscription-state-entitlements
branch
from
July 31, 2026 09:35
93ab60b to
055b82b
Compare
Mujhtech
force-pushed
the
phase/9b-subscription-state-entitlements
branch
from
July 31, 2026 09:36
055b82b to
1aba739
Compare
Mujhtech
force-pushed
the
phase/9b-subscription-state-entitlements
branch
from
July 31, 2026 09:36
1aba739 to
9e40039
Compare
Mujhtech
force-pushed
the
phase/9b-subscription-state-entitlements
branch
from
July 31, 2026 09:37
9e40039 to
cef527a
Compare
Mujhtech
force-pushed
the
phase/9b-subscription-state-entitlements
branch
from
July 31, 2026 09:37
cef527a to
0ac9a0d
Compare
Mujhtech
force-pushed
the
phase/9b-subscription-state-entitlements
branch
from
July 31, 2026 09:37
0ac9a0d to
702728a
Compare
Mujhtech
force-pushed
the
phase/9b-subscription-state-entitlements
branch
from
July 31, 2026 09:38
702728a to
d9da93b
Compare
Mujhtech
force-pushed
the
phase/9b-subscription-state-entitlements
branch
from
July 31, 2026 09:39
d9da93b to
4d8eb5f
Compare
… 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.
… 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
force-pushed
the
phase/9b-subscription-state-entitlements
branch
from
July 31, 2026 09:39
4d8eb5f to
3315cb3
Compare
Base automatically changed from
phase/9a-transaction-ingestion-validation
to
development
July 31, 2026 09:40
Mujhtech
marked this pull request as ready for review
July 31, 2026 09:40
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.