Skip to content

PAY-002B2A — Persist immutable command registration and first audit event #165

Closed

Description

@daliu

Officer impact: None. This is unused server-only infrastructure. It does not add checkout, refund, admin, or officer actions.

Officer documentation: None — no officer-visible behavior, procedure, permission, provider setting, collected data, or deployed surface changes. Commerce remains NOT AVAILABLE YET.

Deployment evidence: Closure proves source, local tests, independent review, merge, and exact-commit hosted CI only. It must not claim website publication, Firebase deployment, Stripe configuration, provider objects, or live behavior.

Parent and dependencies

First persistence child of catalogued PAY-002B2.

  • PAY-002B1/PAY-002B1 — Define caller-scoped command identity and deterministic Stripe keys #163 is merged as 6c5d39ad3204c24928c11f194de9f3330198dc6d and supplies canonical command/payload hashes.
  • Existing Firestore Rules already deny browser reads and writes to root checkoutRequests and auditEvents.
  • No endpoint-schema dependency exists because this child has no runtime adopter.
  • PAY-002B2B will own lease/fence/terminal-pointer behavior without provider-send permission.
  • PAY-002B2C will own immutable provider-plan binding, safe-send cutoff, ambiguous outcome, and reconciliation.

No live PAY-002B2 issue, claim, or open pull request existed at creation. The live status:ready queue was empty.

Problem

B1 can derive stable hashes but creates no durable record. Two Functions can therefore see the same command at the same time without a transactional winner. Reusing one caller command ID with a different operation or payload also has no persistent conflict boundary.

This slice creates the first durable command record and its first audit event. It deliberately creates no lease and no permission to contact Stripe.

Atomic outcome

Add registerCommerceCommand(...), an unused server-only Firestore transaction that:

  1. validates and derives B1 command identity before the transaction;
  2. captures one trusted server timestamp before the transaction callback;
  3. reads the deterministic command and audit documents before any write;
  4. creates one immutable registration plus one deterministic append-oriented audit event atomically when both are absent;
  5. returns a fixed read-only existing classification for an exact prior registration;
  6. rejects reuse of the same B1 command key with another command type, endpoint schema version, or payload fingerprint;
  7. rejects malformed, missing-pair, future-version, or unexpected-state records without repair;
  8. performs no non-Firestore side effect inside or after the transaction.

Registration is not authorization and is not execution permission.

Exact API

registerCommerceCommand({
  db,
  environment,
  callerScope,
  commandId,
  commandType,
  endpointSchemaVersion,
  payload,
})
  • callerScope, commandId, and payload are passed directly to B1 and are never stored or returned.
  • payload must already be endpoint-schema-validated and deeply frozen.
  • endpointSchemaVersion is a bounded positive safe integer supplied by trusted code.
  • B1 intentionally binds environment and caller-scope kind/value into the document hash. Changing environment or caller scope creates a different B1 identity and document; B2A does not add a forbidden raw-UUID registry or cross-scope query.
  • A stored environment or caller-scope-kind value that disagrees with its derived document input is treated as an unrecognized/corrupt record, not a normal retry conflict.
  • The returned fixed frozen value contains only journalSchemaVersion, outcome, and state.
  • Outcomes are registered_new and registered_existing; state is registered.
  • No output contains the command hash, fingerprint, document path, raw caller, UUID, payload, lease, fence, provider key, provider object, result, or shouldExecute flag.

Unknown Firestore/provider details map to one fixed journal_unavailable error. B1 input failures map to one fixed safe input error. Stored conflicts and unrecognized records have fixed non-reflective reasons.

Command document

Path: checkoutRequests/{commandKeyHash}

Exact version-1 fields:

journalSchemaVersion: 1
commandIdentityVersion: 1
endpointSchemaVersion: bounded positive integer
environment: local | test | staging | production
callerScopeKind: firebase_uid | anonymous_principal | internal_system
commandType: trusted bounded slug
payloadFingerprint: lowercase SHA-256
state: registered
revision: 1
createdAt: Firestore Timestamp
updatedAt: the same Firestore Timestamp

The document ID is the B1 command-key hash. Do not store a raw command ID, caller value, payload, PII, Checkout URL, confirmation token, Stripe key/object ID, provider attempt, result, lease, failure text, or reconciliation evidence.

revision is aggregate/audit revision only. It is not a lease fencing epoch.

Audit document

Path: auditEvents/commerce_command_{commandKeyHash}_0000000001

Exact version-1 fields:

auditSchemaVersion: 1
aggregateType: commerce_command
commandKeyHash: lowercase SHA-256
commandRevision: 1
eventType: command_registered
fromState: null
toState: registered
environment: closed enum
callerScopeKind: closed enum
commandType: trusted bounded slug
occurredAt: the same Firestore Timestamp

A matching retry creates no second event. A rejected conflict creates no event. An existing command is accepted only when the exact deterministic audit partner also exists and matches; missing or malformed pairs fail closed without repair.

State machine

absent pair -> registered pair
registered pair + exact identity -> registered (read-only)
registered pair + mismatch -> reject
orphan/malformed/future/unexpected pair -> reject

There is no leased, executing, provider_sending, completed, failed, retry, result-replay, attempt-advance, or reconciliation state in this child.

Exact source boundary

Create only:

  • functions/commerceCommandJournal.js
  • functions/commerceCommandJournal.test.js
  • functions/commerceCommandJournal.emulator.test.js

Minimum exact status/design hunks only:

  • STRIPE_COMMERCE_DESIGN.md
  • SYSTEM_DESIGN.md
  • SECURITY.md
  • IMPLEMENTATION_PLAN.md
  • GITHUB_ISSUES.md
  • GITHUB_ISSUE_SLICES.md

Post a scope amendment before editing an endpoint, functions/index.js, package/lockfile, Firestore Rules/indexes, workflow, environment file, UI, officer guide, provider setting, or production data.

Security and correctness invariants

  • Command and first audit event commit together or neither commits.
  • All transaction reads occur before writes.
  • The transaction callback may rerun and contains no logger, clock, random generator, network call, Stripe call, or mutable application-state side effect.
  • Within one environment and caller scope, the same command ID plus another semantic operation reaches the same document and conflicts.
  • Matching retry is byte-preserving and performs no write.
  • No API grants provider-send or business-side-effect permission.
  • Stored hashes are pseudonymous server-only data, not anonymization.
  • Browser access remains denied; Admin SDK/IAM remains a separate trust boundary.
  • No TTL is added. Deleting a journal can re-enable a duplicate command; retention must later coordinate with DATA-001B — Inventory personal data and approve a minimization/retention matrix #110.
  • No automatic repair or guessed migration of unknown records.
  • A hash match does not authorize a caller, prove price/capacity/inventory/refund eligibility, or replay a result.

Firestore can rerun a transaction callback after contention, so only Firestore reads/writes belong inside it: https://firebase.google.com/docs/firestore/manage-data/transactions

Acceptance criteria

  • Concurrent identical registration produces one command, one audit event, one new outcome, and existing outcomes for the rest.
  • Within one B1 key, another command type, endpoint version, or payload rejects without overwrite; a stored environment/caller-scope-kind mismatch is rejected as an unrecognized record.
  • Matching registration is read-only and leaves both snapshots equivalent.
  • Orphan, malformed, accessor/proxy, unknown-version, unexpected-state, and timestamp-mismatch records reject without repair.
  • Transaction commit failure leaves neither half of a new pair.
  • Results and errors are frozen, fixed-shape, and contain no raw UUID, caller, payload, canary, path, hash, or Firestore detail.
  • Static tests prove no endpoint/index import and no Stripe/logger/network/filesystem/environment/random dependency.
  • Existing Rules tests continue to deny browser access to both collections.
  • Node 20 focused/unit/local-emulator/full Functions tests and lint, frontend, SPA, Rules, build, diff/sensitive scan, and two independent exact-snapshot reviews pass.
  • Root docs split B2A registration from B2B leases/replay and B2C provider-plan/safe-send/reconciliation.
  • No runtime, deployment, provider, production, or officer behavior is claimed.

The current hosted Functions CI does not start the exact demo-pay002b2-test project or set REQUIRE_COMMERCE_COMMAND_JOURNAL_EMULATOR=1. The standard differently named Functions demo run must safely skip this isolated suite. Closure must state dedicated local-emulator evidence separately and update #105 with this ungated gap; do not imply hosted emulator coverage.

Required tests

Unit/mocked transaction:

  • New pair and exact existing pair.
  • Command type, endpoint-schema, and payload conflicts under one B1 key; stored environment/caller-scope-kind corruption.
  • Exact root fields, versions, enums, hashes, state, revisions, and Timestamp equality.
  • Missing command, missing audit, preexisting audit, malformed pair, custom prototypes, accessors, proxies, symbols, and extra keys.
  • Firestore failure redaction and B1 failure redaction.
  • Repeated callback simulation with no clock/logger/network/random side effect.
  • Frozen fixed output/error and raw-canary absence.
  • Export/import boundary with no execute/send/lease/result API.

Demo-only emulator:

  • Run only with exact opt-in REQUIRE_COMMERCE_COMMAND_JOURNAL_EMULATOR=1, exact demo-pay002b2-test, and an explicit loopback Firestore host; wrong host/project must fail closed, while the standard differently named demo run skips.
  • 20 or more concurrent identical calls.
  • Concurrent two-payload race: one identity wins, matching calls reuse it, conflicting calls reject, pair is never overwritten.
  • Preseed deterministic audit ID and prove failed transaction leaves no orphan command.
  • Existing exact pair remains unchanged after repeated calls.
  • Existing malformed/future pair remains untouched.
  • Cleanup only synthetic deterministic documents.

Migration impact

Additive target schema only. No backfill, production read, provider inventory, or existing business-record mutation. Before any deployment/adoption, privately inventory existing checkoutRequests within #113-approved boundaries. Unknown records fail closed. No new Firestore index is required.

Required follow-ons

  • PAY-002B2B: lease, monotonic fence epoch, stale-worker rejection, terminal server-side pointer, fixed final failure, and append-only transition audit. It still grants no Stripe POST permission.
  • PAY-002B2C: immutable provider request-plan fingerprint plus Stripe account/sandbox scope, API version, endpoint/operation, mode, attempt, key, pre-POST first-send time, conservative cutoff, ambiguous outcome, verified reconciliation, and safe attempt advancement.

B1 payload identity does not prove an identical Stripe POST. Any future send permit must bind the complete provider plan. Stripe may prune an idempotency key after it is at least 24 hours old, so an old deterministic key can execute again: https://docs.stripe.com/api/idempotent_requests

PAY-002C/D, PAY-003B, PAY-004, and PAY-005 must depend on B2C, not B2A or B2B.

Excluded

  • Any endpoint/export/index import or runtime adoption.
  • Lease, fence, provider attempt, provider plan, safe-send deadline, reconciliation, terminal result, or result replay.
  • Stripe call, webhook, checkout, refund, capacity, inventory, mail, admin, rate-limit, or confirmation-capability behavior.
  • Rules, index, package, lockfile, workflow, environment, UI, officer, provider, Firebase deployment, production read/write, or live testing.

Claim protocol

Creation is not a claim. Before editing: confirm exact current main, assign the issue, post CLAIMED by /root at <UTC> with branch codex/issue-<number>-command-registration, apply status:in-progress, and use an isolated worktree.

Activity

  1. added
    priority:P0Launch blocker or urgent security risk
    type:securitySecurity or privacy boundary
    area:firebaseFirebase services and data
    size:SSmall focused issue
    status:readyDependencies complete and ready to claim
    area:stripeStripe payments and financial lifecycle
    status:in-progressClaimed and actively being implemented
    and removed
    status:readyDependencies complete and ready to claim
    on Jul 13, 2026
  2. self-assigned this
    on Jul 13, 2026
  3. daliu commented on Jul 13, 2026

    @daliu
    ContributorAuthor

    CLAIMED by /root at 2026-07-13T16:32:37Z.

    Branch: codex/issue-165-command-registration
    Worktree: isolated Run-MPRC.issue-165
    Exact base: 6c5d39ad3204c24928c11f194de9f3330198dc6d

    Owned files are exactly the three new journal/test files plus the six minimum root documentation/status files named in the issue. No endpoint/index, package/lock, Rule/index, workflow, environment, UI, officer, provider, or production file is claimed.

    B2A creates only an atomic registered command/audit pair and exact match/conflict classification. It grants no lease, execute/send permission, result replay, provider attempt, Stripe call, or reconciliation transition.

  4. daliu commented on Jul 13, 2026

    @daliu
    ContributorAuthor

    Pre-implementation contract clarification (no source change yet):

    B1 includes environment and caller-scope kind/value in commandKeyHash. Therefore a different environment or caller scope intentionally addresses a different document. B2A will not create a raw-UUID registry or query across scopes.

    Normal command_conflict covers a different command type, endpoint schema version, or payload fingerprint under the same B1 key. A hostile/preseeded record whose stored environment or caller-scope kind disagrees with the derived input is journal_record_invalid corruption. The issue body and tests now state this exact boundary.

  5. daliu commented on Jul 13, 2026

    @daliu
    ContributorAuthor

    EXTRACTION FINISHED — 2026-07-13T16:52:45Z

    Exact owned paths: 9 (the three new functions/commerceCommandJournal* files plus the six claimed root design/status documents).

    Exact source/test hashes:

    • source: a12aa577c003c05c93ce4e66e97e317ddea268d82b60f491c2c493a9c0fa4ec0
    • unit: 0451a3129591e8d9b9cc39af98a1e46ee6e7c4b9a89663aadff0537c0207066a
    • emulator: 6f320581b96df19cca132bf8df3b9e1f5200018b70300088110c8d2ac8e9b825

    Local verification:

    • focused journal unit: 43/43
    • dedicated Firestore emulator: 6/6, including 24-way concurrency and conflicting-payload races
    • full Functions: 701 passed, 7 skipped; lint passed
    • frontend: 132/132; SPA: 10/10; Rules: 305/305; diagnostic build passed
    • standard demo-project run safely skipped the dedicated suite; hostile host/project guards failed closed
    • diff, relative-link, and sensitive-pattern checks passed

    Three independent reviews approved the exact code, emulator, documentation, and scope. Hosted CI intentionally does not run the opt-in dedicated emulator suite; that gap is recorded and is not being hidden as hosted evidence.

    This slice registers the command and first immutable audit record atomically. It grants no lease, provider-send permission, Stripe action, result replay, endpoint, deployment, or production behavior.

  6. daliu commented on Jul 13, 2026

    @daliu
    ContributorAuthor

    COMPLETE — merged through PR #166 as exact main 59aa088a50ba2934d830a460c0a865a631a293d6.

    Verified evidence:

    • exact-head PR CI 29268258617: Frontend, Functions, and Rules passed; Netlify preview 6a55180c121b750008a98ec0 was ready
    • post-merge CI 29268403718: Frontend, Functions, and Rules passed on the merge commit
    • dedicated local Firestore emulator: 6/6, including 24-way concurrency and conflicting-payload races
    • full local Functions 701 passed / 7 skipped; frontend 132/132; SPA 10/10; Rules 305/305; lint/build passed
    • three independent exact-snapshot reviews approved

    Deployment truth:

    • protected production release 29268514430 proved exact-current-main and successful push CI, then failed closed before build because the required public App Check/reCAPTCHA site key is missing
    • GitHub Pages and Firebase deployment were skipped
    • automatic Netlify production deploy 6a55188fe61cd200085baa66 errored and was not published
    • runmprc.com remained HTTP 200 on the last ready production commit e86a0f702cff6495f50630c5de3337290db8b8cb; this change is not live
    • no Stripe/provider configuration or production behavior was tested or claimed

    The module remains unused by runtime entrypoints. PAY-002B2B must add lease/fencing without send permission; PAY-002B2C must add the full provider-plan and safe-send/reconciliation boundary before any Stripe caller may adopt it.

    Hosted-gap truth: standard hosted Functions CI safely skips the dedicated demo-pay002b2-test suite because it does not set the opt-in flag. The local emulator proof is recorded separately, and the missing hosted gate is also recorded on #105.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

area:firebaseFirebase services and dataarea:stripeStripe payments and financial lifecyclepriority:P0Launch blocker or urgent security risksize:SSmall focused issuetype:architectureArchitecture and contract designtype:reliabilityReliability and recoverytype:securitySecurity or privacy boundary

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions