diff --git a/.changeset/28508-me-permissions-response-contract.md b/.changeset/28508-me-permissions-response-contract.md new file mode 100644 index 00000000000..775dbf3a86c --- /dev/null +++ b/.changeset/28508-me-permissions-response-contract.md @@ -0,0 +1,22 @@ +--- +'@objectstack/spec': minor +'@objectstack/plugin-hono-server': patch +'@objectstack/plugin-sharing': patch +'@object-ui/permissions': patch +--- + +feat(spec): a strict response contract for `GET /auth/me/permissions`, and `hasAnyQueue` on the ownership-queue contract + +Clause-②: yes (widening) +Metadata-protocol: added + +`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: + +- **`MePermissionsResponseSchema`** (`@objectstack/spec/api`), discriminated by `authenticated`: + - `MePermissionsAnonymousResponseSchema`: `{ authenticated: false }` and nothing else. + - `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`. + - Both branches are strict: an undeclared key is refused, not dropped. A key the server starts sending is declared here first. + - Types `MePermissionsResponse`, `MePermissionsAnonymousResponse`, `MePermissionsAuthenticatedResponse`. +- **`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. +- **`@objectstack/plugin-hono-server`** types the route's body builder against `MePermissionsAuthenticatedResponse`, and reads `hasAnyQueue` through the contract. The wire is unchanged. +- **`@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. diff --git a/content/docs/references/api/auth.mdx b/content/docs/references/api/auth.mdx index fc1ff171330..b50d2e3438c 100644 --- a/content/docs/references/api/auth.mdx +++ b/content/docs/references/api/auth.mdx @@ -18,8 +18,8 @@ and Access Control. ## TypeScript Usage ```typescript -import { AuthProvider, LoginRequestSchema, LoginType, RefreshTokenRequestSchema, RegisterRequestSchema, SessionSchema, SessionResponseSchema, SessionUserSchema, UserProfileResponseSchema } from '@objectstack/spec/api'; -import type { AuthProvider, LoginRequest, LoginType, RefreshTokenRequest, RegisterRequest, Session, SessionResponse, SessionUser, UserProfileResponse } from '@objectstack/spec/api'; +import { AuthProvider, LoginRequestSchema, LoginType, MePermissionsAnonymousResponseSchema, MePermissionsAuthenticatedResponseSchema, MePermissionsResponseSchema, RefreshTokenRequestSchema, RegisterRequestSchema, SessionSchema, SessionResponseSchema, SessionUserSchema, UserProfileResponseSchema } from '@objectstack/spec/api'; +import type { AuthProvider, LoginRequest, LoginType, MePermissionsAnonymousResponse, MePermissionsAuthenticatedResponse, MePermissionsResponse, RefreshTokenRequest, RegisterRequest, Session, SessionResponse, SessionUser, UserProfileResponse } from '@objectstack/spec/api'; // Validate data const result = AuthProvider.parse(data); @@ -68,6 +68,125 @@ const result = AuthProvider.parse(data); * `social` +--- + +## MePermissionsAnonymousResponse + +### Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **authenticated** | `false` | ✅ | No session: the caller is anonymous. | + + +--- + +## MePermissionsAuthenticatedResponse + +### Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **authenticated** | `true` | ✅ | A session was resolved for the caller. | +| **userId** | `string` | ✅ | The caller's user id. | +| **tenantId** | `string \| null` | ✅ | The caller's active organization id, or null when the session has none. | +| **positions** | `string[]` | ✅ | The security positions the caller holds (ADR-0068 D3): built-in identity names plus assigned positions, scope-resolved. | +| **permissionSets** | `string[]` | ✅ | The names of the permission sets resolved for the caller. | +| **objects** | `Record` | ✅ | The effective object permissions, keyed by object name — the map `current_user.can()` reads on the server. | +| **fields** | `Record` | ✅ | The effective field permissions, keyed `object.field`. | +| **systemPermissions** | `string[]` | optional | The effective system capabilities (ADR-0066). Absent only on the degraded body, where it means unknown; an empty array means none. | +| **tabPermissions** | `Record>` | optional | The effective app tab visibilities, keyed by app name. Absent only on the degraded body. | +| **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. | + +### Nested Shape: `MePermissionsAuthenticatedResponse.objects[string]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **allowCreate** | `boolean` | optional (default: `false`) | Create permission | +| **allowRead** | `boolean` | optional (default: `false`) | Read permission | +| **allowEdit** | `boolean` | optional (default: `false`) | Edit permission | +| **allowDelete** | `boolean` | optional (default: `false`) | Delete permission | +| **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. | +| **allowTransfer** | `boolean` | optional (default: `false`) | [RBAC-gated; ENFORCED via the insert/update owner_id guard] Change record ownership (assign/reassign/disown owner_id) | +| **allowRestore** | `never` | optional | [REMOVED] `objects..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. | +| **allowPurge** | `never` | optional | [REMOVED] `objects..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. | +| **viewAllRecords** | `boolean` | optional (default: `false`) | View All Data (Bypass Sharing) | +| **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. | +| **readScope** | `Enum<'own' \| 'own_and_reports' \| 'unit' \| 'unit_and_below' \| 'org'>` | optional | [ADR-0057 D1] Read depth: own\|unit\|unit_and_below\|org | +| **writeScope** | `Enum<'own' \| 'own_and_reports' \| 'unit' \| 'unit_and_below' \| 'org'>` | optional | [ADR-0057 D1] Write depth: own\|unit\|unit_and_below\|org | +| **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. | + +### Nested Shape: `MePermissionsAuthenticatedResponse.fields[string]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **readable** | `boolean` | ✅ | The caller may read the field. | +| **editable** | `boolean` | ✅ | The caller may edit the field on some row (a row-conditioned entry is reported as its lock). | + + +--- + +## MePermissionsResponse + +### Union Options + +This schema accepts one of the following structures: + +#### Option 1 + +### Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **authenticated** | `false` | ✅ | No session: the caller is anonymous. | + +--- + +#### Option 2 + +### Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **authenticated** | `true` | ✅ | A session was resolved for the caller. | +| **userId** | `string` | ✅ | The caller's user id. | +| **tenantId** | `string \| null` | ✅ | The caller's active organization id, or null when the session has none. | +| **positions** | `string[]` | ✅ | The security positions the caller holds (ADR-0068 D3): built-in identity names plus assigned positions, scope-resolved. | +| **permissionSets** | `string[]` | ✅ | The names of the permission sets resolved for the caller. | +| **objects** | `Record` | ✅ | The effective object permissions, keyed by object name — the map `current_user.can()` reads on the server. | +| **fields** | `Record` | ✅ | The effective field permissions, keyed `object.field`. | +| **systemPermissions** | `string[]` | optional | The effective system capabilities (ADR-0066). Absent only on the degraded body, where it means unknown; an empty array means none. | +| **tabPermissions** | `Record>` | optional | The effective app tab visibilities, keyed by app name. Absent only on the degraded body. | +| **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. | + +### Nested Shape: `MePermissionsResponse[authenticated=true].objects[string]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **allowCreate** | `boolean` | optional (default: `false`) | Create permission | +| **allowRead** | `boolean` | optional (default: `false`) | Read permission | +| **allowEdit** | `boolean` | optional (default: `false`) | Edit permission | +| **allowDelete** | `boolean` | optional (default: `false`) | Delete permission | +| **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. | +| **allowTransfer** | `boolean` | optional (default: `false`) | [RBAC-gated; ENFORCED via the insert/update owner_id guard] Change record ownership (assign/reassign/disown owner_id) | +| **allowRestore** | `never` | optional | [REMOVED] `objects..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. | +| **allowPurge** | `never` | optional | [REMOVED] `objects..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. | +| **viewAllRecords** | `boolean` | optional (default: `false`) | View All Data (Bypass Sharing) | +| **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. | +| **readScope** | `Enum<'own' \| 'own_and_reports' \| 'unit' \| 'unit_and_below' \| 'org'>` | optional | [ADR-0057 D1] Read depth: own\|unit\|unit_and_below\|org | +| **writeScope** | `Enum<'own' \| 'own_and_reports' \| 'unit' \| 'unit_and_below' \| 'org'>` | optional | [ADR-0057 D1] Write depth: own\|unit\|unit_and_below\|org | +| **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. | + +### Nested Shape: `MePermissionsResponse[authenticated=true].fields[string]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **readable** | `boolean` | ✅ | The caller may read the field. | +| **editable** | `boolean` | ✅ | The caller may edit the field on some row (a row-conditioned entry is reported as its lock). | + +--- + + --- ## RefreshTokenRequest diff --git a/content/docs/references/index.mdx b/content/docs/references/index.mdx index d918783a56c..f861a1dc511 100644 --- a/content/docs/references/index.mdx +++ b/content/docs/references/index.mdx @@ -1,7 +1,7 @@ --- title: Protocol reference — every schema by module navTitle: Protocol Reference -description: Every schema published by @objectstack/spec — 1584 schemas across 14 protocol modules +description: Every schema published by @objectstack/spec — 1587 schemas across 14 protocol modules --- {/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */} @@ -21,7 +21,7 @@ counts are sums of the rows they head. Regenerate with | Module | Pages | Schemas | Description | | :--- | ---: | ---: | :--- | | [AI Protocol](/docs/references/ai) | 12 | 68 | Agents, tools, skills, RAG and knowledge sources, model registry, conversations. | -| [API Protocol](/docs/references/api) | 34 | 460 | REST contracts, endpoints, routing, realtime, batch, discovery. | +| [API Protocol](/docs/references/api) | 34 | 463 | REST contracts, endpoints, routing, realtime, batch, discovery. | | [Automation Protocol](/docs/references/automation) | 15 | 77 | Flows and their nodes, approvals, ETL pipelines, webhooks, state machines, execution records. | | [Data Protocol](/docs/references/data) | 31 | 174 | Objects, fields, queries, filters, datasources and drivers — the ObjectQL layer. | | [Identity Protocol](/docs/references/identity) | 5 | 27 | Users and accounts, organizations, positions, SCIM provisioning. | @@ -34,7 +34,7 @@ counts are sums of the rows they head. Regenerate with | [Studio Protocol](/docs/references/studio) | 3 | 35 | Studio designer metadata — the authoring surfaces for the protocols above. | | [System Protocol](/docs/references/system) | 37 | 287 | The runtime environment — logging, jobs, cache, metrics, notifications, i18n and compliance. | | [UI Protocol](/docs/references/ui) | 16 | 175 | Apps, pages, views, dashboards, reports, actions and themes — the ObjectUI layer. | -| **Total** | **205** | **1584** | 14 protocol modules | +| **Total** | **205** | **1587** | 14 protocol modules | --- @@ -63,7 +63,7 @@ Agents, tools, skills, RAG and knowledge sources, model registry, conversations. ## API Protocol -**Source:** `packages/spec/src/api/` · **Import:** `@objectstack/spec/api` · **34 pages, 460 schemas** +**Source:** `packages/spec/src/api/` · **Import:** `@objectstack/spec/api` · **34 pages, 463 schemas** REST contracts, endpoints, routing, realtime, batch, discovery. @@ -71,7 +71,7 @@ REST contracts, endpoints, routing, realtime, batch, discovery. | :--- | :--- | | [`analytics.zod.ts`](/docs/references/api/analytics) | `AnalyticsEndpoint`, `AnalyticsMetadataResponse`, `AnalyticsQueryRequest`, `AnalyticsResultResponse`, `AnalyticsSqlResponse`, `DatasetCompareTo`, `DatasetSelection`, `DatasetTotals`, `GetAnalyticsMetaRequest` | | [`app-bundle.zod.ts`](/docs/references/api/app-bundle) | `AppMetadataBundleResponse` | -| [`auth.zod.ts`](/docs/references/api/auth) | `AuthProvider`, `LoginRequest`, `LoginType`, `RefreshTokenRequest`, `RegisterRequest`, `Session`, `SessionResponse`, `SessionUser`, `UserProfileResponse` | +| [`auth.zod.ts`](/docs/references/api/auth) | `AuthProvider`, `LoginRequest`, `LoginType`, `MePermissionsAnonymousResponse`, `MePermissionsAuthenticatedResponse`, `MePermissionsResponse`, `RefreshTokenRequest`, `RegisterRequest`, `Session`, `SessionResponse`, `SessionUser`, `UserProfileResponse` | | [`auth-endpoints.zod.ts`](/docs/references/api/auth-endpoints) | `AuthEndpoint`, `AuthFeaturesConfig`, `AuthProviderInfo`, `DeviceRequestResponse`, `DeviceTokenResponse`, `EmailPasswordConfigPublic`, `GetAuthConfigResponse` | | [`automation-api.zod.ts`](/docs/references/api/automation-api) | `AutomationApiErrorCode`, `AutomationFlowPathParams`, `AutomationRunPathParams`, `CloneFlowRequest`, `CloneFlowResponse`, `CreateFlowRequest`, `CreateFlowResponse`, `DeleteFlowRequest`, `DeleteFlowResponse`, `FlowFailureDetails`, `FlowInnerRefusal`, `GetFlowRequest`, `GetFlowResponse`, `GetRunRequest`, `GetRunResponse`, `ListRunsRequest`, `ListRunsResponse`, `ResumeFailureDetails`, `ToggleFlowRequest`, `ToggleFlowResponse`, `TriggerFlowRequest`, `TriggerFlowResponse`, `UpdateFlowRequest`, `UpdateFlowResponse` | | [`batch.zod.ts`](/docs/references/api/batch) | `BatchConfig`, `BatchOperationResult`, `BatchOperationType`, `BatchOptions`, `BatchRecord`, `BatchUpdateRequest`, `BatchUpdateResponse`, `CrossObjectBatchDroppedFields`, `CrossObjectBatchOperation`, `CrossObjectBatchRequest`, `CrossObjectBatchResponse`, `DeleteManyRequest`, `UpdateManyRecord`, `UpdateManyRequest` | diff --git a/docs/audits/2026-07-unknown-key-strictness-ledger.counts/api.md b/docs/audits/2026-07-unknown-key-strictness-ledger.counts/api.md index 6f0f8431d55..026d1041d08 100644 --- a/docs/audits/2026-07-unknown-key-strictness-ledger.counts/api.md +++ b/docs/audits/2026-07-unknown-key-strictness-ledger.counts/api.md @@ -19,4 +19,4 @@ hand-patch a number here** — fix the code or the verdict and regenerate. | Dir | Sites | |---|---| -| `api/` | 474 | +| `api/` | 477 | diff --git a/packages/plugins/plugin-hono-server/src/current-user-endpoints-has-any-queue.test.ts b/packages/plugins/plugin-hono-server/src/current-user-endpoints-has-any-queue.test.ts index d6fd0563289..d2f7d982c8a 100644 --- a/packages/plugins/plugin-hono-server/src/current-user-endpoints-has-any-queue.test.ts +++ b/packages/plugins/plugin-hono-server/src/current-user-endpoints-has-any-queue.test.ts @@ -32,6 +32,7 @@ import { describe, it, expect, vi } from 'vitest'; import { Hono } from 'hono'; import { OWNERSHIP_QUEUE_SERVICE } from '@objectstack/spec/contracts'; +import { MePermissionsResponseSchema } from '@objectstack/spec/api'; import { registerCurrentUserEndpoints } from './current-user-endpoints'; const ME_PERMISSIONS = '/api/v1/auth/me/permissions'; @@ -116,10 +117,19 @@ function mount( return { app, warn, hasAnyQueue }; } +/** + * Read the body, and hold it to the declared contract: every body this file + * provokes (full, degraded, closed, anonymous) parses through the STRICT + * `MePermissionsResponseSchema`, so a key the handler sends that the spec does + * not declare reds here. + */ async function read(app: Hono) { const res = await app.request(`http://localhost${ME_PERMISSIONS}`); expect(res.status).toBe(200); - return res.json(); + const body = await res.json(); + const parsed = MePermissionsResponseSchema.safeParse(body); + expect(parsed.success ? [] : parsed.error.issues).toEqual([]); + return body; } describe('[#28508] /auth/me/permissions carries hasAnyQueue from the ownership-queue service', () => { diff --git a/packages/plugins/plugin-hono-server/src/current-user-endpoints.ts b/packages/plugins/plugin-hono-server/src/current-user-endpoints.ts index e95ce1bcb86..27850db1cc3 100644 --- a/packages/plugins/plugin-hono-server/src/current-user-endpoints.ts +++ b/packages/plugins/plugin-hono-server/src/current-user-endpoints.ts @@ -50,11 +50,13 @@ import { type RequestAuthService, } from '@objectstack/core'; import type { ExecutionContext } from '@objectstack/spec/kernel'; +import type { MePermissionsAuthenticatedResponse } from '@objectstack/spec/api'; import { preferredLocaleFromHeader } from '@objectstack/spec/system'; import { OWNERSHIP_QUEUE_SERVICE, type IMetadataService, type IObjectQLEngine, + type IOwnershipQueueService, type ISecurityService, type Logger, } from '@objectstack/spec/contracts'; @@ -308,15 +310,10 @@ function closedMePermissionsGrants(): MePermissionsGrants { } /** - * [#28508] The ownership-queue service's one "any queue at all?" answer, as - * this handler reads it. Not yet on the spec contract - * (`IOwnershipQueueService`), which gains it with this payload's response - * schema; until then the method is optional here, like the contract's own - * `declaredQueueNamed`. + * [#28508] The slice of the ownership-queue contract this handler reads: its + * one "any queue at all?" answer. */ -interface AnyQueueReader { - hasAnyQueue?: (organizationId: string | null) => Promise; -} +type AnyQueueReader = Pick; /** * [#28508] Does the caller's organization have at least one ownership queue, @@ -344,9 +341,12 @@ async function organizationHasAnyQueue( ): Promise { try { const service = (() => { - try { return ctx.getService(OWNERSHIP_QUEUE_SERVICE) ?? null; } + try { return ctx.getService>(OWNERSHIP_QUEUE_SERVICE) ?? null; } catch { return null; } })(); + // The contract requires the method, but the locator hands back + // whatever a host registered under the name, which may be an + // implementation older than the contract: that one answers `false`. const hasAnyQueue = service?.hasAnyQueue; if (typeof hasAnyQueue !== 'function') return false; return (await hasAnyQueue.call(service, organizationId)) === true; @@ -373,7 +373,7 @@ async function organizationHasAnyQueue( function mePermissionsBody( identity: { userId: string; tenantId: string | null }, grants: MePermissionsGrants, -) { +): MePermissionsAuthenticatedResponse { return { authenticated: true as const, userId: identity.userId, diff --git a/packages/plugins/plugin-sharing/src/ownership-queue-service.ts b/packages/plugins/plugin-sharing/src/ownership-queue-service.ts index c4217df2c78..0cd711c0ef9 100644 --- a/packages/plugins/plugin-sharing/src/ownership-queue-service.ts +++ b/packages/plugins/plugin-sharing/src/ownership-queue-service.ts @@ -215,9 +215,8 @@ export class OwnershipQueueService implements IOwnershipQueueService { * * The session payload's one boolean (`/auth/me/permissions` `hasAnyQueue`): * the console's list toolbar asks it once per session, for a member who may - * not read `sys_queue` themselves. It discloses no name and no count. Not - * yet on the spec contract, which gains it with the payload's response - * schema. + * not read `sys_queue` themselves. It discloses no name and no count. The + * contract states it on {@link IOwnershipQueueService.hasAnyQueue}. */ async hasAnyQueue(organizationId: string | null): Promise { const engine = this.engine(); diff --git a/packages/spec/api-surface/api.json b/packages/spec/api-surface/api.json index 95b10f2bc9f..7fc574e19ff 100644 --- a/packages/spec/api-surface/api.json +++ b/packages/spec/api-surface/api.json @@ -647,6 +647,12 @@ "MarkNotificationsReadRequestSchema (const)", "MarkNotificationsReadResponse (type)", "MarkNotificationsReadResponseSchema (const)", + "MePermissionsAnonymousResponse (type)", + "MePermissionsAnonymousResponseSchema (const)", + "MePermissionsAuthenticatedResponse (type)", + "MePermissionsAuthenticatedResponseSchema (const)", + "MePermissionsResponse (type)", + "MePermissionsResponseSchema (const)", "MetadataAuthoringClient (type)", "MetadataAuthoringClientSchema (const)", "MetadataAuthoringSurface (type)", diff --git a/packages/spec/authorable-surface/api.json b/packages/spec/authorable-surface/api.json index 5fc2984b66e..0053ba102cf 100644 --- a/packages/spec/authorable-surface/api.json +++ b/packages/spec/authorable-surface/api.json @@ -1092,6 +1092,17 @@ "api/MarkNotificationsReadRequest:ids", "api/MarkNotificationsReadResponse:readCount", "api/MarkNotificationsReadResponse:success", + "api/MePermissionsAnonymousResponse:authenticated", + "api/MePermissionsAuthenticatedResponse:authenticated", + "api/MePermissionsAuthenticatedResponse:fields", + "api/MePermissionsAuthenticatedResponse:hasAnyQueue", + "api/MePermissionsAuthenticatedResponse:objects", + "api/MePermissionsAuthenticatedResponse:permissionSets", + "api/MePermissionsAuthenticatedResponse:positions", + "api/MePermissionsAuthenticatedResponse:systemPermissions", + "api/MePermissionsAuthenticatedResponse:tabPermissions", + "api/MePermissionsAuthenticatedResponse:tenantId", + "api/MePermissionsAuthenticatedResponse:userId", "api/MetadataAuthoringClient:id", "api/MetadataAuthoringClient:name", "api/MetadataBulkRegisterRequest:continueOnError", diff --git a/packages/spec/declaration-map/api.json b/packages/spec/declaration-map/api.json index b60c10ce26d..3cb01388007 100644 --- a/packages/spec/declaration-map/api.json +++ b/packages/spec/declaration-map/api.json @@ -475,6 +475,12 @@ "MarkNotificationsReadRequestSchema": "api/MarkNotificationsReadRequest", "MarkNotificationsReadResponse": "api/MarkNotificationsReadResponse", "MarkNotificationsReadResponseSchema": "api/MarkNotificationsReadResponse", + "MePermissionsAnonymousResponse": "api/MePermissionsAnonymousResponse", + "MePermissionsAnonymousResponseSchema": "api/MePermissionsAnonymousResponse", + "MePermissionsAuthenticatedResponse": "api/MePermissionsAuthenticatedResponse", + "MePermissionsAuthenticatedResponseSchema": "api/MePermissionsAuthenticatedResponse", + "MePermissionsResponse": "api/MePermissionsResponse", + "MePermissionsResponseSchema": "api/MePermissionsResponse", "MetadataAuthoringClient": "api/MetadataAuthoringClient", "MetadataAuthoringClientSchema": "api/MetadataAuthoringClient", "MetadataAuthoringSurface": "api/MetadataAuthoringSurface", diff --git a/packages/spec/export-origins/api.json b/packages/spec/export-origins/api.json index 73133f764d9..336781c7b53 100644 --- a/packages/spec/export-origins/api.json +++ b/packages/spec/export-origins/api.json @@ -622,6 +622,12 @@ "MarkNotificationsReadRequestSchema": "src/api/protocol.zod.ts#MarkNotificationsReadRequestSchema (const)", "MarkNotificationsReadResponse": "src/api/protocol.zod.ts#MarkNotificationsReadResponse (type)", "MarkNotificationsReadResponseSchema": "src/api/protocol.zod.ts#MarkNotificationsReadResponseSchema (const)", + "MePermissionsAnonymousResponse": "src/api/auth.zod.ts#MePermissionsAnonymousResponse (type)", + "MePermissionsAnonymousResponseSchema": "src/api/auth.zod.ts#MePermissionsAnonymousResponseSchema (const)", + "MePermissionsAuthenticatedResponse": "src/api/auth.zod.ts#MePermissionsAuthenticatedResponse (type)", + "MePermissionsAuthenticatedResponseSchema": "src/api/auth.zod.ts#MePermissionsAuthenticatedResponseSchema (const)", + "MePermissionsResponse": "src/api/auth.zod.ts#MePermissionsResponse (type)", + "MePermissionsResponseSchema": "src/api/auth.zod.ts#MePermissionsResponseSchema (const)", "MetadataAuthoringClient": "src/api/protocol.zod.ts#MetadataAuthoringClient (type)", "MetadataAuthoringClientSchema": "src/api/protocol.zod.ts#MetadataAuthoringClientSchema (const)", "MetadataAuthoringSurface": "src/api/protocol.zod.ts#MetadataAuthoringSurface (type)", diff --git a/packages/spec/json-schema.manifest/api.json b/packages/spec/json-schema.manifest/api.json index 9a261b3e900..41ae5600d9d 100644 --- a/packages/spec/json-schema.manifest/api.json +++ b/packages/spec/json-schema.manifest/api.json @@ -261,6 +261,9 @@ "api/MarkAllNotificationsReadResponse", "api/MarkNotificationsReadRequest", "api/MarkNotificationsReadResponse", + "api/MePermissionsAnonymousResponse", + "api/MePermissionsAuthenticatedResponse", + "api/MePermissionsResponse", "api/MetadataAuthoringClient", "api/MetadataAuthoringSurface", "api/MetadataBulkRegisterRequest", diff --git a/packages/spec/scripts/lib/skill-map-guards.ts b/packages/spec/scripts/lib/skill-map-guards.ts index bc5d4a416a7..7d6188cfd43 100644 --- a/packages/spec/scripts/lib/skill-map-guards.ts +++ b/packages/spec/scripts/lib/skill-map-guards.ts @@ -280,6 +280,28 @@ export const TRANSITIVE_ALLOWLIST: Record = { // declaration those keys must match -- lives here. 'data/field.zod.ts', ], + // The closure as it stood before #28508. `api/auth.zod.ts` (core) gained one + // import edge then -- `security/permission.zod.ts`, for the effective object + // permissions `MePermissionsAuthenticatedResponseSchema` carries -- and that + // edge alone dragged 22 pointers into this index (fields, objects, views, + // actions, RLS, expressions) that the objectstack-api SKILL.md does not teach. + // These are the transitive pointers the index already published; a new one + // joins by a row here, not by an import edge. + 'objectstack-api': [ + 'api/contract.zod.ts', + 'api/error-code-ledger.zod.ts', + 'api/realtime-shared.zod.ts', + 'data/data-engine.zod.ts', + 'data/filter.zod.ts', + 'data/query.zod.ts', + 'kernel/execution-context.zod.ts', + 'kernel/metadata-protection.zod.ts', + 'security/explain.zod.ts', + 'shared/duration.zod.ts', + 'shared/epoch.zod.ts', + 'shared/http.zod.ts', + 'shared/suggestions.zod.ts', + ], }; /** Whether a `packages/spec/src`-relative path exists on disk; injected so the guard stays pure. */ diff --git a/packages/spec/src/api/auth.zod.ts b/packages/spec/src/api/auth.zod.ts index b06b60a5145..59a0e9f0d23 100644 --- a/packages/spec/src/api/auth.zod.ts +++ b/packages/spec/src/api/auth.zod.ts @@ -2,6 +2,7 @@ import { z } from 'zod'; import { BaseResponseSchema } from './contract.zod'; +import { EffectiveObjectPermissionSchema } from '../security/permission.zod'; /** * Authentication Service Protocol @@ -131,6 +132,82 @@ export const UserProfileResponseSchema = lazySchema(() => BaseResponseSchema.ext data: SessionUserSchema, })); +// ========================================== +// GET /auth/me/permissions +// ========================================== + +/** + * [#28508] The anonymous answer of `GET /auth/me/permissions`: a caller with no + * session is told only that, at 200, so the console can tell "signed out" from + * a fault. Strict: nothing about any user is disclosed here. + */ +export const MePermissionsAnonymousResponseSchema = lazySchema(() => z.object({ + authenticated: z.literal(false).describe('No session: the caller is anonymous.'), +}).strict()); + +/** + * [#28508] The authenticated answer of `GET /auth/me/permissions`: the caller's + * effective permissions, resolved once by the server so the console never + * re-derives them (`@objectstack/plugin-hono-server`, `current-user-endpoints.ts`). + * + * Three bodies take this one shape: + * + * - **full** — a stack with SecurityPlugin: every key present; + * - **closed** — the server knew the caller but failed to compute the grants: + * every key present and empty, `hasAnyQueue: false`; + * - **degraded** — no SecurityPlugin (or no metadata service): `systemPermissions` + * and `tabPermissions` are ABSENT on purpose. Absent means "unknown", which + * the console fails open on, because there the server enforces nothing + * either; present-and-empty means "none". The two must never be conflated. + * + * Strict: an undeclared key is refused, so a key the server starts sending is + * declared here first (`Clause-②`). + * + * Not to be confused with `GetEffectivePermissionsResponseSchema` + * (`api/protocol.zod.ts`), the older, non-strict answer of the optional + * `getEffectivePermissions` protocol method: it declares only `objects` and + * `systemPermissions`, and no route serves it. This schema is the body + * `/auth/me/permissions` actually answers. + */ +export const MePermissionsAuthenticatedResponseSchema = lazySchema(() => z.object({ + authenticated: z.literal(true).describe('A session was resolved for the caller.'), + userId: z.string().describe('The caller\'s user id.'), + tenantId: z.string().nullable().describe('The caller\'s active organization id, or null when the session has none.'), + positions: z.array(z.string()).describe( + 'The security positions the caller holds (ADR-0068 D3): built-in identity names plus assigned positions, scope-resolved.', + ), + permissionSets: z.array(z.string()).describe('The names of the permission sets resolved for the caller.'), + objects: z.record(z.string(), EffectiveObjectPermissionSchema).describe( + 'The effective object permissions, keyed by object name — the map `current_user.can()` reads on the server.', + ), + fields: z.record(z.string(), z.object({ + readable: z.boolean().describe('The caller may read the field.'), + editable: z.boolean().describe('The caller may edit the field on some row (a row-conditioned entry is reported as its lock).'), + }).strict()).describe('The effective field permissions, keyed `object.field`.'), + systemPermissions: z.array(z.string()).optional().describe( + 'The effective system capabilities (ADR-0066). Absent only on the degraded body, where it means unknown; ' + + 'an empty array means none.', + ), + tabPermissions: z.record(z.string(), z.enum(['visible', 'hidden', 'default_on', 'default_off'])).optional().describe( + 'The effective app tab visibilities, keyed by app name. Absent only on the degraded body.', + ), + hasAnyQueue: z.boolean().describe( + '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.', + ), +}).strict()); + +/** + * [#28508] The response of `GET /auth/me/permissions`, discriminated by + * `authenticated`. The console reads it once per session. + */ +export const MePermissionsResponseSchema = lazySchema(() => z.discriminatedUnion('authenticated', [ + MePermissionsAnonymousResponseSchema, + MePermissionsAuthenticatedResponseSchema, +])); + export type AuthProvider = z.input; export type SessionUser = z.input; /** Post-parse shape of {@link SessionUser} — defaults applied, transforms run (ADR-0122). */ @@ -148,3 +225,6 @@ export type SessionResponseParsed = z.infer; export type UserProfileResponse = z.input; /** Post-parse shape of {@link UserProfileResponse} — defaults applied, transforms run (ADR-0122). */ export type UserProfileResponseParsed = z.infer; +export type MePermissionsAnonymousResponse = z.input; +export type MePermissionsAuthenticatedResponse = z.input; +export type MePermissionsResponse = z.input; diff --git a/packages/spec/src/api/me-permissions-response.test.ts b/packages/spec/src/api/me-permissions-response.test.ts new file mode 100644 index 00000000000..7f2c3f6899e --- /dev/null +++ b/packages/spec/src/api/me-permissions-response.test.ts @@ -0,0 +1,120 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. +// +// #28508: `GET /auth/me/permissions` has a declared, STRICT response contract. +// The console reads this body once per session for its whole permission layer, +// and since #28508 for `hasAnyQueue` too. A key the server starts sending is +// declared here first, and an undeclared one is refused rather than dropped. +// +// The handler's own bodies are parsed through this schema in +// plugin-hono-server's `current-user-endpoints-has-any-queue.test.ts`; this +// file pins the contract itself. + +import { describe, it, expect } from 'vitest'; +import { + MePermissionsResponseSchema, + MePermissionsAuthenticatedResponseSchema, + MePermissionsAnonymousResponseSchema, +} from './auth.zod'; + +/** The full body: a stack with SecurityPlugin. */ +const FULL = { + authenticated: true, + userId: 'usr_1', + tenantId: 'org_1', + positions: ['org_member', 'everyone'], + permissionSets: ['member_default'], + objects: { crm_account: { allowRead: true, allowEdit: true, apiOperations: ['get', 'list'] } }, + fields: { 'crm_account.revenue': { readable: true, editable: false } }, + systemPermissions: ['setup.access'], + tabPermissions: { sales: 'visible' }, + hasAnyQueue: true, +} as const; + +/** The closed body: the caller is known, the grants could not be computed. */ +const CLOSED = { + authenticated: true, + userId: 'usr_1', + tenantId: 'org_1', + positions: [], + permissionSets: [], + objects: {}, + fields: {}, + systemPermissions: [], + tabPermissions: {}, + hasAnyQueue: false, +} as const; + +/** The degraded body: no SecurityPlugin; capabilities unknown, so the two keys are absent. */ +const DEGRADED = { + authenticated: true, + userId: 'usr_1', + tenantId: null, + positions: ['everyone'], + permissionSets: [], + objects: {}, + fields: {}, + hasAnyQueue: false, +} as const; + +const issuesOf = (value: unknown) => { + const r = MePermissionsResponseSchema.safeParse(value); + return r.success ? [] : r.error.issues; +}; + +describe('[#28508] MePermissionsResponseSchema — the declared /auth/me/permissions body', () => { + it.each([ + ['anonymous', { authenticated: false }], + ['full', FULL], + ['closed', CLOSED], + ['degraded', DEGRADED], + ] as const)('the %s body parses', (_label, body) => { + expect(issuesOf(body)).toEqual([]); + }); + + it('hasAnyQueue is declared, and is a boolean', () => { + expect(MePermissionsAuthenticatedResponseSchema.shape.hasAnyQueue).toBeDefined(); + expect(MePermissionsResponseSchema.parse(FULL)).toMatchObject({ hasAnyQueue: true }); + expect(issuesOf({ ...FULL, hasAnyQueue: 'yes' }).map((i) => i.path.join('.'))).toEqual(['hasAnyQueue']); + expect(issuesOf({ ...FULL, hasAnyQueue: 2 }).map((i) => i.path.join('.'))).toEqual(['hasAnyQueue']); + }); + + it('hasAnyQueue is required on an authenticated body — every authenticated branch carries it', () => { + const { hasAnyQueue: _dropped, ...withoutIt } = FULL; + expect(issuesOf(withoutIt).map((i) => i.path.join('.'))).toEqual(['hasAnyQueue']); + }); + + it('an unknown sibling is refused on the authenticated body, not dropped', () => { + const issues = issuesOf({ ...FULL, queueNames: ['billing'] }); + expect(issues).toHaveLength(1); + expect(issues[0].code).toBe('unrecognized_keys'); + expect((issues[0] as { keys?: string[] }).keys).toEqual(['queueNames']); + }); + + it('an unknown sibling is refused on the anonymous body: nothing about a user rides on it', () => { + const issues = MePermissionsAnonymousResponseSchema.safeParse({ authenticated: false, hasAnyQueue: true }); + expect(issues.success).toBe(false); + expect(issues.success ? [] : issues.error.issues.map((i) => i.code)).toEqual(['unrecognized_keys']); + expect(issuesOf({ authenticated: false, userId: 'usr_1' }).map((i) => i.code)).toEqual(['unrecognized_keys']); + }); + + it('a field entry is closed too', () => { + const issues = issuesOf({ ...FULL, fields: { 'crm_account.revenue': { readable: true, editable: false, masked: true } } }); + expect(issues.map((i) => i.code)).toEqual(['unrecognized_keys']); + }); + + it('absent systemPermissions is the degraded body\'s "unknown"; present-and-empty is "none" — both parse, and stay distinct', () => { + const degraded = MePermissionsResponseSchema.parse(DEGRADED); + const closed = MePermissionsResponseSchema.parse(CLOSED); + expect('systemPermissions' in degraded).toBe(false); + expect(closed).toMatchObject({ systemPermissions: [] }); + }); + + it('a tab visibility outside the four is refused', () => { + expect(issuesOf({ ...FULL, tabPermissions: { sales: 'shown' } }).map((i) => i.path.join('.'))).toEqual(['tabPermissions.sales']); + }); + + it('the discriminator is the boolean `authenticated`: anything else matches no branch', () => { + expect(issuesOf({ authenticated: 'true' })).not.toEqual([]); + expect(issuesOf({})).not.toEqual([]); + }); +}); diff --git a/packages/spec/src/contracts/ownership-queue-service.ts b/packages/spec/src/contracts/ownership-queue-service.ts index 513167a14fd..9561fa195a8 100644 --- a/packages/spec/src/contracts/ownership-queue-service.ts +++ b/packages/spec/src/contracts/ownership-queue-service.ts @@ -109,6 +109,23 @@ export interface IOwnershipQueueService { */ queuesOf(userId: string, organizationId: string | null): Promise; + /** + * Does `organizationId` have at least one ownership queue? `true` exactly + * when some name would resolve there through {@link resolveQueue}: a + * declared queue the activation ledger leaves on, or an organization's + * `sys_queue` row that does not sit under a declared queue's name. + * + * The one answer `GET /auth/me/permissions` carries as `hasAnyQueue` + * (`MePermissionsAuthenticatedResponseSchema`, `@objectstack/spec/api`), so + * a member who may not read `sys_queue` can still tell whether a queue + * filter can ever return a row. It discloses no name and no count. + * + * Optional, as {@link declaredQueueNamed} is, so an implementation written + * before it still satisfies the contract: the route answers `false` when the + * registered service does not carry it. + */ + hasAnyQueue?(organizationId: string | null): Promise; + /** * The declared queue named `name`, whether or not the activation ledger * switches it off — `null` when no package declares it. Optional: the diff --git a/packages/spec/src/type-alias-convention.pin.test.ts b/packages/spec/src/type-alias-convention.pin.test.ts index f3ea947306c..1de234178af 100644 --- a/packages/spec/src/type-alias-convention.pin.test.ts +++ b/packages/spec/src/type-alias-convention.pin.test.ts @@ -285,7 +285,7 @@ import type * as M194 from './automation/assignment-rule.zod.js'; import type * as M195 from './security/queue.zod.js'; // --------------------------------------------------------------------------- -// 808 isomorphic aliases: `z.input` === `z.infer`, so no `XParsed` is declared. +// 811 isomorphic aliases: `z.input` === `z.infer`, so no `XParsed` is declared. // // That number is machine-checked, not hand-kept. The runtime companion at the // bottom of this file recomputes the pin count from the source and asserts that @@ -429,6 +429,9 @@ export type Iso_api_authEndpoints__EmailPasswordConfigPublicSchema = Assert, z.infer< typeof M13.AuthProvider > >>; export type Iso_api_auth__LoginType = Assert, z.infer< typeof M13.LoginType > >>; +export type Iso_api_auth__MePermissionsAnonymousResponseSchema = Assert, z.infer< typeof M13.MePermissionsAnonymousResponseSchema > >>; +export type Iso_api_auth__MePermissionsAuthenticatedResponseSchema = Assert, z.infer< typeof M13.MePermissionsAuthenticatedResponseSchema > >>; +export type Iso_api_auth__MePermissionsResponseSchema = Assert, z.infer< typeof M13.MePermissionsResponseSchema > >>; export type Iso_api_auth__RefreshTokenRequestSchema = Assert, z.infer< typeof M13.RefreshTokenRequestSchema > >>; export type Iso_api_auth__RegisterRequestSchema = Assert, z.infer< typeof M13.RegisterRequestSchema > >>; export type Iso_api_auth__SessionSchema = Assert, z.infer< typeof M13.SessionSchema > >>; @@ -1742,7 +1745,7 @@ describe('ADR-0122 type-alias convention', () => { // this title and the section header above the pin list — are now asserted // against the recomputed count below, so neither can go stale without a red // test naming it. - it('still declares all 808 isomorphic pins', () => { + it('still declares all 811 isomorphic pins', () => { // The truth of each pin is proved by tsc, not here — an `Assert>` // that stops holding is a compile error with the alias named. What tsc // cannot notice is a pin that was DELETED: removing the assertion removes @@ -2716,7 +2719,11 @@ describe('ADR-0122 type-alias convention', () => { // shape, no default and no transform // (Iso_shared_connectorAuth__ConnectorInstanceSignedAlipayV3AuthSchema). // +1 added. - expect(pins).toHaveLength(808); + // 808 -> 811 is #28508: the `GET /auth/me/permissions` response contract + // (`api/auth.zod.ts`), strict objects with no default and no transform — + // the anonymous branch, the authenticated branch and their discriminated + // union (Iso_api_auth__MePermissions*ResponseSchema). +3 added. + expect(pins).toHaveLength(811); // The count is stated in PROSE twice as well — this case's title and the // section header above the pin list — and until commit c6b05c76a nothing read either diff --git a/packages/spec/variant-docs.json b/packages/spec/variant-docs.json index 4ec7c340e61..a8aabe88ccf 100644 --- a/packages/spec/variant-docs.json +++ b/packages/spec/variant-docs.json @@ -188,6 +188,12 @@ "content/docs/automation/flows.mdx" ], "note": "The automation engine's verdict the flow meta read serves beside a stored flow (#23316). Not authored — the engine answers it — but GOVERNED rather than exempt: a reader acts on each state differently, and automation/flows.mdx ('Discovery & Registration') names all three in its state table." + }, + { + "key": "authenticated:false|true", + "label": "/auth/me/permissions response body", + "exempt": "not-authorable", + "reason": "Wire response of GET /auth/me/permissions (#28508): the server answers the anonymous or the authenticated body from the session, and nobody writes either into metadata. content/docs/references/api/auth.mdx is generated from MePermissionsResponseSchema." } ] } diff --git a/packages/verify/src/me-permissions-has-any-queue.test.ts b/packages/verify/src/me-permissions-has-any-queue.test.ts index e57b53f9f0e..1b4982cd3c0 100644 --- a/packages/verify/src/me-permissions-has-any-queue.test.ts +++ b/packages/verify/src/me-permissions-has-any-queue.test.ts @@ -32,6 +32,7 @@ import { describe, it, expect, beforeAll, afterAll } from 'vitest'; import { defineStack } from '@objectstack/spec'; import { ObjectSchema, Field } from '@objectstack/spec/data'; import { OWNERSHIP_QUEUE_SERVICE } from '@objectstack/spec/contracts'; +import { MePermissionsResponseSchema } from '@objectstack/spec/api'; import { bootStack, type VerifyStack } from './harness.js'; import { warmUpBoot } from './boot-warm-up.testkit.js'; @@ -78,7 +79,12 @@ const stackWithDeclaredQueue = () => async function hasAnyQueueAs(stack: VerifyStack, token: string): Promise { const res = await stack.apiAs(token, 'GET', '/auth/me/permissions'); expect(res.status).toBe(200); - const body = (await res.json()) as { authenticated?: unknown; hasAnyQueue?: unknown }; + const raw: unknown = await res.json(); + // The real full-stack body holds to the declared STRICT contract: an + // undeclared key the handler sends reds here. + const parsed = MePermissionsResponseSchema.safeParse(raw); + expect(parsed.success ? [] : parsed.error.issues).toEqual([]); + const body = raw as { authenticated?: unknown; hasAnyQueue?: unknown }; expect(body.authenticated).toBe(true); return body.hasAnyQueue; } diff --git a/ui/apps/console/src/__tests__/App.homePermissions-12700.test.tsx b/ui/apps/console/src/__tests__/App.homePermissions-12700.test.tsx index a2b135b3b01..bd31b1c87db 100644 --- a/ui/apps/console/src/__tests__/App.homePermissions-12700.test.tsx +++ b/ui/apps/console/src/__tests__/App.homePermissions-12700.test.tsx @@ -36,7 +36,7 @@ import { useContext, type ReactNode } from 'react'; import { useNavigate } from 'react-router-dom'; import { MetadataCtx, type MetadataContextValue } from '@object-ui/react'; import { __resetVisibilityPredicateWarnings } from '../../../../packages/react/src/utils/visibilityDiagnostic'; -import { usePermissions, type MePermissionsResponse } from '@object-ui/permissions'; +import { usePermissions, type MePermissionsAuthenticatedResponse } from '@object-ui/permissions'; // Not on the package's entry: the context object itself, read by its module id // (`@object-ui/permissions` resolves to `packages/permissions/src`, so this is // the same module instance the provider writes). @@ -80,12 +80,13 @@ const METADATA: MetadataContextValue = { getTypeStatus: () => 'ready', } as unknown as MetadataContextValue; -function permissionsAnswer(): MePermissionsResponse { +function permissionsAnswer(): MePermissionsAuthenticatedResponse { return { authenticated: true, userId: 'u_agent', tenantId: null, - roles: [], + positions: [], + hasAnyQueue: false, permissionSets: ['service_agent'], systemPermissions: ['studio.access'], objects: { @@ -94,7 +95,7 @@ function permissionsAnswer(): MePermissionsResponse { crm_opportunity: { allowRead: net.opportunityRead }, }, fields: {}, - } as MePermissionsResponse; + } as MePermissionsAuthenticatedResponse; } function respond(input: unknown): Promise { diff --git a/ui/apps/console/src/components/studioEntry.ts b/ui/apps/console/src/components/studioEntry.ts index 781c106bc4c..c59f0f3ddc8 100644 --- a/ui/apps/console/src/components/studioEntry.ts +++ b/ui/apps/console/src/components/studioEntry.ts @@ -191,7 +191,8 @@ export function useStudioEntry(options?: { } const json = (await res.json()) as MePermissionsResponse; if (cancelled) return; - const reported = json?.systemPermissions; + // [objectstack#28508] Only the authenticated body carries systemPermissions. + const reported = json?.authenticated === true ? json.systemPermissions : undefined; setState({ status: 'loaded', systemPermissions: Array.isArray(reported) ? reported : undefined, diff --git a/ui/apps/console/src/pages/consoleFaces.displayLocale-9909.test.tsx b/ui/apps/console/src/pages/consoleFaces.displayLocale-9909.test.tsx index 4ab86582b06..9f4dc30cfc7 100644 --- a/ui/apps/console/src/pages/consoleFaces.displayLocale-9909.test.tsx +++ b/ui/apps/console/src/pages/consoleFaces.displayLocale-9909.test.tsx @@ -34,7 +34,7 @@ import '@testing-library/jest-dom/vitest'; import { describe, it, expect, vi, afterEach } from 'vitest'; import { render, screen, cleanup } from '@testing-library/react'; import { MemoryRouter, Routes, Route } from 'react-router-dom'; -import { MePermissionsProvider, type MePermissionsResponse } from '@object-ui/permissions'; +import { MePermissionsProvider, type MePermissionsAuthenticatedResponse } from '@object-ui/permissions'; import { LocalizationProvider } from '@object-ui/i18n'; import { isMachineLocale, recordLocaleArgumentsAsync } from '@object-ui/test-support'; @@ -123,12 +123,13 @@ afterEach(() => { vi.unstubAllGlobals(); }); -function permissions(): MePermissionsResponse { +function permissions(): MePermissionsAuthenticatedResponse { return { authenticated: true, userId: 'u_1', tenantId: 't_1', - roles: [], + positions: [], + hasAnyQueue: false, permissionSets: [], objects: {}, fields: {}, diff --git a/ui/apps/console/src/pages/settings/__tests__/WorkspaceTimezonePrompt.afterBootPaint-12980.test.tsx b/ui/apps/console/src/pages/settings/__tests__/WorkspaceTimezonePrompt.afterBootPaint-12980.test.tsx index 38145348677..9cf1171471b 100644 --- a/ui/apps/console/src/pages/settings/__tests__/WorkspaceTimezonePrompt.afterBootPaint-12980.test.tsx +++ b/ui/apps/console/src/pages/settings/__tests__/WorkspaceTimezonePrompt.afterBootPaint-12980.test.tsx @@ -14,7 +14,7 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { act, cleanup, render, screen } from '@testing-library/react'; import { I18nProvider } from '@object-ui/i18n'; -import { MePermissionsProvider, type MePermissionsResponse } from '@object-ui/permissions'; +import { MePermissionsProvider, type MePermissionsResponse, type MePermissionsAuthenticatedResponse } from '@object-ui/permissions'; const { AUTH, toastError, toastSuccess } = vi.hoisted(() => ({ AUTH: { user: { id: 'u_admin' }, activeOrganization: { id: 'org_1' } }, @@ -114,11 +114,12 @@ const settingsCalls = (method: string, url = '/api/settings/localization') => fetchMock.mock.calls.filter(([u, init]) => u === url && ((init as RequestInit | undefined)?.method ?? 'GET') === method); function permissionsPayload(systemPermissions: string[] | undefined): MePermissionsResponse { - const base: MePermissionsResponse = { + const base: MePermissionsAuthenticatedResponse = { authenticated: true, userId: 'u_admin', tenantId: 'org_1', - roles: [], + positions: [], + hasAnyQueue: false, permissionSets: [], objects: {}, fields: {}, diff --git a/ui/apps/console/src/pages/settings/__tests__/WorkspaceTimezonePrompt.test.tsx b/ui/apps/console/src/pages/settings/__tests__/WorkspaceTimezonePrompt.test.tsx index ea0240d8ca0..fa509024061 100644 --- a/ui/apps/console/src/pages/settings/__tests__/WorkspaceTimezonePrompt.test.tsx +++ b/ui/apps/console/src/pages/settings/__tests__/WorkspaceTimezonePrompt.test.tsx @@ -24,7 +24,7 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { act, cleanup, fireEvent, render, screen, waitFor, within } from '@testing-library/react'; import { I18nProvider } from '@object-ui/i18n'; -import { MePermissionsProvider, type MePermissionsResponse } from '@object-ui/permissions'; +import { MePermissionsProvider, type MePermissionsResponse, type MePermissionsAuthenticatedResponse } from '@object-ui/permissions'; const { AUTH, toastError, toastSuccess } = vi.hoisted(() => ({ AUTH: { user: { id: 'u_admin' }, activeOrganization: { id: 'org_1' } }, @@ -130,11 +130,12 @@ const settingsCalls = (method: string, url = '/api/settings/localization') => fetchMock.mock.calls.filter(([u, init]) => u === url && ((init as RequestInit | undefined)?.method ?? 'GET') === method); function permissionsPayload(systemPermissions: string[] | undefined): MePermissionsResponse { - const base: MePermissionsResponse = { + const base: MePermissionsAuthenticatedResponse = { authenticated: true, userId: 'u_admin', tenantId: 'org_1', - roles: [], + positions: [], + hasAnyQueue: false, permissionSets: [], objects: {}, fields: {}, diff --git a/ui/apps/console/src/pages/system/ApprovalsInboxPage.displayLocale-10375.test.tsx b/ui/apps/console/src/pages/system/ApprovalsInboxPage.displayLocale-10375.test.tsx index 572e5744e19..9a4a8d44a14 100644 --- a/ui/apps/console/src/pages/system/ApprovalsInboxPage.displayLocale-10375.test.tsx +++ b/ui/apps/console/src/pages/system/ApprovalsInboxPage.displayLocale-10375.test.tsx @@ -32,7 +32,7 @@ import '@testing-library/jest-dom/vitest'; import { afterEach, describe, expect, it, vi } from 'vitest'; import { cleanup, render, screen } from '@testing-library/react'; import { MemoryRouter, Route, Routes } from 'react-router-dom'; -import { MePermissionsProvider, type MePermissionsResponse } from '@object-ui/permissions'; +import { MePermissionsProvider, type MePermissionsAuthenticatedResponse } from '@object-ui/permissions'; import { I18nProvider, LocalizationProvider } from '@object-ui/i18n'; import { isMachineLocale, recordLocaleArgumentsAsync } from '@object-ui/test-support'; @@ -99,12 +99,13 @@ import { ApprovalsInboxPage } from './ApprovalsInboxPage'; afterEach(() => cleanup()); -function permissions(): MePermissionsResponse { +function permissions(): MePermissionsAuthenticatedResponse { return { authenticated: true, userId: 'u_1', tenantId: 't_1', - roles: [], + positions: [], + hasAnyQueue: false, permissionSets: [], objects: {}, fields: {}, diff --git a/ui/apps/console/src/pages/system/ApprovalsInboxPage.queueHiddenAmount.test.tsx b/ui/apps/console/src/pages/system/ApprovalsInboxPage.queueHiddenAmount.test.tsx index 28bd4b0bee1..f509f7198bb 100644 --- a/ui/apps/console/src/pages/system/ApprovalsInboxPage.queueHiddenAmount.test.tsx +++ b/ui/apps/console/src/pages/system/ApprovalsInboxPage.queueHiddenAmount.test.tsx @@ -75,7 +75,7 @@ import '@testing-library/jest-dom/vitest'; import { describe, it, expect, vi, beforeAll, beforeEach, afterEach } from 'vitest'; import { render, screen, cleanup, within, waitFor, fireEvent } from '@testing-library/react'; import { MemoryRouter, Routes, Route } from 'react-router-dom'; -import { MePermissionsProvider, type MePermissionsResponse } from '@object-ui/permissions'; +import { MePermissionsProvider, type MePermissionsAuthenticatedResponse } from '@object-ui/permissions'; const APP = 'com.objectstack.account'; const PURCHASE = 'showcase_purchase'; @@ -216,12 +216,13 @@ vi.mock('../../services/approvalsApi', async (importOriginal) => ({ // Imported after the mocks so the page picks them up. import { ApprovalsInboxPage } from './ApprovalsInboxPage'; -function permissionsPayload(): MePermissionsResponse { +function permissionsPayload(): MePermissionsAuthenticatedResponse { return { authenticated: true, userId: 'u_1', tenantId: 't_1', - roles: [], + positions: [], + hasAnyQueue: false, permissionSets: [], objects: {}, fields: {}, diff --git a/ui/apps/console/src/pages/system/ApprovalsInboxPage.readerLocaleLabels-26883.test.tsx b/ui/apps/console/src/pages/system/ApprovalsInboxPage.readerLocaleLabels-26883.test.tsx index 41730af18a5..e5f16cb1ac5 100644 --- a/ui/apps/console/src/pages/system/ApprovalsInboxPage.readerLocaleLabels-26883.test.tsx +++ b/ui/apps/console/src/pages/system/ApprovalsInboxPage.readerLocaleLabels-26883.test.tsx @@ -31,7 +31,7 @@ import { useEffect, useState, type ReactNode } from 'react'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { cleanup, fireEvent, render, screen, waitFor, within } from '@testing-library/react'; import { MemoryRouter, Route, Routes } from 'react-router-dom'; -import { MePermissionsProvider, type MePermissionsResponse } from '@object-ui/permissions'; +import { MePermissionsProvider, type MePermissionsAuthenticatedResponse } from '@object-ui/permissions'; import { I18nProvider } from '@object-ui/i18n'; const { ADAPTER, AUTH } = vi.hoisted(() => ({ @@ -123,12 +123,13 @@ afterEach(() => { vi.unstubAllGlobals(); }); -function permissions(): MePermissionsResponse { +function permissions(): MePermissionsAuthenticatedResponse { return { authenticated: true, userId: 'u_1', tenantId: 't_1', - roles: [], + positions: [], + hasAnyQueue: false, permissionSets: [], objects: {}, fields: {}, diff --git a/ui/apps/console/src/services/approvalRequestsDataSource.servedPageMemberPermissions-13324.test.tsx b/ui/apps/console/src/services/approvalRequestsDataSource.servedPageMemberPermissions-13324.test.tsx index e1df79707e8..0108e4a8881 100644 --- a/ui/apps/console/src/services/approvalRequestsDataSource.servedPageMemberPermissions-13324.test.tsx +++ b/ui/apps/console/src/services/approvalRequestsDataSource.servedPageMemberPermissions-13324.test.tsx @@ -46,7 +46,7 @@ vi.mock('sonner', () => ({ import { AdapterCtx, MetadataCtx, useAdapter } from '@object-ui/react'; import { I18nProvider, type I18nConfig } from '@object-ui/i18n'; -import { MePermissionsProvider, usePermissions, type MePermissionsResponse } from '@object-ui/permissions'; +import { MePermissionsProvider, usePermissions, type MePermissionsAuthenticatedResponse } from '@object-ui/permissions'; import { RecordDetailView } from '@object-ui/app-shell'; import { APPROVAL_ACTION_OBJECT, APPROVAL_REQUEST_OBJECT, routeApprovalRecordReads } from './approvalRequestsDataSource'; import { ApprovalRecordReads } from '../AppContent'; @@ -204,11 +204,12 @@ const SERVED_PAGE = { * approval object, so `checkField` takes its authenticated unknown-object * default (closed) for both. */ -const MEMBER_ANSWER: MePermissionsResponse = { +const MEMBER_ANSWER: MePermissionsAuthenticatedResponse = { authenticated: true, userId: 'u1', tenantId: 'org_1', - roles: ['member'], + positions: ['member'], + hasAnyQueue: false, permissionSets: ['member_default'], systemPermissions: [], objects: { diff --git a/ui/apps/console/src/services/approvalRequestsDataSource.servedPageSummary-12483.test.tsx b/ui/apps/console/src/services/approvalRequestsDataSource.servedPageSummary-12483.test.tsx index 749aeafdff2..9804f43fa28 100644 --- a/ui/apps/console/src/services/approvalRequestsDataSource.servedPageSummary-12483.test.tsx +++ b/ui/apps/console/src/services/approvalRequestsDataSource.servedPageSummary-12483.test.tsx @@ -302,10 +302,11 @@ function readerWithout(...denied: string[]): MePermissionsResponse { authenticated: true, userId: 'u1', tenantId: 't1', - roles: [], + positions: [], + hasAnyQueue: false, permissionSets: [], objects: { '*': all } as never, - fields: Object.fromEntries(denied.map((f) => [`invoice.${f}`, { readable: false }])), + fields: Object.fromEntries(denied.map((f) => [`invoice.${f}`, { readable: false, editable: false }])), systemPermissions: [], }; } diff --git a/ui/apps/console/src/services/approvalRequestsDataSource.servedPageSummaryAmount-12534.test.tsx b/ui/apps/console/src/services/approvalRequestsDataSource.servedPageSummaryAmount-12534.test.tsx index 7b54dcd6fd0..fa9baf83486 100644 --- a/ui/apps/console/src/services/approvalRequestsDataSource.servedPageSummaryAmount-12534.test.tsx +++ b/ui/apps/console/src/services/approvalRequestsDataSource.servedPageSummaryAmount-12534.test.tsx @@ -382,10 +382,11 @@ function readerWithout(...denied: string[]): MePermissionsResponse { authenticated: true, userId: 'u1', tenantId: 't1', - roles: [], + positions: [], + hasAnyQueue: false, permissionSets: [], objects: { '*': all } as never, - fields: Object.fromEntries(denied.map((f) => [`showcase_expense_report.${f}`, { readable: false }])), + fields: Object.fromEntries(denied.map((f) => [`showcase_expense_report.${f}`, { readable: false, editable: false }])), systemPermissions: [], }; } diff --git a/ui/packages/app-shell/src/console/__tests__/AppContent.declaredLanding-28450.test.tsx b/ui/packages/app-shell/src/console/__tests__/AppContent.declaredLanding-28450.test.tsx index 56eea698a88..62ef3b8e437 100644 --- a/ui/packages/app-shell/src/console/__tests__/AppContent.declaredLanding-28450.test.tsx +++ b/ui/packages/app-shell/src/console/__tests__/AppContent.declaredLanding-28450.test.tsx @@ -28,7 +28,7 @@ import { describe, it, expect, vi, beforeEach, type MockInstance } from 'vitest' import React from 'react'; import { render, screen } from '@testing-library/react'; import { MemoryRouter, Routes, Route, useLocation } from 'react-router-dom'; -import { MePermissionsProvider, type MePermissionsResponse } from '@object-ui/permissions'; +import { MePermissionsProvider, type MePermissionsAuthenticatedResponse } from '@object-ui/permissions'; // AGENTS.md §测试纪律 — the lazy page modules are stubbed so no unbounded // `import()` races a bounded `findBy` window. @@ -129,19 +129,20 @@ function LocationProbe() { } /** The member's `/me/permissions` answer: every object readable unless denied. */ -function permissionsDenying(...unreadable: string[]): MePermissionsResponse { +function permissionsDenying(...unreadable: string[]): MePermissionsAuthenticatedResponse { return { authenticated: true, userId: 'u_1', tenantId: null, - roles: [], + positions: [], + hasAnyQueue: false, permissionSets: [], objects: { '*': { allowRead: true }, ...Object.fromEntries(unreadable.map((name) => [name, { allowRead: false }])), }, fields: {}, - } as MePermissionsResponse; + } as MePermissionsAuthenticatedResponse; } function renderConsoleAt(initialUrl: string, permissions: Partial>) { diff --git a/ui/packages/app-shell/src/console/__tests__/AppContent.hasCapability-13083.test.tsx b/ui/packages/app-shell/src/console/__tests__/AppContent.hasCapability-13083.test.tsx index cf38e70ed3c..75a0baec780 100644 --- a/ui/packages/app-shell/src/console/__tests__/AppContent.hasCapability-13083.test.tsx +++ b/ui/packages/app-shell/src/console/__tests__/AppContent.hasCapability-13083.test.tsx @@ -33,7 +33,7 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import React from 'react'; import { render, cleanup, waitFor } from '@testing-library/react'; import { MemoryRouter, Routes, Route } from 'react-router-dom'; -import { MePermissionsProvider, type MePermissionsResponse } from '@object-ui/permissions'; +import { MePermissionsProvider, type MePermissionsAuthenticatedResponse } from '@object-ui/permissions'; // The reporter's dedupe `Set` is module state: reset it so every arm's // "nothing reported" is a reading, not a leftover from an earlier arm. import { __resetVisibilityPredicateWarnings } from '../../../../react/src/utils/visibilityDiagnostic'; @@ -130,12 +130,13 @@ vi.mock('@object-ui/react', async (importOriginal) => ({ import { AppContent } from '../AppContent'; -function payload(systemPermissions: string[]): MePermissionsResponse { +function payload(systemPermissions: string[]): MePermissionsAuthenticatedResponse { return { authenticated: true, userId: 'u1', tenantId: 'org_1', - roles: [], + positions: [], + hasAnyQueue: false, permissionSets: ['member'], systemPermissions, objects: { org_member: { allowRead: true, allowCreate: true, allowEdit: true, allowDelete: false } }, diff --git a/ui/packages/app-shell/src/console/__tests__/AppContent.landingMemberGates-12426.test.tsx b/ui/packages/app-shell/src/console/__tests__/AppContent.landingMemberGates-12426.test.tsx index 8cf32457b8d..a8ebabce665 100644 --- a/ui/packages/app-shell/src/console/__tests__/AppContent.landingMemberGates-12426.test.tsx +++ b/ui/packages/app-shell/src/console/__tests__/AppContent.landingMemberGates-12426.test.tsx @@ -34,7 +34,7 @@ import { dirname, join } from 'node:path'; import { fileURLToPath } from 'node:url'; import { render, screen } from '@testing-library/react'; import { MemoryRouter, Routes, Route, useLocation } from 'react-router-dom'; -import { MePermissionsProvider, type MePermissionsResponse } from '@object-ui/permissions'; +import { MePermissionsProvider, type MePermissionsAuthenticatedResponse } from '@object-ui/permissions'; // AGENTS.md §测试纪律 — the lazy page modules are stubbed so no unbounded // `import()` races a bounded `findBy` window. @@ -131,19 +131,20 @@ function LocationProbe() { } /** The member's `/me/permissions` answer: every object readable unless denied. */ -function permissionsDenying(...unreadable: string[]): MePermissionsResponse { +function permissionsDenying(...unreadable: string[]): MePermissionsAuthenticatedResponse { return { authenticated: true, userId: 'u_1', tenantId: null, - roles: [], + positions: [], + hasAnyQueue: false, permissionSets: [], objects: { '*': { allowRead: true }, ...Object.fromEntries(unreadable.map((name) => [name, { allowRead: false }])), }, fields: {}, - } as MePermissionsResponse; + } as MePermissionsAuthenticatedResponse; } function renderConsoleAt(initialUrl: string, permissions: Partial>) { diff --git a/ui/packages/app-shell/src/console/__tests__/AppContent.landingWaitsReads-13097.test.tsx b/ui/packages/app-shell/src/console/__tests__/AppContent.landingWaitsReads-13097.test.tsx index 50e2f77ae88..a02e362edce 100644 --- a/ui/packages/app-shell/src/console/__tests__/AppContent.landingWaitsReads-13097.test.tsx +++ b/ui/packages/app-shell/src/console/__tests__/AppContent.landingWaitsReads-13097.test.tsx @@ -139,7 +139,8 @@ const EVERYTHING_READABLE = { authenticated: true, userId: 'u_1', tenantId: null, - roles: [], + positions: [], + hasAnyQueue: false, permissionSets: [], objects: { '*': { allowRead: true } }, fields: {}, diff --git a/ui/packages/app-shell/src/console/__tests__/AppContent.noReadableObjectLanding-25338.test.tsx b/ui/packages/app-shell/src/console/__tests__/AppContent.noReadableObjectLanding-25338.test.tsx index 8a11293a530..605cfca4f1b 100644 --- a/ui/packages/app-shell/src/console/__tests__/AppContent.noReadableObjectLanding-25338.test.tsx +++ b/ui/packages/app-shell/src/console/__tests__/AppContent.noReadableObjectLanding-25338.test.tsx @@ -31,7 +31,7 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; import React from 'react'; import { render, screen } from '@testing-library/react'; import { MemoryRouter, Routes, Route, useLocation } from 'react-router-dom'; -import { MePermissionsProvider, type MePermissionsResponse } from '@object-ui/permissions'; +import { MePermissionsProvider, type MePermissionsAuthenticatedResponse } from '@object-ui/permissions'; // AGENTS.md §测试纪律 — the lazy page modules are stubbed so no unbounded // `import()` races a bounded `findBy` window. @@ -131,19 +131,20 @@ function LocationProbe() { } /** The member's `/me/permissions` answer: every object readable unless denied. */ -function permissionsDenying(...unreadable: string[]): MePermissionsResponse { +function permissionsDenying(...unreadable: string[]): MePermissionsAuthenticatedResponse { return { authenticated: true, userId: 'u_1', tenantId: null, - roles: [], + positions: [], + hasAnyQueue: false, permissionSets: [], objects: { '*': { allowRead: true }, ...Object.fromEntries(unreadable.map((name) => [name, { allowRead: false }])), }, fields: {}, - } as MePermissionsResponse; + } as MePermissionsAuthenticatedResponse; } function renderConsoleAt(initialUrl: string, permissions: Partial>) { diff --git a/ui/packages/app-shell/src/console/ai/__tests__/ExcelImportBar.importGrant-12082.test.tsx b/ui/packages/app-shell/src/console/ai/__tests__/ExcelImportBar.importGrant-12082.test.tsx index ee466f5fe44..4b7f1c32d75 100644 --- a/ui/packages/app-shell/src/console/ai/__tests__/ExcelImportBar.importGrant-12082.test.tsx +++ b/ui/packages/app-shell/src/console/ai/__tests__/ExcelImportBar.importGrant-12082.test.tsx @@ -21,7 +21,7 @@ import '@testing-library/jest-dom/vitest'; import * as React from 'react'; import { describe, it, expect, vi, beforeEach } from 'vitest'; import { render, cleanup, fireEvent, waitFor, screen, within, act } from '@testing-library/react'; -import { MePermissionsProvider, type MePermissionsResponse } from '@object-ui/permissions'; +import { MePermissionsProvider, type MePermissionsResponse, type MePermissionsAuthenticatedResponse } from '@object-ui/permissions'; const h = vi.hoisted(() => ({ /** The objects the bar lists as import targets. */ @@ -64,11 +64,12 @@ const SHAPES: Array<{ shape: string; bits: Record; create: bool ]; const FULL = SHAPES[3].bits; -const envelope = (objects: Record>): MePermissionsResponse => ({ +const envelope = (objects: Record>): MePermissionsAuthenticatedResponse => ({ authenticated: true, userId: 'u-pin', tenantId: null, - roles: ['member'], + positions: ['member'], + hasAnyQueue: false, permissionSets: ['member'], objects: objects as never, fields: {}, diff --git a/ui/packages/app-shell/src/console/home/__tests__/HomePage.aiStudioDisabled.test.tsx b/ui/packages/app-shell/src/console/home/__tests__/HomePage.aiStudioDisabled.test.tsx index 3046114c64d..f6397d9c377 100644 --- a/ui/packages/app-shell/src/console/home/__tests__/HomePage.aiStudioDisabled.test.tsx +++ b/ui/packages/app-shell/src/console/home/__tests__/HomePage.aiStudioDisabled.test.tsx @@ -57,7 +57,7 @@ import '@testing-library/jest-dom/vitest'; import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import React from 'react'; import { render, screen, cleanup } from '@testing-library/react'; -import { MePermissionsProvider, type MePermissionsResponse } from '@object-ui/permissions'; +import { MePermissionsProvider, type MePermissionsAuthenticatedResponse } from '@object-ui/permissions'; const navigateMock = vi.fn(); @@ -248,17 +248,18 @@ async function bootOnNoConfig() { * open and only the DEPLOYMENT flag can move the verdict. Without * `manage_metadata` here, every denial below would pass for the wrong reason. */ -const permissions = (): MePermissionsResponse => +const permissions = (): MePermissionsAuthenticatedResponse => ({ authenticated: true, userId: 'u1', tenantId: 'acme', - roles: ['org_owner', 'everyone'], + positions: ['org_owner', 'everyone'], + hasAnyQueue: false, permissionSets: ['organization_admin', 'member_default'], systemPermissions: ['manage_org_users', 'setup.access', 'setup.write', 'manage_metadata'], objects: {}, fields: {}, - }) as MePermissionsResponse; + }) as MePermissionsAuthenticatedResponse; function renderHome() { return render( diff --git a/ui/packages/app-shell/src/console/home/__tests__/HomePage.authoringCapabilityGate.test.tsx b/ui/packages/app-shell/src/console/home/__tests__/HomePage.authoringCapabilityGate.test.tsx index ac04b32b674..e688c5d4522 100644 --- a/ui/packages/app-shell/src/console/home/__tests__/HomePage.authoringCapabilityGate.test.tsx +++ b/ui/packages/app-shell/src/console/home/__tests__/HomePage.authoringCapabilityGate.test.tsx @@ -46,7 +46,7 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import React from 'react'; import { render, screen, cleanup } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -import { MePermissionsProvider, type MePermissionsResponse } from '@object-ui/permissions'; +import { MePermissionsProvider, type MePermissionsResponse, type MePermissionsAuthenticatedResponse } from '@object-ui/permissions'; const navigateMock = vi.fn(); @@ -222,17 +222,18 @@ afterEach(() => { * workspace owner. `systemPermissions` is present and NON-empty — it simply * does not contain `manage_metadata`. */ -const eePayload = (systemPermissions?: string[]): MePermissionsResponse => +const eePayload = (systemPermissions?: string[]): MePermissionsAuthenticatedResponse => ({ authenticated: true, userId: 'u1', tenantId: 'acme', - roles: ['org_owner', 'everyone'], + positions: ['org_owner', 'everyone'], + hasAnyQueue: false, permissionSets: ['organization_admin', 'member_default'], ...(systemPermissions ? { systemPermissions } : {}), objects: {}, fields: {}, - }) as MePermissionsResponse; + }) as MePermissionsAuthenticatedResponse; const OWNER_CAPS = ['manage_org_users', 'setup.access', 'setup.write']; diff --git a/ui/packages/app-shell/src/console/home/__tests__/HomePage.draftsReadGate-25894.test.tsx b/ui/packages/app-shell/src/console/home/__tests__/HomePage.draftsReadGate-25894.test.tsx index 734bfcdd4d1..c4759373c04 100644 --- a/ui/packages/app-shell/src/console/home/__tests__/HomePage.draftsReadGate-25894.test.tsx +++ b/ui/packages/app-shell/src/console/home/__tests__/HomePage.draftsReadGate-25894.test.tsx @@ -26,7 +26,7 @@ import '@testing-library/jest-dom/vitest'; import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import React from 'react'; import { act, render, screen, cleanup } from '@testing-library/react'; -import { MePermissionsProvider, type MePermissionsResponse } from '@object-ui/permissions'; +import { MePermissionsProvider, type MePermissionsResponse, type MePermissionsAuthenticatedResponse } from '@object-ui/permissions'; vi.mock('react-router-dom', async () => { const { createElement } = await import('react'); @@ -123,17 +123,18 @@ afterEach(() => { vi.unstubAllGlobals(); }); -const payload = (systemPermissions?: string[]): MePermissionsResponse => +const payload = (systemPermissions?: string[]): MePermissionsAuthenticatedResponse => ({ authenticated: true, userId: 'u1', tenantId: 'acme', - roles: ['everyone'], + positions: ['everyone'], + hasAnyQueue: false, permissionSets: ['member_default'], ...(systemPermissions ? { systemPermissions } : {}), objects: {}, fields: {}, - }) as MePermissionsResponse; + }) as MePermissionsAuthenticatedResponse; async function renderHome(permissions: MePermissionsResponse) { render( diff --git a/ui/packages/app-shell/src/console/home/__tests__/HomePage.marketplaceDisabled.test.tsx b/ui/packages/app-shell/src/console/home/__tests__/HomePage.marketplaceDisabled.test.tsx index c10b69a3fc2..10f5b4b8152 100644 --- a/ui/packages/app-shell/src/console/home/__tests__/HomePage.marketplaceDisabled.test.tsx +++ b/ui/packages/app-shell/src/console/home/__tests__/HomePage.marketplaceDisabled.test.tsx @@ -36,7 +36,7 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import React from 'react'; import { render, screen, cleanup } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -import { MePermissionsProvider, type MePermissionsResponse } from '@object-ui/permissions'; +import { MePermissionsProvider, type MePermissionsAuthenticatedResponse } from '@object-ui/permissions'; const navigateMock = vi.fn(); @@ -216,17 +216,18 @@ async function bootOnNoConfig() { } /** An admin who MAY author metadata — so only the marketplace gate is in play. */ -const permissions = (): MePermissionsResponse => +const permissions = (): MePermissionsAuthenticatedResponse => ({ authenticated: true, userId: 'u1', tenantId: 'acme', - roles: ['org_owner', 'everyone'], + positions: ['org_owner', 'everyone'], + hasAnyQueue: false, permissionSets: ['organization_admin', 'member_default'], systemPermissions: ['manage_org_users', 'setup.access', 'setup.write', 'manage_metadata'], objects: {}, fields: {}, - }) as MePermissionsResponse; + }) as MePermissionsAuthenticatedResponse; function renderHome() { return render( diff --git a/ui/packages/app-shell/src/hooks/__tests__/navEntryDataSource-13029.test.tsx b/ui/packages/app-shell/src/hooks/__tests__/navEntryDataSource-13029.test.tsx index a8e7b9aa948..f64b790222d 100644 --- a/ui/packages/app-shell/src/hooks/__tests__/navEntryDataSource-13029.test.tsx +++ b/ui/packages/app-shell/src/hooks/__tests__/navEntryDataSource-13029.test.tsx @@ -40,7 +40,7 @@ import '@object-ui/components'; import { SidebarProvider } from '@object-ui/components'; import { NavigationRenderer } from '@object-ui/layout'; import { SchemaRenderer, SchemaRendererContext, MetadataCtx, type ApiFetch } from '@object-ui/react'; -import { MePermissionsProvider, type MePermissionsResponse } from '@object-ui/permissions'; +import { MePermissionsProvider, type MePermissionsAuthenticatedResponse } from '@object-ui/permissions'; import { createI18n, I18nProvider } from '@object-ui/i18n'; import type { NavigationItem } from '@object-ui/types'; import { ExpressionProvider } from '../../providers/ExpressionProvider'; @@ -52,11 +52,12 @@ import '../../views/nav-menu-renderer'; type Surface = 'sidebar' | 'nav:menu' | 'palette'; const SURFACES: Surface[] = ['sidebar', 'nav:menu', 'palette']; -const ANSWER: MePermissionsResponse = { +const ANSWER: MePermissionsAuthenticatedResponse = { authenticated: true, userId: 'u_member', tenantId: null, - roles: [], + positions: [], + hasAnyQueue: false, permissionSets: [], systemPermissions: [], objects: {}, diff --git a/ui/packages/app-shell/src/hooks/__tests__/navEntryReadReuse-13323.test.tsx b/ui/packages/app-shell/src/hooks/__tests__/navEntryReadReuse-13323.test.tsx index 86a8aed5c70..b3638e82af5 100644 --- a/ui/packages/app-shell/src/hooks/__tests__/navEntryReadReuse-13323.test.tsx +++ b/ui/packages/app-shell/src/hooks/__tests__/navEntryReadReuse-13323.test.tsx @@ -40,17 +40,18 @@ import { MemoryRouter, Route, Routes, useLocation, useNavigate, type NavigateFun // Module scope, never a `beforeAll` (AGENTS.md §测试纪律, objectui#3010). import { ComponentRegistry } from '@object-ui/core'; import { SchemaRenderer, SchemaRendererContext, MetadataCtx, notifyDataChanged, type ApiFetch } from '@object-ui/react'; -import { MePermissionsProvider, type MePermissionsResponse } from '@object-ui/permissions'; +import { MePermissionsProvider, type MePermissionsAuthenticatedResponse } from '@object-ui/permissions'; import type { NavigationItem } from '@object-ui/types'; import { ExpressionProvider } from '../../providers/ExpressionProvider'; import { ConsoleElementApiOpen } from '../../console/ConsoleElementApiOpen'; import { useNavMemberGates, navEntriesShownToMember } from '../useNavMemberGates'; -const ANSWER: MePermissionsResponse = { +const ANSWER: MePermissionsAuthenticatedResponse = { authenticated: true, userId: 'u_member', tenantId: null, - roles: [], + positions: [], + hasAnyQueue: false, permissionSets: [], systemPermissions: [], objects: {}, diff --git a/ui/packages/app-shell/src/hooks/__tests__/sharedUserFeeds.activityGrant-12081.test.tsx b/ui/packages/app-shell/src/hooks/__tests__/sharedUserFeeds.activityGrant-12081.test.tsx index 1ea4ee8682a..6e8b81ea192 100644 --- a/ui/packages/app-shell/src/hooks/__tests__/sharedUserFeeds.activityGrant-12081.test.tsx +++ b/ui/packages/app-shell/src/hooks/__tests__/sharedUserFeeds.activityGrant-12081.test.tsx @@ -30,7 +30,7 @@ import '@testing-library/jest-dom/vitest'; import * as React from 'react'; import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { act, renderHook } from '@testing-library/react'; -import { MePermissionsProvider, type MePermissionsResponse } from '@object-ui/permissions'; +import { MePermissionsProvider, type MePermissionsResponse, type MePermissionsAuthenticatedResponse } from '@object-ui/permissions'; // The feed reads only the objects an app lists (objectui#12530); `crm_lead` is // on the default roster, so the grant is all this file varies. import { withRoster } from './activityRoster'; @@ -79,11 +79,12 @@ const { useSharedActivityFeed, __resetSharedUserFeeds, __markFirstPainted } = aw const settle = () => act(async () => { await vi.advanceTimersByTimeAsync(0); }); -const payload = (over: Partial): MePermissionsResponse => ({ +const payload = (over: Partial): MePermissionsAuthenticatedResponse => ({ authenticated: true, userId: 'u1', tenantId: null, - roles: [], + positions: [], + hasAnyQueue: false, permissionSets: [], objects: {}, fields: {}, @@ -160,7 +161,7 @@ describe('every uncertainty still reads — a wrong skip is the expensive mistak }); it('the anonymous payload (no resolvable permissions by design) ⇒ reads', async () => { - const { result } = await mountFeed(payload({ authenticated: false, userId: null })); + const { result } = await mountFeed({ authenticated: false }); expect(reads).toEqual(['u1']); expect(result.current.readable).toBe(true); }); diff --git a/ui/packages/app-shell/src/hooks/__tests__/sharedUserFeeds.activityRoster-12530.test.tsx b/ui/packages/app-shell/src/hooks/__tests__/sharedUserFeeds.activityRoster-12530.test.tsx index 7cde10f15f1..983300d19b5 100644 --- a/ui/packages/app-shell/src/hooks/__tests__/sharedUserFeeds.activityRoster-12530.test.tsx +++ b/ui/packages/app-shell/src/hooks/__tests__/sharedUserFeeds.activityRoster-12530.test.tsx @@ -27,7 +27,7 @@ import '@testing-library/jest-dom/vitest'; import * as React from 'react'; import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { act, renderHook } from '@testing-library/react'; -import { MePermissionsProvider, type MePermissionsResponse } from '@object-ui/permissions'; +import { MePermissionsProvider, type MePermissionsAuthenticatedResponse } from '@object-ui/permissions'; import { MetadataCtx, type MetadataContextValue } from '@object-ui/react'; import { appListing, rosterRegistry, withRoster } from './activityRoster'; @@ -158,11 +158,12 @@ describe('the feed reads the objects the user’s apps list, in the query (objec }); it('an object entry the member may not read lends the roster nothing (the sidebar’s own prune)', async () => { - const perms: MePermissionsResponse = { + const perms: MePermissionsAuthenticatedResponse = { authenticated: true, userId: 'u_admin', tenantId: null, - roles: [], + positions: [], + hasAnyQueue: false, permissionSets: [], objects: { sys_activity: { allowRead: true }, crm_lead: { allowRead: true } }, fields: {}, diff --git a/ui/packages/app-shell/src/layout/__tests__/AppHeader.aboutBuild-12970.test.tsx b/ui/packages/app-shell/src/layout/__tests__/AppHeader.aboutBuild-12970.test.tsx index 0544a15bac6..b3faf8cc56e 100644 --- a/ui/packages/app-shell/src/layout/__tests__/AppHeader.aboutBuild-12970.test.tsx +++ b/ui/packages/app-shell/src/layout/__tests__/AppHeader.aboutBuild-12970.test.tsx @@ -20,7 +20,7 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { render, screen, fireEvent, within } from '@testing-library/react'; import React from 'react'; import { MemoryRouter, Routes, Route } from 'react-router-dom'; -import { MePermissionsProvider, type MePermissionsResponse } from '@object-ui/permissions'; +import { MePermissionsProvider, type MePermissionsAuthenticatedResponse } from '@object-ui/permissions'; vi.mock('@object-ui/i18n', async (importOriginal) => ({ ...(await importOriginal>()), @@ -160,11 +160,12 @@ vi.mock('../../providers/AdapterProvider', () => ({ import { AppHeader, type AppHeaderVariant } from '../AppHeader'; -const MEMBER: MePermissionsResponse = { +const MEMBER: MePermissionsAuthenticatedResponse = { authenticated: true, userId: 'u1', tenantId: 'org_1', - roles: [], + positions: [], + hasAnyQueue: false, permissionSets: [], systemPermissions: [], objects: {}, diff --git a/ui/packages/app-shell/src/layout/__tests__/AppHeader.appBarLeftGroup-12461.test.tsx b/ui/packages/app-shell/src/layout/__tests__/AppHeader.appBarLeftGroup-12461.test.tsx index 4bdb00c7933..42aa9771d52 100644 --- a/ui/packages/app-shell/src/layout/__tests__/AppHeader.appBarLeftGroup-12461.test.tsx +++ b/ui/packages/app-shell/src/layout/__tests__/AppHeader.appBarLeftGroup-12461.test.tsx @@ -34,7 +34,7 @@ import '@testing-library/jest-dom/vitest'; import { describe, it, expect, vi, beforeEach } from 'vitest'; import { render, screen, within } from '@testing-library/react'; import React from 'react'; -import { MePermissionsProvider, type MePermissionsResponse } from '@object-ui/permissions'; +import { MePermissionsProvider, type MePermissionsAuthenticatedResponse } from '@object-ui/permissions'; const route = vi.hoisted(() => ({ pathname: '/apps/crm/lead', search: '' })); const nav = vi.hoisted(() => ({ recordTitle: undefined as string | undefined })); @@ -149,11 +149,12 @@ vi.mock('../../providers/AdapterProvider', () => ({ import { AppHeader } from '../AppHeader'; import { CommandPaletteProvider } from '../../context/CommandPaletteProvider'; -const AUTHOR: MePermissionsResponse = { +const AUTHOR: MePermissionsAuthenticatedResponse = { authenticated: true, userId: 'u1', tenantId: 'org_1', - roles: [], + positions: [], + hasAnyQueue: false, permissionSets: [], systemPermissions: ['manage_metadata', 'studio.access', 'setup.access'], objects: {}, diff --git a/ui/packages/app-shell/src/layout/__tests__/AppHeader.appIdentity-12201.test.tsx b/ui/packages/app-shell/src/layout/__tests__/AppHeader.appIdentity-12201.test.tsx index 084b2a5e553..0cfed91ec49 100644 --- a/ui/packages/app-shell/src/layout/__tests__/AppHeader.appIdentity-12201.test.tsx +++ b/ui/packages/app-shell/src/layout/__tests__/AppHeader.appIdentity-12201.test.tsx @@ -32,7 +32,7 @@ import '@testing-library/jest-dom/vitest'; import { describe, it, expect, vi, beforeEach } from 'vitest'; import { render, screen } from '@testing-library/react'; import React from 'react'; -import { MePermissionsProvider, type MePermissionsResponse } from '@object-ui/permissions'; +import { MePermissionsProvider, type MePermissionsResponse, type MePermissionsAuthenticatedResponse } from '@object-ui/permissions'; vi.mock('react-router-dom', () => ({ useLocation: () => ({ pathname: '/apps/cloud_control/page/welcome', search: '', hash: '', state: null, key: 't' }), @@ -175,12 +175,13 @@ import { AppHeader } from '../AppHeader'; import { CommandPaletteProvider } from '../../context/CommandPaletteProvider'; import { getProductName } from '../../runtime-config'; -function mePermissions(systemPermissions: string[]): MePermissionsResponse { +function mePermissions(systemPermissions: string[]): MePermissionsAuthenticatedResponse { return { authenticated: true, userId: 'u1', tenantId: 'org_1', - roles: [], + positions: [], + hasAnyQueue: false, permissionSets: [], systemPermissions, objects: {}, diff --git a/ui/packages/app-shell/src/layout/__tests__/AppHeader.centeredSearch-12472.test.tsx b/ui/packages/app-shell/src/layout/__tests__/AppHeader.centeredSearch-12472.test.tsx index 8310b2f78e4..701d37ae5ca 100644 --- a/ui/packages/app-shell/src/layout/__tests__/AppHeader.centeredSearch-12472.test.tsx +++ b/ui/packages/app-shell/src/layout/__tests__/AppHeader.centeredSearch-12472.test.tsx @@ -41,7 +41,7 @@ import '@testing-library/jest-dom/vitest'; import { describe, it, expect, vi, beforeEach } from 'vitest'; import { render, screen } from '@testing-library/react'; import React from 'react'; -import { MePermissionsProvider, type MePermissionsResponse } from '@object-ui/permissions'; +import { MePermissionsProvider, type MePermissionsAuthenticatedResponse } from '@object-ui/permissions'; const route = vi.hoisted(() => ({ pathname: '/apps/crm/lead', search: '' })); @@ -140,11 +140,12 @@ vi.mock('../../providers/AdapterProvider', () => ({ import { AppHeader, type AppHeaderVariant } from '../AppHeader'; import { CommandPaletteProvider } from '../../context/CommandPaletteProvider'; -const AUTHOR: MePermissionsResponse = { +const AUTHOR: MePermissionsAuthenticatedResponse = { authenticated: true, userId: 'u1', tenantId: 'org_1', - roles: [], + positions: [], + hasAnyQueue: false, permissionSets: [], systemPermissions: ['manage_metadata', 'studio.access', 'setup.access'], objects: {}, diff --git a/ui/packages/app-shell/src/layout/__tests__/AppHeader.crumbFold-12702.test.tsx b/ui/packages/app-shell/src/layout/__tests__/AppHeader.crumbFold-12702.test.tsx index 539648804ec..6c36bc4c3af 100644 --- a/ui/packages/app-shell/src/layout/__tests__/AppHeader.crumbFold-12702.test.tsx +++ b/ui/packages/app-shell/src/layout/__tests__/AppHeader.crumbFold-12702.test.tsx @@ -31,7 +31,7 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { render, screen, within, act } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import React from 'react'; -import { MePermissionsProvider, type MePermissionsResponse } from '@object-ui/permissions'; +import { MePermissionsProvider, type MePermissionsAuthenticatedResponse } from '@object-ui/permissions'; const route = vi.hoisted(() => ({ pathname: '/apps/crm/system/marketplace', search: '' })); @@ -134,11 +134,12 @@ vi.mock('../../providers/AdapterProvider', () => ({ import { AppHeader, AppBarCrumbTrail } from '../AppHeader'; import { CommandPaletteProvider } from '../../context/CommandPaletteProvider'; -const AUTHOR: MePermissionsResponse = { +const AUTHOR: MePermissionsAuthenticatedResponse = { authenticated: true, userId: 'u1', tenantId: 'org_1', - roles: [], + positions: [], + hasAnyQueue: false, permissionSets: [], systemPermissions: ['manage_metadata', 'studio.access', 'setup.access'], objects: {}, diff --git a/ui/packages/app-shell/src/layout/__tests__/AppHeader.dashboardPageNoCrumb-12526.test.tsx b/ui/packages/app-shell/src/layout/__tests__/AppHeader.dashboardPageNoCrumb-12526.test.tsx index c2c12f7abad..56b774db979 100644 --- a/ui/packages/app-shell/src/layout/__tests__/AppHeader.dashboardPageNoCrumb-12526.test.tsx +++ b/ui/packages/app-shell/src/layout/__tests__/AppHeader.dashboardPageNoCrumb-12526.test.tsx @@ -29,7 +29,7 @@ import '@testing-library/jest-dom/vitest'; import { describe, it, expect, vi, beforeEach } from 'vitest'; import { render, screen, within } from '@testing-library/react'; import React from 'react'; -import { MePermissionsProvider, type MePermissionsResponse } from '@object-ui/permissions'; +import { MePermissionsProvider, type MePermissionsAuthenticatedResponse } from '@object-ui/permissions'; const route = vi.hoisted(() => ({ pathname: '/apps/crm/dashboard/ops_board' })); const served = vi.hoisted(() => ({ @@ -126,11 +126,12 @@ vi.mock('../../providers/AdapterProvider', () => ({ import { AppHeader } from '../AppHeader'; -const MEMBER: MePermissionsResponse = { +const MEMBER: MePermissionsAuthenticatedResponse = { authenticated: true, userId: 'u1', tenantId: 'org_1', - roles: [], + positions: [], + hasAnyQueue: false, permissionSets: [], systemPermissions: [], objects: {}, diff --git a/ui/packages/app-shell/src/layout/__tests__/AppHeader.deferredChromeReads-12980.test.tsx b/ui/packages/app-shell/src/layout/__tests__/AppHeader.deferredChromeReads-12980.test.tsx index bfd127ee88d..b77cd6932dd 100644 --- a/ui/packages/app-shell/src/layout/__tests__/AppHeader.deferredChromeReads-12980.test.tsx +++ b/ui/packages/app-shell/src/layout/__tests__/AppHeader.deferredChromeReads-12980.test.tsx @@ -184,7 +184,7 @@ const fakeAdapter = { }; vi.mock('../../providers/AdapterProvider', () => ({ useAdapter: () => fakeAdapter })); -import { MePermissionsProvider, type MePermissionsResponse } from '@object-ui/permissions'; +import { MePermissionsProvider, type MePermissionsAuthenticatedResponse } from '@object-ui/permissions'; import { AppHeader } from '../AppHeader'; import { __resetSharedUserFeeds, @@ -192,11 +192,12 @@ import { __markFirstPainted, } from '../../hooks/sharedUserFeeds'; -const READER: MePermissionsResponse = { +const READER: MePermissionsAuthenticatedResponse = { authenticated: true, userId: 'u1', tenantId: 'org_1', - roles: [], + positions: [], + hasAnyQueue: false, permissionSets: [], systemPermissions: [], objects: { diff --git a/ui/packages/app-shell/src/layout/__tests__/AppHeader.designInStudioNeedsAuthoring-10899.test.tsx b/ui/packages/app-shell/src/layout/__tests__/AppHeader.designInStudioNeedsAuthoring-10899.test.tsx index 041331d094f..55a292d87b1 100644 --- a/ui/packages/app-shell/src/layout/__tests__/AppHeader.designInStudioNeedsAuthoring-10899.test.tsx +++ b/ui/packages/app-shell/src/layout/__tests__/AppHeader.designInStudioNeedsAuthoring-10899.test.tsx @@ -21,7 +21,7 @@ import '@testing-library/jest-dom/vitest'; import { describe, it, expect, vi, beforeEach } from 'vitest'; import { render, screen } from '@testing-library/react'; import React from 'react'; -import { MePermissionsProvider, type MePermissionsResponse } from '@object-ui/permissions'; +import { MePermissionsProvider, type MePermissionsResponse, type MePermissionsAuthenticatedResponse } from '@object-ui/permissions'; vi.mock('react-router-dom', () => ({ useLocation: () => ({ pathname: '/apps/cloud_control/page/welcome', search: '', hash: '', state: null, key: 't' }), @@ -153,12 +153,13 @@ import { AppHeader } from '../AppHeader'; // reads that trigger as its "the header rendered" control. import { CommandPaletteProvider } from '../../context/CommandPaletteProvider'; -function mePermissions(systemPermissions: string[]): MePermissionsResponse { +function mePermissions(systemPermissions: string[]): MePermissionsAuthenticatedResponse { return { authenticated: true, userId: 'u1', tenantId: 'org_1', - roles: [], + positions: [], + hasAnyQueue: false, permissionSets: [], systemPermissions, objects: {}, diff --git a/ui/packages/app-shell/src/layout/__tests__/AppHeader.editAppFollowsPackagePosture-12615.test.tsx b/ui/packages/app-shell/src/layout/__tests__/AppHeader.editAppFollowsPackagePosture-12615.test.tsx index ccf3e490a21..0eb215a1e21 100644 --- a/ui/packages/app-shell/src/layout/__tests__/AppHeader.editAppFollowsPackagePosture-12615.test.tsx +++ b/ui/packages/app-shell/src/layout/__tests__/AppHeader.editAppFollowsPackagePosture-12615.test.tsx @@ -33,7 +33,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { act, cleanup, render, screen } from '@testing-library/react'; import React from 'react'; import { MemoryRouter } from 'react-router-dom'; -import { MePermissionsProvider, type MePermissionsResponse } from '@object-ui/permissions'; +import { MePermissionsProvider, type MePermissionsResponse, type MePermissionsAuthenticatedResponse } from '@object-ui/permissions'; import { SidebarMenu, SidebarMenuItem, SidebarProvider } from '@object-ui/components'; import type { NavigationItem } from '@object-ui/types'; @@ -172,11 +172,12 @@ vi.mock('../../providers/AdapterProvider', () => ({ import { AppHeader } from '../AppHeader'; import { SidebarItemMenu, sidebarItemDesignPath } from '../SidebarItemMenu'; -const AUTHOR: MePermissionsResponse = { +const AUTHOR: MePermissionsAuthenticatedResponse = { authenticated: true, userId: 'u1', tenantId: 'org_1', - roles: [], + positions: [], + hasAnyQueue: false, permissionSets: [], systemPermissions: ['manage_metadata', 'studio.access', 'setup.access'], objects: {}, diff --git a/ui/packages/app-shell/src/layout/__tests__/AppHeader.homeMark-12605.test.tsx b/ui/packages/app-shell/src/layout/__tests__/AppHeader.homeMark-12605.test.tsx index 2e4e8c86515..afb0b895d48 100644 --- a/ui/packages/app-shell/src/layout/__tests__/AppHeader.homeMark-12605.test.tsx +++ b/ui/packages/app-shell/src/layout/__tests__/AppHeader.homeMark-12605.test.tsx @@ -29,7 +29,7 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { render, screen, fireEvent, cleanup, act } from '@testing-library/react'; import React from 'react'; import { MemoryRouter, Routes, Route, useLocation, useNavigate, useNavigationType } from 'react-router-dom'; -import { MePermissionsProvider, type MePermissionsResponse } from '@object-ui/permissions'; +import { MePermissionsProvider, type MePermissionsAuthenticatedResponse } from '@object-ui/permissions'; vi.mock('@object-ui/i18n', async (importOriginal) => ({ ...(await importOriginal>()), @@ -121,11 +121,12 @@ import { CommandPaletteProvider } from '../../context/CommandPaletteProvider'; import { GO_HOME_SHORTCUT, useAdvertisedShortcuts, type ShortcutChord } from '../../chrome/advertisedShortcuts'; import { getProductName } from '../../runtime-config'; -const MEMBER: MePermissionsResponse = { +const MEMBER: MePermissionsAuthenticatedResponse = { authenticated: true, userId: 'u1', tenantId: 'org_1', - roles: [], + positions: [], + hasAnyQueue: false, permissionSets: [], systemPermissions: [], objects: {}, diff --git a/ui/packages/app-shell/src/layout/__tests__/AppHeader.hostSingleApp-12913.test.tsx b/ui/packages/app-shell/src/layout/__tests__/AppHeader.hostSingleApp-12913.test.tsx index ee15adc441d..856946ebcfd 100644 --- a/ui/packages/app-shell/src/layout/__tests__/AppHeader.hostSingleApp-12913.test.tsx +++ b/ui/packages/app-shell/src/layout/__tests__/AppHeader.hostSingleApp-12913.test.tsx @@ -24,7 +24,7 @@ import '@testing-library/jest-dom/vitest'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { render, screen } from '@testing-library/react'; import React from 'react'; -import { MePermissionsProvider, type MePermissionsResponse } from '@object-ui/permissions'; +import { MePermissionsProvider, type MePermissionsAuthenticatedResponse } from '@object-ui/permissions'; const served = vi.hoisted(() => ({ apps: [] as Array> })); @@ -115,11 +115,12 @@ import { AppHeader } from '../AppHeader'; import { CommandPaletteProvider } from '../../context/CommandPaletteProvider'; import { initRuntimeConfig, resetRuntimeConfigForTesting } from '../../runtime-config'; -const MEMBER: MePermissionsResponse = { +const MEMBER: MePermissionsAuthenticatedResponse = { authenticated: true, userId: 'u1', tenantId: 'org_1', - roles: [], + positions: [], + hasAnyQueue: false, permissionSets: [], systemPermissions: [], objects: {}, diff --git a/ui/packages/app-shell/src/layout/__tests__/AppHeader.inboxVariant.test.tsx b/ui/packages/app-shell/src/layout/__tests__/AppHeader.inboxVariant.test.tsx index c30489b1b36..f17ac961c1e 100644 --- a/ui/packages/app-shell/src/layout/__tests__/AppHeader.inboxVariant.test.tsx +++ b/ui/packages/app-shell/src/layout/__tests__/AppHeader.inboxVariant.test.tsx @@ -263,7 +263,7 @@ vi.mock('../../providers/AdapterProvider', () => ({ useAdapter: () => fakeAdapter, })); -import { MePermissionsProvider, type MePermissionsResponse } from '@object-ui/permissions'; +import { MePermissionsProvider, type MePermissionsResponse, type MePermissionsAuthenticatedResponse } from '@object-ui/permissions'; import { AppHeader } from '../AppHeader'; import { useHomeInbox } from '../../hooks/useHomeInbox'; import { __resetSharedUserFeeds, __markFirstPainted } from '../../hooks/sharedUserFeeds'; @@ -821,11 +821,12 @@ describe('AppHeader — the bell panel renders what the inbox returns, under BOT */ describe('AppHeader — the Activity tab asks only a caller who may read sys_activity (objectui#12081)', () => { /** `/auth/me/permissions` for an authenticated member — `member_default`'s two inbox reads, no `*`. */ - const MEMBER: MePermissionsResponse = { + const MEMBER: MePermissionsAuthenticatedResponse = { authenticated: true, userId: 'u1', tenantId: null, - roles: [], + positions: [], + hasAnyQueue: false, permissionSets: ['member_default'], objects: { sys_inbox_message: { allowRead: true, allowCreate: false, allowEdit: false, allowDelete: false }, diff --git a/ui/packages/app-shell/src/layout/__tests__/AppHeader.pageRouteNoAuthoringHandles-12198.test.tsx b/ui/packages/app-shell/src/layout/__tests__/AppHeader.pageRouteNoAuthoringHandles-12198.test.tsx index 623545ab836..8bb237cff4a 100644 --- a/ui/packages/app-shell/src/layout/__tests__/AppHeader.pageRouteNoAuthoringHandles-12198.test.tsx +++ b/ui/packages/app-shell/src/layout/__tests__/AppHeader.pageRouteNoAuthoringHandles-12198.test.tsx @@ -37,7 +37,7 @@ import '@testing-library/jest-dom/vitest'; import { describe, it, expect, vi, beforeEach } from 'vitest'; import { act, render, screen, fireEvent } from '@testing-library/react'; import React from 'react'; -import { MePermissionsProvider, type MePermissionsResponse } from '@object-ui/permissions'; +import { MePermissionsProvider, type MePermissionsResponse, type MePermissionsAuthenticatedResponse } from '@object-ui/permissions'; // The page's pencil is a button that navigates, so its target is read off the // navigate call it makes (objectui#12540). @@ -198,12 +198,13 @@ import { PageView } from '../../views/PageView'; import { ExpressionProvider } from '../../providers/ExpressionProvider'; /** `/me/permissions` for a persona, with the fields these surfaces do not read left empty. */ -function mePermissions(systemPermissions: string[]): MePermissionsResponse { +function mePermissions(systemPermissions: string[]): MePermissionsAuthenticatedResponse { return { authenticated: true, userId: 'u1', tenantId: 'org_1', - roles: [], + positions: [], + hasAnyQueue: false, permissionSets: [], systemPermissions, objects: {}, diff --git a/ui/packages/app-shell/src/layout/__tests__/AppHeader.sidebarTrigger-12429.test.tsx b/ui/packages/app-shell/src/layout/__tests__/AppHeader.sidebarTrigger-12429.test.tsx index 8da3a8011f0..269bac914f4 100644 --- a/ui/packages/app-shell/src/layout/__tests__/AppHeader.sidebarTrigger-12429.test.tsx +++ b/ui/packages/app-shell/src/layout/__tests__/AppHeader.sidebarTrigger-12429.test.tsx @@ -21,7 +21,7 @@ import '@testing-library/jest-dom/vitest'; import { describe, it, expect, vi, beforeEach } from 'vitest'; import { render, screen } from '@testing-library/react'; import React from 'react'; -import { MePermissionsProvider, type MePermissionsResponse } from '@object-ui/permissions'; +import { MePermissionsProvider, type MePermissionsAuthenticatedResponse } from '@object-ui/permissions'; vi.mock('react-router-dom', () => ({ useLocation: () => ({ pathname: '/apps/cloud_control/page/welcome', search: '', hash: '', state: null, key: 't' }), @@ -165,11 +165,12 @@ vi.mock('../../providers/AdapterProvider', () => ({ import { AppHeader } from '../AppHeader'; import { CommandPaletteProvider } from '../../context/CommandPaletteProvider'; -const AUTHOR: MePermissionsResponse = { +const AUTHOR: MePermissionsAuthenticatedResponse = { authenticated: true, userId: 'u1', tenantId: 'org_1', - roles: [], + positions: [], + hasAnyQueue: false, permissionSets: [], systemPermissions: ['manage_metadata', 'studio.access', 'setup.access'], objects: {}, diff --git a/ui/packages/app-shell/src/layout/__tests__/SidebarItemMenu.designOpensStudio-12520.test.tsx b/ui/packages/app-shell/src/layout/__tests__/SidebarItemMenu.designOpensStudio-12520.test.tsx index 9ba2be7b527..e4679f28ec9 100644 --- a/ui/packages/app-shell/src/layout/__tests__/SidebarItemMenu.designOpensStudio-12520.test.tsx +++ b/ui/packages/app-shell/src/layout/__tests__/SidebarItemMenu.designOpensStudio-12520.test.tsx @@ -32,7 +32,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { act, cleanup, render, screen } from '@testing-library/react'; import React from 'react'; import { MemoryRouter } from 'react-router-dom'; -import { MePermissionsProvider, type MePermissionsResponse } from '@object-ui/permissions'; +import { MePermissionsProvider, type MePermissionsAuthenticatedResponse } from '@object-ui/permissions'; import type { NavigationItem } from '@object-ui/types'; vi.mock('@object-ui/i18n', async (importOriginal) => ({ @@ -159,11 +159,12 @@ import { resolveHref } from '@object-ui/layout'; import { AppHeader } from '../AppHeader'; import { sidebarItemDesignPath, favoriteDesignPath } from '../SidebarItemMenu'; -const AUTHOR: MePermissionsResponse = { +const AUTHOR: MePermissionsAuthenticatedResponse = { authenticated: true, userId: 'u1', tenantId: 'org_1', - roles: [], + positions: [], + hasAnyQueue: false, permissionSets: [], systemPermissions: ['manage_metadata', 'studio.access', 'setup.access'], objects: {}, diff --git a/ui/packages/app-shell/src/layout/__tests__/UnifiedSidebar.navCanPermissions-12700.test.tsx b/ui/packages/app-shell/src/layout/__tests__/UnifiedSidebar.navCanPermissions-12700.test.tsx index edf3abc0dee..059b6244e30 100644 --- a/ui/packages/app-shell/src/layout/__tests__/UnifiedSidebar.navCanPermissions-12700.test.tsx +++ b/ui/packages/app-shell/src/layout/__tests__/UnifiedSidebar.navCanPermissions-12700.test.tsx @@ -25,7 +25,7 @@ import React from 'react'; import { render, screen, waitFor, act } from '@testing-library/react'; import { MemoryRouter } from 'react-router-dom'; import type { NavigationItem } from '@object-ui/types'; -import { MePermissionsProvider, type MePermissionsResponse } from '@object-ui/permissions'; +import { MePermissionsProvider, type MePermissionsResponse, type MePermissionsAuthenticatedResponse } from '@object-ui/permissions'; import { __resetVisibilityPredicateWarnings } from '../../../../react/src/utils/visibilityDiagnostic'; vi.mock('@object-ui/i18n', async (importOriginal) => ({ @@ -94,12 +94,13 @@ const NAVIGATION: NavigationItem[] = [ const CRM_APP = { name: 'crm', label: 'CRM', active: true, navigation: NAVIGATION }; -function answerFor(opportunityRead: boolean): MePermissionsResponse { +function answerFor(opportunityRead: boolean): MePermissionsAuthenticatedResponse { return { authenticated: true, userId: 'u_agent', tenantId: null, - roles: [], + positions: [], + hasAnyQueue: false, permissionSets: ['service_agent'], systemPermissions: [], objects: { @@ -107,7 +108,7 @@ function answerFor(opportunityRead: boolean): MePermissionsResponse { crm_opportunity: { allowRead: opportunityRead }, }, fields: {}, - } as MePermissionsResponse; + } as MePermissionsAuthenticatedResponse; } function adapterFor(app: Record) { diff --git a/ui/packages/app-shell/src/layout/__tests__/UnifiedSidebar.objectReadGate-12109.test.tsx b/ui/packages/app-shell/src/layout/__tests__/UnifiedSidebar.objectReadGate-12109.test.tsx index fbcfd6dea12..3c2651c3f10 100644 --- a/ui/packages/app-shell/src/layout/__tests__/UnifiedSidebar.objectReadGate-12109.test.tsx +++ b/ui/packages/app-shell/src/layout/__tests__/UnifiedSidebar.objectReadGate-12109.test.tsx @@ -25,7 +25,7 @@ import React from 'react'; import { render, screen, waitFor } from '@testing-library/react'; import { MemoryRouter } from 'react-router-dom'; import type { NavigationArea, NavigationItem } from '@object-ui/types'; -import { MePermissionsProvider, type MePermissionsResponse } from '@object-ui/permissions'; +import { MePermissionsProvider, type MePermissionsResponse, type MePermissionsAuthenticatedResponse } from '@object-ui/permissions'; vi.mock('@object-ui/i18n', async (importOriginal) => ({ ...(await importOriginal>()), @@ -113,12 +113,13 @@ const NAVIGATION: NavigationItem[] = [ { id: 'nav_help', type: 'url', url: '/help', label: 'Help' }, ]; -function answerFor(opportunityRead: boolean): MePermissionsResponse { +function answerFor(opportunityRead: boolean): MePermissionsAuthenticatedResponse { return { authenticated: true, userId: 'u_agent', tenantId: null, - roles: [], + positions: [], + hasAnyQueue: false, permissionSets: ['service_agent'], systemPermissions: [], objects: { diff --git a/ui/packages/app-shell/src/providers/__tests__/currentUserCan-4421.render.test.tsx b/ui/packages/app-shell/src/providers/__tests__/currentUserCan-4421.render.test.tsx index b6eb03862b2..b52e5996aad 100644 --- a/ui/packages/app-shell/src/providers/__tests__/currentUserCan-4421.render.test.tsx +++ b/ui/packages/app-shell/src/providers/__tests__/currentUserCan-4421.render.test.tsx @@ -56,7 +56,7 @@ import { describe, it, expect, vi, afterEach } from 'vitest'; import { render, screen, cleanup } from '@testing-library/react'; import { ComponentRegistry } from '@object-ui/core'; import { ActionProvider, RecordContextProvider } from '@object-ui/react'; -import { MePermissionsProvider, usePermissions, type MePermissionsResponse } from '@object-ui/permissions'; +import { MePermissionsProvider, usePermissions, type MePermissionsAuthenticatedResponse } from '@object-ui/permissions'; import { ExpressionProvider } from '../ExpressionProvider'; // Module-scope side-effect imports: the two registry renderers must be // registered before the first render (AGENTS.md §测试纪律 — import phase, not a @@ -88,12 +88,13 @@ const RECORD = { id: 'acc_1', name: 'Northwind', status: 'open' }; type State = 'not-loaded' | 'granted' | 'denied'; -function payload(allowDelete: boolean): MePermissionsResponse { +function payload(allowDelete: boolean): MePermissionsAuthenticatedResponse { return { authenticated: true, userId: 'u1', tenantId: null, - roles: [], + positions: [], + hasAnyQueue: false, permissionSets: ['account_clerk'], // `allowEdit` is granted in BOTH loaded states, so a verb mix-up (reading // edit for delete) would show the action in the denied state. diff --git a/ui/packages/app-shell/src/providers/__tests__/currentUserCan-disabledLegs-11242.render.test.tsx b/ui/packages/app-shell/src/providers/__tests__/currentUserCan-disabledLegs-11242.render.test.tsx index 25b457886bc..17e33a4b3e5 100644 --- a/ui/packages/app-shell/src/providers/__tests__/currentUserCan-disabledLegs-11242.render.test.tsx +++ b/ui/packages/app-shell/src/providers/__tests__/currentUserCan-disabledLegs-11242.render.test.tsx @@ -67,7 +67,7 @@ import { describe, it, expect, vi, afterEach } from 'vitest'; import { render, screen, cleanup, fireEvent, waitFor } from '@testing-library/react'; import { ComponentRegistry, type ActionResult } from '@object-ui/core'; import { ActionProvider, RecordContextProvider, useAction } from '@object-ui/react'; -import { MePermissionsProvider, type MePermissionsResponse } from '@object-ui/permissions'; +import { MePermissionsProvider, type MePermissionsAuthenticatedResponse } from '@object-ui/permissions'; import { ExpressionProvider } from '../ExpressionProvider'; // Module-scope side-effect imports: the registry renderers must be registered // before the first render (AGENTS.md §测试纪律 — import phase, not a hook). @@ -101,12 +101,13 @@ const nameOf = (leg: string, state: State | 'unbound') => `void_${leg}_${state.r const USER = { id: 'u1', name: 'Ada', email: 'ada@example.com', role: 'user', positions: ['everyone'] }; const RECORD = { id: 'acc_1', name: 'Northwind', status: 'open' }; -function payload(allowDelete: boolean): MePermissionsResponse { +function payload(allowDelete: boolean): MePermissionsAuthenticatedResponse { return { authenticated: true, userId: 'u1', tenantId: null, - roles: [], + positions: [], + hasAnyQueue: false, permissionSets: ['account_clerk'], // `allowEdit` is granted in BOTH loaded states, so a verb mix-up (reading // edit for delete) would enable the action in the denied state. diff --git a/ui/packages/app-shell/src/providers/__tests__/currentUserCan-failClosed-11212.render.test.tsx b/ui/packages/app-shell/src/providers/__tests__/currentUserCan-failClosed-11212.render.test.tsx index ebcbf17ef22..2fb31330669 100644 --- a/ui/packages/app-shell/src/providers/__tests__/currentUserCan-failClosed-11212.render.test.tsx +++ b/ui/packages/app-shell/src/providers/__tests__/currentUserCan-failClosed-11212.render.test.tsx @@ -62,7 +62,7 @@ import { describe, it, expect, vi, afterEach } from 'vitest'; import { render, screen, cleanup, fireEvent, waitFor } from '@testing-library/react'; import { ComponentRegistry } from '@object-ui/core'; import { ActionProvider, RecordContextProvider } from '@object-ui/react'; -import { MePermissionsProvider, type MePermissionsResponse } from '@object-ui/permissions'; +import { MePermissionsProvider, type MePermissionsAuthenticatedResponse } from '@object-ui/permissions'; import { ExpressionProvider } from '../ExpressionProvider'; // Module-scope side-effect imports: the registry renderers must be registered // before the first render (AGENTS.md §测试纪律 — import phase, not a hook). @@ -94,12 +94,13 @@ const nameOf = (leg: string, state: State | 'unbound') => `void_${leg}_${state.r const USER = { id: 'u1', name: 'Ada', email: 'ada@example.com', role: 'user', positions: ['everyone'] }; const RECORD = { id: 'acc_1', name: 'Northwind', status: 'open' }; -function payload(allowDelete: boolean): MePermissionsResponse { +function payload(allowDelete: boolean): MePermissionsAuthenticatedResponse { return { authenticated: true, userId: 'u1', tenantId: null, - roles: [], + positions: [], + hasAnyQueue: false, permissionSets: ['account_clerk'], // `allowEdit` is granted in BOTH loaded states, so a verb mix-up (reading // edit for delete) would show the action in the denied state. diff --git a/ui/packages/app-shell/src/providers/__tests__/hasCapability-13006.render.test.tsx b/ui/packages/app-shell/src/providers/__tests__/hasCapability-13006.render.test.tsx index 8310cf8f210..fd320e744c8 100644 --- a/ui/packages/app-shell/src/providers/__tests__/hasCapability-13006.render.test.tsx +++ b/ui/packages/app-shell/src/providers/__tests__/hasCapability-13006.render.test.tsx @@ -50,7 +50,7 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { render, screen, cleanup, waitFor } from '@testing-library/react'; import { ComponentRegistry, subjectCapabilitiesOf, subjectPermissionsOf } from '@object-ui/core'; import { ActionProvider, SchemaRenderer, useAction, usePredicateScope } from '@object-ui/react'; -import { MePermissionsProvider, type MePermissionsResponse } from '@object-ui/permissions'; +import { MePermissionsProvider, type MePermissionsResponse, type MePermissionsAuthenticatedResponse } from '@object-ui/permissions'; // The reporter's dedupe `Set` is module state: reset it so every arm's // "nothing reported" is a reading, not a leftover from an earlier arm. import { __resetVisibilityPredicateWarnings } from '../../../../react/src/utils/visibilityDiagnostic'; @@ -89,12 +89,13 @@ const PRINCIPALS = { }, } as const; -function payload(userId: string, systemPermissions: string[] | undefined): MePermissionsResponse { +function payload(userId: string, systemPermissions: string[] | undefined): MePermissionsAuthenticatedResponse { return { authenticated: true, userId, tenantId: 'org_1', - roles: [], + positions: [], + hasAnyQueue: false, permissionSets: ['member'], ...(systemPermissions !== undefined ? { systemPermissions } : {}), objects: { account: { allowRead: true, allowCreate: true, allowEdit: true, allowDelete: false } }, diff --git a/ui/packages/app-shell/src/views/ObjectView.listActionsReadGate-12109.test.tsx b/ui/packages/app-shell/src/views/ObjectView.listActionsReadGate-12109.test.tsx index 15036e0c496..a1d03f5785f 100644 --- a/ui/packages/app-shell/src/views/ObjectView.listActionsReadGate-12109.test.tsx +++ b/ui/packages/app-shell/src/views/ObjectView.listActionsReadGate-12109.test.tsx @@ -31,7 +31,7 @@ import * as React from 'react'; import { describe, it, expect, vi, afterEach } from 'vitest'; import { render, cleanup, screen, waitFor } from '@testing-library/react'; import { MemoryRouter, Routes, Route } from 'react-router-dom'; -import { MePermissionsProvider, type MePermissionsResponse } from '@object-ui/permissions'; +import { MePermissionsProvider, type MePermissionsResponse, type MePermissionsAuthenticatedResponse } from '@object-ui/permissions'; vi.mock('@object-ui/auth', async (importOriginal) => ({ ...(await importOriginal>()), @@ -101,12 +101,13 @@ const OBJECTS = [ }, ]; -function answerFor(opportunityRead: boolean): MePermissionsResponse { +function answerFor(opportunityRead: boolean): MePermissionsAuthenticatedResponse { return { authenticated: true, userId: 'u_agent', tenantId: null, - roles: [], + positions: [], + hasAnyQueue: false, permissionSets: ['service_agent'], systemPermissions: [], objects: { [OBJECT_NAME]: { allowRead: opportunityRead, allowEdit: true } }, diff --git a/ui/packages/app-shell/src/views/ObjectView.managedByBadgeAuthoring-12194.test.tsx b/ui/packages/app-shell/src/views/ObjectView.managedByBadgeAuthoring-12194.test.tsx index 712483dc4d1..a1780e68075 100644 --- a/ui/packages/app-shell/src/views/ObjectView.managedByBadgeAuthoring-12194.test.tsx +++ b/ui/packages/app-shell/src/views/ObjectView.managedByBadgeAuthoring-12194.test.tsx @@ -31,7 +31,7 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { render, cleanup, screen } from '@testing-library/react'; import { MemoryRouter, Routes, Route } from 'react-router-dom'; import { createI18n, I18nProvider } from '@object-ui/i18n'; -import { MePermissionsProvider, type MePermissionsResponse } from '@object-ui/permissions'; +import { MePermissionsProvider, type MePermissionsResponse, type MePermissionsAuthenticatedResponse } from '@object-ui/permissions'; vi.mock('@object-ui/auth', async (importOriginal) => ({ ...(await importOriginal>()), @@ -88,12 +88,13 @@ const OBJECTS = [ ]; /** A `/me/permissions` answer holding exactly `held`. */ -function answer(held: string[]): MePermissionsResponse { +function answer(held: string[]): MePermissionsAuthenticatedResponse { return { authenticated: true, userId: 'u1', tenantId: null, - roles: [], + positions: [], + hasAnyQueue: false, permissionSets: [], systemPermissions: held, objects: { '*': { allowRead: true, allowCreate: true, allowEdit: true, allowDelete: true } }, diff --git a/ui/packages/app-shell/src/views/ObjectView.setupListBadge-13385.test.tsx b/ui/packages/app-shell/src/views/ObjectView.setupListBadge-13385.test.tsx index a79335966b1..4aaf90b04b4 100644 --- a/ui/packages/app-shell/src/views/ObjectView.setupListBadge-13385.test.tsx +++ b/ui/packages/app-shell/src/views/ObjectView.setupListBadge-13385.test.tsx @@ -27,7 +27,7 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { render, cleanup, screen } from '@testing-library/react'; import { MemoryRouter, Routes, Route } from 'react-router-dom'; import { createI18n, I18nProvider } from '@object-ui/i18n'; -import { MePermissionsProvider, type MePermissionsResponse } from '@object-ui/permissions'; +import { MePermissionsProvider, type MePermissionsResponse, type MePermissionsAuthenticatedResponse } from '@object-ui/permissions'; vi.mock('@object-ui/auth', async (importOriginal) => ({ ...(await importOriginal>()), @@ -84,12 +84,13 @@ const OBJECTS = [ ]; /** A `/me/permissions` answer holding exactly `held`. */ -function answer(held: string[]): MePermissionsResponse { +function answer(held: string[]): MePermissionsAuthenticatedResponse { return { authenticated: true, userId: 'u1', tenantId: null, - roles: [], + positions: [], + hasAnyQueue: false, permissionSets: [], systemPermissions: held, objects: { '*': { allowRead: true, allowCreate: true, allowEdit: true, allowDelete: true } }, diff --git a/ui/packages/app-shell/src/views/ObjectView.viewRefusal-12112.test.tsx b/ui/packages/app-shell/src/views/ObjectView.viewRefusal-12112.test.tsx index 877a189ee49..039a0e3ce64 100644 --- a/ui/packages/app-shell/src/views/ObjectView.viewRefusal-12112.test.tsx +++ b/ui/packages/app-shell/src/views/ObjectView.viewRefusal-12112.test.tsx @@ -34,7 +34,7 @@ import * as React from 'react'; import { describe, it, expect, vi, afterEach } from 'vitest'; import { render, cleanup, screen, fireEvent } from '@testing-library/react'; import { MemoryRouter, Routes, Route } from 'react-router-dom'; -import { MePermissionsProvider, type MePermissionsResponse } from '@object-ui/permissions'; +import { MePermissionsProvider, type MePermissionsAuthenticatedResponse } from '@object-ui/permissions'; import { AdapterCtx } from '@object-ui/react'; import { createI18n, en, I18nProvider } from '@object-ui/i18n'; @@ -115,11 +115,12 @@ const OBJECTS = [ }, ]; -const PERMISSIONS: MePermissionsResponse = { +const PERMISSIONS: MePermissionsAuthenticatedResponse = { authenticated: true, userId: 'u_author', tenantId: null, - roles: [], + positions: [], + hasAnyQueue: false, permissionSets: ['studio_user'], systemPermissions: ['studio.access'], objects: { [OBJECT_NAME]: { allowRead: true, allowEdit: true } }, diff --git a/ui/packages/app-shell/src/views/RecordDetailView.authoredActivityThreading-12249.test.tsx b/ui/packages/app-shell/src/views/RecordDetailView.authoredActivityThreading-12249.test.tsx index 3ab75399773..fb37b49e3c4 100644 --- a/ui/packages/app-shell/src/views/RecordDetailView.authoredActivityThreading-12249.test.tsx +++ b/ui/packages/app-shell/src/views/RecordDetailView.authoredActivityThreading-12249.test.tsx @@ -31,7 +31,7 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { render, cleanup, fireEvent, within, act, waitFor } from '@testing-library/react'; import { MemoryRouter } from 'react-router-dom'; import { MetadataCtx } from '@object-ui/react'; -import { MePermissionsProvider, type MePermissionsResponse } from '@object-ui/permissions'; +import { MePermissionsProvider, type MePermissionsResponse, type MePermissionsAuthenticatedResponse } from '@object-ui/permissions'; import { DETAIL_DEFAULT_TRANSLATIONS } from '@object-ui/plugin-detail'; vi.mock('@object-ui/auth', async (importOriginal) => ({ @@ -184,16 +184,17 @@ const CREATE_ONLY = { allowRead: true, allowCreate: true, allowEdit: false, allo const READ_ONLY = { allowRead: true, allowCreate: false, allowEdit: false, allowDelete: false }; /** The caller's `/me/permissions` answer: full on the record's object, `bits` on `sys_comment`. */ -function answer(bits: Record): MePermissionsResponse { +function answer(bits: Record): MePermissionsAuthenticatedResponse { return { authenticated: true, userId: 'u1', tenantId: null, - roles: ['member'], + positions: ['member'], + hasAnyQueue: false, permissionSets: ['member'], objects: { [OBJECT_NAME]: FULL, [COMMENT]: bits }, fields: {}, - } as MePermissionsResponse; + } as MePermissionsAuthenticatedResponse; } interface MountOptions { diff --git a/ui/packages/app-shell/src/views/RecordDetailView.feedCommentEditDelete-12268.test.tsx b/ui/packages/app-shell/src/views/RecordDetailView.feedCommentEditDelete-12268.test.tsx index e5d66a0212d..f79d5c08ce8 100644 --- a/ui/packages/app-shell/src/views/RecordDetailView.feedCommentEditDelete-12268.test.tsx +++ b/ui/packages/app-shell/src/views/RecordDetailView.feedCommentEditDelete-12268.test.tsx @@ -97,7 +97,7 @@ vi.mock('./MetadataInspector', () => ({ import { toast } from 'sonner'; import { I18nProvider } from '@object-ui/i18n'; import { MetadataCtx } from '@object-ui/react'; -import { MePermissionsProvider, type MePermissionsResponse } from '@object-ui/permissions'; +import { MePermissionsProvider, type MePermissionsResponse, type MePermissionsAuthenticatedResponse } from '@object-ui/permissions'; import { RecordDetailView } from './RecordDetailView'; type Pack = { @@ -252,16 +252,17 @@ function metadataFor(pages: unknown[]) { const FULL = { allowRead: true, allowCreate: true, allowEdit: true, allowDelete: true }; /** The caller's `/me/permissions` answer: full on the record's object, `bits` on `sys_comment`. */ -function answer(bits: Record = FULL): MePermissionsResponse { +function answer(bits: Record = FULL): MePermissionsAuthenticatedResponse { return { authenticated: true, userId: auth.user.id, tenantId: null, - roles: ['member'], + positions: ['member'], + hasAnyQueue: false, permissionSets: ['member'], objects: { [OBJECT_NAME]: FULL, [COMMENT]: bits }, fields: {}, - } as MePermissionsResponse; + } as MePermissionsAuthenticatedResponse; } interface MountOptions { diff --git a/ui/packages/app-shell/src/views/RecordDetailView.feedGrant-12082.test.tsx b/ui/packages/app-shell/src/views/RecordDetailView.feedGrant-12082.test.tsx index 5526c191051..13e6373aa47 100644 --- a/ui/packages/app-shell/src/views/RecordDetailView.feedGrant-12082.test.tsx +++ b/ui/packages/app-shell/src/views/RecordDetailView.feedGrant-12082.test.tsx @@ -34,7 +34,7 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { render, screen, cleanup, fireEvent, within, act } from '@testing-library/react'; import { MemoryRouter } from 'react-router-dom'; import { MetadataCtx } from '@object-ui/react'; -import { MePermissionsProvider, type MePermissionsResponse } from '@object-ui/permissions'; +import { MePermissionsProvider, type MePermissionsResponse, type MePermissionsAuthenticatedResponse } from '@object-ui/permissions'; import { DETAIL_DEFAULT_TRANSLATIONS } from '@object-ui/plugin-detail'; import { effectiveOperationsArray, resolveEffectiveApiMethods } from '@objectstack/spec/data'; @@ -121,16 +121,17 @@ const SHAPE = { type Bits = Record; /** The caller's answer: full on the record's own object, `feed` on the feed's objects (absent keys are absent). */ -function answer(feed: { [COMMENT]?: Bits; [REACTION]?: Bits }): MePermissionsResponse { +function answer(feed: { [COMMENT]?: Bits; [REACTION]?: Bits }): MePermissionsAuthenticatedResponse { return { authenticated: true, userId: 'u1', tenantId: null, - roles: ['member'], + positions: ['member'], + hasAnyQueue: false, permissionSets: ['member'], objects: { [OBJECT_NAME]: SHAPE.full, ...feed }, fields: {}, - } as MePermissionsResponse; + } as MePermissionsAuthenticatedResponse; } /** diff --git a/ui/packages/app-shell/src/views/RecordDetailView.managedByBadgeAuthoring-12194.test.tsx b/ui/packages/app-shell/src/views/RecordDetailView.managedByBadgeAuthoring-12194.test.tsx index c6328c77ecb..06e5d6c3d49 100644 --- a/ui/packages/app-shell/src/views/RecordDetailView.managedByBadgeAuthoring-12194.test.tsx +++ b/ui/packages/app-shell/src/views/RecordDetailView.managedByBadgeAuthoring-12194.test.tsx @@ -45,7 +45,7 @@ import { describe, it, expect, vi, beforeAll, beforeEach, afterEach } from 'vite import { render, waitFor, cleanup } from '@testing-library/react'; import { MemoryRouter } from 'react-router-dom'; import { MetadataCtx } from '@object-ui/react'; -import { MePermissionsProvider, type MePermissionsResponse } from '@object-ui/permissions'; +import { MePermissionsProvider, type MePermissionsResponse, type MePermissionsAuthenticatedResponse } from '@object-ui/permissions'; vi.mock('@object-ui/auth', async (importOriginal) => ({ ...(await importOriginal>()), @@ -88,12 +88,13 @@ const FIELDS = { const RECORD = { id: REC, name: 'Acme Workspace', city: 'London' }; /** A `/me/permissions` answer; `systemPermissions` omitted when `held` is `undefined`. */ -function answer(held: string[] | undefined): MePermissionsResponse { +function answer(held: string[] | undefined): MePermissionsAuthenticatedResponse { return { authenticated: true, userId: 'u1', tenantId: null, - roles: [], + positions: [], + hasAnyQueue: false, permissionSets: [], ...(held === undefined ? {} : { systemPermissions: held }), objects: { '*': { allowRead: true, allowCreate: true, allowEdit: true, allowDelete: true } }, diff --git a/ui/packages/app-shell/src/views/RecordDetailView.replyCountOnRead-12144.test.tsx b/ui/packages/app-shell/src/views/RecordDetailView.replyCountOnRead-12144.test.tsx index db8cf62a584..01deb23f0dd 100644 --- a/ui/packages/app-shell/src/views/RecordDetailView.replyCountOnRead-12144.test.tsx +++ b/ui/packages/app-shell/src/views/RecordDetailView.replyCountOnRead-12144.test.tsx @@ -33,7 +33,7 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { render, cleanup, fireEvent, within, act, waitFor } from '@testing-library/react'; import { MemoryRouter } from 'react-router-dom'; import { MetadataCtx } from '@object-ui/react'; -import { MePermissionsProvider, type MePermissionsResponse } from '@object-ui/permissions'; +import { MePermissionsProvider, type MePermissionsResponse, type MePermissionsAuthenticatedResponse } from '@object-ui/permissions'; import { DETAIL_DEFAULT_TRANSLATIONS } from '@object-ui/plugin-detail'; vi.mock('@object-ui/auth', async (importOriginal) => ({ @@ -148,16 +148,17 @@ const CREATE_ONLY = { allowRead: true, allowCreate: true, allowEdit: false, allo const READ_ONLY = { allowRead: true, allowCreate: false, allowEdit: false, allowDelete: false }; /** The caller's `/me/permissions` answer: full on the record's object, `bits` on `sys_comment`. */ -function answer(bits: Record): MePermissionsResponse { +function answer(bits: Record): MePermissionsAuthenticatedResponse { return { authenticated: true, userId: 'u1', tenantId: null, - roles: ['member'], + positions: ['member'], + hasAnyQueue: false, permissionSets: ['member'], objects: { [OBJECT_NAME]: FULL, [COMMENT]: bits }, fields: {}, - } as MePermissionsResponse; + } as MePermissionsAuthenticatedResponse; } interface MountOptions { diff --git a/ui/packages/app-shell/src/views/RecordDetailView.setupManagedByChip-13385.test.tsx b/ui/packages/app-shell/src/views/RecordDetailView.setupManagedByChip-13385.test.tsx index 41c3ad0b623..b2c03085217 100644 --- a/ui/packages/app-shell/src/views/RecordDetailView.setupManagedByChip-13385.test.tsx +++ b/ui/packages/app-shell/src/views/RecordDetailView.setupManagedByChip-13385.test.tsx @@ -33,7 +33,7 @@ import { describe, it, expect, vi, beforeAll, beforeEach, afterEach } from 'vite import { render, waitFor, cleanup } from '@testing-library/react'; import { MemoryRouter, Routes, Route } from 'react-router-dom'; import { MetadataCtx } from '@object-ui/react'; -import { MePermissionsProvider, type MePermissionsResponse } from '@object-ui/permissions'; +import { MePermissionsProvider, type MePermissionsResponse, type MePermissionsAuthenticatedResponse } from '@object-ui/permissions'; vi.mock('@object-ui/auth', async (importOriginal) => ({ ...(await importOriginal>()), @@ -76,12 +76,13 @@ const FIELDS = { const RECORD = { id: REC, name: 'Acme Workspace', city: 'London' }; /** A `/me/permissions` answer; `systemPermissions` omitted when `held` is `undefined`. */ -function answer(held: string[] | undefined): MePermissionsResponse { +function answer(held: string[] | undefined): MePermissionsAuthenticatedResponse { return { authenticated: true, userId: 'u1', tenantId: null, - roles: [], + positions: [], + hasAnyQueue: false, permissionSets: [], ...(held === undefined ? {} : { systemPermissions: held }), objects: { '*': { allowRead: true, allowCreate: true, allowEdit: true, allowDelete: true } }, diff --git a/ui/packages/app-shell/src/views/RecordFormPage.hasCapability-13083.test.tsx b/ui/packages/app-shell/src/views/RecordFormPage.hasCapability-13083.test.tsx index 2cbbf0beb9e..15402eb126f 100644 --- a/ui/packages/app-shell/src/views/RecordFormPage.hasCapability-13083.test.tsx +++ b/ui/packages/app-shell/src/views/RecordFormPage.hasCapability-13083.test.tsx @@ -37,7 +37,7 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { render, cleanup, waitFor } from '@testing-library/react'; import { MemoryRouter, Routes, Route } from 'react-router-dom'; import { I18nProvider } from '@object-ui/i18n'; -import { MePermissionsProvider, type MePermissionsResponse } from '@object-ui/permissions'; +import { MePermissionsProvider, type MePermissionsAuthenticatedResponse } from '@object-ui/permissions'; // The reporter's dedupe `Set` is module state: reset it so every arm's // "nothing reported" is a reading, not a leftover from an earlier arm. import { __resetVisibilityPredicateWarnings } from '../../../react/src/utils/visibilityDiagnostic'; @@ -92,12 +92,13 @@ const ORG = { const USER = { id: 'u1', name: 'Ada', email: 'ada@example.com', role: 'user', positions: ['everyone'] }; -function payload(systemPermissions: string[]): MePermissionsResponse { +function payload(systemPermissions: string[]): MePermissionsAuthenticatedResponse { return { authenticated: true, userId: 'u1', tenantId: 'org_1', - roles: [], + positions: [], + hasAnyQueue: false, permissionSets: ['member'], systemPermissions, objects: { org_member: { allowRead: true, allowCreate: true, allowEdit: true, allowDelete: false } }, diff --git a/ui/packages/app-shell/src/views/RecordFormPage.managedByBadgeAuthoring-12194.test.tsx b/ui/packages/app-shell/src/views/RecordFormPage.managedByBadgeAuthoring-12194.test.tsx index 28e5937cbce..c48f650359f 100644 --- a/ui/packages/app-shell/src/views/RecordFormPage.managedByBadgeAuthoring-12194.test.tsx +++ b/ui/packages/app-shell/src/views/RecordFormPage.managedByBadgeAuthoring-12194.test.tsx @@ -33,7 +33,7 @@ import { describe, it, expect, vi, afterEach } from 'vitest'; import { render, screen, cleanup } from '@testing-library/react'; import { MemoryRouter, Routes, Route } from 'react-router-dom'; import { createI18n, I18nProvider } from '@object-ui/i18n'; -import { MePermissionsProvider, type MePermissionsResponse } from '@object-ui/permissions'; +import { MePermissionsProvider, type MePermissionsResponse, type MePermissionsAuthenticatedResponse } from '@object-ui/permissions'; import { RecordFormPage } from './RecordFormPage'; vi.mock('sonner', () => ({ @@ -75,12 +75,13 @@ vi.mock('../providers/AdapterProvider', () => ({ })); /** A `/me/permissions` answer holding exactly `held`. */ -function answer(held: string[]): MePermissionsResponse { +function answer(held: string[]): MePermissionsAuthenticatedResponse { return { authenticated: true, userId: 'u1', tenantId: null, - roles: [], + positions: [], + hasAnyQueue: false, permissionSets: [], systemPermissions: held, objects: { '*': { allowRead: true, allowCreate: true, allowEdit: true, allowDelete: true } }, diff --git a/ui/packages/app-shell/src/views/__tests__/AppReportIndex-13020.test.tsx b/ui/packages/app-shell/src/views/__tests__/AppReportIndex-13020.test.tsx index 4fecd53c747..16e00419cd7 100644 --- a/ui/packages/app-shell/src/views/__tests__/AppReportIndex-13020.test.tsx +++ b/ui/packages/app-shell/src/views/__tests__/AppReportIndex-13020.test.tsx @@ -31,7 +31,7 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; import { render, screen, waitFor, cleanup } from '@testing-library/react'; import { MemoryRouter, Route, Routes } from 'react-router-dom'; import { createI18n, I18nProvider } from '@object-ui/i18n'; -import { MePermissionsProvider, type MePermissionsResponse } from '@object-ui/permissions'; +import { MePermissionsProvider, type MePermissionsAuthenticatedResponse } from '@object-ui/permissions'; vi.mock('@object-ui/auth', async (importOriginal) => ({ ...(await importOriginal>()), @@ -81,11 +81,12 @@ const DATASETS = [ { name: 'ds_cases', object: 'crm_case' }, ]; -const ANSWER: MePermissionsResponse = { +const ANSWER: MePermissionsAuthenticatedResponse = { authenticated: true, userId: 'u_agent', tenantId: null, - roles: [], + positions: [], + hasAnyQueue: false, permissionSets: ['service_agent'], systemPermissions: [], objects: { diff --git a/ui/packages/app-shell/src/views/__tests__/AppReportIndex.groupsViews-27526.test.tsx b/ui/packages/app-shell/src/views/__tests__/AppReportIndex.groupsViews-27526.test.tsx index bfb43e7d3aa..e532ee2d99a 100644 --- a/ui/packages/app-shell/src/views/__tests__/AppReportIndex.groupsViews-27526.test.tsx +++ b/ui/packages/app-shell/src/views/__tests__/AppReportIndex.groupsViews-27526.test.tsx @@ -35,7 +35,7 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; import { render, screen, cleanup, fireEvent, within } from '@testing-library/react'; import { MemoryRouter, Route, Routes } from 'react-router-dom'; import { createI18n, I18nProvider } from '@object-ui/i18n'; -import { MePermissionsProvider, type MePermissionsResponse } from '@object-ui/permissions'; +import { MePermissionsProvider, type MePermissionsAuthenticatedResponse } from '@object-ui/permissions'; vi.mock('@object-ui/auth', async (importOriginal) => ({ ...(await importOriginal>()), @@ -103,11 +103,12 @@ const OBJECTS = [ { name: 'crm_case', label: 'Case', pluralLabel: 'Cases', _packageId: PKG }, ]; -const ANSWER: MePermissionsResponse = { +const ANSWER: MePermissionsAuthenticatedResponse = { authenticated: true, userId: 'u_agent', tenantId: null, - roles: [], + positions: [], + hasAnyQueue: false, permissionSets: ['sales'], systemPermissions: [], objects: { diff --git a/ui/packages/app-shell/src/views/__tests__/AppReportIndex.navContributions-13273.test.tsx b/ui/packages/app-shell/src/views/__tests__/AppReportIndex.navContributions-13273.test.tsx index 9dd4fc7a32a..ec56dcc7482 100644 --- a/ui/packages/app-shell/src/views/__tests__/AppReportIndex.navContributions-13273.test.tsx +++ b/ui/packages/app-shell/src/views/__tests__/AppReportIndex.navContributions-13273.test.tsx @@ -29,7 +29,7 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; import { render, screen, cleanup } from '@testing-library/react'; import { MemoryRouter, Route, Routes } from 'react-router-dom'; import { createI18n, I18nProvider } from '@object-ui/i18n'; -import { MePermissionsProvider, type MePermissionsResponse } from '@object-ui/permissions'; +import { MePermissionsProvider, type MePermissionsAuthenticatedResponse } from '@object-ui/permissions'; import { MetadataProvider } from '../../providers/MetadataProvider'; import { ExpressionProvider } from '../../providers/ExpressionProvider'; @@ -90,11 +90,12 @@ const DATASETS = [ { name: 'ds_cases', object: 'crm_case' }, ]; -const ANSWER: MePermissionsResponse = { +const ANSWER: MePermissionsAuthenticatedResponse = { authenticated: true, userId: 'u_agent', tenantId: null, - roles: [], + positions: [], + hasAnyQueue: false, permissionSets: ['service_agent'], systemPermissions: [], objects: { diff --git a/ui/packages/app-shell/src/views/__tests__/AppReportIndex.navGates-26892.test.tsx b/ui/packages/app-shell/src/views/__tests__/AppReportIndex.navGates-26892.test.tsx index 56febeb1a09..7fc25256939 100644 --- a/ui/packages/app-shell/src/views/__tests__/AppReportIndex.navGates-26892.test.tsx +++ b/ui/packages/app-shell/src/views/__tests__/AppReportIndex.navGates-26892.test.tsx @@ -48,7 +48,7 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; import { render, screen, waitFor, cleanup, act } from '@testing-library/react'; import { MemoryRouter, Route, Routes } from 'react-router-dom'; import { createI18n, I18nProvider } from '@object-ui/i18n'; -import { MePermissionsProvider, type MePermissionsResponse } from '@object-ui/permissions'; +import { MePermissionsProvider, type MePermissionsAuthenticatedResponse } from '@object-ui/permissions'; import { SchemaRendererContext, type ApiFetch } from '@object-ui/react'; vi.mock('@object-ui/auth', async (importOriginal) => ({ @@ -105,12 +105,13 @@ const gammaEntry = (extra: Entry = {}): Entry => ({ ...extra, }); -function answer(forecastRead: boolean): MePermissionsResponse { +function answer(forecastRead: boolean): MePermissionsAuthenticatedResponse { return { authenticated: true, userId: 'u_agent', tenantId: null, - roles: [], + positions: [], + hasAnyQueue: false, permissionSets: ['service_agent'], systemPermissions: [], objects: { diff --git a/ui/packages/app-shell/src/views/__tests__/AppReportIndex.otherApps-27356.test.tsx b/ui/packages/app-shell/src/views/__tests__/AppReportIndex.otherApps-27356.test.tsx index ada9a8e7c45..2f55dad13b8 100644 --- a/ui/packages/app-shell/src/views/__tests__/AppReportIndex.otherApps-27356.test.tsx +++ b/ui/packages/app-shell/src/views/__tests__/AppReportIndex.otherApps-27356.test.tsx @@ -22,7 +22,7 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; import { render, screen, cleanup, within } from '@testing-library/react'; import { MemoryRouter, Route, Routes } from 'react-router-dom'; import { createI18n, I18nProvider } from '@object-ui/i18n'; -import { MePermissionsProvider, type MePermissionsResponse } from '@object-ui/permissions'; +import { MePermissionsProvider, type MePermissionsAuthenticatedResponse } from '@object-ui/permissions'; vi.mock('@object-ui/auth', async (importOriginal) => ({ ...(await importOriginal>()), @@ -48,11 +48,12 @@ const DATASETS = [ { name: 'ds_salaries', object: 'hr_salary' }, ]; -const ANSWER: MePermissionsResponse = { +const ANSWER: MePermissionsAuthenticatedResponse = { authenticated: true, userId: 'u_admin', tenantId: null, - roles: [], + positions: [], + hasAnyQueue: false, permissionSets: ['admin'], systemPermissions: [], objects: { diff --git a/ui/packages/app-shell/src/views/__tests__/PageView.editInStudioFollowsPackagePosture-12615.test.tsx b/ui/packages/app-shell/src/views/__tests__/PageView.editInStudioFollowsPackagePosture-12615.test.tsx index 8604aa89d8a..6b886fbc6db 100644 --- a/ui/packages/app-shell/src/views/__tests__/PageView.editInStudioFollowsPackagePosture-12615.test.tsx +++ b/ui/packages/app-shell/src/views/__tests__/PageView.editInStudioFollowsPackagePosture-12615.test.tsx @@ -22,7 +22,7 @@ import '@testing-library/jest-dom/vitest'; import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { act, cleanup, render, screen, fireEvent } from '@testing-library/react'; import React from 'react'; -import { MePermissionsProvider, type MePermissionsResponse } from '@object-ui/permissions'; +import { MePermissionsProvider, type MePermissionsAuthenticatedResponse } from '@object-ui/permissions'; const navigateSpy = vi.hoisted(() => vi.fn()); vi.mock('react-router-dom', () => ({ @@ -63,11 +63,12 @@ import { PageView } from '../PageView'; import { ExpressionProvider } from '../../providers/ExpressionProvider'; /** A metadata author: holds `manage_metadata`. */ -const AUTHOR: MePermissionsResponse = { +const AUTHOR: MePermissionsAuthenticatedResponse = { authenticated: true, userId: 'u1', tenantId: 'org_1', - roles: [], + positions: [], + hasAnyQueue: false, permissionSets: [], systemPermissions: ['manage_metadata', 'studio.access', 'setup.access'], objects: {}, diff --git a/ui/packages/app-shell/src/views/__tests__/PageView.editInStudioNeedsAuthoring-10899.test.tsx b/ui/packages/app-shell/src/views/__tests__/PageView.editInStudioNeedsAuthoring-10899.test.tsx index 885e2febc04..675d620db2d 100644 --- a/ui/packages/app-shell/src/views/__tests__/PageView.editInStudioNeedsAuthoring-10899.test.tsx +++ b/ui/packages/app-shell/src/views/__tests__/PageView.editInStudioNeedsAuthoring-10899.test.tsx @@ -25,7 +25,7 @@ import '@testing-library/jest-dom/vitest'; import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { act, cleanup, render, screen } from '@testing-library/react'; import React from 'react'; -import { MePermissionsProvider, type MePermissionsResponse } from '@object-ui/permissions'; +import { MePermissionsProvider, type MePermissionsResponse, type MePermissionsAuthenticatedResponse } from '@object-ui/permissions'; vi.mock('react-router-dom', () => ({ useParams: () => ({ pageName: 'welcome' }), @@ -78,12 +78,13 @@ const APP = { name: 'cloud_control', label: 'Cloud', _packageId: 'com.objectstac const EXPRESSION_USER = {}; /** `/me/permissions` for a persona, with the fields this surface does not read left empty. */ -function mePermissions(systemPermissions: string[] | undefined): MePermissionsResponse { +function mePermissions(systemPermissions: string[] | undefined): MePermissionsAuthenticatedResponse { return { authenticated: true, userId: 'u1', tenantId: 'org_1', - roles: [], + positions: [], + hasAnyQueue: false, permissionSets: [], ...(systemPermissions ? { systemPermissions } : {}), objects: {}, diff --git a/ui/packages/app-shell/src/views/__tests__/PageView.editInStudioOpensStudio-12540.test.tsx b/ui/packages/app-shell/src/views/__tests__/PageView.editInStudioOpensStudio-12540.test.tsx index 44d133932ad..cd63293389b 100644 --- a/ui/packages/app-shell/src/views/__tests__/PageView.editInStudioOpensStudio-12540.test.tsx +++ b/ui/packages/app-shell/src/views/__tests__/PageView.editInStudioOpensStudio-12540.test.tsx @@ -29,7 +29,7 @@ import '@testing-library/jest-dom/vitest'; import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { act, cleanup, render, screen, fireEvent } from '@testing-library/react'; import React from 'react'; -import { MePermissionsProvider, type MePermissionsResponse } from '@object-ui/permissions'; +import { MePermissionsProvider, type MePermissionsAuthenticatedResponse } from '@object-ui/permissions'; const navigateSpy = vi.hoisted(() => vi.fn()); vi.mock('react-router-dom', () => ({ @@ -71,11 +71,12 @@ import { ExpressionProvider } from '../../providers/ExpressionProvider'; import { appStudioRoutePath } from '../../utils/appRoute'; /** A metadata author: holds `manage_metadata`. */ -const AUTHOR: MePermissionsResponse = { +const AUTHOR: MePermissionsAuthenticatedResponse = { authenticated: true, userId: 'u1', tenantId: 'org_1', - roles: [], + positions: [], + hasAnyQueue: false, permissionSets: [], systemPermissions: ['manage_metadata', 'studio.access', 'setup.access'], objects: {}, diff --git a/ui/packages/app-shell/src/views/__tests__/RecordAttachmentsPanel.affordanceGrant-12047.test.tsx b/ui/packages/app-shell/src/views/__tests__/RecordAttachmentsPanel.affordanceGrant-12047.test.tsx index 2a0740451d3..4bb9ce62b65 100644 --- a/ui/packages/app-shell/src/views/__tests__/RecordAttachmentsPanel.affordanceGrant-12047.test.tsx +++ b/ui/packages/app-shell/src/views/__tests__/RecordAttachmentsPanel.affordanceGrant-12047.test.tsx @@ -34,7 +34,7 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; import { render, screen, waitFor, within, act } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -import { MePermissionsProvider, type MePermissionsResponse } from '@object-ui/permissions'; +import { MePermissionsProvider, type MePermissionsAuthenticatedResponse } from '@object-ui/permissions'; /** The fake presigned adapter: `upload` resolving means a `sys_file` was committed. */ const adapterUpload = vi.fn(); @@ -70,12 +70,13 @@ const OWN_ROW = { type Grant = { allowCreate?: boolean; allowDelete?: boolean }; /** A `/me/permissions` answer for an authenticated caller with this `sys_attachment` grant. */ -function payload(grant: Grant): MePermissionsResponse { +function payload(grant: Grant): MePermissionsAuthenticatedResponse { return { authenticated: true, userId: ME, tenantId: 'org-1', - roles: [], + positions: [], + hasAnyQueue: false, permissionSets: ['member'], objects: { sys_attachment: { diff --git a/ui/packages/app-shell/src/views/__tests__/nav-object-read-gate-12109.render.test.tsx b/ui/packages/app-shell/src/views/__tests__/nav-object-read-gate-12109.render.test.tsx index 1264d305d74..202f38aacb7 100644 --- a/ui/packages/app-shell/src/views/__tests__/nav-object-read-gate-12109.render.test.tsx +++ b/ui/packages/app-shell/src/views/__tests__/nav-object-read-gate-12109.render.test.tsx @@ -24,7 +24,7 @@ import { MemoryRouter, Routes, Route } from 'react-router-dom'; // Module scope, never a `beforeAll` (AGENTS.md §测试纪律, objectui#3010). import '@object-ui/components'; import { SchemaRenderer, MetadataCtx } from '@object-ui/react'; -import { MePermissionsProvider, type MePermissionsResponse } from '@object-ui/permissions'; +import { MePermissionsProvider, type MePermissionsResponse, type MePermissionsAuthenticatedResponse } from '@object-ui/permissions'; import '../nav-menu-renderer'; const NAVIGATION = [ @@ -54,12 +54,13 @@ const AREAS = [ }, ]; -function answerFor(opportunityRead: boolean): MePermissionsResponse { +function answerFor(opportunityRead: boolean): MePermissionsAuthenticatedResponse { return { authenticated: true, userId: 'u_agent', tenantId: null, - roles: [], + positions: [], + hasAnyQueue: false, permissionSets: ['service_agent'], systemPermissions: [], objects: { crm_account: { allowRead: true }, crm_opportunity: { allowRead: opportunityRead } }, diff --git a/ui/packages/app-shell/src/views/metadata-admin/ResourceEditPage.tabDeepLink-12496.test.tsx b/ui/packages/app-shell/src/views/metadata-admin/ResourceEditPage.tabDeepLink-12496.test.tsx index e8d361ad4a0..74f6052a071 100644 --- a/ui/packages/app-shell/src/views/metadata-admin/ResourceEditPage.tabDeepLink-12496.test.tsx +++ b/ui/packages/app-shell/src/views/metadata-admin/ResourceEditPage.tabDeepLink-12496.test.tsx @@ -19,7 +19,7 @@ import '@testing-library/jest-dom/vitest'; import { describe, it, expect, vi, afterEach } from 'vitest'; import { render, screen, cleanup, waitFor, within, fireEvent } from '@testing-library/react'; import { MemoryRouter } from 'react-router-dom'; -import { MePermissionsProvider, type MePermissionsResponse } from '@object-ui/permissions'; +import { MePermissionsProvider, type MePermissionsResponse, type MePermissionsAuthenticatedResponse } from '@object-ui/permissions'; const POSITION = { name: 'env_position', label: 'Environment Position' }; @@ -57,12 +57,13 @@ vi.mock('./useMetadata', async (importOriginal) => ({ import { MetadataResourceEditPage } from './ResourceEditPage'; -function mePermissions(systemPermissions: string[]): MePermissionsResponse { +function mePermissions(systemPermissions: string[]): MePermissionsAuthenticatedResponse { return { authenticated: true, userId: 'u1', tenantId: 'org_1', - roles: [], + positions: [], + hasAnyQueue: false, permissionSets: [], systemPermissions, objects: {}, diff --git a/ui/packages/app-shell/src/views/metadata-admin/setupHoldersGrant-12082.test.tsx b/ui/packages/app-shell/src/views/metadata-admin/setupHoldersGrant-12082.test.tsx index 1cdb6bcb3b9..b2c1fc79f96 100644 --- a/ui/packages/app-shell/src/views/metadata-admin/setupHoldersGrant-12082.test.tsx +++ b/ui/packages/app-shell/src/views/metadata-admin/setupHoldersGrant-12082.test.tsx @@ -27,7 +27,7 @@ import * as React from 'react'; import '@testing-library/jest-dom/vitest'; import { describe, it, expect, vi, afterEach } from 'vitest'; import { render, screen, cleanup, waitFor } from '@testing-library/react'; -import { MePermissionsProvider, type MePermissionsResponse } from '@object-ui/permissions'; +import { MePermissionsProvider, type MePermissionsResponse, type MePermissionsAuthenticatedResponse } from '@object-ui/permissions'; vi.mock('@object-ui/react', async (importOriginal) => ({ ...(await importOriginal>()), @@ -99,16 +99,17 @@ const SHAPE = { full: { allowRead: true, allowCreate: true, allowEdit: true, allowDelete: true }, } as const; -function answer(objects: MePermissionsResponse['objects']): MePermissionsResponse { +function answer(objects: MePermissionsAuthenticatedResponse['objects']): MePermissionsAuthenticatedResponse { return { authenticated: true, userId: 'u-admin', tenantId: null, - roles: ['member'], + positions: ['member'], + hasAnyQueue: false, permissionSets: ['member'], objects, fields: {}, - } as MePermissionsResponse; + } as MePermissionsAuthenticatedResponse; } /** A super-user wildcard, as the organization administrator set carries one. */ @@ -136,7 +137,7 @@ const ORG_ADMIN = answer({ '*': WILDCARD, [GRANT_OBJECT]: SHAPE.createAndDelete, [HOLDING_OBJECT]: SHAPE.createAndDelete, -} as MePermissionsResponse['objects']); +} as MePermissionsAuthenticatedResponse['objects']); /** * CONTROL: the same wildcard beside explicit entries with neither create nor @@ -146,7 +147,7 @@ const WILDCARD_BESIDE_READ_ONLY = answer({ '*': WILDCARD, [GRANT_OBJECT]: SHAPE.readOnly, [HOLDING_OBJECT]: SHAPE.readOnly, -} as MePermissionsResponse['objects']); +} as MePermissionsAuthenticatedResponse['objects']); const SECTIONS = [ { diff --git a/ui/packages/app-shell/src/views/metadata-admin/trailDoorsGate-12475.test.tsx b/ui/packages/app-shell/src/views/metadata-admin/trailDoorsGate-12475.test.tsx index 4d89b6e55ab..7f5359da988 100644 --- a/ui/packages/app-shell/src/views/metadata-admin/trailDoorsGate-12475.test.tsx +++ b/ui/packages/app-shell/src/views/metadata-admin/trailDoorsGate-12475.test.tsx @@ -20,7 +20,7 @@ import '@testing-library/jest-dom/vitest'; import { describe, it, expect, vi, afterEach } from 'vitest'; import { render, screen, cleanup, waitFor } from '@testing-library/react'; import { MemoryRouter } from 'react-router-dom'; -import { MePermissionsProvider, type MePermissionsResponse } from '@object-ui/permissions'; +import { MePermissionsProvider, type MePermissionsResponse, type MePermissionsAuthenticatedResponse } from '@object-ui/permissions'; const POSITION = { name: 'env_position', label: 'Environment Position' }; const SET = { name: 'sales_perms', label: 'Sales', objects: {}, fields: {} }; @@ -62,12 +62,13 @@ import { MetadataResourceEditPage } from './ResourceEditPage'; import { PermissionMatrixEditPage } from './PermissionMatrixEditor'; /** `/me/permissions` for a persona; `undefined` = a backend that reports none. */ -function mePermissions(systemPermissions: string[] | undefined): MePermissionsResponse { +function mePermissions(systemPermissions: string[] | undefined): MePermissionsAuthenticatedResponse { return { authenticated: true, userId: 'u1', tenantId: 'org_1', - roles: [], + positions: [], + hasAnyQueue: false, permissionSets: [], ...(systemPermissions ? { systemPermissions } : {}), objects: {}, diff --git a/ui/packages/app-shell/src/views/studio-design/StudioDesignSurface.itemTools-28239.test.tsx b/ui/packages/app-shell/src/views/studio-design/StudioDesignSurface.itemTools-28239.test.tsx index bca2f4a6517..f3c070b66cf 100644 --- a/ui/packages/app-shell/src/views/studio-design/StudioDesignSurface.itemTools-28239.test.tsx +++ b/ui/packages/app-shell/src/views/studio-design/StudioDesignSurface.itemTools-28239.test.tsx @@ -149,7 +149,7 @@ registerMetadataDefaultInspector('page', PageDefaultInspector); vi.stubGlobal('fetch', vi.fn(async () => new Response('null', { status: 404, headers: { 'content-type': 'application/json' } }))); function mePermissions(systemPermissions: string[]): MePermissionsResponse { - return { authenticated: true, userId: 'u1', tenantId: 'org_1', roles: [], permissionSets: [], systemPermissions, objects: {}, fields: {} }; + return { authenticated: true, userId: 'u1', tenantId: 'org_1', hasAnyQueue: false, positions: [], permissionSets: [], systemPermissions, objects: {}, fields: {} }; } const SETUP_ONLY = mePermissions(['manage_org_users', 'setup.access', 'setup.write']); diff --git a/ui/packages/fields/src/widgets/LookupField.createGrant-12082.test.tsx b/ui/packages/fields/src/widgets/LookupField.createGrant-12082.test.tsx index c6841934ebe..6282268fe99 100644 --- a/ui/packages/fields/src/widgets/LookupField.createGrant-12082.test.tsx +++ b/ui/packages/fields/src/widgets/LookupField.createGrant-12082.test.tsx @@ -40,7 +40,7 @@ import { render, screen, fireEvent, waitFor, act, cleanup } from '@testing-libra import '@testing-library/jest-dom'; import React from 'react'; import { describe, it, expect, vi, afterEach } from 'vitest'; -import { MePermissionsProvider, type MePermissionsResponse } from '@object-ui/permissions'; +import { MePermissionsProvider, type MePermissionsResponse, type MePermissionsAuthenticatedResponse } from '@object-ui/permissions'; import { LookupField } from './LookupField'; afterEach(cleanup); @@ -68,11 +68,12 @@ function makeDataSource() { type Grant = { allowCreate: boolean; apiOperations?: string[] }; /** `/me/permissions` with `target` and `source` grants on the two objects. */ -const envelope = (target: Grant, source: Grant = { allowCreate: true }): MePermissionsResponse => ({ +const envelope = (target: Grant, source: Grant = { allowCreate: true }): MePermissionsAuthenticatedResponse => ({ authenticated: true, userId: 'u-requester', tenantId: null, - roles: ['clm_requester'], + hasAnyQueue: false, + positions: ['clm_requester'], permissionSets: ['clm_requester'], objects: { [TARGET]: { allowRead: true, allowEdit: false, allowDelete: false, ...target }, diff --git a/ui/packages/fields/src/widgets/RecordPickerDialog.fieldReadRule-10594.test.tsx b/ui/packages/fields/src/widgets/RecordPickerDialog.fieldReadRule-10594.test.tsx index c753644f481..f334e484528 100644 --- a/ui/packages/fields/src/widgets/RecordPickerDialog.fieldReadRule-10594.test.tsx +++ b/ui/packages/fields/src/widgets/RecordPickerDialog.fieldReadRule-10594.test.tsx @@ -48,7 +48,7 @@ import { describe, it, expect, vi, afterEach } from 'vitest'; import { render, screen, cleanup, waitFor, act, fireEvent } from '@testing-library/react'; import '@testing-library/jest-dom'; import { SchemaRendererContext } from '@object-ui/react'; -import { MePermissionsProvider, type MePermissionsResponse } from '@object-ui/permissions'; +import { MePermissionsProvider, type MePermissionsAuthenticatedResponse } from '@object-ui/permissions'; import { RecordPickerDialog } from './RecordPickerDialog'; import { getCellRenderer } from '../index'; @@ -78,15 +78,16 @@ function makeBackend(prefix: string) { * An authenticated `/me/permissions` payload: `account` is readable, and each * named `account` field is not. */ -function policyDenying(...fields: string[]): MePermissionsResponse { +function policyDenying(...fields: string[]): MePermissionsAuthenticatedResponse { return { authenticated: true, userId: 'u1', tenantId: null, - roles: ['viewer'], + positions: ['viewer'], + hasAnyQueue: false, permissionSets: [], objects: { account: { allowRead: true } }, - fields: Object.fromEntries(fields.map((f) => [`account.${f}`, { readable: false }])), + fields: Object.fromEntries(fields.map((f) => [`account.${f}`, { readable: false, editable: false }])), }; } diff --git a/ui/packages/permissions/src/MePermissionsProvider.tsx b/ui/packages/permissions/src/MePermissionsProvider.tsx index 6cf0d345d4e..ff4638be537 100644 --- a/ui/packages/permissions/src/MePermissionsProvider.tsx +++ b/ui/packages/permissions/src/MePermissionsProvider.tsx @@ -21,52 +21,21 @@ import type { } from '@object-ui/types'; import { objectPermissionGrants, type ObjectPermissionVerbTarget } from '@objectstack/spec/security'; import { SystemFieldName } from '@objectstack/spec/system'; +import type { MePermissionsResponse } from '@objectstack/spec/api'; import { PermCtx, type PermissionContextValue } from './PermissionContext.js'; import { createDiscardProofCache } from './discardProofCache.js'; /** - * Shape of the upstream `/api/v1/auth/me/permissions` response. - * See framework/packages/plugins/plugin-hono-server. + * The `/api/v1/auth/me/permissions` response, DERIVED from the strict contract + * `@objectstack/spec/api` declares for it (objectstack#28508): an anonymous + * `{ authenticated: false }`, or the authenticated body with `positions`, + * `permissionSets`, `objects`, `fields`, the optional `systemPermissions` / + * `tabPermissions`, and `hasAnyQueue`. It replaces a hand-written interface + * that had drifted from the wire: it read a `roles` key the server never + * sends (the server's key is `positions`), so `roles` in this provider's + * context was always empty. */ -export interface MePermissionsResponse { - authenticated: boolean; - userId: string | null; - tenantId: string | null; - roles: string[]; - permissionSets: string[]; - /** [ADR-0066] System capabilities (union of permission-set systemPermissions): manage_users, setup.access, … */ - systemPermissions?: string[]; - /** - * object-level perms: { "*": {...}, "account": {...} }. A row grants only - * what the spec's `objectPermissionGrants` reads from it — a bit the row - * omits is NOT granted, exactly as the server enforces it. - */ - objects: Record; - /** field-level perms keyed `"object.field"` */ - fields: Record; -} +export type { MePermissionsResponse, MePermissionsAuthenticatedResponse } from '@objectstack/spec/api'; export interface MePermissionsProviderProps { /** Absolute or relative URL to the /me/permissions endpoint */ @@ -311,6 +280,8 @@ export function MePermissionsProvider({ ); const dataKey: object = data ?? NO_DATA; + // Only the authenticated body carries grants; the anonymous one is `{ authenticated: false }`. + const authed = data?.authenticated === true ? data : null; const checkField = CHECK_FIELD([dataKey], () => (object: string, field: string, action: 'read' | 'write' | 'create'): boolean => { @@ -319,8 +290,8 @@ export function MePermissionsProvider({ // pass schema.objectName as "Account" / "account" interchangeably. const objKey = (object ?? '').toLowerCase(); const key = `${objKey}.${field}`; - const fieldPerm = data.fields?.[key] ?? data.fields?.[`${object}.${field}`]; - const objPerm = data.objects?.[objKey] ?? data.objects?.[object] ?? data.objects?.['*']; + const fieldPerm = authed?.fields[key] ?? authed?.fields[`${object}.${field}`]; + const objPerm = authed?.objects[objKey] ?? authed?.objects[object] ?? authed?.objects['*']; // [objectui#12103] An update that writes the record's OWNER is a // transfer: the server's security step refuses it unless the caller holds // the transfer grant, whatever the field's own entry says. So `write` for @@ -376,7 +347,7 @@ export function MePermissionsProvider({ if (!data) return { allowed: false, reason: 'permissions-loading' }; // Same key normalization as checkField: the backend keys objects lowercase. const objKey = (object ?? '').toLowerCase(); - const objPerm = data.objects?.[objKey] ?? data.objects?.[object] ?? data.objects?.['*']; + const objPerm = authed?.objects[objKey] ?? authed?.objects[object] ?? authed?.objects['*']; const map: Record = { read: 'allowRead', view: 'allowRead', @@ -409,7 +380,7 @@ export function MePermissionsProvider({ if (!data) return []; const prefix = `${object}.`; const out: FieldLevelPermission[] = []; - for (const [key, value] of Object.entries(data.fields ?? {})) { + for (const [key, value] of Object.entries(authed?.fields ?? {})) { if (!key.startsWith(prefix)) continue; const field = key.slice(prefix.length); out.push({ @@ -428,7 +399,7 @@ export function MePermissionsProvider({ const objKey = (object ?? '').toLowerCase(); // Per-object only — the `*` wildcard carries no apiOperations. Absent → // undefined so consumers fall back to their current (default-allow) path. - const objPerm = data.objects?.[objKey] ?? data.objects?.[object]; + const objPerm = authed?.objects[objKey] ?? authed?.objects[object]; const ops = objPerm?.apiOperations; return Array.isArray(ops) ? ops : undefined; }, @@ -444,18 +415,20 @@ export function MePermissionsProvider({ getFieldPermissions, getRowFilter: NO_ROW_FILTER, getObjectApiOperations, - roles: data?.roles ?? [], + // The server's `positions` (objectstack#28508: the console used to read a + // `roles` key the server never sends, so this was always empty). + roles: authed?.positions ?? [], // [objectui#5683] `null` while unloaded/anonymous — never ''. Consumers // treat null as "unknown" and defer to the server. - userId: data?.userId ?? null, + userId: authed?.userId ?? null, // [objectui#4656] Forward the raw signal — do NOT `?? []` this. A // backend predating ADR-0066 omits `systemPermissions` from the // response entirely, and defaulting that to `[]` here made it // indistinguishable from a genuinely empty grant to every consumer // downstream (this provider's own `hasCapabilities` included). - systemPermissions: data?.systemPermissions, + systemPermissions: authed?.systemPermissions, hasCapabilities: (required: string[]) => { - const perms = data?.systemPermissions; + const perms = authed?.systemPermissions; // Unknown (backend never reported systemPermissions) fails OPEN — see // the doctrine on `PermissionContextValue.hasCapabilities`. if (!Array.isArray(perms)) return true; @@ -465,7 +438,7 @@ export function MePermissionsProvider({ // [objectui#4421] The response's `objects` map, verbatim — the data // `current_user.can(object, verb)` is answered from. Keyed on `dataKey` // like every member here, so it is the SAME object for the same payload. - effectiveObjects: data?.objects, + effectiveObjects: authed?.objects, isLoaded, })); diff --git a/ui/packages/permissions/src/__tests__/MePermissionsProvider.absentBitDenies-26193.test.tsx b/ui/packages/permissions/src/__tests__/MePermissionsProvider.absentBitDenies-26193.test.tsx index be804d667c2..cd537137d02 100644 --- a/ui/packages/permissions/src/__tests__/MePermissionsProvider.absentBitDenies-26193.test.tsx +++ b/ui/packages/permissions/src/__tests__/MePermissionsProvider.absentBitDenies-26193.test.tsx @@ -27,16 +27,17 @@ import { describe, it, expect, afterEach } from 'vitest'; import { render, cleanup } from '@testing-library/react'; import React from 'react'; -import { MePermissionsProvider, type MePermissionsResponse } from '../MePermissionsProvider'; +import { MePermissionsProvider, type MePermissionsResponse, type MePermissionsAuthenticatedResponse } from '../MePermissionsProvider'; import { usePermissions } from '../usePermissions'; afterEach(cleanup); -const envelope = (objects: MePermissionsResponse['objects']): MePermissionsResponse => ({ +const envelope = (objects: MePermissionsAuthenticatedResponse['objects']): MePermissionsAuthenticatedResponse => ({ authenticated: true, userId: 'u-auditor', tenantId: null, - roles: ['auditor'], + hasAnyQueue: false, + positions: ['auditor'], permissionSets: ['auditor'], objects, fields: {}, diff --git a/ui/packages/permissions/src/__tests__/MePermissionsProvider.capabilitiesReported.test.tsx b/ui/packages/permissions/src/__tests__/MePermissionsProvider.capabilitiesReported.test.tsx index bea00bdf8c2..88eddd910a6 100644 --- a/ui/packages/permissions/src/__tests__/MePermissionsProvider.capabilitiesReported.test.tsx +++ b/ui/packages/permissions/src/__tests__/MePermissionsProvider.capabilitiesReported.test.tsx @@ -56,7 +56,8 @@ describe('objectui#4656 — absent-vs-empty systemPermissions truth table', () = authenticated: true, userId: 'u1', tenantId: 't1', - roles: ['org_owner'], + hasAnyQueue: false, + positions: ['org_owner'], permissionSets: ['organization_admin'], // No `systemPermissions` key at all — pre-ADR-0066 shape. objects: {}, @@ -81,7 +82,8 @@ describe('objectui#4656 — absent-vs-empty systemPermissions truth table', () = authenticated: true, userId: 'u1', tenantId: 't1', - roles: ['org_owner'], + hasAnyQueue: false, + positions: ['org_owner'], permissionSets: ['organization_admin'], systemPermissions: [], // a REAL answer: holds zero capabilities objects: {}, @@ -105,7 +107,8 @@ describe('objectui#4656 — absent-vs-empty systemPermissions truth table', () = authenticated: true, userId: 'u1', tenantId: 't1', - roles: [], + hasAnyQueue: false, + positions: [], permissionSets: [], systemPermissions: [], objects: {}, @@ -125,7 +128,8 @@ describe('objectui#4656 — absent-vs-empty systemPermissions truth table', () = authenticated: true, userId: 'u1', tenantId: 't1', - roles: ['org_owner'], + hasAnyQueue: false, + positions: ['org_owner'], permissionSets: ['organization_admin'], systemPermissions: ['manage_org_users', 'setup.access', 'manage_metadata'], objects: {}, @@ -146,7 +150,8 @@ describe('objectui#4656 — absent-vs-empty systemPermissions truth table', () = authenticated: true, userId: 'u1', tenantId: 't1', - roles: ['org_owner'], + hasAnyQueue: false, + positions: ['org_owner'], permissionSets: ['organization_admin'], // The objectstack#8270 EE owner's exact set — non-empty, no manage_metadata. systemPermissions: ['manage_org_users', 'setup.access', 'setup.write'], @@ -203,7 +208,8 @@ describe('objectui#4656 — absent-vs-empty systemPermissions truth table', () = { authenticated: true, userId: 'u1', tenantId: 't1', - roles: ['member'], + positions: ['member'], + hasAnyQueue: false, permissionSets: ['restricted'], objects: { '*': { allowRead: true, allowEdit: true } }, fields: { @@ -68,7 +69,8 @@ describe('MePermissionsProvider', () => { authenticated: true, userId: 'u1', tenantId: 't1', - roles: ['viewer'], + positions: ['viewer'], + hasAnyQueue: false, permissionSets: ['viewer_readonly'], objects: { '*': { allowRead: true, allowEdit: false } }, fields: {}, @@ -107,7 +109,8 @@ describe('MePermissionsProvider', () => { authenticated: true, userId: 'u1', tenantId: 't1', - roles: ['member'], + positions: ['member'], + hasAnyQueue: false, permissionSets: ['restricted'], objects: { account: { allowRead: true, allowEdit: true } }, // no '*', nothing for 'project' fields: {}, @@ -151,7 +154,8 @@ describe('MePermissionsProvider', () => { authenticated: true, userId: 'u1', tenantId: 't1', - roles: [], + positions: [], + hasAnyQueue: false, permissionSets: [], objects: { '*': { allowRead: true, allowEdit: true } }, fields: {}, @@ -178,10 +182,11 @@ describe('MePermissionsProvider', () => { authenticated: true, userId: 'u', tenantId: 't', - roles: [], + positions: [], permissionSets: [], objects: {}, - fields: { 'account.secret': { readable: false } }, + fields: { 'account.secret': { readable: false, editable: false } }, + hasAnyQueue: false, }} > @@ -210,7 +215,7 @@ describe('MePermissionsProvider', () => { { {String(perms.isLoaded)}; } -function response(objects: MePermissionsResponse['objects']): MePermissionsResponse { +function response(objects: MePermissionsAuthenticatedResponse['objects']): MePermissionsAuthenticatedResponse { return { authenticated: true, userId: 'u1', tenantId: null, - roles: [], + hasAnyQueue: false, + positions: [], permissionSets: ['account_clerk'], objects, fields: {}, diff --git a/ui/packages/permissions/src/__tests__/providerCtxIdentity.discarded.test.tsx b/ui/packages/permissions/src/__tests__/providerCtxIdentity.discarded.test.tsx index a085720b12c..0124c0e41f9 100644 --- a/ui/packages/permissions/src/__tests__/providerCtxIdentity.discarded.test.tsx +++ b/ui/packages/permissions/src/__tests__/providerCtxIdentity.discarded.test.tsx @@ -53,7 +53,7 @@ import type { ObjectPermissionConfig, RoleDefinition } from '@object-ui/types'; import { PermCtx, type PermissionContextValue } from '../PermissionContext'; import { usePermissions } from '../usePermissions'; import { PermissionProvider } from '../PermissionProvider'; -import { MePermissionsProvider, type MePermissionsResponse } from '../MePermissionsProvider'; +import { MePermissionsProvider, type MePermissionsResponse, type MePermissionsAuthenticatedResponse } from '../MePermissionsProvider'; const memoProxy = vi.hoisted(() => ({ armed: false, epoch: 0 })); @@ -137,11 +137,12 @@ const PERMISSIONS: ObjectPermissionConfig[] = [ ]; const USER_ROLES = ['restricted']; -const ME: MePermissionsResponse = { +const ME: MePermissionsAuthenticatedResponse = { authenticated: true, userId: 'u-1', tenantId: 't-1', - roles: ['restricted'], + hasAnyQueue: false, + positions: ['restricted'], permissionSets: ['ps-1'], systemPermissions: ['manage_app'], objects: { accounts: { allowRead: true, allowEdit: false, apiOperations: ['find'] } }, diff --git a/ui/packages/permissions/src/index.ts b/ui/packages/permissions/src/index.ts index 47ee7d6f644..c854a2b6894 100644 --- a/ui/packages/permissions/src/index.ts +++ b/ui/packages/permissions/src/index.ts @@ -20,7 +20,12 @@ */ export { PermissionProvider, type PermissionProviderProps } from './PermissionProvider.js'; -export { MePermissionsProvider, type MePermissionsProviderProps, type MePermissionsResponse } from './MePermissionsProvider.js'; +export { + MePermissionsProvider, + type MePermissionsProviderProps, + type MePermissionsResponse, + type MePermissionsAuthenticatedResponse, +} from './MePermissionsProvider.js'; export { ServerReadScope, type ServerReadScopeProps } from './ServerReadScope.js'; export { RecordFieldAccessScope, diff --git a/ui/packages/plugin-calendar/src/ObjectCalendar.quickCreateHandoff-12291.test.tsx b/ui/packages/plugin-calendar/src/ObjectCalendar.quickCreateHandoff-12291.test.tsx index ae8d994f6c1..ac7103b8bec 100644 --- a/ui/packages/plugin-calendar/src/ObjectCalendar.quickCreateHandoff-12291.test.tsx +++ b/ui/packages/plugin-calendar/src/ObjectCalendar.quickCreateHandoff-12291.test.tsx @@ -35,7 +35,7 @@ import React from 'react'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { act, cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react'; -import { MePermissionsProvider, type MePermissionsResponse } from '@object-ui/permissions'; +import { MePermissionsProvider, type MePermissionsResponse, type MePermissionsAuthenticatedResponse } from '@object-ui/permissions'; import { SchemaRenderer, SchemaRendererProvider } from '@object-ui/react'; const probe = vi.hoisted(() => ({ @@ -227,11 +227,12 @@ describe('objectui#12291 — an object quick-create cannot satisfy hands the day it('a closed `calendarQuickCreate` row withholds the hand-off exactly as it withholds the dialog', async () => { const onEdit = vi.fn(); - const readOnly: MePermissionsResponse = { + const readOnly: MePermissionsAuthenticatedResponse = { authenticated: true, userId: 'u-pin', tenantId: null, - roles: ['member'], + hasAnyQueue: false, + positions: ['member'], permissionSets: ['member'], objects: { showcase_task: { allowCreate: false, allowRead: true, allowEdit: false, allowDelete: false } as never }, fields: {}, diff --git a/ui/packages/plugin-calendar/src/ObjectCalendar.writeGrant-12082.test.tsx b/ui/packages/plugin-calendar/src/ObjectCalendar.writeGrant-12082.test.tsx index 7d335aaa623..dfe248423ee 100644 --- a/ui/packages/plugin-calendar/src/ObjectCalendar.writeGrant-12082.test.tsx +++ b/ui/packages/plugin-calendar/src/ObjectCalendar.writeGrant-12082.test.tsx @@ -32,7 +32,7 @@ import React from 'react'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { act, cleanup, render, screen, waitFor } from '@testing-library/react'; -import { MePermissionsProvider, type MePermissionsResponse } from '@object-ui/permissions'; +import { MePermissionsProvider, type MePermissionsResponse, type MePermissionsAuthenticatedResponse } from '@object-ui/permissions'; const probe = vi.hoisted(() => ({ props: null as null | Record, @@ -78,11 +78,12 @@ const SHAPES: Array<{ shape: string; bits: Record; create: bool { shape: 'full', bits: { allowCreate: true, allowRead: true, allowEdit: true, allowDelete: true }, create: true, update: true }, ]; -const envelope = (bits: Record): MePermissionsResponse => ({ +const envelope = (bits: Record): MePermissionsAuthenticatedResponse => ({ authenticated: true, userId: 'u-pin', tenantId: null, - roles: ['member'], + hasAnyQueue: false, + positions: ['member'], permissionSets: ['member'], objects: { [OBJECT]: bits as never }, fields: {}, diff --git a/ui/packages/plugin-detail/src/__tests__/DetailSection.fieldWriteGrant-12103.test.tsx b/ui/packages/plugin-detail/src/__tests__/DetailSection.fieldWriteGrant-12103.test.tsx index 1d03940c97d..888fa1c98a6 100644 --- a/ui/packages/plugin-detail/src/__tests__/DetailSection.fieldWriteGrant-12103.test.tsx +++ b/ui/packages/plugin-detail/src/__tests__/DetailSection.fieldWriteGrant-12103.test.tsx @@ -10,7 +10,7 @@ import { describe, it, expect, vi, beforeAll, afterEach } from 'vitest'; import { render, screen, fireEvent, cleanup } from '@testing-library/react'; import React from 'react'; import { InlineEditProvider, useInlineEdit } from '@object-ui/react'; -import { MePermissionsProvider, type MePermissionsResponse } from '@object-ui/permissions'; +import { MePermissionsProvider, type MePermissionsResponse, type MePermissionsAuthenticatedResponse } from '@object-ui/permissions'; import { SystemFieldName } from '@objectstack/spec/system'; import type { DetailViewSection } from '@object-ui/types'; import { DetailSection } from '../DetailSection'; @@ -39,12 +39,13 @@ const OBJECT = 'crm_account'; const OWNER = SystemFieldName.OWNER_ID; /** The measured envelope: may edit the object, may not edit annual revenue, may not transfer. */ -function envelope(objectBits: Record = {}): MePermissionsResponse { +function envelope(objectBits: Record = {}): MePermissionsAuthenticatedResponse { return { authenticated: true, userId: 'u-rep', tenantId: null, - roles: [], + hasAnyQueue: false, + positions: [], permissionSets: ['rep'], objects: { [OBJECT]: { diff --git a/ui/packages/plugin-detail/src/__tests__/RecordDetailPanel.editGrant-12082.test.tsx b/ui/packages/plugin-detail/src/__tests__/RecordDetailPanel.editGrant-12082.test.tsx index d9633019c9d..d484cc3b73d 100644 --- a/ui/packages/plugin-detail/src/__tests__/RecordDetailPanel.editGrant-12082.test.tsx +++ b/ui/packages/plugin-detail/src/__tests__/RecordDetailPanel.editGrant-12082.test.tsx @@ -30,7 +30,7 @@ import React from 'react'; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'; import { cleanup, fireEvent, render, screen } from '@testing-library/react'; -import { MePermissionsProvider, type MePermissionsResponse } from '@object-ui/permissions'; +import { MePermissionsProvider, type MePermissionsResponse, type MePermissionsAuthenticatedResponse } from '@object-ui/permissions'; import { RecordDetailPanel } from '../RecordDetailPanel'; const OBJECT = 'crm_opportunity'; @@ -51,11 +51,12 @@ const FULL = SHAPES[3].bits; * the field, so the field question answers yes and only the object-level row * can decide. */ -const envelope = (bits: Record, named = true): MePermissionsResponse => ({ +const envelope = (bits: Record, named = true): MePermissionsAuthenticatedResponse => ({ authenticated: true, userId: 'u-pin', tenantId: null, - roles: ['member'], + hasAnyQueue: false, + positions: ['member'], permissionSets: ['member'], objects: { [OBJECT]: bits as never }, fields: named ? { [`${OBJECT}.note`]: { readable: true, editable: true } } : {}, diff --git a/ui/packages/plugin-detail/src/__tests__/RecordRelatedListRenderer.refusalAudience-11768.test.tsx b/ui/packages/plugin-detail/src/__tests__/RecordRelatedListRenderer.refusalAudience-11768.test.tsx index af96e6fb9a9..10859b1a5f7 100644 --- a/ui/packages/plugin-detail/src/__tests__/RecordRelatedListRenderer.refusalAudience-11768.test.tsx +++ b/ui/packages/plugin-detail/src/__tests__/RecordRelatedListRenderer.refusalAudience-11768.test.tsx @@ -52,7 +52,7 @@ import { type RelatedRecordActionsValue, type RelatedRowActionDef, } from '@object-ui/react'; -import { MePermissionsProvider, type MePermissionsResponse } from '@object-ui/permissions'; +import { MePermissionsProvider, type MePermissionsAuthenticatedResponse } from '@object-ui/permissions'; // Registers `record:related_list`, the block under test. import '../index'; @@ -114,19 +114,20 @@ const makeDataSource = () => ({ * The child object is readable, so the list's automatic read gate stays open * and every verdict below is the notice's own. */ -function me(systemPermissions: string[] | undefined): MePermissionsResponse { +function me(systemPermissions: string[] | undefined): MePermissionsAuthenticatedResponse { return { authenticated: true, userId: 'u1', tenantId: 't1', - roles: [], + positions: [], + hasAnyQueue: false, permissionSets: [], ...(systemPermissions === undefined ? {} : { systemPermissions }), objects: { contact: { allowRead: true, allowCreate: false, allowEdit: false, allowDelete: false }, }, fields: {}, - } as MePermissionsResponse; + } as MePermissionsAuthenticatedResponse; } const doc = (actions: string[]) => ({ diff --git a/ui/packages/plugin-detail/src/__tests__/RecordRelatedListRenderer.removeGrant-12082.test.tsx b/ui/packages/plugin-detail/src/__tests__/RecordRelatedListRenderer.removeGrant-12082.test.tsx index c7ec0927f7f..927eb1aa471 100644 --- a/ui/packages/plugin-detail/src/__tests__/RecordRelatedListRenderer.removeGrant-12082.test.tsx +++ b/ui/packages/plugin-detail/src/__tests__/RecordRelatedListRenderer.removeGrant-12082.test.tsx @@ -30,7 +30,7 @@ import { RelatedRecordActionsProvider, type RelatedRecordActionsValue, } from '@object-ui/react'; -import { MePermissionsProvider, type MePermissionsResponse } from '@object-ui/permissions'; +import { MePermissionsProvider, type MePermissionsResponse, type MePermissionsAuthenticatedResponse } from '@object-ui/permissions'; import { RecordRelatedListRenderer } from '../renderers/record-related-list'; const h = vi.hoisted(() => ({ captured: null as any })); @@ -52,16 +52,17 @@ const SHAPE = { full: { allowRead: true, allowCreate: true, allowEdit: true, allowDelete: true }, } as const; -function answer(bits: Record): MePermissionsResponse { +function answer(bits: Record): MePermissionsAuthenticatedResponse { return { authenticated: true, userId: 'u-pin', tenantId: null, - roles: ['member'], + positions: ['member'], + hasAnyQueue: false, permissionSets: ['member'], objects: { [JUNCTION]: bits }, fields: {}, - } as MePermissionsResponse; + } as MePermissionsAuthenticatedResponse; } const ds = { find: vi.fn(async () => []), delete: vi.fn(async () => undefined) }; diff --git a/ui/packages/plugin-detail/src/__tests__/RelatedList.addGrant-12082.test.tsx b/ui/packages/plugin-detail/src/__tests__/RelatedList.addGrant-12082.test.tsx index 2a855b3843e..08d77e36103 100644 --- a/ui/packages/plugin-detail/src/__tests__/RelatedList.addGrant-12082.test.tsx +++ b/ui/packages/plugin-detail/src/__tests__/RelatedList.addGrant-12082.test.tsx @@ -26,7 +26,7 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { render, screen, waitFor, cleanup } from '@testing-library/react'; import * as React from 'react'; -import { MePermissionsProvider, type MePermissionsResponse } from '@object-ui/permissions'; +import { MePermissionsProvider, type MePermissionsResponse, type MePermissionsAuthenticatedResponse } from '@object-ui/permissions'; import { RelatedList } from '../RelatedList'; const JUNCTION = 'sys_user_permission_set'; @@ -41,16 +41,17 @@ const SHAPE = { full: { allowRead: true, allowCreate: true, allowEdit: true, allowDelete: true }, } as const; -function answer(objects: Record>): MePermissionsResponse { +function answer(objects: Record>): MePermissionsAuthenticatedResponse { return { authenticated: true, userId: 'u-pin', tenantId: null, - roles: ['member'], + positions: ['member'], + hasAnyQueue: false, permissionSets: ['member'], objects, fields: {}, - } as MePermissionsResponse; + } as MePermissionsAuthenticatedResponse; } /** diff --git a/ui/packages/plugin-detail/src/queueClaim-25769.test.tsx b/ui/packages/plugin-detail/src/queueClaim-25769.test.tsx index 2f615bfc7e0..6ccdbecb8f1 100644 --- a/ui/packages/plugin-detail/src/queueClaim-25769.test.tsx +++ b/ui/packages/plugin-detail/src/queueClaim-25769.test.tsx @@ -22,7 +22,7 @@ import * as React from 'react'; import { describe, it, expect, vi, afterEach } from 'vitest'; import { renderHook, waitFor } from '@testing-library/react'; -import { MePermissionsProvider, type MePermissionsResponse } from '@object-ui/permissions'; +import { MePermissionsProvider, type MePermissionsAuthenticatedResponse } from '@object-ui/permissions'; import { recordSitsInQueue, queueClaimAdmitted, @@ -30,12 +30,13 @@ import { claimQueuedRecord, } from './queueClaim'; -function me(userId: string): MePermissionsResponse { +function me(userId: string): MePermissionsAuthenticatedResponse { return { authenticated: true, userId, tenantId: 't1', - roles: [], + hasAnyQueue: false, + positions: [], permissionSets: [], objects: { ticket: { allowRead: true, allowCreate: true, allowEdit: true, allowDelete: true } }, fields: {}, diff --git a/ui/packages/plugin-detail/src/renderers/__tests__/record-blocks.recordFieldAccess-27146.test.tsx b/ui/packages/plugin-detail/src/renderers/__tests__/record-blocks.recordFieldAccess-27146.test.tsx index 127b835b000..1ea1c00ff41 100644 --- a/ui/packages/plugin-detail/src/renderers/__tests__/record-blocks.recordFieldAccess-27146.test.tsx +++ b/ui/packages/plugin-detail/src/renderers/__tests__/record-blocks.recordFieldAccess-27146.test.tsx @@ -34,7 +34,7 @@ import { describe, it, expect, vi, beforeAll, beforeEach, afterEach } from 'vite import { render, screen, waitFor, act, cleanup, fireEvent } from '@testing-library/react'; import '@testing-library/jest-dom'; import { HighlightFieldsProvider, InlineEditProvider, RecordContextProvider, useInlineEdit } from '@object-ui/react'; -import { MePermissionsProvider, type MePermissionsResponse } from '@object-ui/permissions'; +import { MePermissionsProvider, type MePermissionsAuthenticatedResponse } from '@object-ui/permissions'; import { RecordDetailsRenderer } from '../record-details'; import { RecordHighlightsRenderer } from '../record-highlights'; @@ -55,11 +55,12 @@ const OBJECT_SCHEMA = { }, }; -const PRINCIPAL: MePermissionsResponse = { +const PRINCIPAL: MePermissionsAuthenticatedResponse = { authenticated: true, userId: 'u-1', tenantId: null, - roles: ['finance'], + positions: ['finance'], + hasAnyQueue: false, permissionSets: ['rfa_fixture_member'], objects: { [OBJECT]: { allowCreate: true, allowRead: true, allowEdit: true, allowDelete: false } }, // As `/auth/me/permissions` serves them: the conditioned entry as its lock. @@ -67,7 +68,7 @@ const PRINCIPAL: MePermissionsResponse = { [`${OBJECT}.summary`]: { readable: true, editable: false }, [`${OBJECT}.ref_no`]: { readable: true, editable: false }, }, -} as MePermissionsResponse; +} as MePermissionsAuthenticatedResponse; const RECORD = { id: 'c1', name: 'MSA', region: 'EMEA', summary: 'Draft terms', ref_no: 'R-1' }; diff --git a/ui/packages/plugin-detail/src/renderers/__tests__/record-blocks.requiredPermissions-gate.test.tsx b/ui/packages/plugin-detail/src/renderers/__tests__/record-blocks.requiredPermissions-gate.test.tsx index e32925e4e92..87e07d4ba6e 100644 --- a/ui/packages/plugin-detail/src/renderers/__tests__/record-blocks.requiredPermissions-gate.test.tsx +++ b/ui/packages/plugin-detail/src/renderers/__tests__/record-blocks.requiredPermissions-gate.test.tsx @@ -47,7 +47,7 @@ import { MePermissionsProvider, PermissionProvider, usePermissions, - type MePermissionsResponse, + type MePermissionsAuthenticatedResponse, } from '@object-ui/permissions'; import { RecordDetailsRenderer } from '../record-details'; import { RecordHighlightsRenderer } from '../record-highlights'; @@ -99,12 +99,13 @@ const ds = { find: vi.fn(async () => []) }; * lets the discriminators below fail — a gate still reading the object-action * path lands on a different verdict for at least one of them. */ -function me(systemPermissions: string[] | undefined, objectBits?: Record): MePermissionsResponse { +function me(systemPermissions: string[] | undefined, objectBits?: Record): MePermissionsAuthenticatedResponse { return { authenticated: true, userId: 'u1', tenantId: 't1', - roles: [], + positions: [], + hasAnyQueue: false, permissionSets: [], ...(systemPermissions === undefined ? {} : { systemPermissions }), objects: { @@ -116,7 +117,7 @@ function me(systemPermissions: string[] | undefined, objectBits?: Record): MePermissionsResponse { +function me(systemPermissions: string[] | undefined, objectBits?: Record): MePermissionsAuthenticatedResponse { return { authenticated: true, userId: 'u1', tenantId: 't1', - roles: [], + positions: [], + hasAnyQueue: false, permissionSets: [], ...(systemPermissions === undefined ? {} : { systemPermissions }), objects: { crm_account: { allowRead: true, allowCreate: false, allowEdit: false, allowDelete: false, ...objectBits }, }, fields: {}, - } as MePermissionsResponse; + } as MePermissionsAuthenticatedResponse; } function bar(schema: Record, objectName = 'crm_account') { diff --git a/ui/packages/plugin-detail/src/useRecordEditable.inflightShare-11699.test.tsx b/ui/packages/plugin-detail/src/useRecordEditable.inflightShare-11699.test.tsx index edb377e7b5c..60eed3ee2d4 100644 --- a/ui/packages/plugin-detail/src/useRecordEditable.inflightShare-11699.test.tsx +++ b/ui/packages/plugin-detail/src/useRecordEditable.inflightShare-11699.test.tsx @@ -32,7 +32,7 @@ import * as React from 'react'; import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { act, renderHook, waitFor } from '@testing-library/react'; import { notifyDataChanged } from '@object-ui/react'; -import { MePermissionsProvider, type MePermissionsResponse } from '@object-ui/permissions'; +import { MePermissionsProvider, type MePermissionsAuthenticatedResponse } from '@object-ui/permissions'; import { useRecordEditable, __clearRecordEditableCache } from './useRecordEditable'; interface HeldAsk { @@ -63,12 +63,13 @@ function heldExplain() { return { asks, fetchMock }; } -function me(userId: string): MePermissionsResponse { +function me(userId: string): MePermissionsAuthenticatedResponse { return { authenticated: true, userId, tenantId: 't1', - roles: [], + hasAnyQueue: false, + positions: [], permissionSets: [], objects: { note: { allowRead: true, allowCreate: true, allowEdit: true, allowDelete: true } }, fields: {}, diff --git a/ui/packages/plugin-detail/src/useRecordEditable.invalidation.test.tsx b/ui/packages/plugin-detail/src/useRecordEditable.invalidation.test.tsx index ae6744b5ef2..52a19beffb7 100644 --- a/ui/packages/plugin-detail/src/useRecordEditable.invalidation.test.tsx +++ b/ui/packages/plugin-detail/src/useRecordEditable.invalidation.test.tsx @@ -37,16 +37,17 @@ import * as React from 'react'; import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { act, renderHook, waitFor } from '@testing-library/react'; import { notifyDataChanged } from '@object-ui/react'; -import { MePermissionsProvider, type MePermissionsResponse } from '@object-ui/permissions'; +import { MePermissionsProvider, type MePermissionsAuthenticatedResponse } from '@object-ui/permissions'; import { useRecordEditable, __clearRecordEditableCache } from './useRecordEditable'; /** A `/me/permissions` payload for one principal; the object grant is constant. */ -function me(userId: string): MePermissionsResponse { +function me(userId: string): MePermissionsAuthenticatedResponse { return { authenticated: true, userId, tenantId: 't1', - roles: [], + hasAnyQueue: false, + positions: [], permissionSets: [], objects: { note: { allowRead: true, allowCreate: true, allowEdit: true, allowDelete: true } }, fields: {}, diff --git a/ui/packages/plugin-detail/src/useRecordEditable.principalScope.test.tsx b/ui/packages/plugin-detail/src/useRecordEditable.principalScope.test.tsx index a92a0b68600..2b60e8d1eb9 100644 --- a/ui/packages/plugin-detail/src/useRecordEditable.principalScope.test.tsx +++ b/ui/packages/plugin-detail/src/useRecordEditable.principalScope.test.tsx @@ -42,18 +42,19 @@ import * as React from 'react'; import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { renderHook, waitFor } from '@testing-library/react'; -import { MePermissionsProvider, type MePermissionsResponse } from '@object-ui/permissions'; +import { MePermissionsProvider, type MePermissionsAuthenticatedResponse } from '@object-ui/permissions'; import { useRecordEditable, __clearRecordEditableCache } from './useRecordEditable'; /** A `/me/permissions` payload for one principal. The object bits are constant * across principals on purpose: the OBJECT-level grant is not what this file * measures, so it cannot be the reason a verdict changes. */ -function me(userId: string): MePermissionsResponse { +function me(userId: string): MePermissionsAuthenticatedResponse { return { authenticated: true, userId, tenantId: 't1', - roles: [], + hasAnyQueue: false, + positions: [], permissionSets: [], objects: { note: { allowRead: true, allowCreate: true, allowEdit: true, allowDelete: true } }, fields: {}, diff --git a/ui/packages/plugin-detail/src/useRecordEditable.recheckOnce-13341.test.tsx b/ui/packages/plugin-detail/src/useRecordEditable.recheckOnce-13341.test.tsx index 15007a26ca7..b2ccc3445ae 100644 --- a/ui/packages/plugin-detail/src/useRecordEditable.recheckOnce-13341.test.tsx +++ b/ui/packages/plugin-detail/src/useRecordEditable.recheckOnce-13341.test.tsx @@ -64,7 +64,8 @@ const asA = ({ children }: { children: React.ReactNode }) => ( authenticated: true, userId: 'userA', tenantId: 't1', - roles: [], + hasAnyQueue: false, + positions: [], permissionSets: [], objects: { note: { allowRead: true, allowCreate: true, allowEdit: true, allowDelete: true } }, fields: {}, diff --git a/ui/packages/plugin-form/src/LineItemsPanel.lineGrant-12082.test.tsx b/ui/packages/plugin-form/src/LineItemsPanel.lineGrant-12082.test.tsx index 64112edc1e9..dafc9135c26 100644 --- a/ui/packages/plugin-form/src/LineItemsPanel.lineGrant-12082.test.tsx +++ b/ui/packages/plugin-form/src/LineItemsPanel.lineGrant-12082.test.tsx @@ -27,7 +27,7 @@ import { describe, it, expect, vi, afterEach } from 'vitest'; import { render, screen, waitFor, cleanup } from '@testing-library/react'; import React from 'react'; -import { MePermissionsProvider, type MePermissionsResponse } from '@object-ui/permissions'; +import { MePermissionsProvider, type MePermissionsResponse, type MePermissionsAuthenticatedResponse } from '@object-ui/permissions'; import { registerAllFields } from '@object-ui/fields'; import { SchemaRendererProvider } from '@object-ui/react'; import { LineItemsPanel } from './LineItemsPanel'; @@ -60,11 +60,12 @@ const SHAPES: Array<{ shape: string; bits: Record; add: boolean { shape: 'full', bits: { allowCreate: true, allowRead: true, allowEdit: true, allowDelete: true }, add: true, remove: true }, ]; -const envelope = (bits: Record): MePermissionsResponse => ({ +const envelope = (bits: Record): MePermissionsAuthenticatedResponse => ({ authenticated: true, userId: 'u-pin', tenantId: null, - roles: ['member'], + hasAnyQueue: false, + positions: ['member'], permissionSets: ['member'], // The parent is fully granted: only the CHILD's grant may decide. objects: { diff --git a/ui/packages/plugin-form/src/MasterDetailForm.lineGrant-12082.test.tsx b/ui/packages/plugin-form/src/MasterDetailForm.lineGrant-12082.test.tsx index ebe7c788efc..2c58afcbfa4 100644 --- a/ui/packages/plugin-form/src/MasterDetailForm.lineGrant-12082.test.tsx +++ b/ui/packages/plugin-form/src/MasterDetailForm.lineGrant-12082.test.tsx @@ -32,7 +32,7 @@ import { describe, it, expect, vi, afterEach } from 'vitest'; import { render, screen, waitFor, fireEvent, cleanup } from '@testing-library/react'; import React from 'react'; -import { MePermissionsProvider, type MePermissionsResponse } from '@object-ui/permissions'; +import { MePermissionsProvider, type MePermissionsResponse, type MePermissionsAuthenticatedResponse } from '@object-ui/permissions'; import { registerAllFields } from '@object-ui/fields'; import { MasterDetailForm } from './MasterDetailForm'; @@ -51,11 +51,12 @@ const SHAPES: Array<{ shape: string; bits: Record; create: bool { shape: 'full', bits: { allowCreate: true, allowRead: true, allowEdit: true, allowDelete: true }, create: true, remove: true }, ]; -const envelope = (child: Record): MePermissionsResponse => ({ +const envelope = (child: Record): MePermissionsAuthenticatedResponse => ({ authenticated: true, userId: 'u-pin', tenantId: null, - roles: ['member'], + hasAnyQueue: false, + positions: ['member'], permissionSets: ['member'], objects: { [PARENT]: { allowCreate: true, allowRead: true, allowEdit: true, allowDelete: true }, diff --git a/ui/packages/plugin-form/src/affordanceGrantMap-12082.test.tsx b/ui/packages/plugin-form/src/affordanceGrantMap-12082.test.tsx index a8612be09a5..28f9faaf812 100644 --- a/ui/packages/plugin-form/src/affordanceGrantMap-12082.test.tsx +++ b/ui/packages/plugin-form/src/affordanceGrantMap-12082.test.tsx @@ -74,7 +74,7 @@ import { type ConsoleAffordance, type FieldAffordance, } from '@object-ui/core'; -import { MePermissionsProvider, usePermissions, type MePermissionsResponse } from '@object-ui/permissions'; +import { MePermissionsProvider, usePermissions, type MePermissionsResponse, type MePermissionsAuthenticatedResponse } from '@object-ui/permissions'; import { SystemFieldName } from '@objectstack/spec/system'; import { closedFormAffordance, fieldWriteGate, gateFormFields } from './fieldWriteGate'; import ts from 'typescript'; @@ -182,11 +182,12 @@ const SHAPES: Array<{ shape: string; bits: Record; holds: Reado }, ]; -const envelope = (bits: Record, fields: MePermissionsResponse['fields'] = {}): MePermissionsResponse => ({ +const envelope = (bits: Record, fields: MePermissionsAuthenticatedResponse['fields'] = {}): MePermissionsAuthenticatedResponse => ({ authenticated: true, userId: 'u-pin', tenantId: null, - roles: ['member'], + hasAnyQueue: false, + positions: ['member'], permissionSets: ['member'], objects: { [OBJECT]: bits }, fields, diff --git a/ui/packages/plugin-form/src/createFormGrant-12082.test.tsx b/ui/packages/plugin-form/src/createFormGrant-12082.test.tsx index 78478b9ebf6..f944452af90 100644 --- a/ui/packages/plugin-form/src/createFormGrant-12082.test.tsx +++ b/ui/packages/plugin-form/src/createFormGrant-12082.test.tsx @@ -48,7 +48,7 @@ import { describe, it, expect, vi, afterEach } from 'vitest'; import { render, screen, waitFor, fireEvent, cleanup } from '@testing-library/react'; import React from 'react'; -import { MePermissionsProvider, type MePermissionsResponse } from '@object-ui/permissions'; +import { MePermissionsProvider, type MePermissionsResponse, type MePermissionsAuthenticatedResponse } from '@object-ui/permissions'; import { registerAllFields } from '@object-ui/fields'; import { ObjectForm } from './ObjectForm'; import { MasterDetailForm } from './MasterDetailForm'; @@ -66,11 +66,12 @@ const FIELDS = { const RECORD = { id: 'v1', contract: 'C-1', version_no: 1, notes: 'first' }; /** `/me/permissions` for the card's requester: create, never edit. */ -const createOnly = (fields: MePermissionsResponse['fields'] = {}): MePermissionsResponse => ({ +const createOnly = (fields: MePermissionsAuthenticatedResponse['fields'] = {}): MePermissionsAuthenticatedResponse => ({ authenticated: true, userId: 'u-requester', tenantId: null, - roles: ['clm_requester'], + hasAnyQueue: false, + positions: ['clm_requester'], permissionSets: ['clm_requester'], objects: { [OBJECT]: { allowCreate: true, allowRead: true, allowEdit: false, allowDelete: false }, diff --git a/ui/packages/plugin-form/src/listNewAbsentCreateBit-26193.test.tsx b/ui/packages/plugin-form/src/listNewAbsentCreateBit-26193.test.tsx index 23ddfa4765e..7d7ccbe66c8 100644 --- a/ui/packages/plugin-form/src/listNewAbsentCreateBit-26193.test.tsx +++ b/ui/packages/plugin-form/src/listNewAbsentCreateBit-26193.test.tsx @@ -27,17 +27,18 @@ import { describe, it, expect, afterEach } from 'vitest'; import { render, cleanup } from '@testing-library/react'; import React from 'react'; import { resolveAffordance, type ConsoleAffordance } from '@object-ui/core'; -import { MePermissionsProvider, usePermissions, type MePermissionsResponse } from '@object-ui/permissions'; +import { MePermissionsProvider, usePermissions, type MePermissionsResponse, type MePermissionsAuthenticatedResponse } from '@object-ui/permissions'; afterEach(cleanup); const OBJECT = 'invoice'; -const envelope = (objects: MePermissionsResponse['objects']): MePermissionsResponse => ({ +const envelope = (objects: MePermissionsAuthenticatedResponse['objects']): MePermissionsAuthenticatedResponse => ({ authenticated: true, userId: 'u-auditor', tenantId: null, - roles: ['auditor'], + hasAnyQueue: false, + positions: ['auditor'], permissionSets: ['auditor'], objects, fields: {}, diff --git a/ui/packages/plugin-form/src/recordFieldAccess-26594.test.tsx b/ui/packages/plugin-form/src/recordFieldAccess-26594.test.tsx index 95b7e51d508..77f84b5055f 100644 --- a/ui/packages/plugin-form/src/recordFieldAccess-26594.test.tsx +++ b/ui/packages/plugin-form/src/recordFieldAccess-26594.test.tsx @@ -50,7 +50,8 @@ const PRINCIPAL: any = { authenticated: true, userId: 'u-1', tenantId: null, - roles: ['finance'], + positions: ['finance'], + hasAnyQueue: false, permissionSets: ['rfa_fixture_member'], objects: { rfa_contract: { allowCreate: true, allowRead: true, allowEdit: true, allowDelete: false } }, // As `/auth/me/permissions` serves them: the conditioned entry as its lock. diff --git a/ui/packages/plugin-gantt/src/ObjectGantt.writeGrant-12082.test.tsx b/ui/packages/plugin-gantt/src/ObjectGantt.writeGrant-12082.test.tsx index df21f47b628..fd6e797195e 100644 --- a/ui/packages/plugin-gantt/src/ObjectGantt.writeGrant-12082.test.tsx +++ b/ui/packages/plugin-gantt/src/ObjectGantt.writeGrant-12082.test.tsx @@ -30,7 +30,7 @@ import React from 'react'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { act, cleanup, render, screen, waitFor } from '@testing-library/react'; -import { MePermissionsProvider, type MePermissionsResponse } from '@object-ui/permissions'; +import { MePermissionsProvider, type MePermissionsResponse, type MePermissionsAuthenticatedResponse } from '@object-ui/permissions'; const probe = vi.hoisted(() => ({ props: null as null | Record, @@ -92,11 +92,12 @@ const SHAPES: Array<{ shape: string; bits: Record; update: bool { shape: 'full', bits: { allowCreate: true, allowRead: true, allowEdit: true, allowDelete: true }, update: true, remove: true }, ]; -const envelope = (bits: Record): MePermissionsResponse => ({ +const envelope = (bits: Record): MePermissionsAuthenticatedResponse => ({ authenticated: true, userId: 'u-pin', tenantId: null, - roles: ['member'], + hasAnyQueue: false, + positions: ['member'], permissionSets: ['member'], objects: { [OBJECT]: bits as never }, fields: {}, diff --git a/ui/packages/plugin-grid/src/__tests__/ObjectGrid.emptyStateAccess-25898.test.tsx b/ui/packages/plugin-grid/src/__tests__/ObjectGrid.emptyStateAccess-25898.test.tsx index 53990f9f60e..508f9211073 100644 --- a/ui/packages/plugin-grid/src/__tests__/ObjectGrid.emptyStateAccess-25898.test.tsx +++ b/ui/packages/plugin-grid/src/__tests__/ObjectGrid.emptyStateAccess-25898.test.tsx @@ -26,7 +26,7 @@ import { describe, it, expect, afterEach } from 'vitest'; import { render, screen, cleanup } from '@testing-library/react'; import '@testing-library/jest-dom'; import { FilterScopeProvider } from '@object-ui/react'; -import { MePermissionsProvider, type MePermissionsResponse } from '@object-ui/permissions'; +import { MePermissionsProvider, type MePermissionsAuthenticatedResponse } from '@object-ui/permissions'; // Registers `object-grid`, the block under test. import { ObjectGridRenderer } from '../index'; @@ -38,11 +38,12 @@ const USER = 'u-rep'; const READ_ONLY = { allowCreate: false, allowRead: true, allowEdit: false, allowDelete: false }; const FULL = { allowCreate: true, allowRead: true, allowEdit: true, allowDelete: true }; -const envelope = (bits: Record): MePermissionsResponse => ({ +const envelope = (bits: Record): MePermissionsAuthenticatedResponse => ({ authenticated: true, userId: USER, tenantId: null, - roles: ['member'], + hasAnyQueue: false, + positions: ['member'], permissionSets: ['member'], objects: { [OBJECT]: bits }, fields: {}, diff --git a/ui/packages/plugin-grid/src/__tests__/bulkUpdateGrant-12082.test.tsx b/ui/packages/plugin-grid/src/__tests__/bulkUpdateGrant-12082.test.tsx index e514afdb958..cf1d3defbd4 100644 --- a/ui/packages/plugin-grid/src/__tests__/bulkUpdateGrant-12082.test.tsx +++ b/ui/packages/plugin-grid/src/__tests__/bulkUpdateGrant-12082.test.tsx @@ -29,7 +29,7 @@ import userEvent from '@testing-library/user-event'; import '@testing-library/jest-dom'; import React from 'react'; -import { MePermissionsProvider, type MePermissionsResponse } from '@object-ui/permissions'; +import { MePermissionsProvider, type MePermissionsResponse, type MePermissionsAuthenticatedResponse } from '@object-ui/permissions'; import { registerAllFields } from '@object-ui/fields'; import { ActionProvider, SchemaRendererProvider } from '@object-ui/react'; import { ObjectGrid } from '../ObjectGrid'; @@ -57,11 +57,12 @@ const SHAPES: Array<{ shape: string; bits: Record; update: bool { shape: 'full', bits: { allowCreate: true, allowRead: true, allowEdit: true, allowDelete: true }, update: true, remove: true }, ]; -const envelope = (bits: Record): MePermissionsResponse => ({ +const envelope = (bits: Record): MePermissionsAuthenticatedResponse => ({ authenticated: true, userId: 'u-pin', tenantId: null, - roles: ['member'], + hasAnyQueue: false, + positions: ['member'], permissionSets: ['member'], objects: { [OBJECT]: bits as never }, fields: {}, diff --git a/ui/packages/plugin-grid/src/__tests__/inlineEditFieldWriteGrant-12103.test.tsx b/ui/packages/plugin-grid/src/__tests__/inlineEditFieldWriteGrant-12103.test.tsx index 31304e389f7..61d4cb75a94 100644 --- a/ui/packages/plugin-grid/src/__tests__/inlineEditFieldWriteGrant-12103.test.tsx +++ b/ui/packages/plugin-grid/src/__tests__/inlineEditFieldWriteGrant-12103.test.tsx @@ -27,7 +27,7 @@ import { describe, it, expect, vi, beforeAll, beforeEach, afterEach } from 'vite import { render, screen, waitFor, fireEvent, cleanup } from '@testing-library/react'; import '@testing-library/jest-dom'; import React from 'react'; -import { MePermissionsProvider, type MePermissionsResponse } from '@object-ui/permissions'; +import { MePermissionsProvider, type MePermissionsResponse, type MePermissionsAuthenticatedResponse } from '@object-ui/permissions'; import { SystemFieldName } from '@objectstack/spec/system'; import { ObjectGrid } from '../ObjectGrid'; import { __clearRecordCrudVerdictCache } from '../hooks/useRecordCrudVerdicts'; @@ -56,12 +56,13 @@ afterEach(() => { }); /** The measured envelope: may edit the object, may not edit annual revenue, may not transfer. */ -function envelope(objectBits: Record = {}): MePermissionsResponse { +function envelope(objectBits: Record = {}): MePermissionsAuthenticatedResponse { return { authenticated: true, userId: 'u-rep', tenantId: null, - roles: [], + hasAnyQueue: false, + positions: [], permissionSets: ['rep'], objects: { [OBJECT]: { diff --git a/ui/packages/plugin-grid/src/hooks/useRecordCrudVerdicts.afterAction-13341.test.tsx b/ui/packages/plugin-grid/src/hooks/useRecordCrudVerdicts.afterAction-13341.test.tsx index c511f3d03d5..5858e9ae761 100644 --- a/ui/packages/plugin-grid/src/hooks/useRecordCrudVerdicts.afterAction-13341.test.tsx +++ b/ui/packages/plugin-grid/src/hooks/useRecordCrudVerdicts.afterAction-13341.test.tsx @@ -39,7 +39,7 @@ import * as React from 'react'; import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { act, renderHook, waitFor } from '@testing-library/react'; import { notifyDataChanged } from '@object-ui/react'; -import { MePermissionsProvider, type MePermissionsResponse } from '@object-ui/permissions'; +import { MePermissionsProvider, type MePermissionsAuthenticatedResponse } from '@object-ui/permissions'; import { useRecordCrudVerdicts, __clearRecordCrudVerdictCache, @@ -48,12 +48,13 @@ import { const OBJECT = 'sys_business_unit'; -function me(userId: string): MePermissionsResponse { +function me(userId: string): MePermissionsAuthenticatedResponse { return { authenticated: true, userId, tenantId: 't1', - roles: [], + hasAnyQueue: false, + positions: [], permissionSets: [], objects: { [OBJECT]: { allowRead: true, allowCreate: true, allowEdit: true, allowDelete: true } }, fields: {}, diff --git a/ui/packages/plugin-grid/src/hooks/useRecordCrudVerdicts.invalidation.test.tsx b/ui/packages/plugin-grid/src/hooks/useRecordCrudVerdicts.invalidation.test.tsx index 569617f32e0..4738276f199 100644 --- a/ui/packages/plugin-grid/src/hooks/useRecordCrudVerdicts.invalidation.test.tsx +++ b/ui/packages/plugin-grid/src/hooks/useRecordCrudVerdicts.invalidation.test.tsx @@ -29,7 +29,7 @@ import * as React from 'react'; import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { act, renderHook, waitFor } from '@testing-library/react'; import { notifyDataChanged } from '@object-ui/react'; -import { MePermissionsProvider, type MePermissionsResponse } from '@object-ui/permissions'; +import { MePermissionsProvider, type MePermissionsAuthenticatedResponse } from '@object-ui/permissions'; import { useRecordCrudVerdicts, __clearRecordCrudVerdictCache, @@ -38,12 +38,13 @@ import { const OBJECT = 'note'; -function me(userId: string): MePermissionsResponse { +function me(userId: string): MePermissionsAuthenticatedResponse { return { authenticated: true, userId, tenantId: 't1', - roles: [], + hasAnyQueue: false, + positions: [], permissionSets: [], objects: { [OBJECT]: { allowRead: true, allowCreate: true, allowEdit: true, allowDelete: true } }, fields: {}, diff --git a/ui/packages/plugin-grid/src/hooks/useRecordCrudVerdicts.principalScope.test.tsx b/ui/packages/plugin-grid/src/hooks/useRecordCrudVerdicts.principalScope.test.tsx index 223bcba989a..4a17b11ebfa 100644 --- a/ui/packages/plugin-grid/src/hooks/useRecordCrudVerdicts.principalScope.test.tsx +++ b/ui/packages/plugin-grid/src/hooks/useRecordCrudVerdicts.principalScope.test.tsx @@ -33,18 +33,19 @@ import * as React from 'react'; import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { act, renderHook, waitFor } from '@testing-library/react'; -import { MePermissionsProvider, type MePermissionsResponse } from '@object-ui/permissions'; +import { MePermissionsProvider, type MePermissionsAuthenticatedResponse } from '@object-ui/permissions'; import { useRecordCrudVerdicts, __clearRecordCrudVerdictCache } from './useRecordCrudVerdicts'; const OBJECT = 'note'; /** A `/me/permissions` payload for one principal; the object grant is constant. */ -function me(userId: string): MePermissionsResponse { +function me(userId: string): MePermissionsAuthenticatedResponse { return { authenticated: true, userId, tenantId: 't1', - roles: [], + hasAnyQueue: false, + positions: [], permissionSets: [], objects: { [OBJECT]: { allowRead: true, allowCreate: true, allowEdit: true, allowDelete: true } }, fields: {}, diff --git a/ui/packages/plugin-kanban/src/ObjectKanban.moveGrant-12082.test.tsx b/ui/packages/plugin-kanban/src/ObjectKanban.moveGrant-12082.test.tsx index 7cdd86a896d..408cd7c2189 100644 --- a/ui/packages/plugin-kanban/src/ObjectKanban.moveGrant-12082.test.tsx +++ b/ui/packages/plugin-kanban/src/ObjectKanban.moveGrant-12082.test.tsx @@ -28,7 +28,7 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { render, screen, act, cleanup, waitFor } from '@testing-library/react'; import { registerAllFields } from '@object-ui/fields'; import { toast } from '@object-ui/components'; -import { MePermissionsProvider, type MePermissionsResponse } from '@object-ui/permissions'; +import { MePermissionsProvider, type MePermissionsResponse, type MePermissionsAuthenticatedResponse } from '@object-ui/permissions'; import type { DataSource, ObjectKanbanSchema } from '@object-ui/types'; import { ObjectKanban } from './ObjectKanban'; @@ -88,11 +88,12 @@ const SHAPES: Array<{ shape: string; bits: Record; update: bool { shape: 'full', bits: { allowCreate: true, allowRead: true, allowEdit: true, allowDelete: true }, update: true }, ]; -const envelope = (bits: Record): MePermissionsResponse => ({ +const envelope = (bits: Record): MePermissionsAuthenticatedResponse => ({ authenticated: true, userId: 'u-pin', tenantId: null, - roles: ['member'], + hasAnyQueue: false, + positions: ['member'], permissionSets: ['member'], objects: { [OBJECT]: bits as never }, fields: {}, diff --git a/ui/packages/plugin-list/src/__tests__/ListView.emptyStateAccess-25898.test.tsx b/ui/packages/plugin-list/src/__tests__/ListView.emptyStateAccess-25898.test.tsx index a0f10ec8bf8..b2981fe45cd 100644 --- a/ui/packages/plugin-list/src/__tests__/ListView.emptyStateAccess-25898.test.tsx +++ b/ui/packages/plugin-list/src/__tests__/ListView.emptyStateAccess-25898.test.tsx @@ -38,7 +38,7 @@ import { render, waitFor, cleanup } from '@testing-library/react'; import { ListView } from '../ListView'; import { filterNamesCurrentUser } from '../ListEmptyState'; import { SchemaRendererProvider, FilterScopeProvider } from '@object-ui/react'; -import { MePermissionsProvider, type MePermissionsResponse } from '@object-ui/permissions'; +import { MePermissionsProvider, type MePermissionsAuthenticatedResponse } from '@object-ui/permissions'; import type { ListViewSchema } from '@object-ui/types'; afterEach(cleanup); @@ -59,11 +59,12 @@ function emptyDataSource() { const READ_ONLY = { allowCreate: false, allowRead: true, allowEdit: false, allowDelete: false }; const FULL = { allowCreate: true, allowRead: true, allowEdit: true, allowDelete: true }; -const envelope = (bits: Record): MePermissionsResponse => ({ +const envelope = (bits: Record): MePermissionsAuthenticatedResponse => ({ authenticated: true, userId: USER, tenantId: null, - roles: ['member'], + hasAnyQueue: false, + positions: ['member'], permissionSets: ['member'], objects: { [OBJECT]: bits }, fields: {}, diff --git a/ui/packages/plugin-list/src/__tests__/ListView.fieldListRead-11984.test.tsx b/ui/packages/plugin-list/src/__tests__/ListView.fieldListRead-11984.test.tsx index 1b6f4b02c05..79bf76c1010 100644 --- a/ui/packages/plugin-list/src/__tests__/ListView.fieldListRead-11984.test.tsx +++ b/ui/packages/plugin-list/src/__tests__/ListView.fieldListRead-11984.test.tsx @@ -49,7 +49,7 @@ import '@testing-library/jest-dom'; import React from 'react'; import type { DataSource, ListViewSchema } from '@object-ui/types'; import { SchemaRendererProvider } from '@object-ui/react'; -import { MePermissionsProvider, type MePermissionsResponse } from '@object-ui/permissions'; +import { MePermissionsProvider, type MePermissionsResponse, type MePermissionsAuthenticatedResponse } from '@object-ui/permissions'; import { ListView } from '../ListView'; const OBJECT = 'fls_ticket'; @@ -66,12 +66,13 @@ const COLUMNS = Object.entries(FIELDS).map(([field, def]) => ({ field, label: de const FIELD_LABELS = COLUMNS.map((c) => c.label); const UNREADABLE = /secret/i; -function answer(fields: MePermissionsResponse['fields']): MePermissionsResponse { +function answer(fields: MePermissionsAuthenticatedResponse['fields']): MePermissionsAuthenticatedResponse { return { authenticated: true, userId: 'u1', tenantId: null, - roles: [], + hasAnyQueue: false, + positions: [], permissionSets: ['fls_member'], objects: { [OBJECT]: { allowRead: true, allowCreate: false, allowEdit: false, allowDelete: false } }, fields, diff --git a/ui/packages/plugin-list/src/__tests__/ListView.filterFieldRead-11925.test.tsx b/ui/packages/plugin-list/src/__tests__/ListView.filterFieldRead-11925.test.tsx index e6e874498d6..d270bfa67ba 100644 --- a/ui/packages/plugin-list/src/__tests__/ListView.filterFieldRead-11925.test.tsx +++ b/ui/packages/plugin-list/src/__tests__/ListView.filterFieldRead-11925.test.tsx @@ -27,7 +27,7 @@ import '@testing-library/jest-dom'; import React from 'react'; import type { DataSource, ListViewSchema } from '@object-ui/types'; import { SchemaRendererProvider } from '@object-ui/react'; -import { MePermissionsProvider, type MePermissionsResponse } from '@object-ui/permissions'; +import { MePermissionsProvider, type MePermissionsResponse, type MePermissionsAuthenticatedResponse } from '@object-ui/permissions'; type Candidate = { value: string; label: string; type: string }; @@ -62,12 +62,13 @@ const COLUMNS = ['title', 'status', 'secret_note']; /** Today's list for a user who may read every field: the columns, then the other business field. */ const FULL_READ_LIST = ['title', 'status', 'secret_note', 'priority']; -function answer(fields: MePermissionsResponse['fields']): MePermissionsResponse { +function answer(fields: MePermissionsAuthenticatedResponse['fields']): MePermissionsAuthenticatedResponse { return { authenticated: true, userId: 'u1', tenantId: null, - roles: [], + hasAnyQueue: false, + positions: [], permissionSets: ['fls_member'], objects: { [OBJECT]: { allowRead: true, allowCreate: false, allowEdit: false, allowDelete: false } }, fields, diff --git a/ui/packages/plugin-list/src/__tests__/ListView.sortFieldRead-11943.test.tsx b/ui/packages/plugin-list/src/__tests__/ListView.sortFieldRead-11943.test.tsx index 9abe76bc04b..ee101563197 100644 --- a/ui/packages/plugin-list/src/__tests__/ListView.sortFieldRead-11943.test.tsx +++ b/ui/packages/plugin-list/src/__tests__/ListView.sortFieldRead-11943.test.tsx @@ -31,7 +31,7 @@ import '@testing-library/jest-dom'; import React from 'react'; import type { DataSource, ListViewSchema } from '@object-ui/types'; import { SchemaRendererProvider } from '@object-ui/react'; -import { MePermissionsProvider, type MePermissionsResponse } from '@object-ui/permissions'; +import { MePermissionsProvider, type MePermissionsResponse, type MePermissionsAuthenticatedResponse } from '@object-ui/permissions'; import { ListView } from '../ListView'; const OBJECT = 'fls_ticket'; @@ -49,12 +49,13 @@ const COLUMNS = ['title', 'status', 'secret_note']; /** Today's Sort picker for a user who may read every field. */ const FULL_READ_LABELS = ['Title', 'Status', 'Secret Note', 'Priority']; -function answer(fields: MePermissionsResponse['fields']): MePermissionsResponse { +function answer(fields: MePermissionsAuthenticatedResponse['fields']): MePermissionsAuthenticatedResponse { return { authenticated: true, userId: 'u1', tenantId: null, - roles: [], + hasAnyQueue: false, + positions: [], permissionSets: ['fls_member'], objects: { [OBJECT]: { allowRead: true, allowCreate: false, allowEdit: false, allowDelete: false } }, fields, diff --git a/ui/packages/plugin-list/src/__tests__/ListView.sortRemovableOnly-11943.test.tsx b/ui/packages/plugin-list/src/__tests__/ListView.sortRemovableOnly-11943.test.tsx index 785c37caf33..6b47de81a80 100644 --- a/ui/packages/plugin-list/src/__tests__/ListView.sortRemovableOnly-11943.test.tsx +++ b/ui/packages/plugin-list/src/__tests__/ListView.sortRemovableOnly-11943.test.tsx @@ -33,7 +33,7 @@ import { resolveObjectSortability } from '@objectstack/spec/api'; import { attachObjectSortability } from '@object-ui/core'; import type { DataSource, ListViewSchema } from '@object-ui/types'; import { SchemaRendererProvider } from '@object-ui/react'; -import { MePermissionsProvider, type MePermissionsResponse } from '@object-ui/permissions'; +import { MePermissionsProvider, type MePermissionsResponse, type MePermissionsAuthenticatedResponse } from '@object-ui/permissions'; import { ListView } from '../ListView'; const OBJECT = 'fls_ticket'; @@ -53,12 +53,13 @@ function definition(first: string) { return { name: OBJECT, label: 'Ticket', fields }; } -function answer(fields: MePermissionsResponse['fields']): MePermissionsResponse { +function answer(fields: MePermissionsAuthenticatedResponse['fields']): MePermissionsAuthenticatedResponse { return { authenticated: true, userId: 'u1', tenantId: null, - roles: [], + hasAnyQueue: false, + positions: [], permissionSets: ['fls_member'], objects: { [OBJECT]: { allowRead: true, allowCreate: false, allowEdit: false, allowDelete: false } }, fields, diff --git a/ui/packages/plugin-list/src/__tests__/ListViewBlock.inertKeys-13074.test.tsx b/ui/packages/plugin-list/src/__tests__/ListViewBlock.inertKeys-13074.test.tsx index f64fef5d5bb..9780dbfd2b7 100644 --- a/ui/packages/plugin-list/src/__tests__/ListViewBlock.inertKeys-13074.test.tsx +++ b/ui/packages/plugin-list/src/__tests__/ListViewBlock.inertKeys-13074.test.tsx @@ -42,7 +42,7 @@ import { type RelatedRecordActionsValue, type RelatedRecordHandlers, } from '@object-ui/react'; -import { MePermissionsProvider, type MePermissionsResponse } from '@object-ui/permissions'; +import { MePermissionsProvider, type MePermissionsAuthenticatedResponse } from '@object-ui/permissions'; import { ComponentPropsMap } from '@objectstack/spec/ui'; // Registers the REAL `list-view`. import '../index'; @@ -102,19 +102,20 @@ function node(properties: Row = {}): Row { } /** A `/me/permissions` payload; the object may be read, never created. */ -function me(systemPermissions: string[] | undefined): MePermissionsResponse { +function me(systemPermissions: string[] | undefined): MePermissionsAuthenticatedResponse { return { authenticated: true, userId: 'u1', tenantId: 't1', - roles: [], + positions: [], + hasAnyQueue: false, permissionSets: [], ...(systemPermissions === undefined ? {} : { systemPermissions }), objects: { [OBJECT]: { allowRead: true, allowCreate: false, allowEdit: false, allowDelete: false }, }, fields: {}, - } as MePermissionsResponse; + } as MePermissionsAuthenticatedResponse; } interface MountOptions { diff --git a/ui/packages/plugin-list/src/__tests__/ObjectGallery.emptyStateAccess-25898.test.tsx b/ui/packages/plugin-list/src/__tests__/ObjectGallery.emptyStateAccess-25898.test.tsx index e1584bf2ddb..f921cc3b017 100644 --- a/ui/packages/plugin-list/src/__tests__/ObjectGallery.emptyStateAccess-25898.test.tsx +++ b/ui/packages/plugin-list/src/__tests__/ObjectGallery.emptyStateAccess-25898.test.tsx @@ -25,7 +25,7 @@ import { describe, it, expect, vi, afterEach } from 'vitest'; import { render, cleanup, waitFor } from '@testing-library/react'; import { I18nProvider } from '@object-ui/i18n'; import { SchemaRendererProvider, FilterScopeProvider } from '@object-ui/react'; -import { MePermissionsProvider, type MePermissionsResponse } from '@object-ui/permissions'; +import { MePermissionsProvider, type MePermissionsAuthenticatedResponse } from '@object-ui/permissions'; import type { DataSource, ObjectGallerySchema } from '@object-ui/types'; import { ObjectGallery } from '../ObjectGallery'; @@ -36,11 +36,12 @@ const USER = 'u-rep'; const READ_ONLY = { allowCreate: false, allowRead: true, allowEdit: false, allowDelete: false }; const FULL = { allowCreate: true, allowRead: true, allowEdit: true, allowDelete: true }; -const envelope = (bits: Record): MePermissionsResponse => ({ +const envelope = (bits: Record): MePermissionsAuthenticatedResponse => ({ authenticated: true, userId: USER, tenantId: null, - roles: ['member'], + hasAnyQueue: false, + positions: ['member'], permissionSets: ['member'], objects: { [OBJECT]: bits }, fields: {}, diff --git a/ui/packages/plugin-report/src/__tests__/DatasetReportRenderer.export-25873.test.tsx b/ui/packages/plugin-report/src/__tests__/DatasetReportRenderer.export-25873.test.tsx index 75cc885be04..df590951347 100644 --- a/ui/packages/plugin-report/src/__tests__/DatasetReportRenderer.export-25873.test.tsx +++ b/ui/packages/plugin-report/src/__tests__/DatasetReportRenderer.export-25873.test.tsx @@ -28,7 +28,7 @@ import { render, screen, fireEvent, waitFor, cleanup, within } from '@testing-li // at once and the parse-back below reads the same module. import ExcelJS from 'exceljs'; import { I18nProvider } from '@object-ui/i18n'; -import { MePermissionsProvider, type MePermissionsResponse } from '@object-ui/permissions'; +import { MePermissionsProvider, type MePermissionsResponse, type MePermissionsAuthenticatedResponse } from '@object-ui/permissions'; import { DatasetReportRenderer } from '../DatasetReportRenderer'; const OBJECT = 'crm_opportunity'; @@ -103,12 +103,13 @@ afterEach(() => { expect(strayUrls).toEqual([]); }); -function permissions(apiOperations?: string[]): MePermissionsResponse { +function permissions(apiOperations?: string[]): MePermissionsAuthenticatedResponse { return { authenticated: true, userId: 'usr_1', tenantId: null, - roles: [], + hasAnyQueue: false, + positions: [], permissionSets: [], objects: { [OBJECT]: { allowRead: true, ...(apiOperations ? { apiOperations } : {}) } }, fields: {}, diff --git a/ui/packages/plugin-view/src/__tests__/ObjectView.createGrant-12082.test.tsx b/ui/packages/plugin-view/src/__tests__/ObjectView.createGrant-12082.test.tsx index 58e533b0211..02237b93e51 100644 --- a/ui/packages/plugin-view/src/__tests__/ObjectView.createGrant-12082.test.tsx +++ b/ui/packages/plugin-view/src/__tests__/ObjectView.createGrant-12082.test.tsx @@ -23,7 +23,7 @@ */ import { describe, it, expect, vi, afterEach } from 'vitest'; import { render, screen, cleanup } from '@testing-library/react'; -import { MePermissionsProvider, type MePermissionsResponse } from '@object-ui/permissions'; +import { MePermissionsProvider, type MePermissionsAuthenticatedResponse } from '@object-ui/permissions'; import type { ObjectViewSchema, DataSource } from '@object-ui/types'; vi.mock('@object-ui/react', async (importOriginal) => { @@ -67,11 +67,12 @@ const SHAPES: Array<{ shape: string; bits: Record; create: bool { shape: 'full', bits: { allowCreate: true, allowRead: true, allowEdit: true, allowDelete: true }, create: true }, ]; -const envelope = (bits: Record): MePermissionsResponse => ({ +const envelope = (bits: Record): MePermissionsAuthenticatedResponse => ({ authenticated: true, userId: 'u-pin', tenantId: null, - roles: ['member'], + hasAnyQueue: false, + positions: ['member'], permissionSets: ['member'], objects: { [OBJECT]: bits }, fields: {},