Skip to content

PAY-002B2C1 — Bind the immutable initial Stripe provider plan #173

Closed

Description

@daliu

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

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

Deployment evidence: Closure may prove source, synthetic local tests, independent review, merge, and exact-commit hosted CI only. It must separately state website publication, runmprc.com, Firebase deployment, Stripe/provider configuration, and live behavior. This issue must not claim any external outcome.

Parent and dependencies

Atomic PAY-002B2C1 child of the catalogued PAY-002 command-safety design.

At creation, exact remote main is 2eff10730e4ac60a6b2de6bcd07fb0ba899d591b. There is no open pull request, duplicate PAY-002B2C issue, or active overlapping claim.

Problem

A fenced worker still has no immutable record of the exact Stripe context it intends to use. Without that record, retries could silently change the Stripe account, test/live mode, API version, endpoint, operation, parameters, or deterministic idempotency key.

This slice binds one immutable initial provider plan while the caller holds the current unexpired lease. It deliberately does not create a send marker or return permission to call Stripe.

Exact API

Add one unused server-only export to functions/commerceCommandJournal.js:

bindInitialStripeProviderPlan({
  db,
  environment,
  callerScope,
  commandId,
  commandType,
  endpointSchemaVersion,
  payload,
  leaseId,
  expectedFenceEpoch,
  stripeAccountId,
  stripeMode,
  stripeApiVersion,
  endpointPath,
  providerOperation,
  providerParameters,
})

The module must:

  • recompute the B1 command key and payload fingerprint; never accept a command hash;
  • hard-code provider stripe, HTTP method POST, and provider attempt 1; never accept any of those from the caller;
  • validate the exact B2A root/revision-1 audit and exact B2B lifecycle/current-audit partner;
  • require lifecycle state leased, the exact current holder fingerprint and fence, and a lease that is active at the one captured trusted timestamp;
  • require deeply frozen, canonically encodable, bounded providerParameters; store no raw parameter;
  • validate the environment/test-live pairing and conservative bounded account, API-version, endpoint-path, and operation forms;
  • derive the exact B1 Stripe idempotency key for attempt 1 and never store, log, or return that raw key.

The current configuration layer proves only test/live key-prefix compatibility. It does not prove that a key belongs to the supplied Stripe account. C1 records a commitment only; account-context/provider verification remains mandatory before C2 may send.

Fixed outputs

Every output is a frozen fixed-shape object:

{
  journalSchemaVersion: 1,
  providerPlanSchemaVersion: 1,
  outcome: provider_plan_bound | provider_plan_existing,
  state: planned
}

No output contains a command hash/path, fence, time, lease ID/fingerprint, Stripe account, API version, endpoint, operation, parameter/key value or fingerprint, provider object, authorization decision, or shouldSend/shouldExecute field.

Immutable provider plan

Create only this first-attempt document:

checkoutRequests/{commandKeyHash}/providerAttempts/0000000001

Exact fields:

providerPlanSchemaVersion: 1
commandIdentityVersion: 1
commandKeyHash: lowercase SHA-256
environment: local | test | staging | production
provider: stripe
providerAttempt: 1
providerOperation: bounded safe slug
stripeMode: test | live
stripeAccountFingerprint: lowercase SHA-256 commitment
stripeApiVersion: strict bounded API-version string
httpMethod: POST
endpointPath: strict bounded Stripe API path without scheme, host, query, fragment, traversal, or encoded ambiguity
parametersFingerprint: lowercase SHA-256 commitment
idempotencyKeyFingerprint: lowercase SHA-256 commitment
boundFenceEpoch: positive bounded integer
boundAt: Firestore Timestamp

The account, parameter, and key fingerprints are versioned, length-framed, domain-separated, and command-bound commitments over the canonical input/B1 value plus commandKeyHash. They are pseudonymous equality evidence, not anonymization, authorization, configured-account proof, provider-execution proof, or response replay.

No raw account ID, provider parameters, idempotency key, secret, Checkout URL, provider object, personal data, business-record path, result, or error text is stored.

Deterministic audit partner

Atomically create:

auditEvents/commerce_provider_attempt_{commandKeyHash}_0000000001

Exact fields:

providerPlanAuditSchemaVersion: 1
aggregateType: commerce_provider_attempt
commandKeyHash: lowercase SHA-256
providerAttempt: 1
eventType: provider_plan_bound
provider: stripe
environment: closed enum
stripeMode: test | live
providerOperation: bounded safe slug
boundFenceEpoch: positive bounded integer
occurredAt: Firestore Timestamp

The audit excludes the account, parameter, and key commitments and every raw provider/request value.

Transaction and retry rules

Before any write, one Firestore transaction reads:

  1. the immutable B2A command root and revision-1 audit;
  2. lifecycle/current and its exact current-revision lifecycle audit;
  3. the first-attempt provider plan and deterministic provider-plan audit partner.

All reads occur before any create. The callback uses only prepared immutable values and Firestore reads/writes. It contains no clock, random generator, logger, network, Stripe, filesystem, environment lookup, or mutable application-state side effect.

Rules:

  • First bind requires the exact current unexpired holder/fence and atomically creates the plan plus audit.
  • Same exact stored plan/audit plus the exact current unexpired holder/fence returns provider_plan_existing with no write.
  • If the lease expires, the caller must reacquire or take over first. An expired holder cannot bind or observe the fixed existing outcome.
  • After takeover, the new exact active holder may read the same exact existing plan without rewriting boundFenceEpoch or boundAt.
  • A captured time earlier than a newly observed lease acquisition is observation-only and can never create a plan. No stale clock observation may mutate state.
  • A conflicting plan, wrong holder/fence, terminal lifecycle, missing/malformed/future/orphan root/lifecycle/plan/audit, preseeded partner, impossible timestamp, or bounded-integer overflow rejects without repair.
  • Exact retry after a lost response is read-only. Every rejection is read-only.
  • B2A root/audit and B2B lifecycle/audit records remain byte-for-byte unchanged.
  • Attempt 2 or later cannot be represented or created by this API.

The fixed Firestore retry maximum remains source-owned. Caller input cannot choose retry count, time, attempt, provider, or method.

State sequence

registered -> leased/fence N
leased/fence N + first exact plan -> plan/audit created (lifecycle unchanged)
leased/fence N + same exact plan -> existing (read-only)
leased/fence N + conflicting plan -> reject (read-only)
expired/wrong/stale/terminal holder -> reject (read-only)
takeover/fence N+1 + same exact plan -> existing (read-only)
attempt > 1 -> not available in C1

The plan is configuration equality evidence only. It does not reserve capacity/inventory, authorize the caller, prove a Stripe key/account relationship, or permit a provider request.

Exact source boundary

Code and tests:

  • functions/commerceCommandJournal.js
  • functions/commerceCommandJournal.test.js
  • functions/commerceCommandJournal.emulator.test.js
  • tests/firestore-rules/shop-and-locked.test.js

Minimum exact design/status hunks only:

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

The existing Rules-v2 recursive denial under checkoutRequests/{document=**} and top-level auditEvents denial should already cover the new documents. Add explicit browser-denial tests for the provider-attempt path. Do not edit firestore.rules unless a read-only audit proves the existing rule is insufficient and a scope amendment is posted first.

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

Required tests

Mock/unit tests:

  • Exact input/output/plan/audit schemas, frozen output, one trusted timestamp, and all reads before creates.
  • Golden command-bound account, parameters, and idempotency-key commitments; raw/canary values absent from storage, output, audit, errors, and logs.
  • Exact B1 key derivation for provider attempt 1 and environment/mode mismatch rejection.
  • Deep-frozen canonical provider-parameter equivalence and conflicts; prototype/accessor/proxy/symbol/extra-key/budget rejection.
  • Conservative account, API-version, endpoint-path, operation, lease UUID, and fence boundaries.
  • First bind, exact lost-response retry, conflicting plan fields, wrong holder, independent wrong fence, expiry equality, takeover, terminal lifecycle, and earlier-captured-time cases.
  • Missing/malformed/future/orphan plan/audit, preseeded audit partner, malformed B2A/B2B partner, and commit failure leave all records unchanged.
  • B2A registration and B2B lifecycle APIs remain byte-preserving before and after plan binding.
  • Closed export/dependency boundary: no endpoint/index import and no send/reconcile/advance/result API, Stripe/logger/network/random dependency, or shouldSend field.

Existing hosted emulator file functions/commerceCommandJournal.emulator.test.js:

  • 24 same-plan/current-holder calls create exactly one plan/audit and return one bound plus read-only existing outcomes.
  • Distinct plans race: exactly one immutable pair wins and every loser conflicts without overwrite.
  • Lost-response retry is byte-preserving and appends no second audit.
  • Expired holder cannot bind or observe existing; one takeover may read the same plan without rewriting it.
  • Wrong holder/fence, terminal lifecycle, preseeded partner, commit failure, and malformed/orphan/future records leave all related records unchanged.
  • B2A root/revision-1 audit and B2B lifecycle/current audit remain byte/update-time equivalent through every provider-plan test.

Rules tests:

  • Anonymous, signed-in member, and browser-admin clients cannot read, create, update, or delete providerAttempts/0000000001.
  • Existing protected collection and collection-group tests remain green.

Run Node 20 focused unit/emulator tests, Functions lint/full tests, frontend Jest, SPA, Rules, diagnostic build, diff/sensitive scan, and at least two independent exact-snapshot reviews.

Acceptance criteria

  • One exact initial Stripe plan and deterministic audit partner are created atomically only for the current unexpired holder/fence.
  • The plan binds account, mode, API version, endpoint, operation, canonical parameters, B1 key, command, and original binding fence without storing raw sensitive/request values.
  • Account/parameter/key commitments are domain-separated and command-bound and are never described as anonymization or provider proof.
  • Exact active-lease retry is read-only; conflicts, stale/expired/wrong/terminal callers, and every malformed/orphan/future/preseeded state fail closed without repair.
  • A valid takeover can observe the same immutable plan but cannot rewrite it.
  • B2A/B2B records and audits remain byte-for-byte unchanged.
  • Browser access to the provider-attempt path is explicitly denied by tests.
  • The existing CI-001B3 — Gate the commerce command journal on the Firestore emulator #167 hosted emulator job runs the expanded focused file and passes on pull-request and merge commits.
  • Full relevant local/hosted checks and two independent reviews pass.
  • Root docs distinguish plan binding, pre-POST evidence, send/retry cutoff, ambiguity, reconciliation, deployment, provider configuration, and live behavior.
  • No runtime adoption, endpoint/index export, Firebase deployment, Stripe/provider action, production read/write, TTL, migration, or officer behavior changes or is claimed.

Migration and rollback

Additive target schema only. No backfill, production read, provider inventory, deployment, or existing business-record mutation. Before adoption/deployment, #113 must approve any existing collection/account context; unknown records fail closed. No index is required.

Rollback is the reviewed unused source/schema/docs commit only. Never delete journal/provider-plan evidence as a casual rollback; deletion can remove the duplicate barrier and remains blocked on #110.

Required follow-on split

  • PAY-002B2C2: atomically store first-send time/evidence before any POST, reuse the exact plan/key within a conservative source-owned window, stop automatic POST after an old or unknown send time, and preserve ambiguous outcomes. No automatic attempt advancement.
  • PAY-002B2C3: reconcile verified Stripe/business/webhook/operator evidence and permit a later provider attempt only after separately proven non-execution or an explicitly safe corrected/new logical operation.

PAY-002C/D still own domain-specific reservations/result envelopes and actual response replay. PAY-003 owns verified webhook evidence. PAY-005 owns refund reconciliation. C1 must not absorb those schemas.

Excluded

  • Stripe/provider call, raw key, provider object/response, account verification, send marker/permission, timeout retry, ambiguity, reconciliation, or attempt advancement.
  • Domain business-record/result-envelope creation or dereferenceable response replay.
  • Endpoint/index export or runtime adoption.
  • Authorization, price, discount, tax, capacity, inventory, refund, webhook, mail, rate-limit, admin, confirmation-capability, or legal/policy behavior.
  • Lease renewal, lifecycle mutation, terminal transition, TTL, deletion, backfill, migration, package/lockfile/workflow/index/environment/UI/officer/Rules-source/provider changes.
  • Firebase deployment, production read/write, live checkout, or live testing.

Claim protocol

Creation is not a claim. Before editing: confirm exact current origin/main, assign the issue, post CLAIMED by /root at <UTC> with branch codex/issue-<number>-provider-plan-binding, 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:MMedium multi-file 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-13T18:16:31Z; branch codex/issue-173-provider-plan-binding; exact base 2eff107.

    Ownership is limited to the 10 paths listed in the issue: the three commerce-command source/test files, the focused existing Rules test, and six minimum root design/status documents. No Rules source, endpoint/index, server config, Stripe adapter, workflow, package/lockfile, environment, provider setting, production data, or officer procedure is claimed. Implementation will remain unused source and synthetic tests.

  4. daliu commented on Jul 13, 2026

    @daliu
    ContributorAuthor

    EXTRACTION FINISHED by /root at 2026-07-13T18:49:13Z.

    Review state: in review via PR #174 from exact commit f1d1dd6.

    Exact scope: the 10 claimed paths only. Exact reviewed pre-commit diff SHA-256: 43cabd3865a5931a7a0d9e72c877e54d5ea5ba8f584b0482cabcbe455ea2991c.

    Local proof: Node 20 focused 176/176; full Functions 834 passed; Java 17 Rules 311/311; frontend 134/134; SPA 10/10; build/lint/link/diff/sensitive scans green; final focused emulator 25/25 three consecutive times. Two independent exact-snapshot reviews approved with no findings after all review blockers were fixed.

    This is still unused source. Website, runmprc.com, Firebase, Stripe/provider configuration, production data, and live behavior remain unchanged/unverified. User standing authorization permits merge/deploy after exact-head hosted checks pass.

  5. daliu commented on Jul 13, 2026

    @daliu
    ContributorAuthor

    COMPLETED / claim released by /root at 2026-07-13T18:54:03Z.

    Source and merge:

    Hosted proof:

    • PR CI run 29276002196: Frontend, Functions, Rules, and named commerce-emulator jobs passed; Netlify deploy preview completed.
    • Post-merge CI run 29276143519: all four required jobs passed, including the 25-case commerce transaction emulator.
    • Two independent exact-snapshot reviews approved after the static-route, historical-lease, real commit-rejection, and dependency-cycle fixes.

    Deployment/provider truth:

    • Protected production release run 29276269838 validated exact current main and its exact green CI.
    • It then failed closed before checkout/build because the protected public REACT_APP_RECAPTCHA_SITE_KEY variable is absent.
    • Firebase deploy and GitHub Pages publication were skipped. No Stripe/provider configuration or call occurred. No production data was read or written. runmprc.com/live behavior was not changed or claimed.

    This issue is complete as its explicitly unused source/test/design slice. PAY-002B2C2 and C3 remain required before any Stripe send/reconciliation behavior. The 10-path claim is released; refresh from acaa961 before touching these files.

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:MMedium multi-file 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