Skip to content

Commit 4d278c9

Browse files
feat(spec): a strict response contract for GET /auth/me/permissions, and hasAnyQueue on IOwnershipQueueService (#28704)
domain:server#3 · os-project-manager · session_01FEkQ5kE5BrPUPNMMyv6oCH <!-- claude-projects-attribution: {"github_login":"os-project-manager"} --> _Requested by **Leehom** · [project thread](https://claude.ai/code/project/chan_01TenzsRopZj4jMbvjX8uz1X?thread=cmsg_01TenzsRopZj4jMbvjX8uz1XRMukpcDXWQXQ4GaVYCkErv)_ 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 派生,不再手写。 - **为什么改**:#28508 的 spec 一半(服务端一半已由 #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 owed:入队前由席位按 `CONTRACT_REVIEW_TIER` 给出 `## Contract review` PASS(567e3dc8 上的首轮为 FAIL,本版逐条修复,待增量复审)。 ## What changes - **`MePermissionsResponseSchema`** (`packages/spec/src/api/auth.zod.ts`), a `z.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 of `EffectiveObjectPermissionSchema`), `fields` (record of a strict `{ readable, editable }`), `systemPermissions` and `tabPermissions` (both optional: absent only on the degraded body, where absent means unknown, and present-and-empty means none), and **`hasAnyQueue: boolean`** (required on every authenticated body). - Types `MePermissionsResponse`, `MePermissionsAnonymousResponse`, `MePermissionsAuthenticatedResponse` (`z.input`). The three schemas are isomorphic, so they take ADR-0122 `Iso_` pins in `type-alias-convention.pin.test.ts` (count 808 → 811) rather than `Parsed` aliases. - **`IOwnershipQueueService.hasAnyQueue?(organizationId: string | null): Promise<boolean>`** (`packages/spec/src/contracts/ownership-queue-service.ts`): `true` exactly when some name would resolve through `resolveQueue`. **Optional**, like `declaredQueueNamed?`, so an implementation written before it still satisfies the contract; the route answers `false` without it. Its docblock names `/auth/me/permissions` as the one carrier and says it discloses no name and no count. plugin-sharing's `OwnershipQueueService` already implements it (#28654); only its docblock changes. - **`@objectstack/plugin-hono-server`** (`current-user-endpoints.ts`): the route reads the method as `Pick<IOwnershipQueueService, 'hasAnyQueue'>` through the kernel locator, keeping the runtime `typeof` guard so an older implementation still answers `false`. `mePermissionsBody` (the full and closed bodies) returns `MePermissionsAuthenticatedResponse`, so tsc refuses a key the schema does not declare. The degraded literal and the anonymous `{ authenticated: false }` stay bare `c.json({...})` literals: an inline `satisfies` hid both from the `check:route-envelope` census (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`): `MePermissionsResponse` is re-exported from `@objectstack/spec/api` (with `MePermissionsAuthenticatedResponse` beside it) in place of the hand-written interface `check:spec-symbols` flagged. The provider reads grants only from the authenticated branch. The interface's `roles` key was drift (the server sends `positions`), so the context's `roles` now carries `positions`. `ui/apps/console/src/components/studioEntry.ts` narrows on `authenticated` before reading `systemPermissions`. Test fixtures across 13 UI packages typed as the response move `roles: [...]` → `positions: [...]` and add `hasAnyQueue: false`; builders that answer `authenticated: true` are typed as the authenticated branch; three `{ readable: false }` field entries gain `editable: false` (the strict entry declares both); the anonymous fixtures are `{ authenticated: false }`. - **Skill index.** An `objectstack-api` row in `TRANSITIVE_ALLOWLIST` (`packages/spec/scripts/lib/skill-map-guards.ts`) lists the 13 transitive pointers the index already published, so the new `auth.zod.ts` → `security/permission.zod.ts` edge no longer drags 22 more into `skills/objectstack-api/references/_index.md` (the file is now identical to main's). - **Variant-doc ledger.** The `authenticated:false|true` union is declared `not-authorable` in `packages/spec/variant-docs.json`. - **Generated artifacts** regenerated with `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.mdx` 1582 → 1585) and the strictness ledger counts (472 → 475). - **Changeset** `.changeset/28508-me-permissions-response-contract.md`: **`minor` for `@objectstack/spec`** (Metadata-protocol `added`: the authorable-surface baseline moves, per the precedent of #28440 / #28447 / #28595), `patch` for `@objectstack/plugin-hono-server`, `@objectstack/plugin-sharing` and `@object-ui/permissions`, with the `roles` → `positions` mapping for a fixture author. ## Tests - **Contract** (`packages/spec/src/api/me-permissions-response.test.ts`, new, 12 cases): the anonymous, full, closed and degraded bodies parse; `hasAnyQueue` is declared, must be a boolean (`'yes'`, `2` refused at path `hasAnyQueue`) and is required; an unknown sibling on the authenticated body is refused with `unrecognized_keys` naming the key, not dropped; an unknown sibling on the anonymous body is refused too (`hasAnyQueue`, `userId`); a field entry is closed; absent and empty `systemPermissions` both parse and stay distinct; a tab visibility outside the four is refused at `tabPermissions.sales`; a non-boolean discriminator matches no branch. - **Route** (`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 through `MePermissionsResponseSchema` and must have no issues. - **HTTP door** (`verify/src/me-permissions-has-any-queue.test.ts`, 5 cases): the raw body each real boot answers is parsed through `MePermissionsResponseSchema` and must have no issues, before `hasAnyQueue` is read. <details><summary>Red first, green after, ablations</summary> - **Red on main.** The contract test against `origin/main`'s `auth.zod.ts`: every case red (the three schemas do not exist). - **Green.** Contract 12/12, route 9/9, door 5/5. - **Ablation A, strictness dropped** (`.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 C, the route sends an undeclared key** (`ablation-replace.mjs` adds `ablation28508ExtraKey` to `mePermissionsBody`; `ablation-dist-preflight` finds the marker in plugin-hono-server `dist/index.js` and `dist/index.mjs`): route test 7 failed / 2 passed (the degraded and anonymous cases do not go through `mePermissionsBody`, 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. - **Ablation B, not measured.** Renaming `hasAnyQueue` in the spec schema was refused by the spec build itself (the `gen:schema` baseline 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. </details> ## Acceptance notes - **Acceptance 1 (declared strictly, unknown sibling refused)**: met by the strict authenticated and anonymous branches and pinned by the contract test. `EffectiveObjectPermissionSchema` (the value type of `objects`) is the existing `security/permission.zod` schema and keeps its own tolerance; this PR makes the top level and the `fields` entries strict and does not re-shape the shared permission schema. - **Contract method optional** (contract review on `567e3dc8`, must-fix 2): `hasAnyQueue?` like `declaredQueueNamed?`; a required member would have been an undeclared break for an out-of-tree implementer. - **No client method.** A typed `auth.mePermissions()` in `@objectstack/client` was tried and dropped: `client-url-conformance` requires every SDK URL to match an exact route-ledger entry, and `/auth/me/permissions` sits in no ledger (it matched only the `* /auth/**` wildcard). The seat files the typed client method as its own card. - **Metadata-protocol `added`, `Clause-②: yes (widening)`** (contract review must-fix 1): the PR moves `packages/spec/authorable-surface/api.json` (11 new schema keys), and `check-changeset` reads a moved baseline under `none` as `none-baseline-moved`. The kind follows the baseline, as in #28440 / #28447 / #28595, so `@objectstack/spec` ships `minor`. - **Console reads the typed contract.** `@object-ui/permissions` now 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 of `hasAnyQueue` (#28428) is not wired here. ## Verification All readings on head `0eb64410` (the final commit), on main `3944d2e1`. The first round (head `567e3dc8`, whole-workspace build 118/118) is in the PR history; this round changes the spec contract, the changeset and `ui/`, so everything below was re-measured after rebuilding the spec, plugin-sharing, plugin-hono-server and `@object-ui/permissions`. - **Contract review fixes.** Must-fix 1: changeset and body declare `Clause-②: yes (widening)` / `Metadata-protocol: added`, `@objectstack/spec` `minor`; `node scripts/check-changeset-no-major.mjs --base origin/main` with `GITHUB_EVENT_PATH` set to a pull-request event carrying this body: exit 0. Red first: the same command with `567e3dc8`'s changeset and the old `none` body exits 1 with CI's refusal ("metadata-protocol declares `none` while 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.md` is identical to main's (`git diff origin/main` empty). Should-fix (b): the doc cross-reference is in `auth.zod.ts`. - **UI.** `pnpm --dir ui run check:spec-symbols`: exit 0 ("0 untriaged collisions"). Typecheck (`tsc --noEmit` and `tsc -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, plus `apps/console` `tsc --noEmit`: 0 errors each. Tests: `@object-ui/permissions` 15 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 and `studioEntry.ts`: exit 0. - **Package suites.** `@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. - **Typecheck.** `pnpm --filter @objectstack/spec --filter @objectstack/plugin-sharing --filter @objectstack/plugin-hono-server --filter @objectstack/verify typecheck`: exit 0. - **Generated artifacts.** `pnpm --filter @objectstack/spec check:generated`: exit 0 after `--fix` regenerated `gen:skill-refs` for the allowlist row. - **Derived gates.** `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack`: 115 commands, **all 115 exit 0 on `0eb64410`**; `--ran`: "115 derived famil(ies) accounted for — 115 run, 0 NOT-MEASURED". - **Not local.** `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. <details><summary>Gate record (115 lines, COMMAND :: exit N)</summary> ```text node scripts/check-adr-0087-registration.mjs --base origin/main :: exit 0 node scripts/check-adr-0087-registration.mjs --self-test :: exit 0 node scripts/check-adr-links.mjs :: exit 0 node scripts/check-adr-links.mjs --self-test :: exit 0 node scripts/check-changeset-no-major.mjs --base origin/main :: exit 0 node scripts/check-closing-keyword-parity.mjs :: exit 0 node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0 node scripts/check-comment-mask-adoption.mjs :: exit 0 node scripts/check-comment-mask-adoption.mjs --self-test :: exit 0 node scripts/check-comment-mask-corpus.mjs :: exit 0 node scripts/check-dev-prereqs.mjs --self-test :: exit 0 node scripts/check-doc-route-spelling.mjs --advisory :: exit 0 node scripts/check-doc-route-spelling.mjs --self-test :: exit 0 node scripts/check-docs-section-name.mjs :: exit 0 node scripts/check-docs-section-name.mjs --self-test :: exit 0 node scripts/check-dts-emitted.mjs --self-test :: exit 0 node scripts/check-empty-changeset.mjs --base origin/main :: exit 0 node scripts/check-empty-changeset.mjs --self-test :: exit 0 node scripts/check-issue-citations.mjs :: exit 0 node scripts/check-plugin-teardown-shape.mjs :: exit 0 node scripts/check-plugin-teardown-shape.mjs --self-test :: exit 0 node scripts/check-sdui-manifest.mjs :: exit 0 node scripts/check-sdui-manifest.mjs --self-test :: exit 0 node scripts/check-section-landing-index.mjs :: exit 0 node scripts/check-section-landing-index.mjs --self-test :: exit 0 node scripts/check-tenant-audit-census.mjs :: exit 0 node scripts/check-tenant-audit-census.mjs --self-test :: exit 0 node scripts/ci/assert-turbo-remote-cache.mjs --self-test :: exit 0 node scripts/ci/plan-test-core-shards.mjs --self-test :: exit 0 node scripts/ci/prune-turbo-cache.mjs --self-test :: exit 0 node scripts/docs-audit/check-affected-docs.mjs :: exit 0 node scripts/docs-audit/check-drift-comment.mjs :: exit 0 node scripts/pm/release-rehearsal-clone.mjs --self-test :: exit 0 node scripts/release-pending-publish.mjs --self-test :: exit 0 pnpm --dir ui exec vitest run scripts/ :: exit 0 pnpm --filter @objectstack/lint run check:doc-formula-expressions :: exit 0 pnpm --filter @objectstack/lint run check:doc-security-posture :: exit 0 pnpm --filter @objectstack/spec run check:api-surface :: exit 0 pnpm --filter @objectstack/spec run check:authorable-surface :: exit 0 pnpm --filter @objectstack/spec run check:browser-reachable-entries :: exit 0 pnpm --filter @objectstack/spec run check:docs :: exit 0 pnpm --filter @objectstack/spec run check:dual-source-exports :: exit 0 pnpm --filter @objectstack/spec run check:duration-unit-keys :: exit 0 pnpm --filter @objectstack/spec run check:empty-state :: exit 0 pnpm --filter @objectstack/spec run check:entry-nameability :: exit 0 pnpm --filter @objectstack/spec run check:export-origins :: exit 0 pnpm --filter @objectstack/spec run check:exported-any :: exit 0 pnpm --filter @objectstack/spec run check:generated :: exit 0 pnpm --filter @objectstack/spec run check:liveness :: exit 0 pnpm --filter @objectstack/spec run check:llms-txt :: exit 0 pnpm --filter @objectstack/spec run check:skill-examples :: exit 0 pnpm --filter @objectstack/spec run check:skill-refs :: exit 0 pnpm --filter @objectstack/spec run check:strictness-ledger :: exit 0 pnpm --filter @objectstack/spec run check:variant-docs :: exit 0 pnpm --filter @objectstack/spec run check:yaml-examples :: exit 0 pnpm check:authz-resolver :: exit 0 pnpm check:changeset-gate-self-tests :: exit 0 pnpm check:ci-filter-parity :: exit 0 pnpm check:closed-ceremony-text :: exit 0 pnpm check:corpus-claim-drift :: exit 0 pnpm check:cross-package-test-inputs :: exit 0 pnpm check:dispatcher-error-vocabulary :: exit 0 pnpm check:doc-anchors :: exit 0 pnpm check:doc-authoring :: exit 0 pnpm check:doc-frontmatter :: exit 0 pnpm check:docs-audit-scope :: exit 0 pnpm check:docs-redirects :: exit 0 pnpm check:docs-single-h1 :: exit 0 pnpm check:docs-spec-enumerations :: exit 0 pnpm check:docs-transcript-drift :: exit 0 pnpm check:driver-memory-census :: exit 0 pnpm check:dts-closure :: exit 0 pnpm check:dual-build-cjs-loads :: exit 0 pnpm check:engine-double-contract :: exit 0 pnpm check:error-status-conformance :: exit 0 pnpm check:gitlink-declared :: exit 0 pnpm check:i18n :: exit 0 pnpm check:i18n-stale-fill :: exit 0 pnpm check:issue-citations :: exit 0 pnpm check:keyed-text-bounds :: exit 0 pnpm check:lean-entry-closure :: exit 0 pnpm check:logger-receiver-detach :: exit 0 pnpm check:merge-driver :: exit 0 pnpm check:nul-bytes :: exit 0 pnpm check:objectql-double-limit :: exit 0 pnpm check:org-identifier :: exit 0 pnpm check:page-declaration-shape :: exit 0 pnpm check:platform-object-tenancy-census :: exit 0 pnpm check:pm-changeset-deadline-census :: exit 0 pnpm check:pm-governed-merges :: exit 0 pnpm check:pm-prior-rulings :: exit 0 pnpm check:pnpm-flag-spellings :: exit 0 pnpm check:published-files :: exit 0 pnpm check:published-readme-links :: exit 0 pnpm check:query-options-erasure :: exit 0 pnpm check:react-page-adapter-contract :: exit 0 pnpm check:refd-timer-probe :: exit 0 pnpm check:registry-log-declared :: exit 0 pnpm check:rest-log-spy-declared :: exit 0 pnpm check:role-word :: exit 0 pnpm check:route-envelope :: exit 0 pnpm check:skill-identifier-liveness :: exit 0 pnpm check:slot-lookup :: exit 0 pnpm check:sourcemap-no-sources-content :: exit 0 pnpm check:spec-docblock-symbol-anchors :: exit 0 pnpm check:spec-parsed-alias :: exit 0 pnpm check:system-context-census :: exit 0 pnpm check:test-source-alias :: exit 0 pnpm check:tier-file-adoption :: exit 0 pnpm check:type-check-coverage :: exit 0 pnpm check:type-check-debt :: exit 0 pnpm check:undeclared-dep-imports :: exit 0 pnpm check:vendor-version-stamps :: exit 0 pnpm check:watch-hint-literal :: exit 0 pnpm check:where-matcher :: exit 0 ``` </details> --- _Generated by [Claude Code](https://claude.ai/code/session_01FEkQ5kcJADmewNSX53U3gf)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 272fec0 commit 4d278c9

143 files changed

Lines changed: 1018 additions & 468 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
---
2+
'@objectstack/spec': minor
3+
'@objectstack/plugin-hono-server': patch
4+
'@objectstack/plugin-sharing': patch
5+
'@object-ui/permissions': patch
6+
---
7+
8+
feat(spec): a strict response contract for `GET /auth/me/permissions`, and `hasAnyQueue` on the ownership-queue contract
9+
10+
Clause-②: yes (widening)
11+
Metadata-protocol: added
12+
13+
`GET /auth/me/permissions` is the payload the console reads once per session for its whole permission layer. Until now nothing in `@objectstack/spec` declared its body. It is now declared, strictly:
14+
15+
- **`MePermissionsResponseSchema`** (`@objectstack/spec/api`), discriminated by `authenticated`:
16+
- `MePermissionsAnonymousResponseSchema`: `{ authenticated: false }` and nothing else.
17+
- `MePermissionsAuthenticatedResponseSchema`: `userId`, `tenantId` (nullable), `positions`, `permissionSets`, `objects` (effective object permissions), `fields` (`{ readable, editable }` per `object.field`), `systemPermissions` and `tabPermissions` (optional: absent only on the body a server without SecurityPlugin answers, where absent means unknown), and `hasAnyQueue: boolean`.
18+
- Both branches are strict: an undeclared key is refused, not dropped. A key the server starts sending is declared here first.
19+
- Types `MePermissionsResponse`, `MePermissionsAnonymousResponse`, `MePermissionsAuthenticatedResponse`.
20+
- **`IOwnershipQueueService.hasAnyQueue?(organizationId)`** (`@objectstack/spec/contracts`): does the organization have at least one ownership queue, `true` exactly when some name would resolve through `resolveQueue`. Optional, like `declaredQueueNamed?`, so an implementation written before it still satisfies the contract; the route answers `false` without it. `@objectstack/plugin-sharing`'s `OwnershipQueueService` implements it.
21+
- **`@objectstack/plugin-hono-server`** types the route's body builder against `MePermissionsAuthenticatedResponse`, and reads `hasAnyQueue` through the contract. The wire is unchanged.
22+
- **`@object-ui/permissions`**: `MePermissionsResponse` is now the spec's type, re-exported from `@objectstack/spec/api` (with `MePermissionsAuthenticatedResponse` beside it), in place of a hand-written interface that had drifted from the wire. The interface read a `roles` key the server never sends; the server's key is `positions`, so `usePermissions().roles` under `MePermissionsProvider` was always empty and now carries the caller's positions. A fixture typed as the response migrates `roles: [...]` → `positions: [...]` and adds `hasAnyQueue`; the anonymous body is `{ authenticated: false }` and nothing else.

‎content/docs/references/api/auth.mdx‎

Lines changed: 121 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@ and Access Control.
1818
## TypeScript Usage
1919

2020
```typescript
21-
import { AuthProvider, LoginRequestSchema, LoginType, RefreshTokenRequestSchema, RegisterRequestSchema, SessionSchema, SessionResponseSchema, SessionUserSchema, UserProfileResponseSchema } from '@objectstack/spec/api';
22-
import type { AuthProvider, LoginRequest, LoginType, RefreshTokenRequest, RegisterRequest, Session, SessionResponse, SessionUser, UserProfileResponse } from '@objectstack/spec/api';
21+
import { AuthProvider, LoginRequestSchema, LoginType, MePermissionsAnonymousResponseSchema, MePermissionsAuthenticatedResponseSchema, MePermissionsResponseSchema, RefreshTokenRequestSchema, RegisterRequestSchema, SessionSchema, SessionResponseSchema, SessionUserSchema, UserProfileResponseSchema } from '@objectstack/spec/api';
22+
import type { AuthProvider, LoginRequest, LoginType, MePermissionsAnonymousResponse, MePermissionsAuthenticatedResponse, MePermissionsResponse, RefreshTokenRequest, RegisterRequest, Session, SessionResponse, SessionUser, UserProfileResponse } from '@objectstack/spec/api';
2323

2424
// Validate data
2525
const result = AuthProvider.parse(data);
@@ -68,6 +68,125 @@ const result = AuthProvider.parse(data);
6868
* `social`
6969

7070

71+
---
72+
73+
## MePermissionsAnonymousResponse
74+
75+
### Properties
76+
77+
| Property | Type | Required | Description |
78+
| :--- | :--- | :--- | :--- |
79+
| **authenticated** | `false` | ✅ | No session: the caller is anonymous. |
80+
81+
82+
---
83+
84+
## MePermissionsAuthenticatedResponse
85+
86+
### Properties
87+
88+
| Property | Type | Required | Description |
89+
| :--- | :--- | :--- | :--- |
90+
| **authenticated** | `true` | ✅ | A session was resolved for the caller. |
91+
| **userId** | `string` | ✅ | The caller's user id. |
92+
| **tenantId** | `string \| null` | ✅ | The caller's active organization id, or null when the session has none. |
93+
| **positions** | `string[]` | ✅ | The security positions the caller holds (ADR-0068 D3): built-in identity names plus assigned positions, scope-resolved. |
94+
| **permissionSets** | `string[]` | ✅ | The names of the permission sets resolved for the caller. |
95+
| **objects** | `Record<string, { allowCreate?: boolean; allowRead?: boolean; allowEdit?: boolean; allowDelete?: boolean; … }>` | ✅ | The effective object permissions, keyed by object name — the map `current_user.can()` reads on the server. |
96+
| **fields** | `Record<string, { readable: boolean; editable: boolean }>` | ✅ | The effective field permissions, keyed `object.field`. |
97+
| **systemPermissions** | `string[]` | optional | The effective system capabilities (ADR-0066). Absent only on the degraded body, where it means unknown; an empty array means none. |
98+
| **tabPermissions** | `Record<string, Enum<'visible' \| 'hidden' \| 'default_on' \| 'default_off'>>` | optional | The effective app tab visibilities, keyed by app name. Absent only on the degraded body. |
99+
| **hasAnyQueue** | `boolean` | ✅ | Whether the caller's organization has at least one ownership queue: a declared queue the activation ledger has not switched off, or a `sys_queue` row of the organization. Resolved server-side through the ownership-queue service, so a member who may not read `sys_queue` still gets the answer; no names and no counts are disclosed. False when no ownership-queue service is registered, and on the closed body. |
100+
101+
### Nested Shape: `MePermissionsAuthenticatedResponse.objects[string]`
102+
103+
| Property | Type | Required | Description |
104+
| :--- | :--- | :--- | :--- |
105+
| **allowCreate** | `boolean` | optional (default: `false`) | Create permission |
106+
| **allowRead** | `boolean` | optional (default: `false`) | Read permission |
107+
| **allowEdit** | `boolean` | optional (default: `false`) | Edit permission |
108+
| **allowDelete** | `boolean` | optional (default: `false`) | Delete permission |
109+
| **allowExport** | `boolean` | optional | User-level export axis over read (opt-in grant). true = export granted (still bounded by read); unset/false = no export. Merged most-permissively like the CRUD bits; NOT implied by viewAllRecords/modifyAllRecords. |
110+
| **allowTransfer** | `boolean` | optional (default: `false`) | [RBAC-gated; ENFORCED via the insert/update owner_id guard] Change record ownership (assign/reassign/disown owner_id) |
111+
| **allowRestore** | `never` | optional | [REMOVED] `objects.<object>.allowRestore` was removed in @objectstack/spec 17 (ADR-0049) — the `restore` ObjectQL operation it claimed to gate has never shipped (roadmap M2), so granting the bit delivered nothing. Delete the key — a dispatched `restore` stays denied fail-closed by the permission evaluator's destructive-operation backstop, and the bit returns with the M2 lifecycle initiative alongside the operation it gates. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. |
112+
| **allowPurge** | `never` | optional | [REMOVED] `objects.<object>.allowPurge` was removed in @objectstack/spec 17 (ADR-0049) — the `purge` ObjectQL operation it claimed to gate has never shipped (roadmap M2), so granting the bit delivered nothing (a compliance/GDPR erase the author believed was permission-locked was not — the operation itself does not exist). Delete the key — a dispatched `purge` stays denied fail-closed by the permission evaluator's destructive-operation backstop, and the bit returns with the M2 lifecycle initiative alongside the operation it gates. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. |
113+
| **viewAllRecords** | `boolean` | optional (default: `false`) | View All Data (Bypass Sharing) |
114+
| **modifyAllRecords** | `boolean` | optional (default: `false`) | Modify All Data (Bypass Sharing) — bypasses sharing rules and ownership on the objects record sharing enforces on; on an object with NO owner field sharing abstains, so the platform created_by write floor still applies. Includes delete: an omitted allowDelete parses to true, and an explicit allowDelete: false beside it is refused. |
115+
| **readScope** | `Enum<'own' \| 'own_and_reports' \| 'unit' \| 'unit_and_below' \| 'org'>` | optional | [ADR-0057 D1] Read depth: own\|unit\|unit_and_below\|org |
116+
| **writeScope** | `Enum<'own' \| 'own_and_reports' \| 'unit' \| 'unit_and_below' \| 'org'>` | optional | [ADR-0057 D1] Write depth: own\|unit\|unit_and_below\|org |
117+
| **apiOperations** | `Enum<'get' \| 'list' \| 'create' \| 'update' \| 'delete' \| 'upsert' \| 'bulk' \| …>[]` | optional | Server-resolved effective API operations for this object. Present only when the object tightens exposure via apiMethods; absent = default-allow. The frontend renders this effective set, never the raw whitelist. Vocabulary is the EFFECTIVE ApiOperation set (six primitives + eight derived verbs), not the authored six-value ApiMethod enum. |
118+
119+
### Nested Shape: `MePermissionsAuthenticatedResponse.fields[string]`
120+
121+
| Property | Type | Required | Description |
122+
| :--- | :--- | :--- | :--- |
123+
| **readable** | `boolean` | ✅ | The caller may read the field. |
124+
| **editable** | `boolean` | ✅ | The caller may edit the field on some row (a row-conditioned entry is reported as its lock). |
125+
126+
127+
---
128+
129+
## MePermissionsResponse
130+
131+
### Union Options
132+
133+
This schema accepts one of the following structures:
134+
135+
#### Option 1
136+
137+
### Properties
138+
139+
| Property | Type | Required | Description |
140+
| :--- | :--- | :--- | :--- |
141+
| **authenticated** | `false` | ✅ | No session: the caller is anonymous. |
142+
143+
---
144+
145+
#### Option 2
146+
147+
### Properties
148+
149+
| Property | Type | Required | Description |
150+
| :--- | :--- | :--- | :--- |
151+
| **authenticated** | `true` | ✅ | A session was resolved for the caller. |
152+
| **userId** | `string` | ✅ | The caller's user id. |
153+
| **tenantId** | `string \| null` | ✅ | The caller's active organization id, or null when the session has none. |
154+
| **positions** | `string[]` | ✅ | The security positions the caller holds (ADR-0068 D3): built-in identity names plus assigned positions, scope-resolved. |
155+
| **permissionSets** | `string[]` | ✅ | The names of the permission sets resolved for the caller. |
156+
| **objects** | `Record<string, { allowCreate?: boolean; allowRead?: boolean; allowEdit?: boolean; allowDelete?: boolean; … }>` | ✅ | The effective object permissions, keyed by object name — the map `current_user.can()` reads on the server. |
157+
| **fields** | `Record<string, { readable: boolean; editable: boolean }>` | ✅ | The effective field permissions, keyed `object.field`. |
158+
| **systemPermissions** | `string[]` | optional | The effective system capabilities (ADR-0066). Absent only on the degraded body, where it means unknown; an empty array means none. |
159+
| **tabPermissions** | `Record<string, Enum<'visible' \| 'hidden' \| 'default_on' \| 'default_off'>>` | optional | The effective app tab visibilities, keyed by app name. Absent only on the degraded body. |
160+
| **hasAnyQueue** | `boolean` | ✅ | Whether the caller's organization has at least one ownership queue: a declared queue the activation ledger has not switched off, or a `sys_queue` row of the organization. Resolved server-side through the ownership-queue service, so a member who may not read `sys_queue` still gets the answer; no names and no counts are disclosed. False when no ownership-queue service is registered, and on the closed body. |
161+
162+
### Nested Shape: `MePermissionsResponse[authenticated=true].objects[string]`
163+
164+
| Property | Type | Required | Description |
165+
| :--- | :--- | :--- | :--- |
166+
| **allowCreate** | `boolean` | optional (default: `false`) | Create permission |
167+
| **allowRead** | `boolean` | optional (default: `false`) | Read permission |
168+
| **allowEdit** | `boolean` | optional (default: `false`) | Edit permission |
169+
| **allowDelete** | `boolean` | optional (default: `false`) | Delete permission |
170+
| **allowExport** | `boolean` | optional | User-level export axis over read (opt-in grant). true = export granted (still bounded by read); unset/false = no export. Merged most-permissively like the CRUD bits; NOT implied by viewAllRecords/modifyAllRecords. |
171+
| **allowTransfer** | `boolean` | optional (default: `false`) | [RBAC-gated; ENFORCED via the insert/update owner_id guard] Change record ownership (assign/reassign/disown owner_id) |
172+
| **allowRestore** | `never` | optional | [REMOVED] `objects.<object>.allowRestore` was removed in @objectstack/spec 17 (ADR-0049) — the `restore` ObjectQL operation it claimed to gate has never shipped (roadmap M2), so granting the bit delivered nothing. Delete the key — a dispatched `restore` stays denied fail-closed by the permission evaluator's destructive-operation backstop, and the bit returns with the M2 lifecycle initiative alongside the operation it gates. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. |
173+
| **allowPurge** | `never` | optional | [REMOVED] `objects.<object>.allowPurge` was removed in @objectstack/spec 17 (ADR-0049) — the `purge` ObjectQL operation it claimed to gate has never shipped (roadmap M2), so granting the bit delivered nothing (a compliance/GDPR erase the author believed was permission-locked was not — the operation itself does not exist). Delete the key — a dispatched `purge` stays denied fail-closed by the permission evaluator's destructive-operation backstop, and the bit returns with the M2 lifecycle initiative alongside the operation it gates. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. |
174+
| **viewAllRecords** | `boolean` | optional (default: `false`) | View All Data (Bypass Sharing) |
175+
| **modifyAllRecords** | `boolean` | optional (default: `false`) | Modify All Data (Bypass Sharing) — bypasses sharing rules and ownership on the objects record sharing enforces on; on an object with NO owner field sharing abstains, so the platform created_by write floor still applies. Includes delete: an omitted allowDelete parses to true, and an explicit allowDelete: false beside it is refused. |
176+
| **readScope** | `Enum<'own' \| 'own_and_reports' \| 'unit' \| 'unit_and_below' \| 'org'>` | optional | [ADR-0057 D1] Read depth: own\|unit\|unit_and_below\|org |
177+
| **writeScope** | `Enum<'own' \| 'own_and_reports' \| 'unit' \| 'unit_and_below' \| 'org'>` | optional | [ADR-0057 D1] Write depth: own\|unit\|unit_and_below\|org |
178+
| **apiOperations** | `Enum<'get' \| 'list' \| 'create' \| 'update' \| 'delete' \| 'upsert' \| 'bulk' \| …>[]` | optional | Server-resolved effective API operations for this object. Present only when the object tightens exposure via apiMethods; absent = default-allow. The frontend renders this effective set, never the raw whitelist. Vocabulary is the EFFECTIVE ApiOperation set (six primitives + eight derived verbs), not the authored six-value ApiMethod enum. |
179+
180+
### Nested Shape: `MePermissionsResponse[authenticated=true].fields[string]`
181+
182+
| Property | Type | Required | Description |
183+
| :--- | :--- | :--- | :--- |
184+
| **readable** | `boolean` | ✅ | The caller may read the field. |
185+
| **editable** | `boolean` | ✅ | The caller may edit the field on some row (a row-conditioned entry is reported as its lock). |
186+
187+
---
188+
189+
71190
---
72191

73192
## RefreshTokenRequest

0 commit comments

Comments
 (0)