Repository navigation
Description
domain:devx#1 · os-warren · session_01JQwxbYXnfyuxgv3GQP9G1h
立卡门 ① — a product defect with a named landing point: packages/plugins/plugin-hono-server/src/adapter.ts, DEFAULT_CORS_ALLOW_HEADERS and DEFAULT_CORS_EXPOSE_HEADERS. Reach: a public surface measured once — a real Chromium page on another origin sending PUT /api/v1/meta/page/NAME?mode=draft to a stock showcase boot.
- Business reason: this helps a team whose browser front end lives on a different origin from its ObjectStack server (an embedded SPA, a custom front end over
@objectstack/client, the CORS allow-origin list the server already supports). The server's own doors speak conditional requests: the/metadraft save takesIf-MatchandIf-None-Match: *(the console'sDraftConflictDialogsendsIf-None-Match: *for a first draft), and the metadata reads answer anETagthat@objectstack/client's cached reads send back asIf-None-Match. The default CORS policy letsIf-Matchthrough (its comment says why: "without it every cross-origin save fails", objectui#2572), but notIf-None-Match, and it exposes noETag. So from another origin the first-draft save and every cached read fail with a bareFailed to fetch, and the client cannot read the version it would need to fall back toIf-Match. If this is not fixed, cross-origin authoring tools cannot create a draft safely and cached reads silently stop working, with an error that names nothing. - Reader who acts: the
domain:serverseat. One PR that addsIf-None-MatchtoDEFAULT_CORS_ALLOW_HEADERSandETagtoDEFAULT_CORS_EXPOSE_HEADERS(the adapter's header says the same three sites read these lists), plus a pin beside the existing CORS-list pin. - Dedupe (
repo:objectstack-ai/objectstack is:issue, open and closed):If-None-Match CORS→ 6 hits (finding(runtime): through the @objectstack/hono catch-all, PUT /meta/:type/:name ignores If-Match, If-None-Match and ?mode=draft — a stale token writes (200, not 409) and a draft save lands ACTIVE #22141, meta(/meta door): the draft read serves no version token, so no client can pin its first draft save with If-Match; the 409 METADATA_CONFLICT body names the current version only in prose #22114, metadata-protocol: the draft save recordsdraftChanges(per-key provenance: which surface, actor and time last changed each key path) and serves it on the strict draft read; the save request declares its surface — the writer of #24298's key #24395, hono adapter:createHonoApp's catch-all flattens a repeated query name to its last value, so every dispatcher arity rule is unreachable on a@objectstack/honohost #23214 closed; [PM seat] domain:cli — ⏳ vacant · archive of domain:server seat 1 (#6367) #6024, [PM seat] triage (objectstack-wide) — 🟢 Routine · session_01AavokzJ5DndAwitDXvKy4U · trig_01NcnCtMS2tH46nUg1a5TaMJ (hourly) · state = body + the round records newer than it #6015 seat posts), none about the CORS lists;If-None-Match preflight→ 13 hits, none about CORS (draft-save semantics metadata-protocol: a first draft pinnedIf-None-Match: *on a sealed package-shipped item gets the create remedy ("name is taken"):sealedWritereads anullparent as a create, the spec as "no row of this lifecycle" #23406, metadata-protocol: a draft save naming no package, of an item never published into a package whose only draft is bound to package Q, writes a package-unbound draft beside it, while the publish and discard naming none act on Q's (after PR #23654) #23657, meta(/meta door): the draft read serves no version token, so no client can pin its first draft save with If-Match; the 409 METADATA_CONFLICT body names the current version only in prose #22114);DEFAULT_CORS_ALLOW_HEADERS→ 7 hits (sharing: the X-Share-Password header declares no encoding, so a share-link password with a character above U+00FF cannot be sent from a browser — while ShareDialog and createLink mint such passwords #22049 closed addedX-Share-Password-Encoding; the rest unrelated);cors allow headers If-None-Match→ 6 hits, the same set as the first query;cross-origin console Failed to fetch→ 30 hits, the nearest console CSP: connect-src must include the storage endpoint the server presigns uploads to — since #25569 every browser upload to S3/R2 fails with 'Failed to fetch' (hosted production) #26200 (CSP connect-src for presigned uploads, closed), none about these headers; re-run just before filing, plusETag expose-headers→ 1 hit andDEFAULT_CORS_EXPOSE_HEADERS→ 2 hits (@objectstack/honoruns its tests against@objectstack/plugin-hono-server's dist — the missing vitest alias was hidden by the unbounded import regex until #12555 #12767 closed, a test-alias fix; and the run record QA run · release-18.3.0-w4-c2b (86/86) · 7fa9873f · 2026-11-02 · 7 PASS / 79 PARTIAL / 0 FAIL / 0 BLOCKED / 0 NOT-RUN #28965).
Scope note: the console's own split-host mode (VITE_SERVER_URL set to another origin) is ruled not to carry credentials on action requests (objectui#5702, app-shell CHANGELOG "split-host setups"), and the committed configurations reach the server same-origin through the /api proxy. This card is not about that mode. It is about the server's CORS defaults, which exist for cross-origin browser clients, disagreeing with the conditional headers its own doors read and send.
QA-source: #28965 · studio-authoring.record-page-roundtrip · observed beside the clauses
Found by the 18.3.0 release-candidate QA run #28965 (wave 4, W4-C2b, Tier 1 pins) at framework 7fa9873f. It first showed as ui/e2e/live/studio-editor.spec.ts ("Add block … the added block is stored") and studio-object-designer.spec.ts F4 timing out 3/3 waiting for the draft PUT when the console ran on localhost:5180 with VITE_SERVER_URL pointing at the API; both pass on the CI topology (same-origin through vite preview's proxy).
Reproduction
- At
7fa9873f, boot the stock showcase:OS_PORT=41501 pnpm -C examples/app-showcase exec objectstack dev --seed-admin -p 41501 -d file:RUN/data.db. - Preflight as a browser on another origin would:
curl -i -X OPTIONS 'http://localhost:41501/api/v1/meta/page/x?mode=draft' -H 'Origin: http://localhost:5180' -H 'Access-Control-Request-Method: PUT' -H 'Access-Control-Request-Headers: content-type,if-match,if-none-match,authorization'
→204,access-control-allow-headers: Content-Type,Authorization,X-Requested-With,X-Tenant-ID,X-Environment-Id,If-Match,X-Share-Password,X-Share-Password-Encoding(noIf-None-Match),access-control-expose-headers: set-auth-token,x-objectstack-dropped-fields(noETag). - In Chromium on a page served from
http://localhost:5180, sign in withfetch(API + '/api/v1/auth/sign-in/email', {method: 'POST', credentials: 'include', …}), take theset-auth-tokenheader, then send the same draftPUT /api/v1/meta/page/zz_cors_probe?mode=draft(bearer +content-type: application/json, body a minimal page) three times:- no conditional header → 200;
If-Match: "1"→ 409 (the door answered: the header passed the preflight);If-None-Match: *→TypeError: Failed to fetch— the browser refused the preflight and no request reached the server.
GET /api/v1/meta/object/showcase_taskwithOrigin: http://localhost:5180answersetag: "…", and the expose list above does not name it, so cross-origin JS readsresponse.headers.get('etag')asnull.
Expected vs actual
- Expected: a cross-origin browser client the CORS policy admits can send the conditional headers the server's doors accept (
If-MatchandIf-None-Match), and can read theETagthe reads answer, asIf-Matchalready can. - Actual:
If-None-Matchis refused at the preflight (Failed to fetch, nothing reaches the server), andETagis invisible to cross-origin JS.
Mechanism
DEFAULT_CORS_ALLOW_HEADERS (packages/plugins/plugin-hono-server/src/adapter.ts) lists If-Match but not If-None-Match; DEFAULT_CORS_EXPOSE_HEADERS lists set-auth-token and x-objectstack-dropped-fields but not ETag. Producers of the missing headers: the Studio first-draft saves in ui/packages/app-shell/src/views/studio-design/ (client.save(…, { mode: 'draft', ifNoneMatch: '*' }) in StudioDesignSurface.tsx, ObjectHooksPanel.tsx, ObjectListViewInspector.tsx, and the recordRead(…, { ifNoneMatch: '*' }) pins in StudioBundleItemEditor.tsx), ui/packages/app-shell/src/views/metadata-admin/DraftConflictDialog.tsx, packages/client/src/index.ts (getCached sends If-None-Match), ui/packages/react/src/hooks/useETagCache.ts.
Acceptance notes
- A cross-origin
PUT …?mode=draftwithIf-None-Match: *reaches the door (a2xx, or a coded409/412when a draft exists), and a cross-origin read can seeETag. - A pin on the two default lists, beside the existing CORS tests, so the next conditional header cannot drift the same way.
Generated by Claude Code
Activity
objectstack-fleet commentedon Nov 2, 2026
triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U
Path: none
Triage: p2 ·
domain:server·area:api·pm:queue· 2026-11-02T05:15Z⛔ Not a claim.
packages/plugins/plugin-hono-server/src/adapter.ts(DEFAULT_CORS_ALLOW_HEADERS/DEFAULT_CORS_EXPOSE_HEADERS) leave outIf-None-MatchandETag. So a cross-origin browser client cannot make the conditional requests the metadata doors require, and a first draft pinned withIf-None-Match: *dies at preflight. A named location with a measured browser reach, and it widens no access: the allowed origins are unchanged.domain:server, p2.Generated by Claude Code
objectstack-fleet commentedon Nov 2, 2026
domain:server#1 · os-project-manager · session_01R1q2xdtYgU6FKrZ7LgjPCC
Claim: PM loop round 1 · card thread · 2026-11-02T06:27Z
Session:
session_01R1q2xdtYgU6FKrZ7LgjPCC(the seat); the card thread issession_01FEkQ5nsjQhYdbu8MX5XyPn(a new card thread, fresh frommainab085a04)Account:
os-project-manager(the seat's linked user; the card's assignee)Branch:
claude/project-thread-ael400(the card thread's harness-pinned branch; it names no issue, so this claim is the identity record)Worktree: the card thread's own container checkout
Domain:
domain:serverSeat:
domain:server#1Ruling-ref: triage#1 graded the card p2 (05:15Z): it widens no access; the allowed origins are unchanged. Route (the dispatching seat's call): add
If-None-Matchto the default allow list andETagto the default expose list, nothing else. Dispatched in the seat-1 split of the server queue under 「队列进了很多任务,抓紧派发」 (Leehom, 2026-11-02T05:15Z) and 「定级你可以自己决定」 (Leehom, 2026-11-01T12:11Z).File surface:
DEFAULT_CORS_ALLOW_HEADERS/DEFAULT_CORS_EXPOSE_HEADERSinpackages/plugins/plugin-hono-server/src/adapter.tsand the sites that read them; their tests and changesets. ⛔ Nopackages/spec/**, no other header, no change to allowed origins, no new gate.Container & model:
S,mode:thread, model and effort as the thread was openedClause-②: yes (widening, expected: two exported default lists gain a value; the dev measures and declares per AGENTS.md)
Metadata-protocol: none (expected)
Responsibility: the code is the default CORS policy of
@objectstack/plugin-hono-server· the platform path is a cross-origin browser client whoseIf-None-Matchdies at the preflight and which cannot readETag· reached by every cross-origin front end over an allowed originThread-read: none
Premise: holds on
main(the card's Chromium measurement)Serial constraints cleared: No open server card edits the CORS lists.
Resume-from: none — fresh from
origin/mainState:
pm:queue→pm:dispatched, assigned toos-project-manager, recorded by this claim.Generated by Claude Code
objectstack-fleet commentedon Nov 2, 2026
domain:server#1 · os-project-manager · session_01FEkQ5nsjQhYdbu8MX5XyPn
os-dev-report
{
"issue": 28970,
"status": "done",
"branch": "claude/project-thread-ael400",
"pr": "#29092",
"session": "session_01FEkQ5nsjQhYdbu8MX5XyPn",
"premise_still_valid": true,
"summary": "DEFAULT_CORS_ALLOW_HEADERS gains If-None-Match and DEFAULT_CORS_EXPOSE_HEADERS gains ETag (packages/plugins/plugin-hono-server/src/adapter.ts), each with a docblock line naming the door that needs it. Both readers (hono-plugin.ts CORS block, @objectstack/hono createHonoApp) take the change; no third spelling exists. http-protocol.mdx updated to the new defaults; patch changeset with Clause-②: yes (widening) and Metadata-protocol: none. Head ae2b0b7, draft PR #29092, no labels (the order named none).",
"tests": "New plugin-hono-server/src/cors-conditional-headers.test.ts (5 cases, real init + real hono/cors via app.request) and 2 new pins in adapters/hono/src/hono.test.ts. Base ab085a0: the 4 new non-control cases red (expected [ 'content-type', …(7) ] to include 'if-none-match'; expected [ 'set-auth-token', …(1) ] to include 'etag'), 3 controls green. Head: all green. Ablation via ablation-replace.mjs (both anchors hit, on-disk count 0): plugin file 2 failed / 3 passed, adapter group 2 failed / 4 passed; restore blob == HEAD (69a105b3), git diff HEAD empty, 5/5 green again; both suites read adapter.ts as source (relative import, vitest alias), no dist on the path. Full suites: plugin-hono-server 38/38 files, 472/472 tests; hono 14/14 files, 233/233 tests. Typecheck both packages exit 0 (incl. check:test-typecheck). Tree f103bad4 == tree of head ae2b0b7.",
"gates": "dispatch-gates --commands derived 92 families on the real diff; 91 exit 0; pnpm check:dual-build-cjs-loads exit 3 = NOT MEASURED, reason: PREREQUISITE NOT MET, 33 packages have no dist/ and a whole-repo build was out of budget (its self-test passed, 93 cases); CI reads it. check-plugin-teardown-shape --self-test and spec check:skill-examples first exit 3 (shallow object, unbuilt client-react), exit 0 after the prerequisite was met. --ran over the exit-coded record: 92 accounted, 91 run, 1 NOT-MEASURED (derived from a recorded exit 3). eslint --no-inline-config on the 3 touched source files exit 0. check-nul-bytes OK. PR check-runs: in_progress at report time (not waited on, per os-dev.md).",
"dogfood_battery": "n/a — no ! in the PR title and no (narrowing) arm",
"mcp_calls": "2 — mcp__github__issue_read (get, get_comments); no write tools",
"api_writes": "2 — relay pr_create (fleet-write repository_dispatch, run 41404989603, PR #29092) and this os-dev-report comment (post-stamped); git push is not a REST write",
"open_questions": [],
"out_of_scope_findings": [
"carrier: none · noted, not filed — the adapter.ts DEFAULT_CORS_ALLOW_HEADERS docblock and a hono.test.ts comment still say three Hono-based CORS sites; only two read the constants today (stale wording, zero pull)"
]
}
Generated by Claude Code
objectstack-fleet commentedon Nov 2, 2026
domain:server#1 · os-project-manager · session_01R1q2xdtYgU6FKrZ7LgjPCC
Release: server seat 1 shift close · 2026-11-02T07:41Z
Session:
session_01R1q2xdtYgU6FKrZ7LgjPCC(the seat that claimed this card); card threadsession_01FEkQ5nsjQhYdbu8MX5XyPn(paused)Who / what / where: Leehom (
os-project-manager) in the server-lane project chat — 「进展中的开发先提交代码然后暂停,先确保已有的pr都合并」 (2026-11-02T07:11Z), 「自动合并中的也交给下一任跟进」 (2026-11-02T07:23Z), 「我说的是pm-dispatch 中车道下班的流程,都要走完」 (2026-11-02T07:33Z). Shift close returns every claim to the queue (seat-post-protocol.md 交接收尾清单 step 2); no stay-behind.Resume-from:
claude/project-thread-ael400@ae2b0b732871a1bcdc0d6d81e65209535cc46184— PR #29092, ACCEPTed by the seat on this head. Contract review 6571473062, ACCEPT 6571479471; auto-merge armed.Successor: read the handoff 6571543819 on seat post #6367 first; land PR #29092 on green, write the landing record, close this card. ⛔ Do not re-dispatch the card from scratch.
State:
pm:dispatched→pm:queue, assigneeos-project-managercleared, recorded by this release.Generated by Claude Code
objectstack-fleet commentedon Nov 2, 2026
domain:server#1 · os-steve · session_01ADHkmQjPiXgNoSaEpqeGvY
Claim: PM loop round 1 · takeover of a released card · 2026-11-02T08:17Z
Session:
session_01ADHkmQjPiXgNoSaEpqeGvYAccount:
os-steve(the card's assignee, set in the same round)Branch:
claude/project-thread-ael400Worktree: the predecessor card thread's (paused); no new dev is dispatched
Domain:
domain:serverSeat:
domain:server#1File surface: unchanged from the predecessor's claim 6570653339 (
packages/plugins/plugin-hono-server/src/adapter.tsCORS defaults); the open PR #29092 is the surface.Container & model:
S,mode:thread, landing only (no new build)Clause-②: yes
Metadata-protocol: none
Responsibility: as in the predecessor's claim (unchanged)
Thread-read: 6571781285
Premise: holds (PR #29092 ACCEPTed by the predecessor seat)
Serial constraints cleared: none new; the seven PRs this seat inherited are its only in-flight surface.
Resume-from:
claude/project-thread-ael400@ae2b0b732871a1bcdc0d6d81e65209535cc46184Takeover record: the predecessor (
os-project-manager,session_01R1q2xdtYgU6FKrZ7LgjPCC) released this card in 6571781285 at its shift close (Leehom: 「自动合并中的也交给下一任跟进」). This seat takes it on the maintainer's summons in the Claude Code project "objectstack spec" (「server 1 号座位你也接手开席位」), seat 2 (#20966) having left it to seat 1. Last pushed head:ae2b0b7328(PR #29092). In the merge queue since 2026-11-02T07:37Z; follow to MERGED.objectstack-fleet commentedon Nov 2, 2026
domain:server#1 · os-steve · session_01ADHkmQjPiXgNoSaEpqeGvY
Landed · 2026-11-02T09:18Z
93153f014a, through the merge queueFixes #28970pm:dispatchedis removed. Seat records: review PASS 6571473062, ACCEPT 6571479471 (predecessor); this seat took the card over to land it.