@company-semantics/contracts 1.14.0 → 1.16.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.14.0",
3
+ "version": "1.16.0",
4
4
  "private": false,
5
5
  "repository": {
6
6
  "type": "git",
@@ -1,3 +1,3 @@
1
1
  // AUTO-GENERATED — do not edit. Run pnpm generate:spec-hash to regenerate.
2
- export const SPEC_HASH = '508bfa08f5fd' as const;
3
- export const SPEC_HASH_FULL = '508bfa08f5fdd3ae4f8d472fa9a6fd9a7dbfca1bb4b63f86c24cc1dbc2fc4cf3' as const;
2
+ export const SPEC_HASH = 'e337542ea39d' as const;
3
+ export const SPEC_HASH_FULL = 'e337542ea39de6f11b03f87d300e3e4c842825267c6918b1a37f2f86502124f8' as const;
@@ -636,7 +636,8 @@ export interface paths {
636
636
  path?: never;
637
637
  cookie?: never;
638
638
  };
639
- get?: never;
639
+ /** Get workspace member detail (roles, units, effective scopes, recent actions) */
640
+ get: operations["getWorkspaceMemberDetail"];
640
641
  put?: never;
641
642
  post?: never;
642
643
  /** Remove workspace member */
@@ -663,6 +664,23 @@ export interface paths {
663
664
  patch: operations["changeMemberRole"];
664
665
  trace?: never;
665
666
  };
667
+ "/api/rbac/roles": {
668
+ parameters: {
669
+ query?: never;
670
+ header?: never;
671
+ path?: never;
672
+ cookie?: never;
673
+ };
674
+ /** List RBAC roles catalog (system + custom) with scopes and member counts */
675
+ get: operations["getRbacRoles"];
676
+ put?: never;
677
+ post?: never;
678
+ delete?: never;
679
+ options?: never;
680
+ head?: never;
681
+ patch?: never;
682
+ trace?: never;
683
+ };
666
684
  "/api/user/orgs": {
667
685
  parameters: {
668
686
  query?: never;
@@ -2344,7 +2362,18 @@ export interface components {
2344
2362
  email: string;
2345
2363
  /** @enum {string} */
2346
2364
  role: "owner" | "admin" | "member" | "auditor";
2365
+ roleNames: string[];
2347
2366
  joinedAt: string;
2367
+ lastActiveAt: string | null;
2368
+ unitMemberships: {
2369
+ unitId: string;
2370
+ unitName: string;
2371
+ unitPath: string;
2372
+ /** @enum {string} */
2373
+ role: "owner" | "manager" | "member";
2374
+ }[];
2375
+ unitMembershipsTruncated: boolean;
2376
+ inviteStatus: ("active" | "pending" | "expired") | null;
2348
2377
  }[];
2349
2378
  nextCursor: string | null;
2350
2379
  hasMore: boolean;
@@ -2514,6 +2543,32 @@ export interface components {
2514
2543
  /** @enum {string} */
2515
2544
  errorCode?: "IDENTITY_CONFLICT" | "DOMAIN_MISMATCH" | "ISSUER_MISMATCH" | "CALLBACK_ERROR";
2516
2545
  };
2546
+ WorkspaceMemberDetail: {
2547
+ id: string;
2548
+ name: string;
2549
+ email: string;
2550
+ /** @enum {string} */
2551
+ role: "owner" | "admin" | "member" | "auditor";
2552
+ roleNames: string[];
2553
+ joinedAt: string;
2554
+ lastActiveAt: string | null;
2555
+ unitMemberships: {
2556
+ unitId: string;
2557
+ unitName: string;
2558
+ unitPath: string;
2559
+ /** @enum {string} */
2560
+ role: "owner" | "manager" | "member";
2561
+ }[];
2562
+ unitMembershipsTruncated: boolean;
2563
+ inviteStatus: ("active" | "pending" | "expired") | null;
2564
+ effectiveScopes: string[];
2565
+ recentActions: {
2566
+ id: string;
2567
+ timestamp: string;
2568
+ action: string;
2569
+ summary: string;
2570
+ }[];
2571
+ };
2517
2572
  RemoveMemberResponse: {
2518
2573
  success: boolean;
2519
2574
  memberId: string;
@@ -2532,6 +2587,16 @@ export interface components {
2532
2587
  /** @enum {string} */
2533
2588
  newRole: "admin" | "member";
2534
2589
  };
2590
+ RoleCatalogResponse: {
2591
+ roles: {
2592
+ name: string;
2593
+ /** @enum {string} */
2594
+ type: "system" | "custom";
2595
+ description: string;
2596
+ scopes: string[];
2597
+ memberCount: number;
2598
+ }[];
2599
+ };
2535
2600
  UserOrgsResponse: {
2536
2601
  orgs: {
2537
2602
  userId: string;
@@ -2750,6 +2815,7 @@ export interface components {
2750
2815
  target: {
2751
2816
  type: string;
2752
2817
  workspaceId?: string;
2818
+ service?: string;
2753
2819
  };
2754
2820
  connectionId?: string;
2755
2821
  returnUrl?: string;
@@ -4371,6 +4437,28 @@ export interface operations {
4371
4437
  };
4372
4438
  };
4373
4439
  };
4440
+ getWorkspaceMemberDetail: {
4441
+ parameters: {
4442
+ query?: never;
4443
+ header?: never;
4444
+ path: {
4445
+ id: string;
4446
+ };
4447
+ cookie?: never;
4448
+ };
4449
+ requestBody?: never;
4450
+ responses: {
4451
+ /** @description Workspace member detail */
4452
+ 200: {
4453
+ headers: {
4454
+ [name: string]: unknown;
4455
+ };
4456
+ content: {
4457
+ "application/json": components["schemas"]["WorkspaceMemberDetail"];
4458
+ };
4459
+ };
4460
+ };
4461
+ };
4374
4462
  removeMember: {
4375
4463
  parameters: {
4376
4464
  query?: never;
@@ -4419,6 +4507,26 @@ export interface operations {
4419
4507
  };
4420
4508
  };
4421
4509
  };
4510
+ getRbacRoles: {
4511
+ parameters: {
4512
+ query?: never;
4513
+ header?: never;
4514
+ path?: never;
4515
+ cookie?: never;
4516
+ };
4517
+ requestBody?: never;
4518
+ responses: {
4519
+ /** @description Role catalog */
4520
+ 200: {
4521
+ headers: {
4522
+ [name: string]: unknown;
4523
+ };
4524
+ content: {
4525
+ "application/json": components["schemas"]["RoleCatalogResponse"];
4526
+ };
4527
+ };
4528
+ };
4529
+ };
4422
4530
  listUserOrgs: {
4423
4531
  parameters: {
4424
4532
  query?: never;
@@ -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
+ }
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 {
package/src/org/index.ts CHANGED
@@ -15,6 +15,11 @@ export type {
15
15
  WorkspaceRole,
16
16
  WorkspaceOverview,
17
17
  WorkspaceMember,
18
+ WorkspaceMemberUnitSummary,
19
+ WorkspaceMemberInviteStatus,
20
+ WorkspaceMemberDetail,
21
+ MemberRecentAction,
22
+ RoleCatalogEntry,
18
23
  AuthMethodConfig,
19
24
  WorkspaceAuthConfig,
20
25
  // SSO self-service setup types (PRD-00193)
@@ -120,6 +125,9 @@ export {
120
125
  WorkspaceAccessResponseSchema,
121
126
  WorkspaceOverviewSchema,
122
127
  WorkspaceMembersResponseSchema,
128
+ WorkspaceMemberDetailSchema,
129
+ RoleCatalogEntrySchema,
130
+ RoleCatalogResponseSchema,
123
131
  WorkspaceAuthConfigSchema,
124
132
  WorkspaceAuditEventSchema,
125
133
  WorkspaceAuditResponseSchema,
@@ -17,12 +17,48 @@ import { CursorPageSchema } from '../api/primitives';
17
17
  // Sub-schemas
18
18
  // ---------------------------------------------------------------------------
19
19
 
20
+ const WorkspaceMemberUnitSummarySchema = z.object({
21
+ unitId: z.string(),
22
+ unitName: z.string(),
23
+ unitPath: z.string(),
24
+ role: z.enum(['owner', 'manager', 'member']),
25
+ });
26
+
20
27
  const WorkspaceMemberSchema = z.object({
21
28
  id: z.string(),
22
29
  name: z.string(),
23
30
  email: z.string(),
24
31
  role: z.enum(['owner', 'admin', 'member', 'auditor']),
32
+ roleNames: z.array(z.string()),
25
33
  joinedAt: z.string(),
34
+ lastActiveAt: z.string().nullable(),
35
+ unitMemberships: z.array(WorkspaceMemberUnitSummarySchema),
36
+ unitMembershipsTruncated: z.boolean(),
37
+ inviteStatus: z.enum(['active', 'pending', 'expired']).nullable(),
38
+ });
39
+
40
+ const MemberRecentActionSchema = z.object({
41
+ id: z.string(),
42
+ timestamp: z.string(),
43
+ action: z.string(),
44
+ summary: z.string(),
45
+ });
46
+
47
+ export const WorkspaceMemberDetailSchema = WorkspaceMemberSchema.extend({
48
+ effectiveScopes: z.array(z.string()),
49
+ recentActions: z.array(MemberRecentActionSchema),
50
+ });
51
+
52
+ export const RoleCatalogEntrySchema = z.object({
53
+ name: z.string(),
54
+ type: z.enum(['system', 'custom']),
55
+ description: z.string(),
56
+ scopes: z.array(z.string()),
57
+ memberCount: z.number().int().nonnegative(),
58
+ });
59
+
60
+ export const RoleCatalogResponseSchema = z.object({
61
+ roles: z.array(RoleCatalogEntrySchema),
26
62
  });
27
63
 
28
64
  const WorkspaceOwnerSchema = z.object({
package/src/org/types.ts CHANGED
@@ -92,6 +92,24 @@ export interface WorkspaceOverview {
92
92
  claimable: boolean;
93
93
  }
94
94
 
95
+ /**
96
+ * Single OrgUnit membership summary attached to a WorkspaceMember row.
97
+ * See ADR-BE-120 for the canonical OrgUnit tree model.
98
+ */
99
+ export interface WorkspaceMemberUnitSummary {
100
+ unitId: string;
101
+ unitName: string;
102
+ /** Human-readable path from the OrgUnit tree (e.g. "Sales / Enterprise"). */
103
+ unitPath: string;
104
+ role: 'owner' | 'manager' | 'member';
105
+ }
106
+
107
+ /**
108
+ * Invite status for a workspace member row. `null` for members that have no
109
+ * associated invite record (joined pre-invite system).
110
+ */
111
+ export type WorkspaceMemberInviteStatus = 'active' | 'pending' | 'expired';
112
+
95
113
  /**
96
114
  * Workspace member for the members list.
97
115
  * Human users only (no agent actors).
@@ -100,8 +118,56 @@ export interface WorkspaceMember {
100
118
  id: string;
101
119
  name: string;
102
120
  email: string;
121
+ /** Display role — derived collapse of RBAC into {owner,admin,member,auditor}. */
103
122
  role: WorkspaceRole;
123
+ /** Raw RBAC role names (e.g. 'org_owner', 'org_admin', 'auditor'). Superset of `role`. */
124
+ roleNames: string[];
104
125
  joinedAt: string;
126
+ /** ISO timestamp of last activity; null if never recorded. */
127
+ lastActiveAt: string | null;
128
+ /** OrgUnit memberships for this member, capped at a small number server-side. */
129
+ unitMemberships: WorkspaceMemberUnitSummary[];
130
+ /** True when server truncated `unitMemberships`; detail endpoint returns the full list. */
131
+ unitMembershipsTruncated: boolean;
132
+ /** Invite lifecycle status. `null` for members without an invite record. */
133
+ inviteStatus: WorkspaceMemberInviteStatus | null;
134
+ }
135
+
136
+ /**
137
+ * Recent audit action attached to a member-detail response.
138
+ * Lighter projection than WorkspaceAuditEvent — no actor block (implied by caller).
139
+ */
140
+ export interface MemberRecentAction {
141
+ id: string;
142
+ timestamp: string;
143
+ action: string;
144
+ summary: string;
145
+ }
146
+
147
+ /**
148
+ * Full member detail, returned by GET /api/workspace/members/:userId and also
149
+ * the shape projected from /api/me for self-view.
150
+ */
151
+ export interface WorkspaceMemberDetail extends WorkspaceMember {
152
+ /** Full resolved scope patterns — union of grants from all assigned roles. */
153
+ effectiveScopes: string[];
154
+ /** Up to N most recent audit actions attributed to this member. */
155
+ recentActions: MemberRecentAction[];
156
+ }
157
+
158
+ /**
159
+ * Entry in the RBAC roles catalog (GET /api/rbac/roles).
160
+ */
161
+ export interface RoleCatalogEntry {
162
+ /** Canonical role name, e.g. 'org_owner'. */
163
+ name: string;
164
+ type: 'system' | 'custom';
165
+ /** One-line description sourced from system-roles.ts (or org-provided for custom). */
166
+ description: string;
167
+ /** Scope patterns granted by this role. May contain wildcards (e.g. 'org.view_*'). */
168
+ scopes: string[];
169
+ /** Number of active assignments in the current org. */
170
+ memberCount: number;
105
171
  }
106
172
 
107
173
  /**