@company-semantics/contracts 86.1.0 → 88.0.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.
Files changed (47) hide show
  1. package/package.json +14 -10
  2. package/src/api/generated-spec-hash.ts +2 -2
  3. package/src/api/generated.ts +50 -262
  4. package/src/chat/README.md +8 -20
  5. package/src/chat/__tests__/compaction-boundary.test.ts +1 -0
  6. package/src/chat/__tests__/continuity.test.ts +1 -0
  7. package/src/chat/__tests__/discussion-scope.test.ts +0 -7
  8. package/src/chat/__tests__/message-mentions.test.ts +1 -12
  9. package/src/chat/__tests__/multi-party.test.ts +15 -6
  10. package/src/chat/__tests__/page-context.test.ts +3 -3
  11. package/src/chat/enums.ts +24 -11
  12. package/src/chat/index.ts +4 -16
  13. package/src/chat/schemas.ts +9 -71
  14. package/src/chat/types.ts +25 -77
  15. package/src/generated/openapi-routes.ts +1 -4
  16. package/src/guards/README.md +0 -1
  17. package/src/guards/config.ts +1 -10
  18. package/src/guards/evolution-config.ts +1 -18
  19. package/src/guards/index.ts +0 -1
  20. package/src/index.ts +15 -1279
  21. package/src/notifications/__tests__/__snapshots__/monospace-budget.test.ts.snap +4 -4
  22. package/src/notifications/__tests__/__snapshots__/render-snapshot.test.ts.snap +50 -49
  23. package/src/notifications/__tests__/fixtures.ts +7 -7
  24. package/src/notifications/__tests__/render-snapshot.test.ts +7 -7
  25. package/src/notifications/kinds/chat-shared.ts +11 -5
  26. package/src/notifications/payloads.ts +5 -5
  27. package/src/org/README.md +0 -212
  28. package/src/org/__tests__/view-scopes.test.ts +0 -2
  29. package/src/org/authority-schemas.ts +242 -0
  30. package/src/org/index.ts +26 -249
  31. package/src/org/invite-schemas.ts +596 -0
  32. package/src/org/org-governance-schemas.ts +198 -0
  33. package/src/org/org-unit-api-schemas.ts +226 -0
  34. package/src/org/org-unit-schemas.ts +223 -0
  35. package/src/org/ownership-transfer-types.ts +51 -0
  36. package/src/org/schemas.ts +18 -1658
  37. package/src/org/sso-stepper-types.ts +81 -0
  38. package/src/org/types.ts +15 -115
  39. package/src/org/view-scopes.ts +0 -1
  40. package/src/org/workspace-admin-schemas.ts +240 -0
  41. package/src/root-exports/README.md +91 -0
  42. package/src/root-exports/agent-runtime.ts +213 -0
  43. package/src/root-exports/collaboration.ts +247 -0
  44. package/src/root-exports/delivery.ts +218 -0
  45. package/src/root-exports/org-structure.ts +214 -0
  46. package/src/root-exports/org-workspace.ts +254 -0
  47. package/src/root-exports/platform.ts +205 -0
@@ -4,22 +4,31 @@
4
4
  * Zod response schemas for workspace core endpoints.
5
5
  * Canonical location for runtime validation of workspace API responses.
6
6
  *
7
- * Covers:
8
- * - workspace.ts (GET /api/workspace, /access, /members, /auth, /audit, etc.)
7
+ * Covers (defined here):
8
+ * - workspace.ts (GET /api/workspace, /access, /members)
9
9
  * - members.ts (DELETE/PATCH /api/workspace/members)
10
10
  * - user-orgs.ts (GET /api/user/orgs, POST /api/user/active-org, /leave)
11
11
  * - scope-check.ts (GET /api/scope/check, POST /api/scope/check-batch)
12
+ * - capabilities (GET /api/capabilities)
13
+ *
14
+ * The rest of the org schema corpus lives in sibling `*-schemas.ts` modules
15
+ * and is re-exported below, so `./schemas` stays the one import path for
16
+ * every org schema.
12
17
  */
13
18
  import { z } from "zod";
14
- import { CursorPageSchema, IsoDateTime } from "../api/primitives";
15
- import { AI_PAUSE_REASONS, MoneyMicrosSchema } from "../billing/vocabulary";
16
- import { AI_FEATURES } from "../usage/features";
19
+ import { CursorPageSchema } from "../api/primitives";
20
+ import { AI_PAUSE_REASONS } from "../billing/vocabulary";
17
21
  import { OrgChartRoleSchema } from "../permissions/orgchart-roles";
18
22
  import { PositionRefSchema } from "../identity/position-ref";
19
- import {
20
- ORG_UNIT_ABSOLUTE_MAX_DEPTH,
21
- UNIT_OWNER_MEMBERSHIP_ROLES,
22
- } from "./org-depth";
23
+ import { AuthorityMutabilitySchema } from "./authority-schemas";
24
+
25
+ // Sibling schema modules — re-exported whole; ./schemas is their only import path.
26
+ export * from "./workspace-admin-schemas";
27
+ export * from "./invite-schemas";
28
+ export * from "./org-governance-schemas";
29
+ export * from "./org-unit-schemas";
30
+ export * from "./authority-schemas";
31
+ export * from "./org-unit-api-schemas";
23
32
 
24
33
  // ---------------------------------------------------------------------------
25
34
  // Sub-schemas
@@ -50,17 +59,6 @@ export const OrgUnitDesignationSchema = z.enum([
50
59
  ]);
51
60
  export type OrgUnitDesignation = z.infer<typeof OrgUnitDesignationSchema>;
52
61
 
53
- /**
54
- * Whether the user can change this authority from this surface.
55
- * - `derived`: change the upstream source instead (the org chart, RBAC roles).
56
- * - `user_managed`: removable from this UI.
57
- *
58
- * Hoisted above {@link WorkspaceMemberUnitDesignationEntrySchema} so the
59
- * per-unit designation can carry it; also consumed by {@link OwnerAuthoritySchema}.
60
- */
61
- export const AuthorityMutabilitySchema = z.enum(["derived", "user_managed"]);
62
- export type AuthorityMutability = z.infer<typeof AuthorityMutabilitySchema>;
63
-
64
62
  export const WorkspaceMemberUnitDesignationEntrySchema = z.object({
65
63
  unitId: z.string(),
66
64
  unitName: z.string(),
@@ -295,236 +293,6 @@ export type WorkspaceMembersResponse = z.infer<
295
293
  typeof WorkspaceMembersResponseSchema
296
294
  >;
297
295
 
298
- // ---------------------------------------------------------------------------
299
- // GET /api/workspace/auth
300
- // ---------------------------------------------------------------------------
301
-
302
- const OidcValidationResultSchema = z.object({
303
- valid: z.boolean(),
304
- issuer: z.string().optional(),
305
- authorizationEndpoint: z.string().optional(),
306
- error: z.string().optional(),
307
- errorCode: z
308
- .enum(["UNREACHABLE", "INVALID_DOCUMENT", "MISSING_FIELDS", "SSRF_BLOCKED"])
309
- .optional(),
310
- });
311
-
312
- const SsoDiscoveryConfigSchema = z.object({
313
- redirectUri: z.string(),
314
- requiredScopes: z.array(z.string()),
315
- isOidcConfigured: z.boolean(),
316
- oidcDiscoveryUrl: z.string().nullable(),
317
- });
318
-
319
- const SsoCredentialStatusSchema = z.object({
320
- hasClientId: z.boolean(),
321
- hasClientSecret: z.boolean(),
322
- });
323
-
324
- const ProviderStatusSchema = z.enum([
325
- "NOT_CONFIGURED",
326
- "CONFIG_SAVED",
327
- "CONFIG_VALID",
328
- "TEST_SUCCESS",
329
- "ENABLED",
330
- ]);
331
-
332
- const SsoStepperStepSchema = z.enum(["configure", "test", "enable", "enforce"]);
333
-
334
- const SsoOperationalStateSchema = z.object({
335
- providerStatus: ProviderStatusSchema,
336
- currentStep: SsoStepperStepSchema,
337
- stepCompleted: z.boolean(),
338
- activeProvider: z.string().nullable(),
339
- oidcValidation: OidcValidationResultSchema.optional(),
340
- credentialsSavedAt: z.string().optional(),
341
- lastTestSuccessAt: z.string().optional(),
342
- lastTestSuccessProvider: z.string().optional(),
343
- });
344
-
345
- const SsoSetupInfoSchema = SsoDiscoveryConfigSchema.merge(
346
- SsoCredentialStatusSchema,
347
- ).merge(SsoOperationalStateSchema);
348
-
349
- const SsoReadinessCheckSchema = z.object({
350
- code: z.string(),
351
- label: z.string(),
352
- passed: z.boolean(),
353
- message: z.string(),
354
- });
355
-
356
- const SsoReadinessSchema = z.object({
357
- ready: z.boolean(),
358
- checks: z.array(SsoReadinessCheckSchema),
359
- });
360
-
361
- const SsoEnforcementStatusSchema = z.object({
362
- enforced: z.boolean(),
363
- enforcedDomains: z.array(z.string()),
364
- enforcedSince: z.string().nullable(),
365
- });
366
-
367
- const OwnerIdentityInfoSchema = z.object({
368
- userId: z.string(),
369
- name: z.string(),
370
- email: z.string(),
371
- hasSsoIdentity: z.boolean(),
372
- linkedProvider: z.string().nullable(),
373
- lastSsoLoginAt: z.string().nullable(),
374
- });
375
-
376
- const ProviderSuggestionSchema = z.object({
377
- suggestedProvider: z.enum(["google", "microsoft"]).nullable(),
378
- confidence: z.enum(["high", "low"]),
379
- reason: z.string(),
380
- detectedDomain: z.string().optional(),
381
- });
382
-
383
- const AuthMethodConfigSchema = z.object({
384
- enabled: z.boolean(),
385
- provider: z.string().optional(),
386
- });
387
-
388
- export const WorkspaceAuthConfigSchema = z.object({
389
- emailOtp: AuthMethodConfigSchema,
390
- googleSso: AuthMethodConfigSchema,
391
- microsoftSso: AuthMethodConfigSchema,
392
- okta: AuthMethodConfigSchema,
393
- policy: z.object({
394
- requireSSO: z.boolean(),
395
- allowedProviders: z.array(z.string()),
396
- }),
397
- ssoSetup: SsoSetupInfoSchema.optional(),
398
- ssoReadiness: SsoReadinessSchema.optional(),
399
- ssoEnforcement: SsoEnforcementStatusSchema.optional(),
400
- workspaceSsoState: z
401
- .enum(["SSO_DISABLED", "SSO_ENABLED", "SSO_ENFORCED"])
402
- .optional(),
403
- ownerIdentities: z.array(OwnerIdentityInfoSchema).optional(),
404
- providerSuggestion: ProviderSuggestionSchema.optional(),
405
- });
406
-
407
- export type WorkspaceAuthConfig = z.infer<typeof WorkspaceAuthConfigSchema>;
408
-
409
- // ---------------------------------------------------------------------------
410
- // GET /api/workspace/audit
411
- // ---------------------------------------------------------------------------
412
-
413
- export const WorkspaceAuditEventSchema = z.object({
414
- id: z.string(),
415
- timestamp: z.string(),
416
- actor: z.object({
417
- id: z.string(),
418
- name: z.string(),
419
- type: z.enum(["user", "system"]),
420
- }),
421
- action: z.string(),
422
- summary: z.string(),
423
- });
424
-
425
- export const WorkspaceAuditResponseSchema = z.array(WorkspaceAuditEventSchema);
426
-
427
- export type WorkspaceAuditEvent = z.infer<typeof WorkspaceAuditEventSchema>;
428
-
429
- // ---------------------------------------------------------------------------
430
- // POST /api/workspace/resolve-path
431
- // ---------------------------------------------------------------------------
432
-
433
- const ResolvedDeptEntitySchema = z.object({
434
- id: z.string(),
435
- name: z.string(),
436
- slug: z.string(),
437
- memberCount: z.number(),
438
- });
439
-
440
- export const WorkspaceResolvePathResponseSchema = z.object({
441
- layers: z.array(
442
- z.discriminatedUnion("type", [
443
- z.object({ type: z.literal("dept"), entity: ResolvedDeptEntitySchema }),
444
- z.object({ type: z.literal("team"), entity: ResolvedDeptEntitySchema }),
445
- z.object({
446
- type: z.literal("members"),
447
- scope: z.enum(["org", "dept", "team"]),
448
- }),
449
- ]),
450
- ),
451
- });
452
-
453
- export type WorkspaceResolvePathResponse = z.infer<
454
- typeof WorkspaceResolvePathResponseSchema
455
- >;
456
-
457
- // ---------------------------------------------------------------------------
458
- // PATCH /api/workspace/auth-policy
459
- // ---------------------------------------------------------------------------
460
-
461
- export const OrgAuthPolicySchema = z.object({
462
- requireSSO: z.boolean(),
463
- allowedProviders: z.array(z.string()),
464
- selfRevoked: z.boolean().optional(),
465
- });
466
-
467
- export type OrgAuthPolicy = z.infer<typeof OrgAuthPolicySchema>;
468
-
469
- // ---------------------------------------------------------------------------
470
- // POST /api/workspace/auth-policy/validate-oidc
471
- // ---------------------------------------------------------------------------
472
-
473
- export { OidcValidationResultSchema };
474
- export type OidcValidationResult = z.infer<typeof OidcValidationResultSchema>;
475
-
476
- // ---------------------------------------------------------------------------
477
- // POST /api/workspace/resync-slack-logo
478
- // ---------------------------------------------------------------------------
479
-
480
- export const WorkspaceResyncSlackLogoResponseSchema = z.object({
481
- success: z.literal(true),
482
- logoUrl: z.string().nullable(),
483
- });
484
-
485
- export type WorkspaceResyncSlackLogoResponse = z.infer<
486
- typeof WorkspaceResyncSlackLogoResponseSchema
487
- >;
488
-
489
- // ---------------------------------------------------------------------------
490
- // POST /api/workspace/auth-policy/test-sso
491
- // ---------------------------------------------------------------------------
492
-
493
- export const TestSsoInitiationSchema = z.object({
494
- authorizationUrl: z.string(),
495
- attemptId: z.string(),
496
- });
497
-
498
- export type TestSsoInitiation = z.infer<typeof TestSsoInitiationSchema>;
499
-
500
- // ---------------------------------------------------------------------------
501
- // GET /api/workspace/auth-policy/test-sso/:attemptId
502
- // ---------------------------------------------------------------------------
503
-
504
- export const TestSsoResultSchema = z.object({
505
- status: z.enum(["pending", "success", "failed", "expired"]),
506
- claims: z
507
- .object({
508
- sub: z.string(),
509
- email: z.string().optional(),
510
- name: z.string().optional(),
511
- issuer: z.string(),
512
- })
513
- .optional(),
514
- identityLinked: z.boolean().optional(),
515
- error: z.string().optional(),
516
- errorCode: z
517
- .enum([
518
- "IDENTITY_CONFLICT",
519
- "DOMAIN_MISMATCH",
520
- "ISSUER_MISMATCH",
521
- "CALLBACK_ERROR",
522
- ])
523
- .optional(),
524
- });
525
-
526
- export type TestSsoResult = z.infer<typeof TestSsoResultSchema>;
527
-
528
296
  // ---------------------------------------------------------------------------
529
297
  // DELETE /api/workspace/members/:id
530
298
  // ---------------------------------------------------------------------------
@@ -681,1414 +449,6 @@ export type ScopeCheckBatchResponse = z.infer<
681
449
  typeof ScopeCheckBatchResponseSchema
682
450
  >;
683
451
 
684
- // ---------------------------------------------------------------------------
685
- // Invite sub-schema
686
- // ---------------------------------------------------------------------------
687
-
688
- /**
689
- * Role the invitee takes in their home unit on acceptance. `unit_owner` is a
690
- * deferred unit-ownership grant applied at accept time. Source of truth
691
- * for the hand-mirrored `OrgInvite`/`CreateInviteRequest` interface unions in
692
- * `org/types.ts` (enforced by the `schema-type-mirror` guard).
693
- */
694
- export const HomeUnitRoleSchema = z.enum(["member", "unit_owner"]);
695
- export type HomeUnitRole = z.infer<typeof HomeUnitRoleSchema>;
696
-
697
- /** Established title cap shared with org-unit/open-role titles. */
698
- const STAGED_PLACEMENT_TITLE_MAX = 255;
699
- /**
700
- * Upper bound on the number of units an invitee can be staged into as owner or
701
- * contributor. Bounds the replay cost of applying a staged placement at accept
702
- * time (each entry is a deferred membership grant). A generous-but-finite cap.
703
- */
704
- const STAGED_PLACEMENT_UNITS_MAX = 64;
705
-
706
- /**
707
- * Staged member placement: the fuller org position an invitee is slotted into,
708
- * applied as deferred grants when the invite is accepted. A superset of the
709
- * legacy top-level `homeUnitId`/`homeUnitRole` fields, carrying the (optional)
710
- * intended title, manager, and the units the invitee will own or contribute to.
711
- *
712
- * Every field is optional or defaulted so a placement can be partially
713
- * specified at invite time. The array caps bound downstream replay cost.
714
- */
715
- export const StagedPlacementSchema = z.object({
716
- // Intended job/role title for the invitee. Trimmed; non-empty when present.
717
- title: z.string().trim().min(1).max(STAGED_PLACEMENT_TITLE_MAX).optional(),
718
- // The invitee's manager on acceptance (a `manages` designation).
719
- managerUserId: z.string().uuid().optional(),
720
- // Home unit the invitee is placed in (their `users.primary_unit_id`).
721
- homeUnitId: z.string().uuid().optional(),
722
- // Role the invitee takes in `homeUnitId` ('unit_owner' = deferred grant).
723
- homeUnitRole: HomeUnitRoleSchema.optional(),
724
- // Additional units the invitee becomes a unit owner of on acceptance.
725
- unitOwnerOf: z
726
- .array(z.string().uuid())
727
- .max(STAGED_PLACEMENT_UNITS_MAX)
728
- .default([]),
729
- // Units the invitee contributes to (non-owner membership) on acceptance.
730
- contributesTo: z
731
- .array(z.string().uuid())
732
- .max(STAGED_PLACEMENT_UNITS_MAX)
733
- .default([]),
734
- });
735
- export type StagedPlacement = z.infer<typeof StagedPlacementSchema>;
736
-
737
- const OrgInviteSchema = z.object({
738
- id: z.string(),
739
- orgId: z.string(),
740
- email: z.string(),
741
- role: z.enum(["admin", "member"]),
742
- invitedBy: z.object({ id: z.string(), name: z.string() }),
743
- status: z.enum(["pending", "accepted", "expired", "revoked"]),
744
- // Home unit the invitee is placed in on acceptance (users.primary_unit_id).
745
- // Optional/nullable: legacy invites predate this field.
746
- homeUnitId: z.string().uuid().optional(),
747
- // Role the invitee takes in homeUnitId on acceptance ('unit_owner' is a
748
- // deferred unit-ownership grant). Optional/'member' for legacy +
749
- // unit-less invites.
750
- homeUnitRole: HomeUnitRoleSchema.optional(),
751
- // Fuller staged placement applied as deferred grants on acceptance. A
752
- // superset of the top-level homeUnitId/homeUnitRole, kept additive/optional
753
- // so legacy invites and unit-less invites parse unchanged.
754
- stagedPlacement: StagedPlacementSchema.optional(),
755
- createdAt: z.string(),
756
- expiresAt: z.string(),
757
- acceptedAt: z.string().optional(),
758
- });
759
-
760
- // ---------------------------------------------------------------------------
761
- // POST /api/workspace/invites
762
- // ---------------------------------------------------------------------------
763
-
764
- export const InviteResponseSchema = z.object({
765
- invite: OrgInviteSchema,
766
- });
767
-
768
- export type InviteResponse = z.infer<typeof InviteResponseSchema>;
769
-
770
- // ---------------------------------------------------------------------------
771
- // GET /api/workspace/invites
772
- // ---------------------------------------------------------------------------
773
-
774
- export const InviteListResponseSchema = z.array(OrgInviteSchema);
775
-
776
- export type InviteListResponse = z.infer<typeof InviteListResponseSchema>;
777
-
778
- // ---------------------------------------------------------------------------
779
- // POST /api/workspace/invites — request body
780
- // Request schema colocated with the response schema (narrow deviation from
781
- // ADR-CONT-044, per the CreateOpenRoleRequestSchema precedent) because the
782
- // staged-placement shape is the published promise consumers must satisfy.
783
- // Mirrors the hand-written `CreateInviteRequest` interface in org/types.ts and
784
- // extends it with the optional `stagedPlacement` superset.
785
- // ---------------------------------------------------------------------------
786
-
787
- export const CreateInviteRequestSchema = z.object({
788
- email: z.string().email(),
789
- // Invites only grant the restricted {admin, member} domain.
790
- role: z.enum(["admin", "member"]),
791
- // Home unit the invitee is placed in on acceptance. Optional in the contract
792
- // so the field can roll out without breaking older callers.
793
- homeUnitId: z.string().uuid().optional(),
794
- // Role the invitee takes in homeUnitId — only meaningful with a home unit.
795
- homeUnitRole: HomeUnitRoleSchema.optional(),
796
- // Fuller staged placement (title, manager, owned/contributed units) applied
797
- // as deferred grants on acceptance. Optional/additive for back-compat.
798
- stagedPlacement: StagedPlacementSchema.optional(),
799
- });
800
-
801
- export type CreateInviteRequestPayload = z.infer<
802
- typeof CreateInviteRequestSchema
803
- >;
804
-
805
- // ---------------------------------------------------------------------------
806
- // GET /api/workspace/directory
807
- //
808
- // The org's PEOPLE DIRECTORY — every `persons` row, account-bearing or not.
809
- // Distinct from the member list (`user_org_memberships`, RBAC principals) and
810
- // from the org chart (`positions` + occupancy, topology): this is the answer to
811
- // "who does this org know about", which after an HRIS sync is a much larger set
812
- // than either.
813
- //
814
- // It exists because inviting a synced person needs their address, and the org
815
- // chart cannot carry one: `GET /api/users/org-chart` is authenticated-only, so a
816
- // `primaryEmail` there would let any member enumerate the whole company's email
817
- // addresses. This surface is capability-gated instead.
818
- // ---------------------------------------------------------------------------
819
-
820
- export const OrgDirectoryPersonSchema = z.object({
821
- personId: z.string().uuid(),
822
- displayName: z.string(),
823
- /**
824
- * Nullable and NOT optional. A directory person with no address is a real,
825
- * common state — an HRIS export with the email column unmapped produces a
826
- * whole org of them — and it is the difference between "cannot be invited" and
827
- * "not yet loaded". Callers must render the distinction rather than treat a
828
- * missing address as absence of the person.
829
- */
830
- primaryEmail: z.string().nullable(),
831
- /** The linked account, or null when this person has never signed up. */
832
- userId: z.string().uuid().nullable(),
833
- });
834
-
835
- export type OrgDirectoryPerson = z.infer<typeof OrgDirectoryPersonSchema>;
836
-
837
- export const OrgDirectoryResponseSchema = z.object({
838
- people: z.array(OrgDirectoryPersonSchema),
839
- });
840
-
841
- export type OrgDirectoryResponse = z.infer<typeof OrgDirectoryResponseSchema>;
842
-
843
- // ---------------------------------------------------------------------------
844
- // POST /api/workspace/invites/batch — DEPRECATED (removed in contracts 58.0.0)
845
- //
846
- // Superseded by the invite-batch lifecycle below. This route sends real email
847
- // the instant it is called: there is no moment at which a human sees the
848
- // addresses that are about to be written to, and no way to cancel. Everything it
849
- // does correctly — personId keying, per-person transactions, three honest result
850
- // buckets — is preserved by `POST /api/workspace/invite-batches/{id}/send`, with
851
- // a review boundary in front of it.
852
- //
853
- // Kept live for one major so consumers can migrate against a green cascade: a
854
- // single major that deleted these types while the app still imported them could
855
- // not land, because the release cascade gates on each consumer's full CI tier.
856
- // 57.0.0 adds the lifecycle; consumers move; 58.0.0 deletes everything below.
857
- //
858
- // Bulk invite, keyed by `personId` rather than by email. That is a security
859
- // property, not a convenience: the single-invite route takes a caller-supplied
860
- // address and can therefore reach any mailbox on the internet, which is why it
861
- // carries a strict per-minute limit. This route's recipients are resolved
862
- // server-side from the caller's own `persons` rows, so the reachable set is
863
- // bounded by the org's own directory and the arbitrary-recipient risk is gone.
864
- //
865
- // Consequence for callers: the client never needs to hold an email address to
866
- // send an invite, and an unknown or cross-org `personId` is silently omitted
867
- // from the response rather than reported — reporting it would make this a
868
- // membership oracle.
869
- // ---------------------------------------------------------------------------
870
-
871
- /**
872
- * Cap on one batch. Bounds worst-case request latency, since the invite emails
873
- * are sent before the response returns (the raw token exists only in memory and
874
- * must not be parked in a queue).
875
- */
876
- export const BATCH_INVITE_PERSONS_MAX = 100;
877
-
878
- /**
879
- * How many days an invitation link stays redeemable.
880
- *
881
- * Shared vocabulary, not prose: the backend stamps `expires_at` from this when
882
- * it mints an invite, and the review surface states the same window to the
883
- * person about to send one. It lived in both places as a literal and drifted —
884
- * the copy still promised seven days after the policy moved.
885
- *
886
- * A SECURITY parameter as much as a UX one: it bounds how long a leaked invite
887
- * link stays usable, so widening it is a deliberate trust-model change rather
888
- * than a copy edit.
889
- */
890
- export const INVITE_EXPIRY_DAYS = 30;
891
-
892
- /** @deprecated Use {@link CreateInviteBatchRequestSchema}. Removed in 58.0.0. */
893
- export const BatchInviteRequestSchema = z.object({
894
- personIds: z.array(z.string().uuid()).min(1).max(BATCH_INVITE_PERSONS_MAX),
895
- // Same restricted {admin, member} domain as a single invite.
896
- role: z.enum(["admin", "member"]),
897
- });
898
-
899
- export type BatchInviteRequest = z.infer<typeof BatchInviteRequestSchema>;
900
-
901
- /**
902
- * Why one person in a batch did not receive an invite.
903
- *
904
- * - `already-member` — holds an account in this org already.
905
- * - `invite-pending` — an unexpired invite is already outstanding.
906
- * - `no-email` — the directory row carries no address to send to.
907
- * - `send-failed` — the invite row COMMITTED but the email did not leave.
908
- * Recoverable by resending; never a reason to roll the
909
- * invite back.
910
- */
911
- export const BATCH_INVITE_SKIP_REASONS = [
912
- "already-member",
913
- "invite-pending",
914
- "no-email",
915
- "send-failed",
916
- ] as const;
917
-
918
- /** @deprecated Use {@link InviteBatchItemReasonSchema}. Removed in 58.0.0. */
919
- export const BatchInviteSkipReasonSchema = z.enum(BATCH_INVITE_SKIP_REASONS);
920
-
921
- export type BatchInviteSkipReason = z.infer<typeof BatchInviteSkipReasonSchema>;
922
-
923
- const BatchInviteOutcomeSchema = z.object({
924
- personId: z.string().uuid(),
925
- reason: BatchInviteSkipReasonSchema,
926
- });
927
-
928
- /** @deprecated Use {@link InviteBatchResponseSchema}. Removed in 58.0.0. */
929
- export const BatchInviteResponseSchema = z.object({
930
- /** Person ids that now hold a pending invite AND were emailed. */
931
- invited: z.array(z.string().uuid()),
932
- /** Never attempted — see {@link BatchInviteSkipReason}. */
933
- skipped: z.array(BatchInviteOutcomeSchema),
934
- /**
935
- * Invite row exists, delivery did not. Separate from `skipped` because the
936
- * remedy differs: a skip needs the underlying state to change, a failure needs
937
- * a resend.
938
- */
939
- failed: z.array(BatchInviteOutcomeSchema),
940
- });
941
-
942
- export type BatchInviteResponse = z.infer<typeof BatchInviteResponseSchema>;
943
-
944
- // ---------------------------------------------------------------------------
945
- // Invite batches — the Prepare → Review → Send lifecycle
946
- //
947
- // POST /api/workspace/invite-batches create a draft
948
- // GET /api/workspace/invite-batches/{id} review it
949
- // PATCH /api/workspace/invite-batches/{id} change selection/role (bumps revision)
950
- // POST /api/workspace/invite-batches/{id}/prepare evaluate eligibility, draft → ready
951
- // POST /api/workspace/invite-batches/{id}/send send, gated on expectedRevision
952
- // POST /api/workspace/invite-batches/{id}/cancel cancel what has not been sent
953
- //
954
- // WHY A PERSISTED BATCH AT ALL. The one-shot bulk route (`POST
955
- // /api/workspace/invites/batch`, deprecated by this vocabulary) sends real email
956
- // the instant it is called — there is no moment at which a human can see the 88
957
- // addresses that are about to be written to. This lifecycle inserts exactly one:
958
- // `prepare` decides who is eligible, the review surface shows the recipients and
959
- // their addresses, and `send` acts on what was reviewed or refuses.
960
- //
961
- // THE BATCH HOLDS INTENT, NEVER CREDENTIALS. There is no token field on a batch
962
- // or an item, in any form, and there never may be. An invite token exists only
963
- // between generation and the send, in memory; only its SHA-256 is stored, on the
964
- // invite row. A batch that parked a live token at rest would undo that whole
965
- // property in exchange for nothing — the item carries `inviteId`, which is the
966
- // durable handle to the credential without being one.
967
- //
968
- // CONFIRMATION AUTHORIZES THE ADDRESSES SHOWN AT REVIEW TIME. `emailSnapshot` is
969
- // what the reviewer saw. At send, an item whose person now holds a different
970
- // address is `skipped` with `address-changed` — never mailed at the new one. The
971
- // same is true of someone who became a member, or gained an invite by another
972
- // route, between review and send. Approving a list is not approving whatever
973
- // that list becomes.
974
- // ---------------------------------------------------------------------------
975
-
976
- /**
977
- * Batch lifecycle.
978
- *
979
- * - `draft` — created, membership not yet evaluated. Sends nothing.
980
- * - `ready` — `prepare` ran; the item set and its exclusions are the review.
981
- * - `sending` — CLAIMED by a send. See {@link SendInviteBatchRequestSchema}.
982
- * - `completed` — the send finished. Per-person outcomes live on the items.
983
- * - `canceled` — canceled before or between sends; unsent items stay unsent.
984
- *
985
- * There is deliberately no `partially_completed`: it is `failedCount > 0` on a
986
- * `completed` batch, and a second spelling of one fact is a second thing to keep
987
- * true. There is deliberately no `expired` either — nothing sweeps batches, and
988
- * a status no writer ever writes is a promise the API does not keep.
989
- */
990
- export const INVITE_BATCH_STATUSES = [
991
- "draft",
992
- "ready",
993
- "sending",
994
- "completed",
995
- "canceled",
996
- ] as const;
997
-
998
- export const InviteBatchStatusSchema = z.enum(INVITE_BATCH_STATUSES);
999
-
1000
- export type InviteBatchStatus = z.infer<typeof InviteBatchStatusSchema>;
1001
-
1002
- /**
1003
- * Per-recipient outcome within a batch.
1004
- *
1005
- * - `eligible` — will be invited when the batch is sent.
1006
- * - `excluded` — will not, decided at PREPARE time. See {@link InviteBatchItemReason}.
1007
- * - `sent` — an invite row exists AND the email left. Carries `inviteId`.
1008
- * - `failed` — an invite row exists, the email did NOT leave. Also carries
1009
- * `inviteId`: the remedy is a reissue, never a re-send, because
1010
- * the credential in that row was already minted.
1011
- * - `skipped` — eligible at review, refused at SEND because the world moved.
1012
- *
1013
- * `eligible` items on a batch stuck in `sending` are exactly the ones a crashed
1014
- * send never reached — which is why there is no separate `pending_send`.
1015
- */
1016
- export const INVITE_BATCH_ITEM_STATUSES = [
1017
- "eligible",
1018
- "excluded",
1019
- "sent",
1020
- "failed",
1021
- "skipped",
1022
- ] as const;
1023
-
1024
- export const InviteBatchItemStatusSchema = z.enum(INVITE_BATCH_ITEM_STATUSES);
1025
-
1026
- export type InviteBatchItemStatus = z.infer<typeof InviteBatchItemStatusSchema>;
1027
-
1028
- /**
1029
- * Why an item is not `sent`. One vocabulary across `excluded` (decided at
1030
- * prepare) and `skipped` (decided at send) — the reader wants "why did this
1031
- * person not get an invitation", and splitting the answer by which stage
1032
- * noticed would make that two questions.
1033
- *
1034
- * - `already-member` — holds an account in this org.
1035
- * - `invite-pending` — an unexpired invite is already outstanding.
1036
- * - `no-email` — the directory row carries no address.
1037
- * - `invalid-email` — it carries one that is not a deliverable address.
1038
- * An HRIS export with a mangled column produces these;
1039
- * committing an invite row that can never be mailed is
1040
- * the failure this reason exists to prevent.
1041
- * - `removed-by-reviewer` — a human un-ticked them. Persisted rather than
1042
- * deleted so the choice is reversible AND legible:
1043
- * "who was deliberately left out" is a question the
1044
- * batch must be able to answer later.
1045
- * - `address-changed` — SEND-time only. The person's address is no longer
1046
- * the one shown at review. Confirmation authorized the
1047
- * address, not the person.
1048
- * - `person-removed` — SEND-time only. The directory row is gone.
1049
- * - `send-failed` — pairs with `failed`, not with `excluded`/`skipped`.
1050
- * - `inactive-in-hris` — RESERVED, and NOTHING WRITES IT TODAY. `persons`
1051
- * carries no employment-status column, the BambooHR
1052
- * client never reads one, and a "Sync now" full resync
1053
- * reports no departures at all — so a terminated
1054
- * employee is currently indistinguishable from a
1055
- * current one. The name is held here so the eventual
1056
- * implementation does not have to change the wire
1057
- * vocabulary; treating its ABSENCE as evidence that
1058
- * everyone listed is still employed would be wrong.
1059
- */
1060
- export const INVITE_BATCH_ITEM_REASONS = [
1061
- "already-member",
1062
- "invite-pending",
1063
- "no-email",
1064
- "invalid-email",
1065
- "removed-by-reviewer",
1066
- "address-changed",
1067
- "person-removed",
1068
- "send-failed",
1069
- "inactive-in-hris",
1070
- ] as const;
1071
-
1072
- export const InviteBatchItemReasonSchema = z.enum(INVITE_BATCH_ITEM_REASONS);
1073
-
1074
- export type InviteBatchItemReason = z.infer<typeof InviteBatchItemReasonSchema>;
1075
-
1076
- export const InviteBatchItemSchema = z.object({
1077
- personId: z.string().uuid(),
1078
- /**
1079
- * Read live from the directory, NOT snapshotted. A name is for recognizing the
1080
- * human; only the ADDRESS is the thing confirmation authorizes, so only the
1081
- * address is frozen. Null when the directory row has since been deleted.
1082
- */
1083
- displayName: z.string().nullable(),
1084
- /**
1085
- * The address as it stood when `prepare` ran — what the reviewer approved.
1086
- * Null only when the person had none. Compared against the live address at
1087
- * send; a mismatch is `address-changed`, never a send to the new one.
1088
- */
1089
- emailSnapshot: z.string().nullable(),
1090
- role: z.enum(["admin", "member"]),
1091
- status: InviteBatchItemStatusSchema,
1092
- /** Null exactly when `status` is `eligible` or `sent`. */
1093
- reason: InviteBatchItemReasonSchema.nullable(),
1094
- /**
1095
- * The invite row this item produced. Present for `sent` AND for `failed` — a
1096
- * failed item's credential was minted and committed before the send was
1097
- * attempted, so the row exists and reissue is the remedy.
1098
- */
1099
- inviteId: z.string().uuid().nullable(),
1100
- /**
1101
- * The snapshot address is not under a domain this org has VERIFIED. Computed
1102
- * at read time from `org_domains` (status `verified`), never stored: a domain
1103
- * verified after review must stop warning, and one revoked after review must
1104
- * start. Advisory — it does not exclude anyone. Sending outside your own
1105
- * domains is legitimate (contractors, personal addresses); doing it eighty-
1106
- * eight times without noticing is the accident worth surfacing.
1107
- */
1108
- emailOutsideVerifiedDomain: z.boolean(),
1109
- });
1110
-
1111
- export type InviteBatchItem = z.infer<typeof InviteBatchItemSchema>;
1112
-
1113
- /**
1114
- * Denormalized counters, maintained by the batch's own writers in the same
1115
- * transaction as the items they describe.
1116
- *
1117
- * DELIBERATELY NOT DERIVED CLIENT-SIDE. The confirm control reads
1118
- * "Send {eligible} invitations", and that number must be the number the SERVER
1119
- * will act on. A count computed from a page of items can disagree with the set
1120
- * `send` iterates; a count the server wrote cannot.
1121
- */
1122
- export const InviteBatchCountsSchema = z.object({
1123
- /** Every person the batch was created for. Never changes after create. */
1124
- candidate: z.number().int().nonnegative(),
1125
- eligible: z.number().int().nonnegative(),
1126
- excluded: z.number().int().nonnegative(),
1127
- sent: z.number().int().nonnegative(),
1128
- failed: z.number().int().nonnegative(),
1129
- skipped: z.number().int().nonnegative(),
1130
- });
1131
-
1132
- export type InviteBatchCounts = z.infer<typeof InviteBatchCountsSchema>;
1133
-
1134
- export const InviteBatchSchema = z.object({
1135
- id: z.string().uuid(),
1136
- status: InviteBatchStatusSchema,
1137
- /**
1138
- * Where the candidate set came from. OPEN string, `directory` today — the org
1139
- * people directory. Not an enum: a future CSV or SCIM origin should not need a
1140
- * migration to be nameable.
1141
- */
1142
- source: z.string(),
1143
- /**
1144
- * Bumped by every mutation of the reviewable set (selection or role). `send`
1145
- * carries the revision the reviewer saw; a mismatch is
1146
- * {@link INVITE_BATCH_CHANGED} rather than a send of something else.
1147
- */
1148
- revision: z.number().int().nonnegative(),
1149
- counts: InviteBatchCountsSchema,
1150
- createdByUserId: z.string().uuid(),
1151
- /** Who ran `send`, and when they did. Null until then. */
1152
- confirmedByUserId: z.string().uuid().nullable(),
1153
- confirmedAt: z.string().nullable(),
1154
- createdAt: z.string(),
1155
- updatedAt: z.string(),
1156
- });
1157
-
1158
- export type InviteBatch = z.infer<typeof InviteBatchSchema>;
1159
-
1160
- export const InviteBatchResponseSchema = z.object({
1161
- batch: InviteBatchSchema,
1162
- items: z.array(InviteBatchItemSchema),
1163
- /**
1164
- * The org's verified domains, so the review surface can NAME them in the
1165
- * out-of-domain warning rather than just flagging rows. Empty is the common
1166
- * case and means every address is out-of-domain by this definition — render
1167
- * the warning accordingly rather than as eighty-eight alarms.
1168
- */
1169
- verifiedDomains: z.array(z.string()),
1170
- });
1171
-
1172
- export type InviteBatchResponse = z.infer<typeof InviteBatchResponseSchema>;
1173
-
1174
- export const CreateInviteBatchRequestSchema = z.object({
1175
- personIds: z.array(z.string().uuid()).min(1).max(BATCH_INVITE_PERSONS_MAX),
1176
- role: z.enum(["admin", "member"]),
1177
- });
1178
-
1179
- export type CreateInviteBatchRequest = z.infer<
1180
- typeof CreateInviteBatchRequestSchema
1181
- >;
1182
-
1183
- /**
1184
- * Change what a `draft` or `ready` batch will do. Every accepted call bumps
1185
- * `revision`, which is what invalidates a review already on someone's screen.
1186
- *
1187
- * Deselecting does NOT delete the item — it becomes `excluded` with
1188
- * `removed-by-reviewer`, so reselecting is a status flip rather than a
1189
- * re-preparation, and the batch can still answer who was deliberately omitted.
1190
- * Reselecting a person excluded by POLICY (already a member, no address) is
1191
- * refused: `reselect` reverses a human's decision, not the server's.
1192
- */
1193
- export const UpdateInviteBatchRequestSchema = z
1194
- .object({
1195
- deselectPersonIds: z.array(z.string().uuid()).optional(),
1196
- reselectPersonIds: z.array(z.string().uuid()).optional(),
1197
- role: z.enum(["admin", "member"]).optional(),
1198
- })
1199
- .refine(
1200
- (v) =>
1201
- v.deselectPersonIds !== undefined ||
1202
- v.reselectPersonIds !== undefined ||
1203
- v.role !== undefined,
1204
- {
1205
- message:
1206
- "Provide at least one of deselectPersonIds, reselectPersonIds, role",
1207
- },
1208
- );
1209
-
1210
- export type UpdateInviteBatchRequest = z.infer<
1211
- typeof UpdateInviteBatchRequestSchema
1212
- >;
1213
-
1214
- /**
1215
- * `expectedRevision` is REQUIRED and has no default. Sending is the only
1216
- * irreversible verb in this lifecycle — email cannot be recalled — so the one
1217
- * call that performs it must state what it believes it is sending.
1218
- *
1219
- * There is no `Idempotency-Key` here, deliberately. The send claim
1220
- * (`UPDATE ... SET status='sending' WHERE status='ready' AND revision = ?`) is
1221
- * an atomic compare-and-set: the first caller wins and every later one sees zero
1222
- * rows and gets {@link INVITE_BATCH_CHANGED}. That is exactly-once permanently
1223
- * and across every task in the cluster, where an idempotency key would be
1224
- * exactly-once for its retention window and only where the key store is shared.
1225
- * Routing this through a key ALSO means wrapping check + mutation + store in one
1226
- * transaction, which would roll back the record of mail that had already left.
1227
- */
1228
- export const SendInviteBatchRequestSchema = z.object({
1229
- expectedRevision: z.number().int().nonnegative(),
1230
- /**
1231
- * The reviewer's optional note, carried into every invitation this send mails
1232
- * and attributed to the inviter (`NotificationPayloads['org.invite'].message`).
1233
- *
1234
- * It rides the SEND rather than the batch on purpose: it authorizes nothing,
1235
- * so it has no business bumping a revision that every open review reads as
1236
- * "read this again". Not persisted — the invitation is the only thing that
1237
- * ever carries it.
1238
- */
1239
- message: z.string().max(2000).optional(),
1240
- /**
1241
- * Per-person role overrides, keyed by `personId` — the reviewer set a role for
1242
- * ONE recipient rather than for the batch.
1243
- *
1244
- * The batch's own `role` remains what everyone else is invited as; a key here
1245
- * replaces it for that person only. A key naming somebody outside the batch,
1246
- * or somebody it is no longer sending to, is INERT rather than an error: the
1247
- * live directory re-check at send time already decides who is mailed, and a
1248
- * stale override describes a person nobody is inviting.
1249
- */
1250
- roleByPersonId: z
1251
- .record(z.string().uuid(), z.enum(["admin", "member"]))
1252
- .optional(),
1253
- });
1254
-
1255
- export type SendInviteBatchRequest = z.infer<
1256
- typeof SendInviteBatchRequestSchema
1257
- >;
1258
-
1259
- /**
1260
- * The error code a stale `send` (or a mutation of an already-claimed batch)
1261
- * returns, as `error` on the standard 409 envelope.
1262
- *
1263
- * It means: what you reviewed is not what is here now. The client's obligation
1264
- * is to re-fetch and put the CHANGES in front of the human again before the
1265
- * confirm control re-arms — not to retry with the new revision, which would
1266
- * authorize a list nobody read.
1267
- */
1268
- export const INVITE_BATCH_CHANGED = "BATCH_CHANGED" as const;
1269
-
1270
- // ---------------------------------------------------------------------------
1271
- // OrgDomain sub-schema
1272
- // ---------------------------------------------------------------------------
1273
-
1274
- const OrgDomainSchema = z.object({
1275
- id: z.string(),
1276
- orgId: z.string(),
1277
- domain: z.string(),
1278
- status: z.enum(["pending", "verified"]),
1279
- verificationMethod: z.enum(["dns_txt", "email", "idp"]),
1280
- verificationToken: z.string().optional(),
1281
- verifiedAt: z.string().nullable(),
1282
- createdAt: z.string(),
1283
- verifiedBy: z
1284
- .object({ id: z.string(), name: z.string(), email: z.string() })
1285
- .optional(),
1286
- });
1287
-
1288
- // ---------------------------------------------------------------------------
1289
- // GET /api/workspace/domains/:id/verify (single domain)
1290
- // POST /api/workspace/domains/:id/verify (returns verified OrgDomain)
1291
- // ---------------------------------------------------------------------------
1292
-
1293
- export const DomainResponseSchema = OrgDomainSchema;
1294
-
1295
- export type DomainResponse = z.infer<typeof DomainResponseSchema>;
1296
-
1297
- // ---------------------------------------------------------------------------
1298
- // GET /api/workspace/domains
1299
- // ---------------------------------------------------------------------------
1300
-
1301
- export const DomainListResponseSchema = z.array(OrgDomainSchema);
1302
-
1303
- export type DomainListResponse = z.infer<typeof DomainListResponseSchema>;
1304
-
1305
- // ---------------------------------------------------------------------------
1306
- // GET /api/org/deletion-eligibility
1307
- // ---------------------------------------------------------------------------
1308
-
1309
- const MemberClassificationSchema = z.object({
1310
- userId: z.string(),
1311
- email: z.string(),
1312
- suggestedFate: z.enum(["REMOVE_FROM_ORG", "DELETE_ACCOUNT", "BLOCKED"]),
1313
- reason: z.string(),
1314
- });
1315
-
1316
- export const OrgDeletionStatusSchema = z.object({
1317
- orgId: z.string(),
1318
- eligible: z.boolean(),
1319
- isPersonalOrg: z.boolean(),
1320
- ownerStatus: z.string().optional(),
1321
- deletionRequestedAt: z.string().optional(),
1322
- classifications: z.array(MemberClassificationSchema),
1323
- });
1324
-
1325
- export type OrgDeletionStatus = z.infer<typeof OrgDeletionStatusSchema>;
1326
-
1327
- // ---------------------------------------------------------------------------
1328
- // POST /api/org/transfer-ownership (initiate)
1329
- // ---------------------------------------------------------------------------
1330
-
1331
- export const OwnershipTransferResponseSchema = z.object({
1332
- transferId: z.string(),
1333
- expiresAt: z.string(),
1334
- message: z.string(),
1335
- });
1336
-
1337
- export type OwnershipTransferResponse = z.infer<
1338
- typeof OwnershipTransferResponseSchema
1339
- >;
1340
-
1341
- // ---------------------------------------------------------------------------
1342
- // POST /api/org/transfer-ownership/preview
1343
- // ---------------------------------------------------------------------------
1344
-
1345
- export const OwnershipTransferPreviewSchema = z.object({
1346
- workspaceName: z.string(),
1347
- initiatorName: z.string(),
1348
- initiatorEmail: z.string(),
1349
- note: z.string().optional(),
1350
- expiresAt: z.string(),
1351
- token: z.string(),
1352
- });
1353
-
1354
- export type OwnershipTransferPreview = z.infer<
1355
- typeof OwnershipTransferPreviewSchema
1356
- >;
1357
-
1358
- // ---------------------------------------------------------------------------
1359
- // GET /api/org/system-events
1360
- // ---------------------------------------------------------------------------
1361
-
1362
- const OrgSystemEventSchema = z.object({
1363
- id: z.string(),
1364
- type: z.string(),
1365
- payload: z.unknown(),
1366
- createdAt: z.string(),
1367
- });
1368
-
1369
- export const OrgSystemEventsListSchema = z.object({
1370
- events: z.array(OrgSystemEventSchema),
1371
- });
1372
-
1373
- export type OrgSystemEventsList = z.infer<typeof OrgSystemEventsListSchema>;
1374
-
1375
- // ---------------------------------------------------------------------------
1376
- // POST /api/org/system-events/:id/acknowledge
1377
- // ---------------------------------------------------------------------------
1378
-
1379
- export const AcknowledgeSystemEventResponseSchema = z.object({
1380
- success: z.literal(true),
1381
- });
1382
-
1383
- export type AcknowledgeSystemEventResponse = z.infer<
1384
- typeof AcknowledgeSystemEventResponseSchema
1385
- >;
1386
-
1387
- // ---------------------------------------------------------------------------
1388
- // GET /api/orgs/:orgId/ai-usage
1389
- //
1390
- // INV-USAGE-LEDGER: every request, token and dollar here is read from the
1391
- // settled billing ledger (`billing_usage_events`) — one row per billed provider
1392
- // call, whatever feature made it. A `request` is one settled billable provider
1393
- // invocation (retries and fallbacks are separate requests), every amount is
1394
- // the customer charge in integer micro-USD, and `tokens` is input + output +
1395
- // cache read + cache write (reasoning is a subset of output, never added).
1396
- // `chatRuns` alone comes from runtime telemetry: duration and failure
1397
- // diagnostics for chat turns, carrying no money.
1398
- // ---------------------------------------------------------------------------
1399
-
1400
- const OrgAiUsageTotalsSchema = z.object({
1401
- requests: z.number().int().min(0),
1402
- tokens: z.number().int().min(0),
1403
- customerChargeMicros: MoneyMicrosSchema,
1404
- });
1405
-
1406
- const OrgChatRunProfileSchema = z.object({
1407
- profile: z.string(),
1408
- executions: z.number().int().min(0),
1409
- percentOfTotal: z.number(),
1410
- avgDurationMs: z.number(),
1411
- failureRate: z.number(),
1412
- });
1413
-
1414
- export const OrgUsageResponseSchema = z.object({
1415
- /** The org's billing window: `start`/`end` in UTC, days bucketed in `timezone`. */
1416
- period: z.object({
1417
- start: IsoDateTime,
1418
- end: IsoDateTime,
1419
- timezone: z.string(),
1420
- }),
1421
- summary: OrgAiUsageTotalsSchema,
1422
- daily: z.array(
1423
- z.object({
1424
- date: z.string(),
1425
- requests: z.number().int().min(0),
1426
- customerChargeMicros: MoneyMicrosSchema,
1427
- }),
1428
- ),
1429
- byFeature: z.array(
1430
- OrgAiUsageTotalsSchema.extend({ feature: z.enum(AI_FEATURES) }),
1431
- ),
1432
- byModel: z.array(
1433
- OrgAiUsageTotalsSchema.extend({
1434
- provider: z.string(),
1435
- modelId: z.string(),
1436
- }),
1437
- ),
1438
- /** Attributed usage only: system work (no user) counts in the totals, not here. */
1439
- byUser: z.array(
1440
- OrgAiUsageTotalsSchema.extend({
1441
- userId: z.string(),
1442
- email: z.string().nullable(),
1443
- }),
1444
- ),
1445
- chatRuns: z.object({
1446
- executions: z.number().int().min(0),
1447
- avgDurationMs: z.number(),
1448
- failureRate: z.number(),
1449
- byProfile: z.array(OrgChatRunProfileSchema),
1450
- }),
1451
- });
1452
-
1453
- export type OrgUsageResponse = z.infer<typeof OrgUsageResponseSchema>;
1454
-
1455
- // ---------------------------------------------------------------------------
1456
- // OrgUnit canonical model (ADR-BE-120)
1457
- // ---------------------------------------------------------------------------
1458
-
1459
- export const OrgUnitClassificationSchema = z.enum([
1460
- "execution_unit",
1461
- "org_container",
1462
- "leadership",
1463
- "custom",
1464
- ]);
1465
-
1466
- export const OrgUnitSyncModeSchema = z.enum([
1467
- "manual_only",
1468
- "synced_readonly",
1469
- "synced_with_overrides",
1470
- ]);
1471
-
1472
- export const OrgUnitVisibilitySchema = z.enum([
1473
- "public",
1474
- "private",
1475
- "restricted",
1476
- ]);
1477
-
1478
- export const OrgUnitRelationshipTypeSchema = z.enum([
1479
- "collaborates_with",
1480
- "reports_to",
1481
- "depends_on",
1482
- "custom",
1483
- ]);
1484
-
1485
- export const OrgUnitRelationshipRoleSchema = z.enum([
1486
- "owner",
1487
- "participant",
1488
- "supporting",
1489
- ]);
1490
-
1491
- /**
1492
- * Positional slot a member holds in the org-unit tree. ORTHOGONAL to the
1493
- * `OrgChartRole` policy vocabulary (this is an INPUT to authority resolution,
1494
- * not the derived policy role). Level is encoded in the value (l1…l20, one
1495
- * per depth up to `ORG_UNIT_ABSOLUTE_MAX_DEPTH`).
1496
- */
1497
- export const UnitMembershipRoleSchema = z.enum([
1498
- "member",
1499
- ...UNIT_OWNER_MEMBERSHIP_ROLES,
1500
- ]);
1501
-
1502
- export type UnitMembershipRole = z.infer<typeof UnitMembershipRoleSchema>;
1503
-
1504
- export const OrgUnitMembershipStatusSchema = z.enum([
1505
- "active",
1506
- "pending",
1507
- "removed",
1508
- ]);
1509
-
1510
- export const OrgUnitMembershipSourceSchema = z.enum([
1511
- "manual",
1512
- "google_groups",
1513
- "scim",
1514
- "hris",
1515
- ]);
1516
-
1517
- /**
1518
- * Hiring lifecycle of an open role. See `OpenRoleStatus` in `./org-units` for
1519
- * the transition rules. Only `open` and `hiring` are "active" (render a card
1520
- * and count toward `openRoleCount`); `filled`/`closed` are terminal.
1521
- */
1522
- export const OpenRoleStatusSchema = z.enum([
1523
- "open",
1524
- "hiring",
1525
- "filled",
1526
- "closed",
1527
- ]);
1528
-
1529
- export const OrgUnitErrorCodeSchema = z.enum([
1530
- "CYCLE_BLOCKED",
1531
- "DEPTH_EXCEEDED",
1532
- "TARGET_ARCHIVED",
1533
- "ROOT_HAS_NO_PARENT",
1534
- "PARENT_ARCHIVED",
1535
- "SLUG_TAKEN",
1536
- "CROSS_ORG_REPARENT",
1537
- "ORDERKEY_CONFLICT",
1538
- "SIBLING_NOT_FOUND",
1539
- "SIBLINGS_DIFFERENT_PARENT",
1540
- ]);
1541
-
1542
- export const OrgUnitSchema = z.object({
1543
- id: z.string().uuid(),
1544
- orgId: z.string().uuid(),
1545
- parentId: z.string().uuid().nullable(),
1546
- slug: z.string().min(1),
1547
- name: z.string().min(1),
1548
- description: z.string().nullable(),
1549
- typeTag: z.string().min(1),
1550
- classification: OrgUnitClassificationSchema.nullable(),
1551
- orderKey: z.string().min(1),
1552
- path: z.string().min(1),
1553
- syncMode: OrgUnitSyncModeSchema,
1554
- visibilityDefault: OrgUnitVisibilitySchema.nullable(),
1555
- metadata: z.record(z.string(), z.unknown()),
1556
- archivedAt: z.string().nullable(),
1557
- createdAt: z.string(),
1558
- updatedAt: z.string(),
1559
- });
1560
-
1561
- /**
1562
- * Active users home directly at this unit who have no level-(depth+1) home
1563
- * assignment within its subtree. The settings UI surfaces this as a
1564
- * synthetic "[child level label] assignment missing" row above the
1565
- * children list at every level. `null` for leaf-level units (no children
1566
- * configured below them) where the concept is meaningless.
1567
- */
1568
- export const MissingAtNextLevelSchema = z.object({
1569
- count: z.number().int().min(0),
1570
- userIds: z.array(z.string().uuid()),
1571
- });
1572
-
1573
- export const OrgUnitTreeNodeSchema = OrgUnitSchema.extend({
1574
- depth: z.number().int().min(1).max(ORG_UNIT_ABSOLUTE_MAX_DEPTH),
1575
- hasChildren: z.boolean(),
1576
- /**
1577
- * WHAT it counts is the `.describe()` below — the single field-level
1578
- * description, which reaches `openapi/backend.yaml` and the generated API
1579
- * types. This comment carries only WHY, so the two cannot drift into
1580
- * disagreeing.
1581
- *
1582
- * Why accounts: it is the right population for anything that needs an account
1583
- * to act on — the share dialog's fanout, the unit-scoped members table. It is
1584
- * backed by `home_assignments`, whose `user_id` is NOT NULL, so an accountless
1585
- * person structurally cannot appear in it. Deliberately NOT redefined and NOT
1586
- * deprecated; {@link placedPeopleCount} answers the headcount question, and the
1587
- * two legitimately disagree (ADR-CONTRACTS-150, ADR-BE-657).
1588
- */
1589
- memberCount: z
1590
- .number()
1591
- .int()
1592
- .min(0)
1593
- .describe(
1594
- "Accounts with a home assignment, rolled up over this unit's subtree. Legitimately 0 for a fully staffed department in an HRIS-imported org, where almost nobody has signed in — use placedPeopleCount for headcount.",
1595
- ),
1596
- /** Drives the "(N + M open)" count split in the people surfaces. */
1597
- openRoleCount: z
1598
- .number()
1599
- .int()
1600
- .min(0)
1601
- .describe(
1602
- "Active open roles (`open` + `hiring`) rolled up over this unit's subtree. Vacant seats only — an HRIS-imported seat is `lifecycle_owner='import:hris'` and is counted by neither this nor memberCount.",
1603
- ),
1604
- /**
1605
- * Predicate: `placement_role='canonical'`, `status <> 'ended'`, keyed by
1606
- * `positions.unit_id`. Placement and home are independent graphs
1607
- * (ADR-CTRL-159), so a person seated here may be homed elsewhere and vice
1608
- * versa. The `positions.status` non-filter stated in the `.describe()` below
1609
- * is deliberate and load-bearing — the backend pins it with a test whose name
1610
- * says so (ADR-BE-657).
1611
- */
1612
- placedPeopleCount: z
1613
- .number()
1614
- .int()
1615
- .min(0)
1616
- .describe(
1617
- "Distinct people holding a live canonical seat in this unit's subtree, whether or not they have an account. Deliberately does NOT filter positions.status: an occupied `closed` seat still draws a card on the org chart, so a filter here would disagree with it.",
1618
- ),
1619
- missingAtNextLevel: MissingAtNextLevelSchema.nullable(),
1620
- });
1621
-
1622
- export const OrgUnitMembershipSchema = z.object({
1623
- id: z.string().uuid(),
1624
- orgId: z.string().uuid(),
1625
- unitId: z.string().uuid(),
1626
- userId: z.string().uuid(),
1627
- membershipRole: UnitMembershipRoleSchema,
1628
- status: OrgUnitMembershipStatusSchema,
1629
- source: OrgUnitMembershipSourceSchema,
1630
- sourceRef: z.string().nullable(),
1631
- createdAt: z.string(),
1632
- updatedAt: z.string(),
1633
- });
1634
-
1635
- /**
1636
- * An open role — a persisted placeholder for the person who will sit in a unit.
1637
- * Carries unit membership (a seat) but never authority. `title` and
1638
- * `targetStartDate` are optional planning attributes. On `fill`, `filledByUserId`
1639
- * points at the seated user and a real `org_unit_membership` is created; the row
1640
- * is kept as staffing history. `targetStartDate` is an ISO date (YYYY-MM-DD).
1641
- */
1642
- export const OpenRoleSchema = z.object({
1643
- id: z.string().uuid(),
1644
- orgId: z.string().uuid(),
1645
- unitId: z.string().uuid(),
1646
- title: z.string().nullable(),
1647
- targetStartDate: z.string().nullable(),
1648
- status: OpenRoleStatusSchema,
1649
- filledByUserId: z.string().uuid().nullable(),
1650
- // The open role's own reporting edge (ADR-BE-291): defaults to its creator's
1651
- // seat, editable/clearable. Drives chart placement now that the unit-head
1652
- // anchor is retired (ADR-BE-292). Null → unmanaged (Unassigned bucket).
1653
- //
1654
- // The manager POSITION, not a user (ADR-CONTRACTS-131). The retired
1655
- // user-keyed flattening was a lie in two directions: an open-role manager
1656
- // already surfaced as null because it had no user to collapse to, and
1657
- // post-HRIS every open role reporting to an imported (accountless) manager
1658
- // would read null too.
1659
- reportsToPositionId: z.string().uuid().nullable(),
1660
- createdAt: z.string(),
1661
- updatedAt: z.string(),
1662
- });
1663
-
1664
- // ---------------------------------------------------------------------------
1665
- // Authority & Delegation vocabulary
1666
- //
1667
- // The API expresses authority as orthogonal axes (mechanism × locality ×
1668
- // mutability) instead of a single overloaded `source` enum, so future
1669
- // combinations like inherited-delegated or hris-delegated compose by
1670
- // construction rather than forcing each consumer to add a new switch case.
1671
- // The engine's internal AuthoritySource enum stays narrower than the wire
1672
- // shape — it's debug/audit vocabulary, not API vocabulary.
1673
- // ---------------------------------------------------------------------------
1674
-
1675
- /**
1676
- * Engine-internal authority source. Used by the projection storage layer for
1677
- * debugging and drift records; not part of the public API response shape.
1678
- * `manual_membership` was removed when the legacy `l{N}_unit_owner` track was
1679
- * canonicalized onto delegations.
1680
- */
1681
- export const AuthoritySourceSchema = z.enum([
1682
- "structural",
1683
- "delegated",
1684
- "org_admin",
1685
- ]);
1686
- export type AuthoritySource = z.infer<typeof AuthoritySourceSchema>;
1687
-
1688
- export const AuthorityOriginSchema = z.enum([
1689
- "topology",
1690
- "delegation",
1691
- "administrative_override",
1692
- ]);
1693
- export type AuthorityOrigin = z.infer<typeof AuthorityOriginSchema>;
1694
-
1695
- export const AuthorityConfidenceSchema = z.enum(["authoritative", "inferred"]);
1696
- export type AuthorityConfidence = z.infer<typeof AuthorityConfidenceSchema>;
1697
-
1698
- /** How the authority arose. */
1699
- export const AuthorityMechanismSchema = z.enum([
1700
- "structural",
1701
- "delegated",
1702
- "rbac",
1703
- ]);
1704
- export type AuthorityMechanism = z.infer<typeof AuthorityMechanismSchema>;
1705
-
1706
- /** Whether the grant lives on the target unit or is inherited from an ancestor. */
1707
- export const AuthorityLocalitySchema = z.enum(["local", "inherited"]);
1708
- export type AuthorityLocality = z.infer<typeof AuthorityLocalitySchema>;
1709
-
1710
- // AuthorityMutabilitySchema is defined above (hoisted next to
1711
- // WorkspaceMemberUnitDesignationEntrySchema, which also carries it).
1712
-
1713
- export const OwnerAuthoritySchema = z.object({
1714
- mechanism: AuthorityMechanismSchema,
1715
- locality: AuthorityLocalitySchema,
1716
- mutability: AuthorityMutabilitySchema,
1717
- derivedFromUnitId: z.string().uuid(),
1718
- derivedFromUnitName: z.string().min(1).optional(),
1719
- /**
1720
- * True when this authority derives from the org ROOT unit — i.e. the holder
1721
- * is a CEO (a unit owner of the org root, observed locally on the root
1722
- * or as inherited authority on a descendant). Lets consumers classify CEO
1723
- * without a separate org-tree lookup, the same way `mechanism === 'rbac'`
1724
- * identifies an org admin. Omitted (falsy) for non-root derivations.
1725
- */
1726
- derivedFromRoot: z.boolean().optional(),
1727
- derivedFromUserId: z.string().uuid().nullable().optional(),
1728
- /**
1729
- * Human-readable reason this authority applies, built at serialize time from
1730
- * the strategy's derivation metadata. Foundation for trust UI, debugging,
1731
- * AI explainability, governance reports, and audits.
1732
- */
1733
- explanation: z.string(),
1734
- /**
1735
- * Strategy-specific structured metadata, preserved verbatim for tooling that
1736
- * needs more than the explanation string (drift detection, audits).
1737
- */
1738
- derivationMetadata: z.record(z.string(), z.unknown()).optional(),
1739
- /**
1740
- * `org_unit_delegations.id` when `mechanism === 'delegated'`. Surfaced
1741
- * separately so the UI doesn't need to inspect derivationMetadata to render
1742
- * the revoke action.
1743
- */
1744
- delegationId: z.string().uuid().optional(),
1745
- /**
1746
- * Delegation expiry as an ISO-8601 timestamp when `mechanism === 'delegated'`
1747
- * and the delegation is time-boxed; `null` for a permanent delegation; omitted
1748
- * for non-delegated mechanisms. Expired delegations never reach this surface —
1749
- * the projection read filters `expires_at > now()` — so a present value is
1750
- * always in the future.
1751
- */
1752
- expiresAt: IsoDateTime.nullable().optional(),
1753
- });
1754
- export type OwnerAuthority = z.infer<typeof OwnerAuthoritySchema>;
1755
-
1756
- export const OrgUnitOwnerSchema = z.object({
1757
- userId: z.string().uuid(),
1758
- fullName: z.string().min(1),
1759
- jobTitle: z.string().nullable(),
1760
- avatarUrl: z.string().nullable(),
1761
- /** All authority grants this user holds for the unit. At least one entry. */
1762
- authorities: z.array(OwnerAuthoritySchema).min(1),
1763
- });
1764
- export type OrgUnitOwner = z.infer<typeof OrgUnitOwnerSchema>;
1765
-
1766
- /**
1767
- * A lead the structure review chose who is NOT YET an owner, because they have
1768
- * no account to grant to. Mirrors one `staged` row of the backend's lead → owner
1769
- * bridge ledger (ADR-BE-734): the grant is minted the moment the person links an
1770
- * account, attributed to the reviewer who decided it. Person-keyed on purpose —
1771
- * an owner is user-keyed and this person has no user.
1772
- *
1773
- * DISPLAY ONLY. Never an authorization input: a pending owner holds nothing
1774
- * until the grant exists, and consumers must not derive a capability from it.
1775
- */
1776
- export const PendingUnitOwnerSchema = z.object({
1777
- personId: z.string().uuid(),
1778
- fullName: z.string().min(1),
1779
- jobTitle: z.string().nullable(),
1780
- /** When the reviewer decided it — the answer's time, not the row's. */
1781
- decidedAt: IsoDateTime,
1782
- /** Why it is pending. One reason today; a closed enum so a second is a decision. */
1783
- reason: z.enum(["no_account"]),
1784
- });
1785
- export type PendingUnitOwner = z.infer<typeof PendingUnitOwnerSchema>;
1786
-
1787
- /**
1788
- * "No lead for now" (ADR-BE-733): the latest `unit_head` review answer for the
1789
- * unit was an explicit clear, and no later answer superseded it. Distinct from
1790
- * "never had one" — an absence a person recorded, not a default. Display only.
1791
- */
1792
- export const UnitLeadClearedSchema = z.object({
1793
- decidedAt: IsoDateTime,
1794
- });
1795
- export type UnitLeadCleared = z.infer<typeof UnitLeadClearedSchema>;
1796
-
1797
- /**
1798
- * Contract-level invariant: each owner list contains each userId at most once.
1799
- * Duplicate userIds are a serializer bug, not a presentation concern, so we
1800
- * fail Zod parse at the API boundary on both server and client.
1801
- */
1802
- const uniqueByUserId = (arr: ReadonlyArray<{ userId: string }>): boolean =>
1803
- new Set(arr.map((o) => o.userId)).size === arr.length;
1804
- const uniqueByPersonId = (arr: ReadonlyArray<{ personId: string }>): boolean =>
1805
- new Set(arr.map((o) => o.personId)).size === arr.length;
1806
-
1807
- export const OrgUnitOwnersResponseSchema = z.object({
1808
- owners: z
1809
- .array(OrgUnitOwnerSchema)
1810
- .refine(uniqueByUserId, { message: "duplicate userId in owners" }),
1811
- inherited: z
1812
- .array(OrgUnitOwnerSchema)
1813
- .refine(uniqueByUserId, { message: "duplicate userId in inherited" })
1814
- .optional(),
1815
- /**
1816
- * Leads chosen in the structure review who become owners once they have an
1817
- * account. Always present (`[]` when none) — the server knows, so the client
1818
- * never has to guess between "none" and "not asked".
1819
- */
1820
- pending: z
1821
- .array(PendingUnitOwnerSchema)
1822
- .refine(uniqueByPersonId, { message: "duplicate personId in pending" }),
1823
- /** The recorded "no lead for now", or null when no such answer stands. */
1824
- leadCleared: UnitLeadClearedSchema.nullable(),
1825
- });
1826
- export type OrgUnitOwnersResponse = z.infer<typeof OrgUnitOwnersResponseSchema>;
1827
-
1828
- export const DelegationSchema = z.object({
1829
- id: z.string().uuid(),
1830
- orgId: z.string().uuid(),
1831
- unitId: z.string().uuid(),
1832
- userId: z.string().uuid(),
1833
- grantedBy: z.string().uuid().nullable(),
1834
- grantedAt: z.string(),
1835
- revokedAt: z.string().nullable(),
1836
- status: z.enum(["active", "revoked"]),
1837
- note: z.string().nullable(),
1838
- /**
1839
- * ISO-8601 timestamp at which the delegation stops conferring authority, or
1840
- * `null` for a permanent delegation. Enforcement is read-time (the projection
1841
- * read filters `expires_at > now()`); the row itself stays `status='active'`
1842
- * until explicitly revoked.
1843
- */
1844
- expiresAt: z.string().nullable(),
1845
- });
1846
- export type Delegation = z.infer<typeof DelegationSchema>;
1847
-
1848
- export const CreateDelegationRequestSchema = z.object({
1849
- userId: z.string().uuid(),
1850
- note: z.string().max(2000).optional(),
1851
- /**
1852
- * Optional ISO-8601 expiry. Omitted or `null` ⇒ a permanent delegation. The
1853
- * UI captures a date and sends end-of-day in the user's timezone.
1854
- */
1855
- expiresAt: IsoDateTime.nullable().optional(),
1856
- /**
1857
- * When true, the granted user is emailed a notification (carrying `note` as
1858
- * the message). Not persisted — it only gates the side-effect email. Omitted
1859
- * is treated as false; left without a default so the inferred request type
1860
- * keeps `notify` optional rather than forcing every caller to pass it.
1861
- */
1862
- notify: z.boolean().optional(),
1863
- });
1864
- export type CreateDelegationRequest = z.infer<
1865
- typeof CreateDelegationRequestSchema
1866
- >;
1867
-
1868
- /**
1869
- * Change a delegation's expiry without revoking it: extend, shorten, or clear
1870
- * (set `expiresAt: null` to make it permanent). Used by the inline expiry edit
1871
- * on an existing delegate row.
1872
- */
1873
- export const UpdateDelegationRequestSchema = z.object({
1874
- expiresAt: IsoDateTime.nullable(),
1875
- });
1876
- export type UpdateDelegationRequest = z.infer<
1877
- typeof UpdateDelegationRequestSchema
1878
- >;
1879
-
1880
- export const DelegationListResponseSchema = z.object({
1881
- delegations: z.array(DelegationSchema),
1882
- });
1883
- export type DelegationListResponse = z.infer<
1884
- typeof DelegationListResponseSchema
1885
- >;
1886
-
1887
- export const OrgUnitRelationshipSchema = z.object({
1888
- id: z.string().uuid(),
1889
- orgId: z.string().uuid(),
1890
- fromUnit: z.string().uuid(),
1891
- toUnit: z.string().uuid(),
1892
- type: OrgUnitRelationshipTypeSchema,
1893
- role: OrgUnitRelationshipRoleSchema,
1894
- contextKey: z.string().nullable(),
1895
- metadata: z.record(z.string(), z.unknown()),
1896
- createdAt: z.string(),
1897
- });
1898
-
1899
- export const OrgLevelIconSchema = z.enum([
1900
- "city",
1901
- "buildings",
1902
- "users-four",
1903
- "users-three",
1904
- "users",
1905
- ]);
1906
-
1907
- export const OrgLevelConfigSchema = z.object({
1908
- orgId: z.string().uuid(),
1909
- depth: z.number().int().min(1).max(ORG_UNIT_ABSOLUTE_MAX_DEPTH),
1910
- label: z.string().min(1).nullable(),
1911
- labelPlural: z.string().min(1).nullable(),
1912
- icon: OrgLevelIconSchema.nullable(),
1913
- createdAt: z.string(),
1914
- updatedAt: z.string(),
1915
- });
1916
-
1917
- // --- Response schemas (request schemas live in backend per ADR-CONT-044) ---
1918
-
1919
- export const OrgUnitResponseSchema = z.object({ unit: OrgUnitSchema });
1920
-
1921
- export const OrgUnitTreeResponseSchema = z.object({
1922
- nodes: z.array(OrgUnitTreeNodeSchema),
1923
- levelConfig: z.array(OrgLevelConfigSchema),
1924
- });
1925
-
1926
- export const OrgUnitChildrenResponseSchema = z.object({
1927
- parentId: z.string().uuid(),
1928
- children: z.array(OrgUnitTreeNodeSchema),
1929
- });
1930
-
1931
- export const OrgUnitAncestorsResponseSchema = z.object({
1932
- unitId: z.string().uuid(),
1933
- ancestors: z.array(OrgUnitSchema),
1934
- });
1935
-
1936
- export const OrgUnitDescendantsResponseSchema = z.object({
1937
- rootId: z.string().uuid(),
1938
- descendants: z.array(OrgUnitSchema),
1939
- });
1940
-
1941
- export const OrgUnitRelationshipsResponseSchema = z.object({
1942
- unitId: z.string().uuid(),
1943
- incoming: z.array(OrgUnitRelationshipSchema),
1944
- outgoing: z.array(OrgUnitRelationshipSchema),
1945
- });
1946
-
1947
- export const OrgLevelConfigResponseSchema = z.object({
1948
- entries: z.array(OrgLevelConfigSchema),
1949
- });
1950
-
1951
- export const OrgUnitMembershipResponseSchema = z.object({
1952
- membership: OrgUnitMembershipSchema,
1953
- });
1954
-
1955
- export const OrgUnitMembershipListResponseSchema = z.object({
1956
- unitId: z.string().uuid(),
1957
- memberships: z.array(OrgUnitMembershipSchema),
1958
- });
1959
-
1960
- // ---------------------------------------------------------------------------
1961
- // Open roles (PRD open-roles)
1962
- // Request schemas are colocated here (a narrow deviation from ADR-CONT-044, as
1963
- // with UpdateOrgUnitRequest) so the app's add-role form and the backend
1964
- // validate against one published shape.
1965
- // ---------------------------------------------------------------------------
1966
-
1967
- export const OpenRoleResponseSchema = z.object({ openRole: OpenRoleSchema });
1968
-
1969
- export const OpenRoleListResponseSchema = z.object({
1970
- unitId: z.string().uuid(),
1971
- openRoles: z.array(OpenRoleSchema),
1972
- });
1973
-
1974
- /** ISO calendar date (YYYY-MM-DD) — version-agnostic vs `z.string().date()`. */
1975
- const IsoDateSchema = z.string().regex(/^\d{4}-\d{2}-\d{2}$/, {
1976
- message: "must be an ISO date (YYYY-MM-DD)",
1977
- });
1978
-
1979
- export const CreateOpenRoleRequestSchema = z.object({
1980
- title: z.string().min(1).max(255).nullable().optional(),
1981
- targetStartDate: IsoDateSchema.nullable().optional(),
1982
- });
1983
-
1984
- /** Only the non-terminal client transitions are accepted; `fill` is its own
1985
- * endpoint and `open` is the seeded initial state, never set by the client. */
1986
- export const UpdateOpenRoleStatusRequestSchema = z.object({
1987
- status: z.enum(["hiring", "closed"]),
1988
- });
1989
-
1990
- /**
1991
- * Fill an open role by seating an existing org member (`userId`) or inviting a
1992
- * new person by email. Exactly one branch — a discriminated union keyed on `mode`.
1993
- */
1994
- export const FillOpenRoleRequestSchema = z.discriminatedUnion("mode", [
1995
- z.object({ mode: z.literal("existing"), userId: z.string().uuid() }),
1996
- z.object({
1997
- mode: z.literal("invite"),
1998
- email: z.string().email(),
1999
- role: z.enum(["admin", "member"]),
2000
- }),
2001
- ]);
2002
-
2003
- /**
2004
- * Effective manager of an org unit — a user whose membership (direct or
2005
- * inherited from an ancestor) grants the `orgUnit.{id}.manage` capability.
2006
- *
2007
- * Direct grants have `inheritedFromUnitId === unitId`. For inherited grants,
2008
- * `inheritedFromUnitId` identifies the ancestor whose membership propagates
2009
- * authority down the subtree (ltree `path @>` containment).
2010
- */
2011
- export const OrgUnitPermissionsEntrySchema = z.object({
2012
- userId: z.string().uuid(),
2013
- membershipRole: UnitMembershipRoleSchema,
2014
- inheritedFromUnitId: z.string().uuid(),
2015
- inheritedFromUnitName: z.string().min(1),
2016
- });
2017
-
2018
- export const ListOrgUnitPermissionsResponseSchema = z.object({
2019
- unitId: z.string().uuid(),
2020
- entries: z.array(OrgUnitPermissionsEntrySchema),
2021
- });
2022
-
2023
- // ---------------------------------------------------------------------------
2024
- // PATCH /api/org-units/:unitId (PRD-00526)
2025
- // Request schema colocated with the response schema so the app can import
2026
- // the same shape backend validates against. This is a narrow deviation from
2027
- // ADR-CONT-044 (request schemas normally live in backend) because the
2028
- // partial-update + at-least-one-field refinement is the published promise
2029
- // consumers must satisfy.
2030
- // ---------------------------------------------------------------------------
2031
-
2032
- export const UpdateOrgUnitRequestSchema = z
2033
- .object({
2034
- name: z.string().min(1).max(255).optional(),
2035
- description: z.string().max(2000).nullable().optional(),
2036
- })
2037
- .refine((v) => v.name !== undefined || v.description !== undefined, {
2038
- message: "at least one of name or description must be provided",
2039
- });
2040
-
2041
- export const UpdateOrgUnitResponseSchema = z.object({ unit: OrgUnitSchema });
2042
-
2043
- // --- Inferred types ---
2044
-
2045
- export type OrgUnit = z.infer<typeof OrgUnitSchema>;
2046
- export type OrgUnitTreeNode = z.infer<typeof OrgUnitTreeNodeSchema>;
2047
- export type OrgUnitMembership = z.infer<typeof OrgUnitMembershipSchema>;
2048
- export type OrgUnitRelationship = z.infer<typeof OrgUnitRelationshipSchema>;
2049
- export type OrgLevelConfig = z.infer<typeof OrgLevelConfigSchema>;
2050
- export type OrgLevelIcon = z.infer<typeof OrgLevelIconSchema>;
2051
- export type OrgUnitResponse = z.infer<typeof OrgUnitResponseSchema>;
2052
- export type OrgUnitTreeResponse = z.infer<typeof OrgUnitTreeResponseSchema>;
2053
- export type MissingAtNextLevel = z.infer<typeof MissingAtNextLevelSchema>;
2054
- export type OrgUnitChildrenResponse = z.infer<
2055
- typeof OrgUnitChildrenResponseSchema
2056
- >;
2057
- export type OrgUnitAncestorsResponse = z.infer<
2058
- typeof OrgUnitAncestorsResponseSchema
2059
- >;
2060
- export type OrgUnitDescendantsResponse = z.infer<
2061
- typeof OrgUnitDescendantsResponseSchema
2062
- >;
2063
- export type OrgUnitRelationshipsResponse = z.infer<
2064
- typeof OrgUnitRelationshipsResponseSchema
2065
- >;
2066
- export type OrgLevelConfigResponse = z.infer<
2067
- typeof OrgLevelConfigResponseSchema
2068
- >;
2069
- export type OrgUnitMembershipResponse = z.infer<
2070
- typeof OrgUnitMembershipResponseSchema
2071
- >;
2072
- export type OrgUnitMembershipListResponse = z.infer<
2073
- typeof OrgUnitMembershipListResponseSchema
2074
- >;
2075
- export type OpenRole = z.infer<typeof OpenRoleSchema>;
2076
- export type OpenRoleResponse = z.infer<typeof OpenRoleResponseSchema>;
2077
- export type OpenRoleListResponse = z.infer<typeof OpenRoleListResponseSchema>;
2078
- export type CreateOpenRoleRequest = z.infer<typeof CreateOpenRoleRequestSchema>;
2079
- export type UpdateOpenRoleStatusRequest = z.infer<
2080
- typeof UpdateOpenRoleStatusRequestSchema
2081
- >;
2082
- export type FillOpenRoleRequest = z.infer<typeof FillOpenRoleRequestSchema>;
2083
- export type OrgUnitPermissionsEntry = z.infer<
2084
- typeof OrgUnitPermissionsEntrySchema
2085
- >;
2086
- export type ListOrgUnitPermissionsResponse = z.infer<
2087
- typeof ListOrgUnitPermissionsResponseSchema
2088
- >;
2089
- export type UpdateOrgUnitRequest = z.infer<typeof UpdateOrgUnitRequestSchema>;
2090
- export type UpdateOrgUnitResponse = z.infer<typeof UpdateOrgUnitResponseSchema>;
2091
-
2092
452
  // =============================================================================
2093
453
  // Mutation Capabilities (GET /api/capabilities) — ADR-BE-241 / PRD-00707 F4
2094
454
  // =============================================================================