Skip to content

MEMBERS-IDENTITY-001F — Define versioned provider-link lifecycle reconciliation #445

Description

@daliu

Officer impact: None yet. This child defines and tests an unused provider-link lifecycle contract. It does not change signup, membership, discounts, Google, WhatsApp, Strava, officer screens, or any live account.

Officer documentation: Update only a named future-state provider-link lifecycle section in docs/officers/EVENTS_SHOP_MEMBERS.md, including a Mermaid diagram and text alternative. Current officer procedures remain NOT AVAILABLE YET.

Deployment evidence: None at issue creation. Closure must distinguish source, tests, merge, website publication, Firebase deployment, provider configuration, production data, migration, and live behavior. A green workflow does not prove an external surface is live.

Problem

Parent #81 requires a provider-neutral external-account contract with consent, desired and observed state, last reconciliation, safe error evidence, and a stable command ID. Merged child #367 provides a stateless reconciliation classifier and #370 provides effective versioned consent, but neither defines a durable revisioned lifecycle.

The missing lifecycle means duplicate or out-of-order commands, disconnect/relink, a changed provider account, and partial or unknown provider outcomes are not yet represented by one tested contract. Google, WhatsApp, and Strava runtime adoption remains blocked; this child defines only the policy-neutral mechanism they may later consume.

Atomic outcome

Add one unused CommonJS pure module and focused tests that define a versioned provider-link lifecycle snapshot plus an optimistic-concurrency command reducer. The reducer composes the existing #367 reconciliation classifier rather than duplicating its collision/consent/desired-versus-observed policy.

The snapshot carries only:

  • bounded opaque membership and provider-account references;
  • one provider from the existing provider-neutral vocabulary;
  • effective consent disposition;
  • desired and observed link state;
  • an optional opaque bound-membership reference used only for collision evidence;
  • safe-integer revision;
  • last reconciliation sequence, outcome, opaque attempt reference, and fixed error code;
  • the latest stable command ID, command payload hash, and expected revision.

The command vocabulary covers:

  • consent-disposition change;
  • request link or unlink;
  • record a reconciliation success, definitive failure, or outcome-unknown result; and
  • replace a provider-account reference only after unlink is both desired and observed.

Exact latest-command retry is read-only. Changed reuse, stale expected revision, skipped/repeated reconciliation sequence, stale observation, wrong-account evidence, provider-account replacement while linked or unknown, and out-of-order relink fail closed.

Invariants

  • Every record and verdict hard-codes grantsAuthority: false. A provider link never grants membership, price, payment state, role, or admin authority.
  • The module is pure, deterministic, clockless, unused, CommonJS, and imported by no runtime or Functions index.
  • It composes classifyProviderLinkReconciliation; it does not reimplement provider collision, consent gating, or desired/observed drift policy.
  • Effective consent is caller-supplied from the separately reviewed MEMBERS-IDENTITY-001D — Provider-neutral versioned consent-state derivation (source only, unused) #370 contract. This module does not capture consent events or invent policy-version order.
  • Provider-account replacement is never an in-place silent relink while the prior account may remain linked. The prior record must be desired-unlinked and observed-unlinked first.
  • An outcome-unknown reconciliation preserves the desired state and never guesses provider success. A definitive failure preserves the last known observation.
  • A successful observation is accepted only at the exact next reconciliation sequence and becomes the new known observation.
  • Error evidence is one closed non-identifying enum. No raw provider error, status/body, URL, token, email, phone, name, or free text is accepted, returned, stored, or logged.
  • Stable command IDs and payload hashes are bounded opaque values. The latest exact retry is idempotent; conflicting reuse fails. Durable all-history replay protection remains future trusted-runtime work.
  • Inputs are strict exact plain data objects. Proxies, accessors, inherited, extra, missing, symbol, non-enumerable, malformed, or out-of-range fields fail through one fixed non-echoing boundary.
  • Outputs and nested records are frozen.
  • No Firestore schema/Rules/index, Auth/custom claim, endpoint, UI, package, workflow, provider configuration, migration, deployment, production record, or live behavior changes.

Required tests

  • Create one valid unlinked lifecycle record for every existing provider; all results grant no authority.
  • Link intent with active consent yields the existing MEMBERS-IDENTITY-001C — Provider-neutral external-account link + collision contract (source only, unused) #367 reconciliation disposition; non-active consent blocks linking.
  • Link success, unlink intent, unlink success, and same-account relink advance deterministically.
  • Provider-account replacement is admitted only after confirmed unlink and resets observation/reconciliation safely.
  • Collision, consent block, pending observation, definitive failure, and outcome-unknown paths preserve the specified record truth.
  • Exact latest retry returns the same frozen record. Changed command-ID reuse, stale/future/skipped revisions, duplicate/out-of-order reconciliation sequence, stale success, and wrong-account binding fail closed.
  • Hostile values/getters/proxies and raw email/phone/token/error/URL-shaped fields never enter output, error, or logs.
  • Source scans prove only the approved local pure dependency, no runtime/index import, and no clock/env/network/Firebase/provider/logger/random edge.
  • Focused and full Functions tests/lint plus the applicable repository CI gates pass on Node 20.

Exact owned paths

  1. New functions/membershipProviderLinkLifecycle.js
  2. New functions/membershipProviderLinkLifecycle.test.js
  3. Only the named provider-link lifecycle section/diagram in SYSTEM_DESIGN.md
  4. Only the matching future-state section/diagram in docs/officers/EVENTS_SHOP_MEMBERS.md

Dependencies and coordination

Explicitly out of scope

Do not edit Functions index/endpoints, Rules, packages, workflows, existing Google/WhatsApp/Strava runtime, GITHUB_ISSUES.md, GITHUB_ISSUE_SLICES.md, or provider/deployment configuration.

Do not use real members, accounts, provider identifiers, tokens, errors, or production data.

Claim protocol

Creation is not a claim. Before any file edit, assign the issue, create and push a unique branch from exact current main, post CLAIMED by <canonical agent> at <UTC>; branch <branch>; exact base <SHA>, re-read live comments, and move status:ready to status:in-progress. Earliest valid claim wins.

Metadata

Metadata

Assignees

Labels

area:authAuthentication and authorizationarea:firebaseFirebase services and dataarea:membershipMembership lifecycle and reconciliationarea:privacyPersonal data, consent, minimization, retention, and privacy operationspriority:P0Launch blocker or urgent security risksize:SSmall focused issuetype:architectureArchitecture and contract designtype:reliabilityReliability and recoverytype:securitySecurity or privacy boundarytype:testingTest infrastructure and quality gates

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions