Skip to content

fix(spec,platform-objects): InvitationStatus accepts canceled, the value cancel-invitation writes - #7782

Draft
os-zhuang wants to merge 1 commit into
mainfrom
claude/issue-7726-invitation-status-canceled
Draft

fix(spec,platform-objects): InvitationStatus accepts canceled, the value cancel-invitation writes#7782
os-zhuang wants to merge 1 commit into
mainfrom
claude/issue-7726-invitation-status-canceled

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Fixes #7726

The spec's InvitationStatus listed four values while the platform shipped five. POST /api/v1/auth/organization/cancel-invitation writes status: 'canceled' onto the sys_invitation row, and the object declared that value in its select and filtered on it in its "Expired / Canceled" listView — so an invitation the platform had just canceled through its own UI failed validation against InvitationSchema, which composes the enum.

The enum now accepts canceled, and the two definitions of the vocabulary are bound so the next divergence is loud.

Premise verification (the PM ruling hinged on it)

The dispatch made the widening conditional on two facts. Both hold on origin/main:

1. cancel-invitation is a real, user-reachable shipped writer — not dead code. Four independent confirmations:

  • The writer is better-auth's organization plugin: dist/plugins/organization/routes/crud-invites.mjs:448-451 does adapter.updateInvitation({ invitationId, status: "canceled" }) on the cancel endpoint, and again at :163-166 when cancelPendingInvitationsOnReInvite supersedes a pending row. Its own schema enum (schema.mjs:5-10) carries the value.
  • The route is mounted: packages/plugins/plugin-auth/src/auth-route-ledger.ts:146 and :274 declare POST /api/v1/auth/organization/cancel-invitation.
  • A user can reach it from the UI: sys-invitation.object.ts:66-80 declares the cancel_invitation action (list_item location, danger variant, confirm text) targeting exactly that route.
  • And from the SDK: packages/client/src/index.ts:2060-2068, organizations.invitations.cancel.

2. No consumer branches over the enum in a way a fifth value breaks. InvitationStatus has exactly three consumers in the repo: InvitationSchema (composes it), organization.test.ts, and the ADR-0122 type-alias pin (Assert< Eq< z.input, z.infer > >, unaffected by a member). No switch, no exhaustiveness check, no mapping table keyed on it. Positive control for the search: switch (status) does match elsewhere in the repo — datasource-connection-service.ts:196, capability-preflight.ts:70, protocol-handshake.ts:283 — none of them this enum.

So this is a widening that reconciles the contract to shipped behaviour, not a new capability: every part of the mechanism predates the change, consumers gain a value and none lose one.

Worth recording, because it is why the drift was possible at all: the vocabulary is the union of two upstreams. better-auth contributes canceled and has no notion of expiry; expired is ObjectStack's own, driven by expiresAt. Neither side's list was ever complete on its own.

Changes

  • packages/spec/src/identity/organization.zod.tsInvitationStatus gains canceled as its fifth value. The JSDoc states what the value means and who writes it, and that canceled (issuer-side) is distinct from rejected (invitee-side). The .describe() text is left alone: neighbouring describes in this file are one-line summaries and none enumerate per-value semantics, so the explanation goes where this file already puts explanation.
  • packages/platform-objects/src/identity/sys-invitation.object.tsstatus reads its select options from the enum (Field.select([...InvitationStatus.options], …)) instead of repeating them as a literal. This is the shape the neighbouring role field on the same object already uses for the membership-role vocabulary (BUILTIN_MEMBERSHIP_ROLE_OPTIONS, ADR-0108), so it is the established binding here rather than a new convention. Output is byte-identical to the previous literal — same five values, same order — which is why no generated translation moved.
  • Testsorganization.test.ts pins the vocabulary as a list (a "parses without throwing" loop cannot catch a widening) and asserts an out-of-vocabulary value is still refused on the issue's code, not on a bare throw; a widening is exactly the edit that can slip into z.string() and keep every toThrow() test green. New sys-invitation.status-vocabulary.test.ts compares the object's declared options against the enum — modelled on the existing sys-setting.scope-options.test.ts pin — plus cases for canceled, for the listView filter values staying inside the vocabulary, and for the default.
  • content/docs/references/identity/organization.mdx — regenerated (gen:docs), the only stale artifact of the 13.

Reverse verification

Direction predicted before running, and the two halves have to be run where each side's source is read (platform-objects resolves @objectstack/spec through the workspace symlink to dist, the spec suite reads src):

  • Spec enum reverted to four values → predicted red on the three cases that assert the fifth. Actual: 3 failed | 19 passed, failing on the accept-valid-statuses loop, the five-value order pin, and InvitationSchema's all-statuses case, with zod reporting expected one of "pending"|"accepted"|"rejected"|"expired".
  • Object's options re-literalized to the old four-value list (the drift the binding is meant to prevent, simulated) → predicted red on parity. Actual: 2 failed | 3 passed — the parity case and the canceled case.

The second direction is the one worth stating plainly: with the binding in place, reverting only the enum leaves the parity assertion green, because the object follows the enum down. That is by construction, and it is why the canceled case and the listView-filter case exist as separate assertions — they are what goes red in that direction. A parity test alone would have been a test that cannot fail for the original defect.

Both restorations were done with git checkout and a file copy; the tree was confirmed identical to the commit afterwards.

Verification

Gates named for this card, all run locally on the final tree:

  • pnpm --filter @objectstack/spec test378 passed (378) files, 9950 passed (9950) tests
  • pnpm --filter @objectstack/platform-objects test14 passed (14) files, 316 passed (316) tests
  • pnpm --filter @objectstack/spec --filter @objectstack/platform-objects typecheck — both Done
  • pnpm --filter @objectstack/spec check:generatedAll 13 generated artifacts are up to date
  • check:merge-driver, check:adr-anchors, check:spec-parsed-alias — green (1512 bare z.input aliases, 826 pinned isomorphic, 686 paired with an XParsed. OK)
  • node scripts/check-nul-bytes.mjs — OK, 7131 files; plus a targeted control-byte self-scan of the five touched files

One note on check:generated: it flagged api-surface/ stale immediately after the reverse verification, purely because restoring files by checkout bumps src mtimes past dist and the staleness rule is mtime-based. A full pnpm --filter @objectstack/spec build (with declarations, not OS_SKIP_DTS=1) cleared it, and the closure was re-run green on the final tree with no working-tree diff.

Out of scope

Filed as #7781, not fixed here: the client SDK types the same union by hand at packages/client/src/index.ts:2030 and is missing expired — the opposite divergence, in a package this PR does not touch, and whether @objectstack/client should import the spec enum for a response annotation is an unresolved routing question.


Generated by Claude Code

`cancel-invitation` (better-auth's organization plugin, reachable from the
`cancel_invitation` action on `sys_invitation` and from the client SDK's
`organizations.invitations.cancel`) writes `status: 'canceled'`, and
`sys_invitation` declared that value in its select and filtered on it in its
"Expired / Canceled" listView — but the spec enum stopped at four values, so
`InvitationSchema` rejected a row the platform had just written.

Widen `InvitationStatus` to the five shipped values, and bind the platform
object's select options to the enum (the shape the neighbouring `role` field
already uses for the membership-role vocabulary) so the next divergence lands
as a red test instead of a rejected row.

Fixes #7726

Co-authored-by: os-zhuang <hr@objectstack.ai>
@vercel

vercel Bot commented Aug 11, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Aug 11, 2026 2:38pm

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/platform-objects, @objectstack/spec.

106 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/ai/agents.mdx (via @objectstack/spec)
  • content/docs/ai/skills-reference.mdx (via @objectstack/spec)
  • content/docs/ai/skills.mdx (via @objectstack/spec)
  • content/docs/api/client-sdk.mdx (via @objectstack/spec)
  • content/docs/api/environment-routing.mdx (via @objectstack/spec)
  • content/docs/api/error-catalog.mdx (via @objectstack/spec)
  • content/docs/api/error-handling-client.mdx (via @objectstack/spec)
  • content/docs/api/error-handling-server.mdx (via @objectstack/spec)
  • content/docs/api/index.mdx (via @objectstack/spec)
  • content/docs/automation/approvals.mdx (via @objectstack/spec)
  • content/docs/automation/connectors.mdx (via @objectstack/spec)
  • content/docs/automation/flows.mdx (via @objectstack/spec)
  • content/docs/automation/hook-bodies.mdx (via packages/spec)
  • content/docs/automation/hooks.mdx (via @objectstack/spec)
  • content/docs/automation/index.mdx (via @objectstack/spec)
  • content/docs/automation/webhooks.mdx (via @objectstack/spec)
  • content/docs/automation/workflows.mdx (via @objectstack/spec)
  • content/docs/concepts/architecture.mdx (via @objectstack/spec)
  • content/docs/concepts/design-principles.mdx (via packages/spec)
  • content/docs/concepts/index.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-driven.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-lifecycle.mdx (via packages/spec)
  • content/docs/concepts/north-star.mdx (via @objectstack/spec)
  • content/docs/data-modeling/analytics.mdx (via @objectstack/spec)
  • content/docs/data-modeling/drivers.mdx (via @objectstack/spec)
  • content/docs/data-modeling/external-datasources.mdx (via @objectstack/spec)
  • content/docs/data-modeling/field-types.mdx (via @objectstack/spec)
  • content/docs/data-modeling/fields.mdx (via @objectstack/spec)
  • content/docs/data-modeling/formulas.mdx (via @objectstack/spec)
  • content/docs/data-modeling/index.mdx (via @objectstack/spec)
  • content/docs/data-modeling/objects.mdx (via @objectstack/spec)
  • content/docs/data-modeling/queries.mdx (via @objectstack/spec)
  • content/docs/data-modeling/schema-design.mdx (via @objectstack/spec)
  • content/docs/data-modeling/seed-data.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation-rules.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation.mdx (via @objectstack/spec)
  • content/docs/deployment/cli.mdx (via @objectstack/spec)
  • content/docs/deployment/tenancy-modes.mdx (via @objectstack/spec)
  • content/docs/deployment/troubleshooting.mdx (via @objectstack/spec)
  • content/docs/deployment/validating-metadata.mdx (via @objectstack/spec)
  • content/docs/getting-started/build-with-claude-code.mdx (via @objectstack/spec)
  • content/docs/getting-started/common-patterns.mdx (via @objectstack/spec)
  • content/docs/getting-started/examples.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-reference.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-start.mdx (via @objectstack/spec)
  • content/docs/getting-started/your-first-project.mdx (via @objectstack/spec)
  • content/docs/kernel/cluster.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/auth-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/cache-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/data-engine.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/index.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/metadata-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/storage-service.mdx (via @objectstack/spec)
  • content/docs/kernel/index.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/data-service.mdx (via @objectstack/spec)
  • content/docs/kernel/runtime-services/email-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/examples.mdx (via @objectstack/spec)
  • content/docs/kernel/runtime-services/index.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/queue-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/sharing-service.mdx (via @objectstack/spec)
  • content/docs/kernel/runtime-services/sms-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/storage-service.mdx (via @objectstack/spec)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/spec)
  • content/docs/kernel/services.mdx (via @objectstack/spec)
  • content/docs/permissions/authorization.mdx (via @objectstack/spec)
  • content/docs/permissions/permission-sets.mdx (via @objectstack/spec)
  • content/docs/permissions/permissions-matrix.mdx (via @objectstack/spec)
  • content/docs/permissions/positions.mdx (via @objectstack/spec)
  • content/docs/permissions/rls.mdx (via @objectstack/spec)
  • content/docs/permissions/sharing-rules.mdx (via @objectstack/spec)
  • content/docs/permissions/system-context.mdx (via packages/spec)
  • content/docs/plugins/adding-a-metadata-type.mdx (via @objectstack/spec)
  • content/docs/plugins/development.mdx (via @objectstack/spec)
  • content/docs/plugins/index.mdx (via @objectstack/spec)
  • content/docs/plugins/packages.mdx (via @objectstack/platform-objects, @objectstack/spec)
  • content/docs/protocol/backward-compatibility.mdx (via @objectstack/spec)
  • content/docs/protocol/diagram.mdx (via packages/spec)
  • content/docs/protocol/kernel/config-resolution.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/http-protocol.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/i18n-standard.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/index.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/lifecycle.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/plugin-spec.mdx (via @objectstack/spec)
  • content/docs/protocol/knowledge.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/query-syntax.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/schema.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/security.mdx (via packages/spec)
  • content/docs/protocol/objectql/state-machine.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/actions.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/concept.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/layout-dsl.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/record-alert.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/widget-contract.mdx (via @objectstack/spec)
  • content/docs/ui/actions.mdx (via @objectstack/spec)
  • content/docs/ui/apps.mdx (via @objectstack/spec)
  • content/docs/ui/create-vs-edit-form.mdx (via @objectstack/spec)
  • content/docs/ui/dashboards.mdx (via @objectstack/spec)
  • content/docs/ui/field-grouping-and-order.mdx (via @objectstack/spec)
  • content/docs/ui/forms.mdx (via @objectstack/spec)
  • content/docs/ui/index.mdx (via @objectstack/spec)
  • content/docs/ui/public-data-collection.mdx (via @objectstack/spec)
  • content/docs/ui/setup-app.mdx (via @objectstack/platform-objects, @objectstack/spec)
  • content/docs/ui/translations.mdx (via @objectstack/spec)
  • content/docs/ui/views.mdx (via @objectstack/spec)

7 release-owned page(s) also reference the affected code. These are read-only:

  • content/docs/releases/implementation-status.mdx (via @objectstack/spec)
  • content/docs/releases/index.mdx (via @objectstack/spec)
  • content/docs/releases/v12.mdx (via @objectstack/spec)
  • content/docs/releases/v13.mdx (via @objectstack/spec)
  • content/docs/releases/v16.mdx (via @objectstack/spec)
  • content/docs/releases/v17.mdx (via @objectstack/spec)
  • content/docs/releases/v9.mdx (via @objectstack/spec)

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

@os-zhuang os-zhuang left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PM step-7 review (spec lane, session_01JY2Q5Xto1u8YHADgrZDTnk) — ACCEPT. (Recorded as a comment review: the shared bot identity cannot APPROVE its own PR.)

The #7726 dispatch was premise-hinged: the ruling (add canceled to InvitationStatus) held only if better-auth's cancel-invitation really writes that value. The premise is verified in the change itself — the writer (POST /organization/cancel-invitation, plus cancelPendingInvitationsOnReInvite), the cancel_invitation action targeting it, and the "Expired / Canceled" listView filtering on the value — so the hinge holds and the widening reconciles the contract to shipped behaviour.

What carried the review:

  • The drift is made structurally impossible, not just fixed: sys_invitation.status now reads its options from the enum (the same shape the neighbouring role field uses), and the parity test compares the two sides to each other rather than to a hand-copied literal — a future re-literalization lands as a red test.
  • The two-upstreams note on the enum (better-auth contributes canceled, no notion of expiry; expired is ObjectStack's own via expiresAt) records why the divergence was possible, which is what prevents the next one.
  • Value-domain pinned on both sides: out-of-vocabulary still refused (cancelled en-GB spelling asserted on invalid_value, guarding against a slip into z.string()), default remains declared, listView filter values stay inside the vocabulary.
  • Grading correct: spec minor (accepted surface widens; consumers gain a value, none lose one — exhaustive-branch sweep came back empty), platform-objects patch (internal binding refactor).

Landing note (PM-handled, no dev action): this PR regenerates content/docs/references/identity/organization.mdx, an os-regen routed path, so it lands via the one-at-a-time relay behind #7758, #7759 and #7763. I'll run its sync lap and flip when its slot arrives.


Generated by Claude Code

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

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

2 participants