Account linking proves both identities and revocation fences their use
The ask. Issue #2383 is the docs-only decision prerequisite for verified linking and revocation (#2371), within cross-surface continuity (#2369). The directory contract from PR #2382 is on main. This record settles the ceremony, revocation fence, recovery limits and email-bridge retirement left open by proposed record 0079.
Status and authority. This record is proposed. Merging it does not activate access, accept record 0079, change the runtime, add OAuth scopes or credentials, change production configuration, or authorize a person migration. Accepted record 0042 remains unchanged until a separately reviewed cutover. The later implementation owns living spec rows, tests and live proof; this document's negative cases are requirements, not passing tests.
Decision in brief
A human links from an authenticated Cloudflare Access browser session and separately authenticates with Sign in with Slack OpenID Connect. A server-owned, expiring, one-time intent joins those proofs; neither matching email, a Slack message nor a bot token substitutes for either proof. One transaction chooses the person, binds both identities and commits the intent outcome and audit receipts. It never merges people.
Unlink retains a versioned tombstone. Every binding-derived authorization use is ordered against revocation at an authoritative gate; a cached actor, pending confirmation or stream cannot carry the old relationship past that fence. Actor grants stay independent. The cutover disables the email bridge installation-wide, without a grandfathered email-linked population or a rollback that restores it. Exceptional recovery is separately authorized and audited, and is unavailable in the first linking release.
Grounding in the current code
These observations are against main at 4bf04521184b1f4b2a4c2858e7c8eeb9e0ced1fe. They distinguish the existing foundation from the work this proposal requires.
| Existing fact | Source and implication |
|---|---|
External identity is the exact (issuer, tenant, subject) tuple. PersonDirectory exposes createPerson, resolve, change and receipts, not a public link endpoint. | contract.ts, person-directory spec. Its proof labels record what a trusted caller asserts; they do not verify credentials. No production adapter consumes it yet. |
One change atomically checks the expected revision, changes one binding and appends its receipt; person creation is a separate transaction. An identity can never move to a different person, even after revoke. recover only reactivates the same person's tombstone. | engine.ts: decideChange; sqlite.ts: createPerson, change. Two sequential change calls cannot implement the atomic dual-identity ceremony below. |
Access verification pins RS256, issuer, configured audience and token times; its returned AccessIdentity retains subject/email or service commonName, not issuer/audience/expiry. | accessAuth.ts: claimsValid, verifyAccessJwt, AccessIdentity. Linking needs a trusted human-proof projection retaining verified issuer, audience, subject and validity, not identity reconstructed from request parameters. |
Browser identity still links by email, adding Slack self, asUser and Slack-derived memberOf. The reverse lookup caches per email for ten minutes. | commandHttp.ts: resolveAccessActor; slack/lookups.ts: resolvePersonByEmail. This cache is not a revocation mechanism. viewingAs is a read-only admin projection, not proof of the viewed person's control. |
| Configured ingress emails and local CLI email can name a Slack requester. | requester.ts: boundRequester; cli.ts: cliRequester; actor.ts: CLI_ACTOR, resolveChatActor. cli:local holds ALL_GRANTS, not evidence of a human subject. |
self affects private ownership checks, compiled list predicates and self-service commands; actsAsPerson tests for slack:. | authorize.ts: selfIdsOf, actsAsPerson; predicate.ts: compileCondition. No grant union does not mean no authorization effect. |
| Confirmation consumption judges the stored requester against supplied actor ids, then runs the stored requester's message. Stream subscriptions retain a request's access decision. | confirmations.ts: judge; dispatch/confirm.ts: actorIdsOf, consumeAndRun; liveView.ts: index feed and run subscription. Fresh directory resolution at page load alone cannot fence later clicks or emissions. |
One operation, two authenticated human identities
Access is the initiating proof
The first release admits only the access dashboard strategy's authenticated human session. Validate the signed application JWT against the installation's configured Access issuer and audience, pinned algorithm and trusted issuer JWKS, including required finite expiry and valid issuance/not-before times. Require a nonempty human sub and reject service-token or ambiguous human/service shapes. Retain the verified issuer, audience, subject, token validity and authentication kind in an internal proof object. Do not turn a decoded JWT, email header, viewAs cookie or caller-supplied person id into that object.
The directory key is (verified Access iss, null, verified sub): the issuer already names the Access organization. Audience is a required application admission constraint, pinned on the intent, not an email-like identity key. The same subject string under another issuer is unrelated. Recreating an Access user can change sub; an equal email does not transfer the old binding. A signing-key rotation that still verifies the same issuer/subject is not an identity rotation.
Revalidate the Access proof on callback and final consent, require the exact initiating issuer/subject/audience and a server-issued browser-session binding, and reject account switches or expired sessions. Refreshing a JWT for that same session does not extend the intent deadline. The token and none dashboard strategies, Access service tokens, static HTTP/MCP tokens, local CLI and admin view-as cannot initiate this human ceremony. They retain their independent policy-defined rights.
Slack is a separate OpenID Connect proof
Use a confidential-client authorization-code flow through Slack's OpenID endpoints, separate from the bot installation OAuth flow. Request openid profile to obtain the human and workspace identity; do not request email for linking. Additional scopes, registration of a redirect URI and provisioning of the client secret require their own reviewed operational approval. No existing bot credential proves the browser user controls a Slack account, and bot installation scopes must not be mixed into the Sign in flow.
Pin the configured provider to https://slack.com, its discovery/JWKS endpoints, the Sign in client id and an exact HTTPS callback URI. Send independent, unpredictable state and nonce for this intent and an allowlisted workspace hint. At callback, check state and the initiating browser binding before a single server-side code exchange with that exact redirect URI and confidential-client authentication. Validate the ID token's signature against trusted Slack keys, RS256 algorithm, exact issuer, intended client audience (including OIDC authorized-party checks if multiple audiences appear), expiry, issuance time and matching nonce. Reject a token issued before this intent outside the fixed clock-skew allowance. Missing, malformed or mismatching claims never create a binding.
Require a nonempty sub and signed https://slack.com/team_id in the installation's explicit workspace allowlist. Require the signed https://slack.com/user_id to agree with sub for the supported Slack profile. The key is (https://slack.com, signed team_id, signed sub). The requested team is a hint, not evidence of the returned workspace. Another team's identical subject string is unrelated. A bot/app identity or an unverifiable human profile is rejected, not rescued by users.lookupByEmail. Any supplementary Slack profile check may narrow admission, never replace the OIDC proof.
Use explicit local consent showing the two authenticated accounts and workspace before committing. Slack may skip its own consent screen for a previously authorized workspace; that is not consent to bind Switchboard histories. Account names are display-only; differing or equal emails decide nothing. Discard the code and all returned tokens after validation; linking needs no durable Slack refresh token or ongoing user API credential.
What the provider documents actually say
The Slack guide and live discovery were read for this proposal. Both specify the OpenID authorize/token endpoints, code flow, OpenID scopes and RS256; the guide explicitly separates Sign in scopes from normal installation scopes. Its sample discovery advertises form_post, while live discovery advertises query. Choose the currently advertised query callback, not the copied sample; pin and test that transport before launch. Do not silently widen callback methods when discovery changes. The inspected discovery does not advertise PKCE methods, so this decision does not assume Slack supports PKCE or prescribe a public-client flow.
With a query callback, use a dedicated endpoint without third-party resources, Cache-Control: no-store and Referrer-Policy: no-referrer; redact query strings from application and edge logs, and redirect to a clean same-origin URL after exchange. Missing Access authentication or browser binding fails closed, never bypasses the Access gate to make a callback work. Provider secrets and authorization responses must never enter thread logs, model context, run events or audit evidence.
Cloudflare's application-token documentation describes human iss, aud, nonempty sub and separate service-token payloads with empty sub; it says deleting/re-adding a user or changing organization changes the subject. Its validation guidance requires verifying the JWT and matching trusted issuer keys, not trusting the header's presence. Neither an Access email claim nor Slack's email_verified is proof of control of the other account. Provider examples are evidence of their protocol, not an authorization policy for Switchboard.
Durable intent and atomic directory commit
A link intent is a durable, server-minted operation, not a URL containing a person id. Creation requires a same-origin, CSRF-protected explicit POST by the initiating human; the callback's CSRF defense is its state plus browser binding. Final local consent is another protected POST. Bound the number of outstanding intents per actor and rate-limit initiation and callbacks; refusal reveals no other person's bindings.
Persist an opaque operation id, hashes of independent cryptographically random state/nonce/browser-binding values (at least 256 bits each), the authenticated initiating actor/Access tuple and audience, expected Slack issuer/client/workspace, exact callback, policy/proof version, creation/deadline and the versions of any bindings observed. The deadline is the earlier of ten minutes and the initiating Access token's expiry; successful Slack proof can only shorten it to that proof's expiry. Persist only validated identity metadata for the pending consent, not JWTs, codes, client secrets, raw email or refresh tokens. Keep the browser secret in a Secure, HttpOnly, same-site cookie bound to this operation; an operation id alone is not a bearer authorization.
The state machine is pending → exchanging → awaiting-consent → committed, with terminal failed, cancelled and expired outcomes. An atomic claim permits exactly one callback to enter exchanging. Duplicate callbacks never exchange another code. A crash or timeout with an uncertain exchange is terminal failure; do not replay the code after restart. A persisted awaiting-consent operation can finish only with the same authenticated browser, before its unchanged deadline and after fresh directory checks. A callback with invalid state cannot consume a different user's intent. Changing accounts or the proposed target requires a new operation.
Final consent uses one authoritative storage transaction to validate the intent's unconsumed state, deadline, consent and current binding revisions; choose or create the person; write all required bindings and their receipts; and persist the operation's terminal receipt. Any failed precondition or audit write rolls everything back. The production ceremony therefore needs a reviewed transaction-level extension/composition of the directory and intent store, with both durable and in-memory implementations. The existing single-identity change method is insufficient; two calls plus compensation are rejected because a half-link could already have authorized access. No network authentication call runs inside this transaction.
| Current pair, checked in that transaction | Person selection and result |
|---|---|
| Both identities unknown | Mint one opaque person and bind both in the same commit, with dual-authentication proof and initial revisions. Person creation is not an earlier externally visible step. |
| One active, the other unknown | Use the active identity's person only after control of both is proven and the active revision is unchanged. Bind the other identity; never accept a target person chosen by the browser. |
| Both active for the same person | Return an attributable already-linked outcome; do not create a person, gratuitously bump revisions or duplicate binding receipts. The one intent outcome still records consent and the observed versions. |
| Both active for different people | Refuse as a conflict without disclosing the other person's resources. Even dual proof is not permission to merge histories or move either identity. |
| Either revoked | Refuse ordinary link, including when the other identity still names the same person. Only separately authorized recovery may reactivate a tombstone. |
| Any invalid, conflicting or unavailable resolution, changed revision, or expired intent | No binding or person is committed. Fail closed; no email fallback, automatic retry with a new target, or orphan half-link. |
Overlapping intents serialize at the same authority. The first applicable commit wins; an intent whose observed revisions changed ends stale and requires a fresh ceremony, even if retrying could now find a plausible person. Restart or response loss after a commit returns the same durable operation receipt only to the original authenticated initiator/browser binding, without another mutation. After the bounded result-read window expires, a new authenticated account-status read is required; the receipt never becomes a reusable access token. Unlink after commit does not rewrite the historical receipt or let replay restore access.
Binding receipts retain action, authenticated actor tuple, exact external identity, person id, revision, timestamp and the closed proof method/version already defined by the directory. A separate access-controlled operation audit joins both proof identities, their before/after revisions, consent, outcome and operation id without overloading the directory's strict proof schema. Operator evidence, when implemented, is referenced by opaque protected evidence id. No arbitrary proof payload enters a binding. Run-event projection remains the whitelist in runBindingAttribution, not either full audit object. Expired intent secrets/proof staging are removed; audit outcomes and revocation tombstones remain durable, access-controlled facts rather than authorization credentials.
Revocation is an ordering boundary, not a cache timeout
Unlink is a CSRF-protected explicit operation naming one exact identity/binding revision, never “whoever has this email.” An authenticated human may revoke its own exact binding without borrowing that binding's self access, or another active binding of the same person while its own relationship is current. The latter depends on both revisions. A separately authorized operator may revoke with an audited reason. All use the one policy table; linking does not add an admin grant. An expired session, view-as projection or machine email cannot exercise human unlink rights.
Commit the target tombstone, incremented revision, attributable receipt and invalidation of affected pending link intents together. Never delete the binding row, recycle its person, clear audit history or revoke independent actor grants. Revoking one identity does not revoke the person's other identities. A later link of a new identity to that person still cannot resurrect the revoked one.
Chosen fence. The authoritative directory owner serializes binding-derived access-use admissions with revocation. Each use records its actor, operation/resource, person and every relationship revision it relies on. The policy table evaluates current actor authority and these current relationships; the gate atomically verifies their revisions and either admits that bounded use or rejects it. A preliminary resolve followed by asynchronous work and an unfenced authorization decision is not enough. No client-supplied self, stale compiled predicate, historical run attribution or TTL-only positive cache can stand in for this gate.
The linearization point is the gate's admission commit. If revoke commits first, the use fails. If the bounded use commits first, that one use may finish; revoke cannot recall an external effect already admitted or bytes already released. This is not an unlimited lease for a request, stream or agent run: queued work not yet admitted must recheck, and each later binding-dependent action or output batch needs its own gate. Admission records must be single-use, target-bound and non-transferable; retries cannot turn a pre-revoke admission into a new post-revoke operation. Durable dispatch/outbox consumption must preserve the same ordering and idempotency, not issue an authorization token that any worker can spend indefinitely.
| Consumer | Required behavior at and after the fence |
|---|---|
| New requests and cached actors | Resolve current state at the authority for every binding-derived decision. Cache display facts or candidate ids only. After revoke, strip derived self, asUser and linked membership; rebuild point and list predicates identically. A directory outage denies the relationship, not independently granted access. |
| In-flight reads and paginated lists | Query under the current predicate; revalidate dependent revisions at bounded response-release admission after asynchronous data loading. Discard private results if revoke won. A later page is a new use. Bytes already released are not retractable. |
| Streams, reconnects and notifications | Opening a stream grants no standing relationship. Every bounded emission batch crosses the gate; revoke closes/removes the affected subscription before another batch can be admitted. Queued private bytes are discarded if not yet admitted. Invalidation messages improve prompt closure but are not the correctness fence; disconnected replicas fail closed. Reconnect and capability issuance reauthorize. |
| Runs and writes | A write or run admission ordered before revoke may complete its specifically admitted work; historical authorship stays fixed. Unlink does not stop work authorized independently as that actor or cancel all runs belonging to the person. A new turn, queued continuation, later binding-dependent mutation or private result delivery must cross the fence again. |
| Confirmations | Do not pass an old self array to consume. Bind offers to their actual offering actor and any relationship revisions used. Reauthorize the accepting actor's exact action and thread permission, without borrowing the offerer's stronger credential. Consumption/execution admission and revision validation must be one fenced, single-use operation; if not atomic, refuse. Preserve expiry and replay fences. |
Cross-actor/cross-surface confirmation acceptance is unavailable in the first linking release, even for the same person. Same-actor offers that depended on a removed relationship also fail; offers authorized wholly by independent rights may remain valid. At cutover invalidate legacy offers whose actor/proof dependency cannot be established. Extending the current separate confirmation store requires its own implementation proof; a fresh read just before consumeAndRun does not close the race.
The first release must not issue an unfenced bearer capability for binding-derived private access. Existing capability URLs are a separate authority: unlink is not a claim that previously issued independent bearer tokens or an explicit administrator grant stop working. Tests must distinguish these from access granted through the removed relationship. If a consumer cannot preserve the chosen fence, binding-derived access on that consumer stays disabled; it must not silently get a grace period. Thread portability and membership grants are not implied by person equality.
Exceptional recovery, deliberately not a first-release feature
Ordinary linking cannot undo revocation. The exceptional path is a separately authorized, time-bounded recovery case, with the following minimum contract before it can be exposed:
- Independently authenticate the requesting operator and check an explicit recovery action in the central policy table, without deriving it from the target person's link, an email match or a view-as session. Require a second independently authenticated, authorized operator to approve the exact case; neither approval may be supplied by the recovering subject acting for itself.
- Identify the exact original issuer/tenant/subject, existing person and current revoked revision from protected directory history. Require fresh proof of control of that same external subject plus attributable identity-provider/workspace administrator evidence that it is still the original human, not a recycled account. Store a protected evidence reference, reason, proof-policy version, both operator actors, approval expiry and the user's consent. Name/email similarity, a bot token or a support-chat assertion is insufficient.
- Commit the same-person reactivation using
administrator-recoveryand the current expected revision, with a single-use case outcome and audit receipt atomically. A changed revision, expired approval, missing evidence, storage failure or inability to establish original ownership refuses. Recovery never changes grants, rewrites historical ownership or moves an identity between people.
The first release exposes no recovery endpoint, command, tool or direct database runbook, despite the directory already being able to store a trusted recover mutation. A holder of both accounts can create a normal fresh link only when neither identity is tombstoned; losing that convenience after unlink is intentional until recovery is reviewed and proved. Subject replacement after provider deletion/recreation, loss of all human authenticators, person merging/splitting, and reassignment of a recycled identity are unavailable, not covert fallback paths. A new subject may use a new person's independent account, but cannot acquire the old person's histories. Administrative ability to inspect a record is not authority to transfer it.
Migration and explicit retirement of the email bridge
Choose a hard cutover, not a temporary legacy cohort. Record 0042's email-as-proof premise and its configured-email extension for ingress/CLI are retired together when the verified-link implementation's cutover is approved. Email may remain display or a non-authorizing suggestion, never a source of self, requester substitution, membership, personal scope or ownership. This proposal does not retrospectively claim today's bridge was verified.
The cutover PR must append a dated ## Amended section to record 0042 naming this record and the exact retired clauses (email reverse lookup, configured credential/CLI email personhood, and the claim that linking has no authorization effect). Do not mark the entire record superseded: separate actors, no grant union, policy-table checks and MCP promotion by re-issue remain valid. Record 0079's broader ownership, personal-scope and thread-portability decisions remain proposed and separately gated; this record closes only its ceremony/revocation/recovery and temporary-email-bridge choices. Neither earlier record is edited here.
Use a durable installation migration epoch with a monotonic “email linking retired” marker and a minimum compatible resolver version. This is an implementation requirement, not an existing config flag. Before marking retirement, pause affected ingress, drain or fence admitted binding-dependent operations, close old streams, invalidate legacy confirmation offers and replace every serving resolver with a version that understands the marker. Commit the epoch, discard all email-derived resolutions and then reopen only compatible ingress. Include browser, HTTP/MCP requester binding, CLI requester construction and any read-time email-based “mine” or membership projection; omitting an adapter leaves a second link rule alive.
Every startup and request admission must honor the marker. Refuse affected ingress on unavailable migration state or an unsupported epoch; never default to legacy linking. A stale process must be unable to keep serving through its old cache, and an incompatible image must be blocked by deployment/admission fencing, not expected to obey code it does not contain. A pre-cutover backup that lacks newer tombstones/epoch cannot be restored into serving traffic: recover the current durable security state first, or keep affected ingress closed. Rollback can disable new linking and binding-derived access, but cannot lower the epoch, discard tombstones or re-enable email linking.
| Migration state or negative case | Access result and release requirement |
|---|---|
| This proposed record or the directory-only merge | No runtime change. Existing behavior remains explicitly legacy; there is no claim of a deployed unlink fence yet. |
| Link service staged before cutover | Keep production linking and binding-derived access disabled; validate in isolated fixtures. No interval with user-visible “unlink” followed by an active email bridge. |
| Cutover with no verified binding, including an old email match | Keep only the caller's independently authorized rights. No automatic import, grandfathering, bulk person creation or inference from old userId rewrites. The person must complete the new ceremony. |
| Verified pair after cutover | Resolve the durable relationship. Private ownership aliases and personal-scope writes remain off until their separate migration proves provenance, one writable scope and point/list parity; the ceremony alone does not claim legacy history. |
| Equal or changed email; changed Access issuer/subject; wrong Slack workspace | No inferred link or transfer. A new tuple is unknown; a tombstoned tuple stays revoked. Signing-key refresh only authenticates the same exact tuple. |
| Unlink, warm stale cache, replica restart or Slack outage | No email re-resolution. The current directory state governs; unavailable means no relationship. Existing grants do not disappear because the directory or Slack is unavailable. |
Machine token email or SWITCHBOARD_CLI_EMAIL | No human binding or requester substitution, even when the email names an admin. Keep the machine/local actor's own rights and attribution. Future service delegation or person-scoped CLI authentication needs a separate reviewed contract. |
| Conflicting people or reused historical identity | No merge, reassignment or history rewrite. Tombstone and prior receipts survive. Ambiguous legacy resources remain unclaimed for cross-surface self access. |
| Rollback, stale process, missing epoch or old backup | Stop affected access rather than restore record 0042's bridge. Preserve bindings, receipts, tombstones and the retirement marker; prove this with mixed-version and restart tests. |
Historic userId, authenticatedAs and actor attribution remain as recorded. Do not bulk rename them to person ids or copy sealed MCP credentials to new scope keys. Legacy ownership assignment, one authoritative config/memory scope, safe credential reconnection and portable thread writes belong to the later authorization/migration stages of issue #2369. A successful link is not proof those migrations are safe.
Proof obligations and release gates
These are the later implementation's required tests and live receipts, not tests run by this docs-only unit. Add exact living-spec bindings when that code changes. The docs PR is proved by this decision/migration matrix, source references, decisions:check, relevant docs checks and an independent review of its exact pushed head. Maintainer acceptance, production OAuth provisioning and live linking remain separate gates.
| Scenario | Required outcome |
|---|---|
| Valid Access plus Slack proof, same initiating browser and explicit consent | Both bindings, the selected/created person and one operation outcome commit together; actor grants are unchanged. Test both store implementations and a response-lost retry. |
| Missing/forged JWT, wrong issuer/audience, unsafe algorithm, unknown key, expired/future token, service or synthetic browser identity | No human proof and no binding. Valid trusted key rotation for the same tuple succeeds; a changed issuer/subject never inherits the old person by email. |
| Wrong/missing/replayed state or nonce; stolen callback in another browser; account switch; expired/cancelled intent; repeated code | No link, repeated token exchange or duplicate receipt. Restart while exchanging cannot replay credentials; restart at consent cannot skip authentication or extend expiry. |
| Wrong Slack team, mismatched signed user/sub, bot identity, matching email, bot installation token or consent skipped at Slack | No substitute proof; local consent is still mandatory. Query callback secrets are absent from logs, referrers, caches and run events. |
| Concurrent overlapping ceremonies, different-person conflict, stale revision, audit-write failure or crash between attempted writes | One complete eligible outcome or none, never an orphan person/half-link; no automatic person merge or target adoption. Ordinary link cannot reactivate a revoked binding. |
| Revoke versus new request, asynchronous response release, queued write/run admission, page, stream batch or reconnect | Revoke-first refuses the derived use; admission-first permits only that bounded use. Warm caches, dropped invalidation and restart cannot admit another. Point decisions equal list predicates. |
| Pending offer before/after unlink, same person with unequal actor grants, old ambiguous offer | Removed relationship cannot accept or execute; no stronger-offerer credential borrowing. Cross-actor acceptance is unavailable initially. Independent-rights control cases still pass. |
| Authorized same-subject recovery versus missing/expired second approval, reassigned subject, changed revision or different person | First release has no recovery route. Future recovery admits only the exact audited same-person case; failure commits nothing and does not erase the tombstone. |
| Directory/migration authority unavailable, mixed resolver versions, process restart and rollback/backup restoration | Binding-derived access fails closed, independent actor grants remain, and the email bridge never reappears after retirement. |
| Machine/local email and legacy histories/scopes | No person claim or automatic migration. Audit names the authenticating actor and binding revisions without storing credentials or raw email. |
A live test needs two separately authenticated human accounts in the reviewed Access application and Slack workspace, plus an unbound/foreign actor and actors with unequal grants. Demonstrate consent, duplicate callback handling, unlink, a pending offer, an open private stream, restart and denied replay at the same deployed version; test independent-rights controls separately. No production secrets or app changes are authorized by this record. If any release fence cannot be demonstrated, keep the relevant feature disabled rather than weaken the matrix.
Costs, rejected alternatives and acceptance
The cost is a link UX, separate OAuth client operation, durable intent/audit storage and a transaction/fencing extension beyond the current directory. A strict migration removes convenient email-linked access until independently proved; unavailable recovery can leave a person unable to relink a tombstoned account in the first release. These are explicit safety costs, not bugs to repair with an email fallback.
- Email match or bot-token proof: cheaper, but neither proves control of both human identities and neither makes unlink durable.
- Sequential single-binding writes with compensation: reuses the present seam, but exposes a half-link and cannot atomically choose a person under concurrency.
- Cache expiry or best-effort invalidation as revocation: allows post-unlink private access and stale confirmations; the authoritative ordering fence is required.
- Automatic merge or self-service resurrection: attractive recovery UX, but transfers history or erases a deliberate security revocation. Separate evidence and authority must decide.
- Grandfathered legacy email links or rollback to old binaries: preserves convenience at the cost of a second authorization path that can undo unlink. Choose a monotonic retirement boundary.
Acceptance requires reviewer agreement on the all-or-nothing ceremony, the bounded-use revocation semantics, the deliberately unavailable recovery/confirmation paths and the hard migration cutover. If provider protocol or storage cannot support those contracts, revise this proposal before activation. Merging a proposed record is not evidence that the ceremony, revocation fence or cross-surface continuity has shipped.