Skip to content

fix(hono,plugin-hono-server,runtime): one CORS source and one registry key — the last derivable copies from the #3786 sweep - #4282

Merged
os-zhuang merged 1 commit into
mainfrom
claude/spec-checklist-drift-investigation-ewgd6l
Jul 31, 2026
Merged

fix(hono,plugin-hono-server,runtime): one CORS source and one registry key — the last derivable copies from the #3786 sweep#4282
os-zhuang merged 1 commit into
mainfrom
claude/spec-checklist-drift-investigation-ewgd6l

Conversation

@os-zhuang

@os-zhuang os-zhuang commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

First, two corrections

The sweep report was based on an incomplete scan. I said the #3786 sweep was "basically clean". The scan globbed packages/*/src, which is one level deep, so it missed everything under packages/plugins/ and packages/adapters/. Re-running across all 72 packages found the instances below.

And the CI wiring I proposed is already on main, in a better form. #4203 landed check:generated --reconcile-only in lint.yml's unfiltered job. That is strictly better than what this branch first pushed: it runs only the reconciliation instead of re-running all eight artifact gates that already have their own steps, and it sits in an unfiltered required job so it cannot go dormant behind a paths filter (#4203's comment notes ci.yml's generated filter doesn't watch package.json — the one file every offending PR must touch). That change has been dropped from this PR.

The drift: a CORS default, on the one description callers actually read

HonoCorsOptions.allowHeaders' TSDoc promised:

Defaults to ['Content-Type', 'Authorization', 'X-Requested-With'], which is sufficient for cookie and bearer-token auth.

The real default carries three more: X-Tenant-ID and X-Environment-Id (multi-tenant routing) and If-Match (the OCC token on record PATCHes, objectui#2572). Anyone sizing a custom allowHeaders against that sentence drops all three, and every cross-origin save fails in the browser with "Failed to fetch".

The instructive part: three Hono CORS sites each carried their own copy of the defaults under "keep in sync" comments, and the copies all agreed. What drifted was the doc — the only description with no counterpart to be diffed against, and the only one a caller reads.

Both defaults are now single constants, DEFAULT_CORS_ALLOW_HEADERS / DEFAULT_CORS_EXPOSE_HEADERS, exported from @objectstack/plugin-hono-server and imported by the adapter (which already depends on it — no new edge, no cycle). The TSDoc links them rather than restating them, and documents an asymmetry it never mentioned: allowHeaders replaces the default, exposeHeaders merges with it.

hono-plugin.test.ts stopped stubbing ./adapter wholesale and keeps the real constants via importOriginal. It asserts exact header lists, so a mocked copy would have made the test agree with itself instead of with what ships.

A hand-copied service-registry key

runtime's share-links domain resolved 'shareLinks' as a string literal — copied from SHARE_LINK_SERVICE, whose own doc-comment says "keep in sync with the SharingPlugin registration". It now imports the constant. A drifted copy resolves nothing, so every share link answers 501 Sharing is not configured for this environment on an environment where it is configured perfectly well.

A duplicate ledger entry — the same defect one level up

check-generated.ts carried two NO_GENERATOR entries for check:strictness-ledger, because #4203 and #4252 each added one without seeing the other. Functionally harmless (the ledger is read into a Set), but two comments told overlapping versions of one story. #4203's is kept — the fuller account, from the PR that fixed the underlying problem.

Checked and left alone

ApprovalStatus (5 values) and ApprovalActionKind (12 values) versus their plugin-approvals selects: diffed verbatim, no drift. Both remain hand-copied across a package boundary under "keep in sync" comments — real derivation candidates, but correct today and a larger change than this PR's scope.

Verification

mutation result
remove If-Match from DEFAULT_CORS_ALLOW_HEADERS should allow If-Match by default fails, by name
  • turbo test --force across the touched packages — 28/28 tasks green (plugin-hono-server 155 tests, hono 73, runtime 67 files)
  • check:generated --reconcile-only — PASS (16 check: + 10 gen: scripts, all classified)

Note for reviewers: two build failures hit while verifying (service-messaging@objectstack/platform-objects/audit, service-i18n@objectstack/types) were both stale worktree installs after main advanced, fixed by pnpm install. Neither is a code issue.

@vercel

vercel Bot commented Jul 31, 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 Jul 31, 2026 3:10am

Request Review

@os-zhuang
os-zhuang force-pushed the claude/spec-checklist-drift-investigation-ewgd6l branch from 0409afb to f456ee6 Compare July 31, 2026 03:07
@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling size/m labels Jul 31, 2026
@os-zhuang os-zhuang changed the title fix(hono,plugin-hono-server,runtime): close out the #3786 sweep — one CORS source, one registry key, and the meta-gate finally runs fix(hono,plugin-hono-server,runtime): one CORS source and one registry key — the last derivable copies from the #3786 sweep Jul 31, 2026
@github-actions

github-actions Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 3 package(s): @objectstack/hono, @objectstack/plugin-hono-server, @objectstack/runtime.

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

  • content/docs/api/client-sdk.mdx (via packages/runtime)
  • content/docs/api/index.mdx (via @objectstack/runtime)
  • content/docs/api/wire-format.mdx (via @objectstack/runtime)
  • content/docs/automation/hook-bodies.mdx (via @objectstack/runtime)
  • content/docs/concepts/north-star.mdx (via packages/runtime)
  • content/docs/data-modeling/drivers.mdx (via @objectstack/runtime)
  • content/docs/deployment/index.mdx (via @objectstack/runtime)
  • content/docs/deployment/production-readiness.mdx (via @objectstack/runtime)
  • content/docs/deployment/single-project-mode.mdx (via @objectstack/runtime)
  • content/docs/deployment/vercel.mdx (via @objectstack/hono, @objectstack/runtime)
  • content/docs/getting-started/your-first-project.mdx (via @objectstack/plugin-hono-server, @objectstack/runtime)
  • content/docs/permissions/authentication.mdx (via @objectstack/plugin-hono-server, @objectstack/runtime)
  • content/docs/permissions/authorization.mdx (via packages/runtime)
  • content/docs/plugins/index.mdx (via @objectstack/plugin-hono-server)
  • content/docs/plugins/packages.mdx (via @objectstack/hono, @objectstack/plugin-hono-server, @objectstack/runtime)
  • content/docs/protocol/kernel/http-protocol.mdx (via @objectstack/plugin-hono-server, @objectstack/runtime)
  • content/docs/protocol/kernel/index.mdx (via @objectstack/runtime)
  • content/docs/protocol/kernel/lifecycle.mdx (via @objectstack/runtime)
  • content/docs/releases/implementation-status.mdx (via @objectstack/plugin-hono-server, @objectstack/runtime)
  • content/docs/releases/v16.mdx (via @objectstack/plugin-hono-server)

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.

…y key — the last derivable copies from the #3786 sweep

Re-ran the sweep across all 72 packages. The earlier pass globbed
`packages/*/src`, one level deep, so it missed everything under
`packages/plugins/` and `packages/adapters/` — "the sweep is basically clean"
was based on an incomplete scan.

A STALE CORS DEFAULT, on the one description callers actually read.
`HonoCorsOptions.allowHeaders`' TSDoc promised `['Content-Type', 'Authorization',
'X-Requested-With']` "sufficient for cookie and bearer-token auth". The real
default carries three more: `X-Tenant-ID`, `X-Environment-Id` (multi-tenant
routing) and `If-Match` (OCC token on record PATCHes, objectui#2572). Sizing a
custom allowHeaders against that sentence drops all three and breaks every
cross-origin save.

Three Hono CORS sites each kept their own copy under "keep in sync" comments.
The copies agreed; the DOC — the only description with no counterpart to be
diffed against, and the only one a caller reads — is what drifted. Both defaults
are now single constants exported from plugin-hono-server and imported by the
adapter (which already depends on it). The TSDoc links them instead of
restating, and documents the asymmetry it never mentioned: allowHeaders
REPLACES the default, exposeHeaders MERGES.

hono-plugin.test.ts stopped stubbing ./adapter wholesale and keeps the real
constants via importOriginal — it asserts exact header lists, so a mocked copy
would make the test agree with itself rather than with what ships. Verified:
removing `If-Match` fails `should allow If-Match by default`, by name.

A HAND-COPIED REGISTRY KEY. runtime's share-links domain resolved 'shareLinks'
as a literal, copied from SHARE_LINK_SERVICE — whose own comment says "keep in
sync with the SharingPlugin registration". Now imports the constant. A drifted
copy resolves nothing, so every share link 501s "Sharing is not configured for
this environment" on an environment where it is.

PLUS A DUPLICATE LEDGER ENTRY, the same defect one level up: check-generated.ts
carried two NO_GENERATOR entries for check:strictness-ledger, because #4203 and
#4252 each added one without seeing the other. Harmless (the ledger is read into
a Set) but two comments told overlapping versions of one story. #4203's is kept —
the fuller account, from the PR that fixed the underlying problem.

NOT included, because main already has it: wiring the reconciliation into CI.
#4203 landed it as `check:generated --reconcile-only` in lint.yml's unfiltered
job, which is strictly better than what this branch first proposed — it runs only
the reconciliation rather than re-running all eight artifact gates that already
have their own steps, and it sits in an unfiltered required job so it cannot go
dormant behind a paths filter.

Checked and left alone: ApprovalStatus (5 values) and ApprovalActionKind (12
values) vs their plugin-approvals selects — diffed verbatim, no drift today,
still hand-copied across a package boundary.

Verified: 28/28 turbo test tasks green (plugin-hono-server 149, hono 73, runtime
67 files), affected builds clean, check:generated --reconcile-only PASS.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UXGj3Z5TmwSV6RK2oGc3cb
@os-zhuang
os-zhuang force-pushed the claude/spec-checklist-drift-investigation-ewgd6l branch from f456ee6 to 5889189 Compare July 31, 2026 03:10
@os-zhuang
os-zhuang marked this pull request as ready for review July 31, 2026 03:30
@os-zhuang
os-zhuang merged commit 2053714 into main Jul 31, 2026
18 checks passed
@os-zhuang
os-zhuang deleted the claude/spec-checklist-drift-investigation-ewgd6l branch July 31, 2026 03:31
os-zhuang pushed a commit that referenced this pull request Jul 31, 2026
The docs-drift check turned up a FOURTH copy of the status set, in the highest-
visibility place: `automation/approvals.mdx`'s "Statuses in full" line. Unlike
the CORS case in #4282 this one is correct today — all five values, same order —
so there is nothing to fix, only something unguarded.

Points at APPROVAL_STATUSES and says plainly that this line is a copy nothing
updates automatically. That is weaker than derivation and I am not pretending
otherwise: prose docs legitimately restate values, and a path-coupled test
asserting this exact sentence would be brittle. Recorded as a follow-up instead —
`check:variant-docs` already solves this shape for schema variants and is the
right place to extend if the set starts moving.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UXGj3Z5TmwSV6RK2oGc3cb
os-zhuang added a commit that referenced this pull request Jul 31, 2026
…, not hand-matched (#3786) (#4295)

* fix(spec,plugin-approvals): the two approval vocabularies are derived, not hand-matched (#3786)

`sys_approval_request.status` and `sys_approval_action.action` spelled their
option lists out — five values and twelve — each under a "Keep in sync with
`ApprovalStatus` / `ApprovalActionKind` (spec/contracts)" comment, while the
contract held the same sets as bare type unions. Seventeen strings matched by
hand across a package boundary with nothing checking them.

They did all still agree — verified verbatim before changing anything. But
agreeing is not being held, and both directions of drift are quiet: a value the
COLUMN accepts and the contract omits is invisible to every consumer typed
against the contract (the row exists and nothing can narrow it), while a value
the CONTRACT declares and the column rejects surfaces only at write time, on
whichever tenant first reaches that transition. An audit vocabulary is a bad
place for either.

The contract now publishes the lists as values — APPROVAL_STATUSES and
APPROVAL_ACTION_KINDS — with ApprovalStatus / ApprovalActionKind derived via
`(typeof X)[number]`, and both columns spread the constants. The per-entry
rationale (which action kinds move the flow, which are thread-only, why
`returned` differs from `recalled`) moved onto the constants, where the values
live.

New exports, no behaviour change: the emitted option lists are byte-identical,
checked against the built artifact before and after. Existing imports of the two
types are unaffected — they resolve to the same unions.

approval-vocabularies.test.ts pins the qualifier derivation alone cannot: the
columns agree with the contract WHILE THE SPREAD IS THERE, so the test fails if
either is re-inlined as a literal that drifted. It guards the guard (an
unresolvable import would compare two empty lists and pass) and asserts the two
vocabularies stay distinct — a copy-paste pointing one column at the other
constant would satisfy "derived from the contract" while being the wrong
vocabulary entirely.

Mutation-tested in both directions: adding a value to APPROVAL_STATUSES
propagates into the BUILT sys_approval_request.status options (the derivation is
live, not a stale build), and re-inlining a drifted literal fails
`sys_approval_request.status offers exactly the contract statuses, in order`.

Verified: plugin-approvals 330 tests green, spec 7169 green, api-surface
regenerated (0 breaking, 2 added), check:generated / exported-any / liveness /
docs all PASS.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UXGj3Z5TmwSV6RK2oGc3cb

* docs(automation): name the source of the approval status list (#3786)

The docs-drift check turned up a FOURTH copy of the status set, in the highest-
visibility place: `automation/approvals.mdx`'s "Statuses in full" line. Unlike
the CORS case in #4282 this one is correct today — all five values, same order —
so there is nothing to fix, only something unguarded.

Points at APPROVAL_STATUSES and says plainly that this line is a copy nothing
updates automatically. That is weaker than derivation and I am not pretending
otherwise: prose docs legitimately restate values, and a path-coupled test
asserting this exact sentence would be brittle. Recorded as a follow-up instead —
`check:variant-docs` already solves this shape for schema variants and is the
right place to extend if the set starts moving.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UXGj3Z5TmwSV6RK2oGc3cb

---------

Co-authored-by: Claude <support@objectstack.ai>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
os-zhuang added a commit that referenced this pull request Jul 31, 2026
…ugin is a transport adapter (#4073) (#4296)

Completes the retirement. HonoServerPlugin now owns the socket, the middleware
and the three current-user endpoints, and nothing else. The data and discovery
APIs have one owner each — @objectstack/rest and the runtime dispatcher
(ADR-0076 D11). Net −678 lines.

Deleted: raw C+R `/api/v1/data/:object` (+ `/:id`), this plugin's third
`/api/v1/discovery` + `/.well-known/objectstack` payload, and the
`registerStandardEndpoints` flag itself — gone, not defaulted off, with a test
pinning that passing the removed option resurrects nothing.

Unaffected: `/auth/me/*` + `/me/apps` (sole supply, unconditional since #4144),
and every composed host — `os serve`, `objectstack dev`, cloud's objectos —
which mount an owner that already answered these routes byte-identically either
way (#4260). A host with NO owner keeps the boot warn, reworded to name both
remedies now that opting in is not one of them.

Tests decided on what each was actually pinning: `hono-discovery` and
`hono-anonymous-deny` deleted (wholly about the retired routes); `server-timing-e2e`
KEPT and repointed at `/auth/me/permissions` (#3361's value is "perf middleware +
a handler that resolves the principal", not the route); `client.hono` KEPT and
repointed at a REST-composed host (it is the only coverage of `client.connect()`
and the discovery handshake); the default-pin suite rewritten as
`hono-transport-only`.

Three gates caught bookkeeping the deletion owed, each fixed on its own terms:

- the #2567 anonymous-deny ledger flagged STALE covers. The row went with its
  surface, and its id came out of HIGH_RISK — but the ratchet's own negative
  test used that row as its FIXTURE, so it was repointed at a live row rather
  than left to pass vacuously and disarm the check that just caught this.
- the docs drift check surfaced `authorization.mdx`, which documented the
  deleted surface twice as part of a SECURITY invariant. Both now read as
  history, matching how the same sentence already handles the retired GraphQL
  endpoint: the surface LEFT the matrix; the invariant is unchanged and simply
  has one fewer door to hold shut.
- a merge conflict with #4282's CORS work: both intents kept.

AGENTS.md gains the four rules this thread paid for — one route one owner,
explicit composition over default magic, absence must be loud, machine-readable
surfaces must not lie — plus the verification note that "who serves this path" is
a question about the composed, PROVISIONED runtime. #4073 was answered wrongly
three times, once per shortcut around that.

Verified: plugin-hono-server 135/135, dogfood 430 passed, client 12/12, runtime
19/19, http-conformance 46/46, full build 71/71, CI 16/16. (Temporal Conformance
timed out once on a live-service step touching no package in this diff; it passed
on re-run.)
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

Development

Successfully merging this pull request may close these issues.

1 participant