@company-semantics/contracts 1.15.0 → 1.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@company-semantics/contracts",
3
- "version": "1.15.0",
3
+ "version": "1.17.0",
4
4
  "private": false,
5
5
  "repository": {
6
6
  "type": "git",
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Admin What-If Simulator — request/response schemas.
3
+ *
4
+ * Mirrors the Fastify schema for `POST /api/admin/authz/simulate`. The endpoint
5
+ * is read-only and returns the full structured AuthDecision verbatim, so that
6
+ * admins can debug denials by reading evaluator name + reason rather than
7
+ * guessing. See PRD-00544 and feedback_decision_logic_must_be_visible.md.
8
+ */
9
+ import { z } from 'zod';
10
+
11
+ export const SimulateRequest = z.object({
12
+ actor: z.object({
13
+ type: z.enum(['user', 'agent']),
14
+ id: z.string().uuid(),
15
+ }),
16
+ scope: z.string(),
17
+ resource_kind: z.enum(['system', 'org', 'team', 'member', 'doc']),
18
+ resource_id: z.string().uuid().optional(),
19
+ });
20
+
21
+ export type SimulateRequest = z.infer<typeof SimulateRequest>;
22
+
23
+ export const SimulateResponse = z.discriminatedUnion('allow', [
24
+ z.object({
25
+ allow: z.literal(true),
26
+ matched_grant: z.unknown(),
27
+ evaluator_name: z.string().optional(),
28
+ evaluator_reason: z.string().optional(),
29
+ }),
30
+ z.object({
31
+ allow: z.literal(false),
32
+ deny_reasons: z.array(z.unknown()),
33
+ missing: z.object({
34
+ scope: z.string().optional(),
35
+ suggested_role: z.string().optional(),
36
+ }),
37
+ }),
38
+ ]);
39
+
40
+ export type SimulateResponse = z.infer<typeof SimulateResponse>;
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Admin Direct Grants — request/response schemas.
3
+ *
4
+ * Mirrors the Fastify schemas for `POST /api/admin/grants/direct` and the
5
+ * persisted grant row. `granted_by` is intentionally absent from the create
6
+ * body — it is set by the server from the authenticated actor (see PRD-00544
7
+ * direct-grant-audit-trail and the `direct-grant-audit` CI guard).
8
+ */
9
+ import { z } from 'zod';
10
+
11
+ export const DirectGrantCreate = z.object({
12
+ subject_id: z.string().uuid(),
13
+ scope_pattern: z.string(),
14
+ resource_filter: z.record(z.string(), z.unknown()).optional(),
15
+ expires_at: z.string().datetime().optional(),
16
+ });
17
+
18
+ export type DirectGrantCreate = z.infer<typeof DirectGrantCreate>;
19
+
20
+ export const DirectGrant = z.object({
21
+ id: z.string().uuid(),
22
+ org_id: z.string().uuid(),
23
+ subject_id: z.string().uuid(),
24
+ scope_pattern: z.string(),
25
+ source: z.literal('direct'),
26
+ granted_by: z.string().uuid(),
27
+ granted_at: z.string().datetime(),
28
+ expires_at: z.string().datetime().nullable(),
29
+ });
30
+
31
+ export type DirectGrant = z.infer<typeof DirectGrant>;
@@ -1,3 +1,3 @@
1
1
  // AUTO-GENERATED — do not edit. Run pnpm generate:spec-hash to regenerate.
2
- export const SPEC_HASH = 'b15100bf7771' as const;
3
- export const SPEC_HASH_FULL = 'b15100bf7771682644b63942432a672ffee7d4fdb1237201f491b4d8d3c6b238' as const;
2
+ export const SPEC_HASH = '6edfe6e7117c' as const;
3
+ export const SPEC_HASH_FULL = '6edfe6e7117c49e81cd7cad51ac9cd5c6f4c9e18cf7a37ff82a5630855404a00' as const;
@@ -72,6 +72,26 @@ export interface paths {
72
72
  patch?: never;
73
73
  trace?: never;
74
74
  };
75
+ "/api/auth/consent/grant": {
76
+ parameters: {
77
+ query?: never;
78
+ header?: never;
79
+ path?: never;
80
+ cookie?: never;
81
+ };
82
+ get?: never;
83
+ put?: never;
84
+ /**
85
+ * Grant incremental consent for a write action
86
+ * @description Records per-(user, org, action) consent for a registered write action. Idempotent: regranting an already-active consent re-emits the audit event but does not create a duplicate row.
87
+ */
88
+ post: operations["grantIncrementalConsent"];
89
+ delete?: never;
90
+ options?: never;
91
+ head?: never;
92
+ patch?: never;
93
+ trace?: never;
94
+ };
75
95
  "/api/me": {
76
96
  parameters: {
77
97
  query?: never;
@@ -647,38 +667,38 @@ export interface paths {
647
667
  patch?: never;
648
668
  trace?: never;
649
669
  };
650
- "/api/rbac/roles": {
670
+ "/api/workspace/members/{id}/role": {
651
671
  parameters: {
652
672
  query?: never;
653
673
  header?: never;
654
674
  path?: never;
655
675
  cookie?: never;
656
676
  };
657
- /** List RBAC roles catalog (system + custom) with scopes and member counts */
658
- get: operations["getRbacRoles"];
677
+ get?: never;
659
678
  put?: never;
660
679
  post?: never;
661
680
  delete?: never;
662
681
  options?: never;
663
682
  head?: never;
664
- patch?: never;
683
+ /** Change member role */
684
+ patch: operations["changeMemberRole"];
665
685
  trace?: never;
666
686
  };
667
- "/api/workspace/members/{id}/role": {
687
+ "/api/rbac/roles": {
668
688
  parameters: {
669
689
  query?: never;
670
690
  header?: never;
671
691
  path?: never;
672
692
  cookie?: never;
673
693
  };
674
- get?: never;
694
+ /** List RBAC roles catalog (system + custom) with scopes and member counts */
695
+ get: operations["getRbacRoles"];
675
696
  put?: never;
676
697
  post?: never;
677
698
  delete?: never;
678
699
  options?: never;
679
700
  head?: never;
680
- /** Change member role */
681
- patch: operations["changeMemberRole"];
701
+ patch?: never;
682
702
  trace?: never;
683
703
  };
684
704
  "/api/user/orgs": {
@@ -2036,6 +2056,15 @@ export interface components {
2036
2056
  /** @constant */
2037
2057
  ok: true;
2038
2058
  };
2059
+ ConsentGrantResponse: {
2060
+ /** @constant */
2061
+ ok: true;
2062
+ /** Format: date-time */
2063
+ grantedAt: string;
2064
+ };
2065
+ ConsentGrantRequest: {
2066
+ actionId: string;
2067
+ };
2039
2068
  MeResponse: {
2040
2069
  /** Format: uuid */
2041
2070
  userId: string;
@@ -2355,49 +2384,26 @@ export interface components {
2355
2384
  WorkspaceHandleRequest: {
2356
2385
  handle: string;
2357
2386
  };
2358
- WorkspaceMemberUnitSummary: {
2359
- unitId: string;
2360
- unitName: string;
2361
- unitPath: string;
2362
- /** @enum {string} */
2363
- role: "owner" | "manager" | "member";
2364
- };
2365
- WorkspaceMember: {
2366
- id: string;
2367
- name: string;
2368
- email: string;
2369
- /** @enum {string} */
2370
- role: "owner" | "admin" | "member" | "auditor";
2371
- roleNames: string[];
2372
- joinedAt: string;
2373
- lastActiveAt: string | null;
2374
- unitMemberships: components["schemas"]["WorkspaceMemberUnitSummary"][];
2375
- unitMembershipsTruncated: boolean;
2376
- inviteStatus: ("active" | "pending" | "expired") | null;
2377
- };
2378
- MemberRecentAction: {
2379
- id: string;
2380
- timestamp: string;
2381
- action: string;
2382
- summary: string;
2383
- };
2384
- WorkspaceMemberDetail: components["schemas"]["WorkspaceMember"] & {
2385
- effectiveScopes: string[];
2386
- recentActions: components["schemas"]["MemberRecentAction"][];
2387
- };
2388
- RoleCatalogEntry: {
2389
- name: string;
2390
- /** @enum {string} */
2391
- type: "system" | "custom";
2392
- description: string;
2393
- scopes: string[];
2394
- memberCount: number;
2395
- };
2396
- RoleCatalogResponse: {
2397
- roles: components["schemas"]["RoleCatalogEntry"][];
2398
- };
2399
2387
  WorkspaceMembersResponse: {
2400
- items: components["schemas"]["WorkspaceMember"][];
2388
+ items: {
2389
+ id: string;
2390
+ name: string;
2391
+ email: string;
2392
+ /** @enum {string} */
2393
+ role: "owner" | "admin" | "member" | "auditor";
2394
+ roleNames: string[];
2395
+ joinedAt: string;
2396
+ lastActiveAt: string | null;
2397
+ unitMemberships: {
2398
+ unitId: string;
2399
+ unitName: string;
2400
+ unitPath: string;
2401
+ /** @enum {string} */
2402
+ role: "owner" | "manager" | "member";
2403
+ }[];
2404
+ unitMembershipsTruncated: boolean;
2405
+ inviteStatus: ("active" | "pending" | "expired") | null;
2406
+ }[];
2401
2407
  nextCursor: string | null;
2402
2408
  hasMore: boolean;
2403
2409
  };
@@ -2566,6 +2572,32 @@ export interface components {
2566
2572
  /** @enum {string} */
2567
2573
  errorCode?: "IDENTITY_CONFLICT" | "DOMAIN_MISMATCH" | "ISSUER_MISMATCH" | "CALLBACK_ERROR";
2568
2574
  };
2575
+ WorkspaceMemberDetail: {
2576
+ id: string;
2577
+ name: string;
2578
+ email: string;
2579
+ /** @enum {string} */
2580
+ role: "owner" | "admin" | "member" | "auditor";
2581
+ roleNames: string[];
2582
+ joinedAt: string;
2583
+ lastActiveAt: string | null;
2584
+ unitMemberships: {
2585
+ unitId: string;
2586
+ unitName: string;
2587
+ unitPath: string;
2588
+ /** @enum {string} */
2589
+ role: "owner" | "manager" | "member";
2590
+ }[];
2591
+ unitMembershipsTruncated: boolean;
2592
+ inviteStatus: ("active" | "pending" | "expired") | null;
2593
+ effectiveScopes: string[];
2594
+ recentActions: {
2595
+ id: string;
2596
+ timestamp: string;
2597
+ action: string;
2598
+ summary: string;
2599
+ }[];
2600
+ };
2569
2601
  RemoveMemberResponse: {
2570
2602
  success: boolean;
2571
2603
  memberId: string;
@@ -2584,6 +2616,16 @@ export interface components {
2584
2616
  /** @enum {string} */
2585
2617
  newRole: "admin" | "member";
2586
2618
  };
2619
+ RoleCatalogResponse: {
2620
+ roles: {
2621
+ name: string;
2622
+ /** @enum {string} */
2623
+ type: "system" | "custom";
2624
+ description: string;
2625
+ scopes: string[];
2626
+ memberCount: number;
2627
+ }[];
2628
+ };
2587
2629
  UserOrgsResponse: {
2588
2630
  orgs: {
2589
2631
  userId: string;
@@ -2802,6 +2844,7 @@ export interface components {
2802
2844
  target: {
2803
2845
  type: string;
2804
2846
  workspaceId?: string;
2847
+ service?: string;
2805
2848
  };
2806
2849
  connectionId?: string;
2807
2850
  returnUrl?: string;
@@ -3538,6 +3581,63 @@ export interface operations {
3538
3581
  };
3539
3582
  };
3540
3583
  };
3584
+ grantIncrementalConsent: {
3585
+ parameters: {
3586
+ query?: never;
3587
+ header?: never;
3588
+ path?: never;
3589
+ cookie?: never;
3590
+ };
3591
+ requestBody: {
3592
+ content: {
3593
+ "application/json": components["schemas"]["ConsentGrantRequest"];
3594
+ };
3595
+ };
3596
+ responses: {
3597
+ /** @description Consent granted */
3598
+ 200: {
3599
+ headers: {
3600
+ [name: string]: unknown;
3601
+ };
3602
+ content: {
3603
+ "application/json": components["schemas"]["ConsentGrantResponse"];
3604
+ };
3605
+ };
3606
+ /** @description Invalid or unregistered actionId */
3607
+ 400: {
3608
+ headers: {
3609
+ [name: string]: unknown;
3610
+ };
3611
+ content: {
3612
+ "application/json": {
3613
+ error: string;
3614
+ message: string;
3615
+ };
3616
+ };
3617
+ };
3618
+ /** @description Not authenticated */
3619
+ 401: {
3620
+ headers: {
3621
+ [name: string]: unknown;
3622
+ };
3623
+ content?: never;
3624
+ };
3625
+ /** @description CSRF token missing or invalid */
3626
+ 403: {
3627
+ headers: {
3628
+ [name: string]: unknown;
3629
+ };
3630
+ content?: never;
3631
+ };
3632
+ /** @description Rate limit exceeded */
3633
+ 429: {
3634
+ headers: {
3635
+ [name: string]: unknown;
3636
+ };
3637
+ content?: never;
3638
+ };
3639
+ };
3640
+ };
3541
3641
  getCurrentUser: {
3542
3642
  parameters: {
3543
3643
  query?: never;
@@ -4467,48 +4567,48 @@ export interface operations {
4467
4567
  };
4468
4568
  };
4469
4569
  };
4470
- getRbacRoles: {
4570
+ changeMemberRole: {
4471
4571
  parameters: {
4472
4572
  query?: never;
4473
4573
  header?: never;
4474
- path?: never;
4574
+ path: {
4575
+ id: string;
4576
+ };
4475
4577
  cookie?: never;
4476
4578
  };
4477
- requestBody?: never;
4579
+ requestBody: {
4580
+ content: {
4581
+ "application/json": components["schemas"]["ChangeMemberRoleRequest"];
4582
+ };
4583
+ };
4478
4584
  responses: {
4479
- /** @description Role catalog */
4585
+ /** @description Role changed successfully */
4480
4586
  200: {
4481
4587
  headers: {
4482
4588
  [name: string]: unknown;
4483
4589
  };
4484
4590
  content: {
4485
- "application/json": components["schemas"]["RoleCatalogResponse"];
4591
+ "application/json": components["schemas"]["ChangeMemberRoleResponse"];
4486
4592
  };
4487
4593
  };
4488
4594
  };
4489
4595
  };
4490
- changeMemberRole: {
4596
+ getRbacRoles: {
4491
4597
  parameters: {
4492
4598
  query?: never;
4493
4599
  header?: never;
4494
- path: {
4495
- id: string;
4496
- };
4600
+ path?: never;
4497
4601
  cookie?: never;
4498
4602
  };
4499
- requestBody: {
4500
- content: {
4501
- "application/json": components["schemas"]["ChangeMemberRoleRequest"];
4502
- };
4503
- };
4603
+ requestBody?: never;
4504
4604
  responses: {
4505
- /** @description Role changed successfully */
4605
+ /** @description Role catalog */
4506
4606
  200: {
4507
4607
  headers: {
4508
4608
  [name: string]: unknown;
4509
4609
  };
4510
4610
  content: {
4511
- "application/json": components["schemas"]["ChangeMemberRoleResponse"];
4611
+ "application/json": components["schemas"]["RoleCatalogResponse"];
4512
4612
  };
4513
4613
  };
4514
4614
  };
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Dispatch-deny vocabulary.
3
+ *
4
+ * The four deny paths on the agent-action dispatch flow — consent missing,
5
+ * write-tier disabled, per-provider kill switch, and execution-budget
6
+ * exhausted — all return the canonical `ErrorResponse` envelope with one of
7
+ * the codes defined below plus a per-code `meta` payload. The shapes are
8
+ * shared between the backend (which emits) and the app (which renders a
9
+ * recovery affordance), so they live in contracts per the promotion rule.
10
+ *
11
+ * HTTP status is carried by the envelope, not duplicated here; conventional
12
+ * mapping in the backend:
13
+ * - `incremental_consent_required` → 402 (grant required to proceed)
14
+ * - `write_tier_disabled` → 403
15
+ * - `provider_access_denied` → 403
16
+ * - `execution_budget_exceeded` → 429
17
+ *
18
+ * @see ../errors/index.ts for the `ErrorResponse` envelope.
19
+ * @see PRD-00519/00521/00522 — the PRDs that introduced these denies.
20
+ */
21
+
22
+ export const DISPATCH_DENY_CODES = [
23
+ 'incremental_consent_required',
24
+ 'write_tier_disabled',
25
+ 'provider_access_denied',
26
+ 'execution_budget_exceeded',
27
+ ] as const;
28
+
29
+ export type DispatchDenyCode = (typeof DISPATCH_DENY_CODES)[number];
30
+
31
+ export type ActionTier = 'SAFE' | 'EXTERNAL' | 'DESTRUCTIVE';
32
+
33
+ /**
34
+ * The user has no active grant row in `incremental_consents` for the
35
+ * `(user_id, org_id, action_id)` triple. Recoverable by the user: they
36
+ * POST `grantPath` to create the grant, then retry the original action.
37
+ */
38
+ export interface IncrementalConsentRequiredMeta {
39
+ actionId: string;
40
+ tier: ActionTier;
41
+ requiredScopes: string[];
42
+ /** HTTP path the app can POST to with `{ actionId }` to create the grant. */
43
+ grantPath: string;
44
+ }
45
+
46
+ /**
47
+ * The global `WRITE_TIER_ENABLED[tier]` kill switch is off. Recoverable
48
+ * only by an operator flipping the config — not user-actionable.
49
+ */
50
+ export interface WriteTierDisabledMeta {
51
+ actionId: string;
52
+ tier: ActionTier;
53
+ }
54
+
55
+ /**
56
+ * Distinct reasons `ProviderAccessGate.canDispatchAction` can deny.
57
+ * Kept separate from the global `WRITE_TIER_ENABLED` codes so the app
58
+ * can phrase the message by cause (provider paused vs. org override vs.
59
+ * tier paused for this provider).
60
+ */
61
+ export type ProviderAccessDenyReason =
62
+ | 'provider-disabled'
63
+ | 'org-override'
64
+ | 'tier-SAFE-disabled'
65
+ | 'tier-EXTERNAL-disabled'
66
+ | 'tier-DESTRUCTIVE-disabled'
67
+ | 'write-tier-SAFE-globally-disabled'
68
+ | 'write-tier-EXTERNAL-globally-disabled'
69
+ | 'write-tier-DESTRUCTIVE-globally-disabled';
70
+
71
+ export interface ProviderAccessDeniedMeta {
72
+ reason: ProviderAccessDenyReason;
73
+ provider: string;
74
+ tier: ActionTier;
75
+ actionType: string;
76
+ }
77
+
78
+ /**
79
+ * The signed execution has hit one of its atomic per-run caps. `kind` tells
80
+ * the app which cap tripped so it can phrase the message correctly; the
81
+ * recovery is always "start a new session / execution".
82
+ */
83
+ export interface ExecutionBudgetExceededMeta {
84
+ kind: 'token_uses' | 'time_window' | 'provider_calls' | 'rows_returned';
85
+ executionId: string;
86
+ }
87
+
88
+ /** Type-level map: deny code → meta shape. Drives the app's deny renderer. */
89
+ export interface DispatchDenyMetaByCode {
90
+ incremental_consent_required: IncrementalConsentRequiredMeta;
91
+ write_tier_disabled: WriteTierDisabledMeta;
92
+ provider_access_denied: ProviderAccessDeniedMeta;
93
+ execution_budget_exceeded: ExecutionBudgetExceededMeta;
94
+ }
95
+
96
+ /** Narrow `unknown` to a `DispatchDenyCode` for error-code dispatch switches. */
97
+ export function isDispatchDenyCode(code: unknown): code is DispatchDenyCode {
98
+ return typeof code === 'string' && (DISPATCH_DENY_CODES as readonly string[]).includes(code);
99
+ }
@@ -9,6 +9,7 @@ export const openApiRoutes = {
9
9
  '/api/account/deletion-eligibility': ['GET'],
10
10
  '/api/account/sessions': ['GET'],
11
11
  '/api/account/sessions/{sessionId}': ['DELETE'],
12
+ '/api/auth/consent/grant': ['POST'],
12
13
  '/api/chats': ['GET', 'POST'],
13
14
  '/api/chats/by-interaction/{interactionId}': ['GET'],
14
15
  '/api/chats/events': ['GET'],
@@ -47,7 +48,8 @@ export const openApiRoutes = {
47
48
  '/api/internal-admin/impersonate/start': ['POST'],
48
49
  '/api/me': ['GET'],
49
50
  '/api/org-units': ['POST'],
50
- '/api/org-units/{unitId}': ['GET'],
51
+ '/api/org-units/tree': ['GET'],
52
+ '/api/org-units/{unitId}': ['GET', 'PATCH'],
51
53
  '/api/org-units/{unitId}/ancestors': ['GET'],
52
54
  '/api/org-units/{unitId}/archive': ['POST'],
53
55
  '/api/org-units/{unitId}/children': ['GET'],
@@ -73,7 +75,7 @@ export const openApiRoutes = {
73
75
  '/api/orgs/{orgId}/billing': ['GET'],
74
76
  '/api/orgs/{orgId}/budget-config': ['GET', 'PUT'],
75
77
  '/api/orgs/{orgId}/level-config': ['GET', 'PUT'],
76
- '/api/orgs/{orgId}/tree': ['GET'],
78
+ '/api/rbac/roles': ['GET'],
77
79
  '/api/scope/check': ['GET'],
78
80
  '/api/scope/check-batch': ['POST'],
79
81
  '/api/shared/{token}': ['GET'],
@@ -103,7 +105,7 @@ export const openApiRoutes = {
103
105
  '/api/workspace/invites/validate': ['GET'],
104
106
  '/api/workspace/invites/{id}': ['DELETE'],
105
107
  '/api/workspace/members': ['GET'],
106
- '/api/workspace/members/{id}': ['DELETE'],
108
+ '/api/workspace/members/{id}': ['DELETE', 'GET'],
107
109
  '/api/workspace/members/{id}/role': ['PATCH'],
108
110
  '/api/workspace/name': ['PATCH'],
109
111
  '/api/workspace/resolve-path': ['POST'],
package/src/index.ts CHANGED
@@ -166,6 +166,22 @@ export type {
166
166
  export type { AuthStartMode, AuthStartResponse } from './auth/index'
167
167
  export { OTPErrorCode } from './auth/index'
168
168
 
169
+ // Dispatch deny vocabulary (PRD-00519/00521/00522)
170
+ export {
171
+ DISPATCH_DENY_CODES,
172
+ isDispatchDenyCode,
173
+ } from './dispatch/index'
174
+ export type {
175
+ ActionTier,
176
+ DispatchDenyCode,
177
+ DispatchDenyMetaByCode,
178
+ ExecutionBudgetExceededMeta,
179
+ IncrementalConsentRequiredMeta,
180
+ ProviderAccessDenyReason,
181
+ ProviderAccessDeniedMeta,
182
+ WriteTierDisabledMeta,
183
+ } from './dispatch/index'
184
+
169
185
  // Email domain types
170
186
  // @see ADR-CONT-034 for design rationale
171
187
  export type {