Repository navigation
feat(spec): a strict response contract for GET /auth/me/permissions, and hasAnyQueue on IOwnershipQueueService - #28704
Conversation
…and hasAnyQueue on IOwnershipQueueService Co-Authored-By: Claude <noreply@anthropic.com>
…its declared contract Co-Authored-By: Claude <noreply@anthropic.com>
…se contract; changeset Co-Authored-By: Claude <noreply@anthropic.com>
… in no route ledger the client conformance test reads The client's URL conformance test (client-url-conformance.test.ts) requires every SDK URL to match an exact ledger route; /auth/me/permissions is mounted by plugin-hono-server and appears in none, so the method would be credited only by the * /auth/** wildcard. The typed read is left to the consumer card. Co-Authored-By: Claude <noreply@anthropic.com>
…ions response schemas; changeset drops the client Co-Authored-By: Claude <noreply@anthropic.com>
…ons response contract on main 3944d2e Co-Authored-By: Claude <noreply@anthropic.com>
…the variant-doc ledger (not-authorable) Co-Authored-By: Claude <noreply@anthropic.com>
…missions bodies as bare literals the route-envelope census counts The inline `satisfies` hid both from check:route-envelope (found 7, pinned 9). The strict schema parse in the route and door tests still binds them. Co-Authored-By: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 3 package(s): 2 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
What this run could not see
Coarse fallback — 170 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 4f8bfa838c2c52683fdd9e0aa3843743adb6775f && git checkout 4f8bfa838c2c52683fdd9e0aa3843743adb6775f
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin db357f6da9f787ff6285c20c70bc02a1bc138d35 e51961f0ab8b123546f06a079c94829219dbb216 && git checkout -B drift-repro db357f6da9f787ff6285c20c70bc02a1bc138d35 && git merge --no-ff e51961f0ab8b123546f06a079c94829219dbb216
node scripts/docs-audit/affected-docs.mjs --json db357f6da9f787ff6285c20c70bc02a1bc138d35
|
|
domain:server#3 · os-project-manager · session_01FEkQ5kE5BrPUPNMMyv6oCH Contract reviewServed-tier: Check-runs on the head at review time (42): ① Derived judgmentsEvery accept-set and public-surface change the diff implies, each named:
② Semver levelMust-fix (gate-red). Must-fix (declaration). The required
③ Boundary flags
④ Design judgment
Summary of findings. Must-fix: (1) changeset/declaration level — Implemented-by: VERDICT: FAIL |
…nge is a widening that adds to the protocol (spec minor) Contract review on 567e3dc: a required member is an undeclared break for an out-of-tree implementer, and the route already guards a missing method. The changeset declares Clause-② yes (widening) and Metadata-protocol added, with @objectstack/spec at minor, since the authorable-surface baseline moves. The objectstack-api skill index keeps its pre-#28508 transitive pointers through a TRANSITIVE_ALLOWLIST row instead of gaining 22 through the new permission.zod edge. The schema's doc names the older GetEffectivePermissionsResponseSchema it is not. Co-Authored-By: Claude <noreply@anthropic.com>
…contract, not hand-written
@object-ui/permissions re-exports MePermissionsResponse (and MePermissionsAuthenticatedResponse) from @objectstack/spec/api. The hand-written interface had drifted from the wire: it read a roles key the server never sends, so usePermissions().roles under MePermissionsProvider was always empty; it now carries the server's positions. Only the authenticated body's grants are read. Test fixtures typed as the response move roles to positions and add hasAnyQueue; the anonymous fixture is { authenticated: false }.
Co-Authored-By: Claude <noreply@anthropic.com>
|
domain:server#3 · os-project-manager · session_01FEkQ5kE5BrPUPNMMyv6oCH Contract reviewServed-tier: Delta record. Round 2 is two commits over the base head ( Check-runs on ① Derived judgmentsDelta over the base record's twelve items:
② Semver levelBoth base-record must-fixes are resolved in the tree and in the declarations:
③ Boundary flags
④ Design judgment
ui widening ( Summary of findings on this head. Must-fix: none — both base-record must-fixes are resolved in the tree and confirmed by the newer Implemented-by: VERDICT: PASS |
…n left unused object-ui/no-unused-imports (Lint UI) flagged 53 test files across 7 packages after their builders moved to MePermissionsAuthenticatedResponse. Co-Authored-By: Claude <noreply@anthropic.com>
|
domain:server#3 · os-project-manager · session_01FEkQ5kE5BrPUPNMMyv6oCH Contract reviewServed-tier: Head re-confirm. This record extends the delta record on What the diff holds, read in the tree. 53 files changed, +53/−53, every one a What it answers. Check-runs on ① Derived judgmentsNo accept-set or public-surface change on this head beyond the delta record's: the diff touches test files only. ② Semver levelUnchanged: ③ Boundary flagsDev flag "test-only removals of an unused import from 53 ui test files, no other change" — verified exactly (above). No open question, no escalation. ④ Design judgmentNothing in the Implemented-by: VERDICT: PASS |
|
domain:server#3 · os-project-manager · session_01FEkQ5kE5BrPUPNMMyv6oCH 席位核对 · ACCEPT · PR #28704 · 2026-11-01T17:03ZHead-sha: 维护者速读(席位终稿)
核对六项
|
domain:server#3 · os-project-manager · session_01FEkQ5kE5BrPUPNMMyv6oCH
Requested by Leehom · project thread
Fixes #28508
Clause-②: yes (widening)
Metadata-protocol: added
维护者速读
GET /auth/me/permissions第一次有了 spec 里的严格响应契约MePermissionsResponseSchema(@objectstack/spec/api),覆盖匿名、降级(无 SecurityPlugin)、完整、故障关闭四种响应体,声明了hasAnyQueue: boolean,未声明的同级键直接拒绝。IOwnershipQueueService契约加上可选方法hasAnyQueue?(organizationId),路由改为通过契约读取,删掉本地的AnyQueueReader可选类型。控制台@object-ui/permissions的MePermissionsResponse改为从 spec 派生,不再手写。sys_queue), with no widening of member read onsys_queue#28508 的 spec 一半(服务端一半已由 feat(plugin-hono-server,plugin-sharing): /auth/me/permissions answers hasAnyQueue from the ownership-queue service #28654 合入)。控制台每个会话读一次这个响应来驱动整个权限层,之前只有 ui 里手写的接口,服务端多发或少发一个键都没人拦。手写接口已经漂移:它读一个服务端从不发送的roles键(服务端的键是positions),所以MePermissionsProvider下的usePermissions().roles一直是空数组。hasAnyQueue?是可选方法(与declaredQueueNamed?一样),仓库外的旧实现照样满足契约,路由对缺这个方法的实现答false。控制台行为只有一处变化:usePermissions().roles从恒为空变为服务端的positions(仓库内没有生产代码读它)。@object-ui/permissions导出的MePermissionsResponse变为 spec 的联合类型,按它写的测试夹具随之迁移(roles→positions,加hasAnyQueue)。回滚即 revert 本 PR。packages/spec/src/api/auth.zod.ts(三个 schema)、packages/spec/src/contracts/ownership-queue-service.ts(新方法)、packages/spec/src/api/me-permissions-response.test.ts(契约测试 12 例)、ui/packages/permissions/src/MePermissionsProvider.tsx(派生类型与roles)。pnpm --filter @objectstack/spec exec vitest run src/api/me-permissions-response.test.ts,预期 12 passed;pnpm --filter @objectstack/plugin-hono-server exec vitest run src/current-user-endpoints-has-any-queue.test.ts,预期 9 passed(每个真实响应体都过严格 schema)。CONTRACT_REVIEW_TIER给出## Contract reviewPASS(567e3dc8 上的首轮为 FAIL,本版逐条修复,待增量复审)。What changes
MePermissionsResponseSchema(packages/spec/src/api/auth.zod.ts), az.discriminatedUnion('authenticated', …)of two strict branches:MePermissionsAnonymousResponseSchema:{ authenticated: false }and nothing else, so nothing about a user can ride on the anonymous body.MePermissionsAuthenticatedResponseSchema:userId,tenantId(nullable),positions,permissionSets,objects(record ofEffectiveObjectPermissionSchema),fields(record of a strict{ readable, editable }),systemPermissionsandtabPermissions(both optional: absent only on the degraded body, where absent means unknown, and present-and-empty means none), andhasAnyQueue: boolean(required on every authenticated body).MePermissionsResponse,MePermissionsAnonymousResponse,MePermissionsAuthenticatedResponse(z.input). The three schemas are isomorphic, so they take ADR-0122Iso_pins intype-alias-convention.pin.test.ts(count 808 → 811) rather thanParsedaliases.IOwnershipQueueService.hasAnyQueue?(organizationId: string | null): Promise<boolean>(packages/spec/src/contracts/ownership-queue-service.ts):trueexactly when some name would resolve throughresolveQueue. Optional, likedeclaredQueueNamed?, so an implementation written before it still satisfies the contract; the route answersfalsewithout it. Its docblock names/auth/me/permissionsas the one carrier and says it discloses no name and no count. plugin-sharing'sOwnershipQueueServicealready implements it (feat(plugin-hono-server,plugin-sharing): /auth/me/permissions answers hasAnyQueue from the ownership-queue service #28654); only its docblock changes.@objectstack/plugin-hono-server(current-user-endpoints.ts): the route reads the method asPick<IOwnershipQueueService, 'hasAnyQueue'>through the kernel locator, keeping the runtimetypeofguard so an older implementation still answersfalse.mePermissionsBody(the full and closed bodies) returnsMePermissionsAuthenticatedResponse, so tsc refuses a key the schema does not declare. The degraded literal and the anonymous{ authenticated: false }stay barec.json({...})literals: an inlinesatisfieshid both from thecheck:route-envelopecensus (found 7, pinned 9), so they are bound by the strict parse in the route and door tests instead. The wire is byte-for-byte unchanged.@object-ui/permissions(ui/packages/permissions/src/MePermissionsProvider.tsx):MePermissionsResponseis re-exported from@objectstack/spec/api(withMePermissionsAuthenticatedResponsebeside it) in place of the hand-written interfacecheck:spec-symbolsflagged. The provider reads grants only from the authenticated branch. The interface'sroleskey was drift (the server sendspositions), so the context'srolesnow carriespositions.ui/apps/console/src/components/studioEntry.tsnarrows onauthenticatedbefore readingsystemPermissions. Test fixtures across 13 UI packages typed as the response moveroles: [...]→positions: [...]and addhasAnyQueue: false; builders that answerauthenticated: trueare typed as the authenticated branch; three{ readable: false }field entries gaineditable: false(the strict entry declares both); the anonymous fixtures are{ authenticated: false }.objectstack-apirow inTRANSITIVE_ALLOWLIST(packages/spec/scripts/lib/skill-map-guards.ts) lists the 13 transitive pointers the index already published, so the newauth.zod.ts→security/permission.zod.tsedge no longer drags 22 more intoskills/objectstack-api/references/_index.md(the file is now identical to main's).authenticated:false|trueunion is declarednot-authorableinpackages/spec/variant-docs.json.pnpm --filter @objectstack/spec check:generated --fix: api-surface, export-origins, declaration-map, authorable-surface and json-schema manifests, the reference docs (content/docs/references/api/auth.mdx,index.mdx1582 → 1585) and the strictness ledger counts (472 → 475)..changeset/28508-me-permissions-response-contract.md:minorfor@objectstack/spec(Metadata-protocoladded: the authorable-surface baseline moves, per the precedent of feat(spec,lint): a form's thank-you panel names the record's engine-generated fields, and the public submit answer has a declared shape #28440 / feat(spec): declare the publish-drafts request body, so the change note is part of the published API contract #28447 / feat(runtime,spec): GET /packages/:id/customizations answers a base's overrides, extensions and switched-off items in one read #28595),patchfor@objectstack/plugin-hono-server,@objectstack/plugin-sharingand@object-ui/permissions, with theroles→positionsmapping for a fixture author.Tests
packages/spec/src/api/me-permissions-response.test.ts, new, 12 cases): the anonymous, full, closed and degraded bodies parse;hasAnyQueueis declared, must be a boolean ('yes',2refused at pathhasAnyQueue) and is required; an unknown sibling on the authenticated body is refused withunrecognized_keysnaming the key, not dropped; an unknown sibling on the anonymous body is refused too (hasAnyQueue,userId); a field entry is closed; absent and emptysystemPermissionsboth parse and stay distinct; a tab visibility outside the four is refused attabPermissions.sales; a non-boolean discriminator matches no branch.plugin-hono-server/src/current-user-endpoints-has-any-queue.test.ts): every body the handler answers in the 9 cases (full, degraded, closed, anonymous, service absent / without the method / throwing) is now also parsed throughMePermissionsResponseSchemaand must have no issues.verify/src/me-permissions-has-any-queue.test.ts, 5 cases): the raw body each real boot answers is parsed throughMePermissionsResponseSchemaand must have no issues, beforehasAnyQueueis read.Red first, green after, ablations
origin/main'sauth.zod.ts: every case red (the three schemas do not exist)..strict()→.passthrough()on the authenticated branch): the contract case "an unknown sibling is refused on the authenticated body" goes red, 1 failed / 11 passed. Restored: blob == HEAD.ablation-replace.mjsaddsablation28508ExtraKeytomePermissionsBody;ablation-dist-preflightfinds the marker in plugin-hono-serverdist/index.jsanddist/index.mjs): route test 7 failed / 2 passed (the degraded and anonymous cases do not go throughmePermissionsBody, by design), door test 5 failed (5). Restored: blob == HEAD, rebuilt, marker absent. This is the reading that the route and door tests are bound to the strict schema.hasAnyQueuein the spec schema was refused by the spec build itself (thegen:schemabaseline gate refuses a key removal), so dist stayed at HEAD and no test reading dist could see it. Ablation A covers the contract side and C the consumer side.Acceptance notes
EffectiveObjectPermissionSchema(the value type ofobjects) is the existingsecurity/permission.zodschema and keeps its own tolerance; this PR makes the top level and thefieldsentries strict and does not re-shape the shared permission schema.567e3dc8, must-fix 2):hasAnyQueue?likedeclaredQueueNamed?; a required member would have been an undeclared break for an out-of-tree implementer.auth.mePermissions()in@objectstack/clientwas tried and dropped:client-url-conformancerequires every SDK URL to match an exact route-ledger entry, and/auth/me/permissionssits in no ledger (it matched only the* /auth/**wildcard). The seat files the typed client method as its own card.added,Clause-②: yes (widening)(contract review must-fix 1): the PR movespackages/spec/authorable-surface/api.json(11 new schema keys), andcheck-changesetreads a moved baseline undernoneasnone-baseline-moved. The kind follows the baseline, as in feat(spec,lint): a form's thank-you panel names the record's engine-generated fields, and the public submit answer has a declared shape #28440 / feat(spec): declare the publish-drafts request body, so the change note is part of the published API contract #28447 / feat(runtime,spec): GET /packages/:id/customizations answers a base's overrides, extensions and switched-off items in one read #28595, so@objectstack/specshipsminor.@object-ui/permissionsnow derives its response type from the spec, so a key the server adds or drops shows up in the console's typecheck. The toolbar consumer ofhasAnyQueue(List toolbar: offer the "In a queue" toggle only when the organization has at least one queue — today every owned object shows it, and in an app with no queues it always returns an empty list #28428) is not wired here.Verification
All readings on head
0eb64410(the final commit), on main3944d2e1. The first round (head567e3dc8, whole-workspace build 118/118) is in the PR history; this round changes the spec contract, the changeset andui/, so everything below was re-measured after rebuilding the spec, plugin-sharing, plugin-hono-server and@object-ui/permissions.Clause-②: yes (widening)/Metadata-protocol: added,@objectstack/specminor;node scripts/check-changeset-no-major.mjs --base origin/mainwithGITHUB_EVENT_PATHset to a pull-request event carrying this body: exit 0. Red first: the same command with567e3dc8's changeset and the oldnonebody exits 1 with CI's refusal ("metadata-protocol declaresnonewhile an authorable-surface or authorable-defaults baseline moved · Moved: packages/spec/authorable-surface/api.json"). Must-fix 2:hasAnyQueue?optional. Should-fix (a):skills/objectstack-api/references/_index.mdis identical to main's (git diff origin/mainempty). Should-fix (b): the doc cross-reference is inauth.zod.ts.pnpm --dir ui run check:spec-symbols: exit 0 ("0 untriaged collisions"). Typecheck (tsc --noEmitandtsc -p tsconfig.test.json) of@object-ui/permissions, plugin-kanban, plugin-calendar, plugin-detail, plugin-gantt, plugin-form, plugin-view, plugin-list, plugin-grid, fields, plugin-report and app-shell, plusapps/consoletsc --noEmit: 0 errors each. Tests:@object-ui/permissions15 files, 166/166; every UI test file that names the response type, the provider or the route (143 files across those packages): Test Files 143 passed (143), Tests 1761 passed (1761). eslint on the provider, the package entry andstudioEntry.ts: exit 0.@objectstack/spec: 23185 passed, 1 todo.@objectstack/plugin-hono-server: 466/466.@objectstack/plugin-sharing: 1302/1302. Door test: 5/5. VERDICT command-exit 0.pnpm --filter @objectstack/spec --filter @objectstack/plugin-sharing --filter @objectstack/plugin-hono-server --filter @objectstack/verify typecheck: exit 0.pnpm --filter @objectstack/spec check:generated: exit 0 after--fixregeneratedgen:skill-refsfor the allowlist row.node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack: 115 commands, all 115 exit 0 on0eb64410;--ran: "115 derived famil(ies) accounted for — 115 run, 0 NOT-MEASURED".test:repo, the full Test Core shards and the full UI suite are CI's. No full dogfood battery is owed: no!in the title and no(narrowing)arm.Gate record (115 lines, COMMAND :: exit N)
Generated by Claude Code