@company-semantics/contracts 56.0.0 → 57.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@company-semantics/contracts",
3
- "version": "56.0.0",
3
+ "version": "57.0.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 = '6eb28e0d82f9' as const;
3
- export const SPEC_HASH_FULL = '6eb28e0d82f9d1d88e4fc954ece5377c85ddd6da30b6171c4d2aa75f347c09ed' as const;
2
+ export const SPEC_HASH = 'ffeb6e367b38' as const;
3
+ export const SPEC_HASH_FULL = 'ffeb6e367b385ad32f7b46e4e74d276786d6cdf175352e57d0fc229dc8f506af' as const;
@@ -926,6 +926,40 @@ export interface paths {
926
926
  patch?: never;
927
927
  trace?: never;
928
928
  };
929
+ "/api/workspace/invites/batch": {
930
+ parameters: {
931
+ query?: never;
932
+ header?: never;
933
+ path?: never;
934
+ cookie?: never;
935
+ };
936
+ get?: never;
937
+ put?: never;
938
+ /** Invite many directory people at once */
939
+ post: operations["batchInvite"];
940
+ delete?: never;
941
+ options?: never;
942
+ head?: never;
943
+ patch?: never;
944
+ trace?: never;
945
+ };
946
+ "/api/workspace/invites/{id}/reissue": {
947
+ parameters: {
948
+ query?: never;
949
+ header?: never;
950
+ path?: never;
951
+ cookie?: never;
952
+ };
953
+ get?: never;
954
+ put?: never;
955
+ /** Reissue an invite whose email was never delivered */
956
+ post: operations["reissueInvite"];
957
+ delete?: never;
958
+ options?: never;
959
+ head?: never;
960
+ patch?: never;
961
+ trace?: never;
962
+ };
929
963
  "/api/workspace/invites/accept": {
930
964
  parameters: {
931
965
  query?: never;
@@ -960,6 +994,92 @@ export interface paths {
960
994
  patch?: never;
961
995
  trace?: never;
962
996
  };
997
+ "/api/workspace/invite-batches": {
998
+ parameters: {
999
+ query?: never;
1000
+ header?: never;
1001
+ path?: never;
1002
+ cookie?: never;
1003
+ };
1004
+ get?: never;
1005
+ put?: never;
1006
+ /** Draft a reviewable invite batch */
1007
+ post: operations["createInviteBatch"];
1008
+ delete?: never;
1009
+ options?: never;
1010
+ head?: never;
1011
+ patch?: never;
1012
+ trace?: never;
1013
+ };
1014
+ "/api/workspace/invite-batches/{id}": {
1015
+ parameters: {
1016
+ query?: never;
1017
+ header?: never;
1018
+ path?: never;
1019
+ cookie?: never;
1020
+ };
1021
+ /** Review an invite batch */
1022
+ get: operations["getInviteBatch"];
1023
+ put?: never;
1024
+ post?: never;
1025
+ delete?: never;
1026
+ options?: never;
1027
+ head?: never;
1028
+ /** Change an invite batch selection or role */
1029
+ patch: operations["updateInviteBatch"];
1030
+ trace?: never;
1031
+ };
1032
+ "/api/workspace/invite-batches/{id}/prepare": {
1033
+ parameters: {
1034
+ query?: never;
1035
+ header?: never;
1036
+ path?: never;
1037
+ cookie?: never;
1038
+ };
1039
+ get?: never;
1040
+ put?: never;
1041
+ /** Evaluate eligibility and make a batch reviewable */
1042
+ post: operations["prepareInviteBatch"];
1043
+ delete?: never;
1044
+ options?: never;
1045
+ head?: never;
1046
+ patch?: never;
1047
+ trace?: never;
1048
+ };
1049
+ "/api/workspace/invite-batches/{id}/send": {
1050
+ parameters: {
1051
+ query?: never;
1052
+ header?: never;
1053
+ path?: never;
1054
+ cookie?: never;
1055
+ };
1056
+ get?: never;
1057
+ put?: never;
1058
+ /** Send a reviewed invite batch */
1059
+ post: operations["sendInviteBatch"];
1060
+ delete?: never;
1061
+ options?: never;
1062
+ head?: never;
1063
+ patch?: never;
1064
+ trace?: never;
1065
+ };
1066
+ "/api/workspace/invite-batches/{id}/cancel": {
1067
+ parameters: {
1068
+ query?: never;
1069
+ header?: never;
1070
+ path?: never;
1071
+ cookie?: never;
1072
+ };
1073
+ get?: never;
1074
+ put?: never;
1075
+ /** Cancel the unsent remainder of an invite batch */
1076
+ post: operations["cancelInviteBatch"];
1077
+ delete?: never;
1078
+ options?: never;
1079
+ head?: never;
1080
+ patch?: never;
1081
+ trace?: never;
1082
+ };
963
1083
  "/api/workspace/invites/validate": {
964
1084
  parameters: {
965
1085
  query?: never;
@@ -977,6 +1097,23 @@ export interface paths {
977
1097
  patch?: never;
978
1098
  trace?: never;
979
1099
  };
1100
+ "/api/workspace/directory": {
1101
+ parameters: {
1102
+ query?: never;
1103
+ header?: never;
1104
+ path?: never;
1105
+ cookie?: never;
1106
+ };
1107
+ /** List the org people directory */
1108
+ get: operations["listOrgDirectory"];
1109
+ put?: never;
1110
+ post?: never;
1111
+ delete?: never;
1112
+ options?: never;
1113
+ head?: never;
1114
+ patch?: never;
1115
+ trace?: never;
1116
+ };
980
1117
  "/api/workspace/domains": {
981
1118
  parameters: {
982
1119
  query?: never;
@@ -4577,6 +4714,32 @@ export interface components {
4577
4714
  contributesTo: string[];
4578
4715
  };
4579
4716
  };
4717
+ BatchInviteResponse: {
4718
+ invited: string[];
4719
+ skipped: {
4720
+ /** Format: uuid */
4721
+ personId: string;
4722
+ /** @enum {string} */
4723
+ reason: "already-member" | "invite-pending" | "no-email" | "send-failed";
4724
+ }[];
4725
+ failed: {
4726
+ /** Format: uuid */
4727
+ personId: string;
4728
+ /** @enum {string} */
4729
+ reason: "already-member" | "invite-pending" | "no-email" | "send-failed";
4730
+ }[];
4731
+ };
4732
+ BatchInviteRequest: {
4733
+ personIds: string[];
4734
+ /** @enum {string} */
4735
+ role: "admin" | "member";
4736
+ };
4737
+ ReissueInviteResponse: {
4738
+ /** Format: uuid */
4739
+ inviteId: string;
4740
+ /** Format: uuid */
4741
+ previousInviteId: string;
4742
+ };
4580
4743
  InviteListResponse: {
4581
4744
  id: string;
4582
4745
  orgId: string;
@@ -4620,11 +4783,72 @@ export interface components {
4620
4783
  AcceptInviteRequest: {
4621
4784
  token: string;
4622
4785
  };
4786
+ InviteBatchResponse: {
4787
+ batch: {
4788
+ /** Format: uuid */
4789
+ id: string;
4790
+ /** @enum {string} */
4791
+ status: "draft" | "ready" | "sending" | "completed" | "cancelled";
4792
+ source: string;
4793
+ revision: number;
4794
+ counts: {
4795
+ candidate: number;
4796
+ eligible: number;
4797
+ excluded: number;
4798
+ sent: number;
4799
+ failed: number;
4800
+ skipped: number;
4801
+ };
4802
+ /** Format: uuid */
4803
+ createdByUserId: string;
4804
+ confirmedByUserId: string | null;
4805
+ confirmedAt: string | null;
4806
+ createdAt: string;
4807
+ updatedAt: string;
4808
+ };
4809
+ items: {
4810
+ /** Format: uuid */
4811
+ personId: string;
4812
+ displayName: string | null;
4813
+ emailSnapshot: string | null;
4814
+ /** @enum {string} */
4815
+ role: "admin" | "member";
4816
+ /** @enum {string} */
4817
+ status: "eligible" | "excluded" | "sent" | "failed" | "skipped";
4818
+ reason: ("already-member" | "invite-pending" | "no-email" | "invalid-email" | "removed-by-reviewer" | "address-changed" | "person-removed" | "send-failed" | "inactive-in-hris") | null;
4819
+ inviteId: string | null;
4820
+ emailOutsideVerifiedDomain: boolean;
4821
+ }[];
4822
+ verifiedDomains: string[];
4823
+ };
4824
+ CreateInviteBatchRequest: {
4825
+ personIds: string[];
4826
+ /** @enum {string} */
4827
+ role: "admin" | "member";
4828
+ };
4829
+ UpdateInviteBatchRequest: {
4830
+ deselectPersonIds?: string[];
4831
+ reselectPersonIds?: string[];
4832
+ /** @enum {string} */
4833
+ role?: "admin" | "member";
4834
+ };
4835
+ SendInviteBatchRequest: {
4836
+ expectedRevision: number;
4837
+ };
4623
4838
  InviteValidationResult: {
4624
4839
  valid: boolean;
4625
4840
  status: string;
4626
4841
  orgName?: string;
4627
4842
  };
4843
+ OrgDirectoryResponse: {
4844
+ people: {
4845
+ /** Format: uuid */
4846
+ personId: string;
4847
+ displayName: string;
4848
+ primaryEmail: string | null;
4849
+ userId: string | null;
4850
+ }[];
4851
+ };
4628
4852
  DomainListResponse: {
4629
4853
  id: string;
4630
4854
  orgId: string;
@@ -8230,6 +8454,52 @@ export interface operations {
8230
8454
  };
8231
8455
  };
8232
8456
  };
8457
+ batchInvite: {
8458
+ parameters: {
8459
+ query?: never;
8460
+ header?: never;
8461
+ path?: never;
8462
+ cookie?: never;
8463
+ };
8464
+ requestBody: {
8465
+ content: {
8466
+ "application/json": components["schemas"]["BatchInviteRequest"];
8467
+ };
8468
+ };
8469
+ responses: {
8470
+ /** @description Per-person outcome; partial success is a normal result */
8471
+ 200: {
8472
+ headers: {
8473
+ [name: string]: unknown;
8474
+ };
8475
+ content: {
8476
+ "application/json": components["schemas"]["BatchInviteResponse"];
8477
+ };
8478
+ };
8479
+ };
8480
+ };
8481
+ reissueInvite: {
8482
+ parameters: {
8483
+ query?: never;
8484
+ header?: never;
8485
+ path: {
8486
+ id: string;
8487
+ };
8488
+ cookie?: never;
8489
+ };
8490
+ requestBody?: never;
8491
+ responses: {
8492
+ /** @description Prior invite revoked and a replacement sent */
8493
+ 200: {
8494
+ headers: {
8495
+ [name: string]: unknown;
8496
+ };
8497
+ content: {
8498
+ "application/json": components["schemas"]["ReissueInviteResponse"];
8499
+ };
8500
+ };
8501
+ };
8502
+ };
8233
8503
  acceptInvite: {
8234
8504
  parameters: {
8235
8505
  query?: never;
@@ -8274,6 +8544,148 @@ export interface operations {
8274
8544
  };
8275
8545
  };
8276
8546
  };
8547
+ createInviteBatch: {
8548
+ parameters: {
8549
+ query?: never;
8550
+ header?: never;
8551
+ path?: never;
8552
+ cookie?: never;
8553
+ };
8554
+ requestBody: {
8555
+ content: {
8556
+ "application/json": components["schemas"]["CreateInviteBatchRequest"];
8557
+ };
8558
+ };
8559
+ responses: {
8560
+ /** @description Draft created; nothing has been sent */
8561
+ 200: {
8562
+ headers: {
8563
+ [name: string]: unknown;
8564
+ };
8565
+ content: {
8566
+ "application/json": components["schemas"]["InviteBatchResponse"];
8567
+ };
8568
+ };
8569
+ };
8570
+ };
8571
+ getInviteBatch: {
8572
+ parameters: {
8573
+ query?: never;
8574
+ header?: never;
8575
+ path: {
8576
+ id: string;
8577
+ };
8578
+ cookie?: never;
8579
+ };
8580
+ requestBody?: never;
8581
+ responses: {
8582
+ /** @description Recipients, exclusions with reasons, and the org verified domains */
8583
+ 200: {
8584
+ headers: {
8585
+ [name: string]: unknown;
8586
+ };
8587
+ content: {
8588
+ "application/json": components["schemas"]["InviteBatchResponse"];
8589
+ };
8590
+ };
8591
+ };
8592
+ };
8593
+ updateInviteBatch: {
8594
+ parameters: {
8595
+ query?: never;
8596
+ header?: never;
8597
+ path: {
8598
+ id: string;
8599
+ };
8600
+ cookie?: never;
8601
+ };
8602
+ requestBody: {
8603
+ content: {
8604
+ "application/json": components["schemas"]["UpdateInviteBatchRequest"];
8605
+ };
8606
+ };
8607
+ responses: {
8608
+ /** @description Applied; revision bumped, invalidating any open review */
8609
+ 200: {
8610
+ headers: {
8611
+ [name: string]: unknown;
8612
+ };
8613
+ content: {
8614
+ "application/json": components["schemas"]["InviteBatchResponse"];
8615
+ };
8616
+ };
8617
+ };
8618
+ };
8619
+ prepareInviteBatch: {
8620
+ parameters: {
8621
+ query?: never;
8622
+ header?: never;
8623
+ path: {
8624
+ id: string;
8625
+ };
8626
+ cookie?: never;
8627
+ };
8628
+ requestBody?: never;
8629
+ responses: {
8630
+ /** @description Eligibility evaluated; batch is ready to review */
8631
+ 200: {
8632
+ headers: {
8633
+ [name: string]: unknown;
8634
+ };
8635
+ content: {
8636
+ "application/json": components["schemas"]["InviteBatchResponse"];
8637
+ };
8638
+ };
8639
+ };
8640
+ };
8641
+ sendInviteBatch: {
8642
+ parameters: {
8643
+ query?: never;
8644
+ header?: never;
8645
+ path: {
8646
+ id: string;
8647
+ };
8648
+ cookie?: never;
8649
+ };
8650
+ requestBody: {
8651
+ content: {
8652
+ "application/json": components["schemas"]["SendInviteBatchRequest"];
8653
+ };
8654
+ };
8655
+ responses: {
8656
+ /** @description Per-recipient outcome; partial success is a normal result */
8657
+ 200: {
8658
+ headers: {
8659
+ [name: string]: unknown;
8660
+ };
8661
+ content: {
8662
+ "application/json": components["schemas"]["InviteBatchResponse"];
8663
+ };
8664
+ };
8665
+ };
8666
+ };
8667
+ cancelInviteBatch: {
8668
+ parameters: {
8669
+ query?: never;
8670
+ header?: never;
8671
+ path: {
8672
+ id: string;
8673
+ };
8674
+ cookie?: never;
8675
+ };
8676
+ requestBody?: never;
8677
+ responses: {
8678
+ /** @description Cancelled; invitations already sent are unaffected */
8679
+ 200: {
8680
+ headers: {
8681
+ [name: string]: unknown;
8682
+ };
8683
+ content: {
8684
+ "application/json": components["schemas"]["InviteBatchResponse"];
8685
+ };
8686
+ };
8687
+ };
8688
+ };
8277
8689
  validateInviteToken: {
8278
8690
  parameters: {
8279
8691
  query: {
@@ -8296,6 +8708,26 @@ export interface operations {
8296
8708
  };
8297
8709
  };
8298
8710
  };
8711
+ listOrgDirectory: {
8712
+ parameters: {
8713
+ query?: never;
8714
+ header?: never;
8715
+ path?: never;
8716
+ cookie?: never;
8717
+ };
8718
+ requestBody?: never;
8719
+ responses: {
8720
+ /** @description Every person in the org directory */
8721
+ 200: {
8722
+ headers: {
8723
+ [name: string]: unknown;
8724
+ };
8725
+ content: {
8726
+ "application/json": components["schemas"]["OrgDirectoryResponse"];
8727
+ };
8728
+ };
8729
+ };
8730
+ };
8299
8731
  listDomains: {
8300
8732
  parameters: {
8301
8733
  query?: never;
@@ -185,14 +185,22 @@ export const openApiRoutes = {
185
185
  '/api/workspace/auth-policy/test-sso': ['POST'],
186
186
  '/api/workspace/auth-policy/test-sso/{attemptId}': ['GET'],
187
187
  '/api/workspace/auth-policy/validate-oidc': ['POST'],
188
+ '/api/workspace/directory': ['GET'],
188
189
  '/api/workspace/domains': ['GET', 'POST'],
189
190
  '/api/workspace/domains/{id}': ['DELETE'],
190
191
  '/api/workspace/domains/{id}/verify': ['POST'],
191
192
  '/api/workspace/handle': ['PATCH'],
193
+ '/api/workspace/invite-batches': ['POST'],
194
+ '/api/workspace/invite-batches/{id}': ['GET', 'PATCH'],
195
+ '/api/workspace/invite-batches/{id}/cancel': ['POST'],
196
+ '/api/workspace/invite-batches/{id}/prepare': ['POST'],
197
+ '/api/workspace/invite-batches/{id}/send': ['POST'],
192
198
  '/api/workspace/invites': ['GET', 'POST'],
193
199
  '/api/workspace/invites/accept': ['POST'],
200
+ '/api/workspace/invites/batch': ['POST'],
194
201
  '/api/workspace/invites/validate': ['GET'],
195
202
  '/api/workspace/invites/{id}': ['DELETE'],
203
+ '/api/workspace/invites/{id}/reissue': ['POST'],
196
204
  '/api/workspace/members': ['GET'],
197
205
  '/api/workspace/members/{id}': ['DELETE', 'GET'],
198
206
  '/api/workspace/members/{id}/role': ['PATCH'],
package/src/index.ts CHANGED
@@ -620,6 +620,64 @@ export type {
620
620
  PositionReporting,
621
621
  } from "./org/index";
622
622
 
623
+ // People directory: every `persons` row in the org, account-bearing or not.
624
+ // Neither the member list (RBAC principals) nor the org chart (topology) — the
625
+ // set an HRIS sync populates, and the only surface carrying a directory email.
626
+ // Capability-gated, deliberately: the org-chart read is authenticated-only.
627
+ export {
628
+ OrgDirectoryPersonSchema,
629
+ OrgDirectoryResponseSchema,
630
+ } from "./org/index";
631
+ export type { OrgDirectoryPerson, OrgDirectoryResponse } from "./org/index";
632
+
633
+ // Bulk invite, keyed by personId so recipients are resolved server-side from the
634
+ // caller's own directory rather than supplied by the caller.
635
+ export {
636
+ BATCH_INVITE_PERSONS_MAX,
637
+ BATCH_INVITE_SKIP_REASONS,
638
+ BatchInviteSkipReasonSchema,
639
+ BatchInviteRequestSchema,
640
+ BatchInviteResponseSchema,
641
+ } from "./org/index";
642
+ export type {
643
+ BatchInviteSkipReason,
644
+ BatchInviteRequest,
645
+ BatchInviteResponse,
646
+ } from "./org/index";
647
+
648
+ // Invite batches: the persisted Prepare → Review → Send lifecycle that puts a
649
+ // review boundary in front of bulk invite. The batch holds INTENT, never
650
+ // credentials — there is no token field on a batch or an item, and there never
651
+ // may be. Supersedes BatchInvite* above, which is removed in 58.0.0.
652
+ export {
653
+ INVITE_BATCH_STATUSES,
654
+ INVITE_BATCH_ITEM_STATUSES,
655
+ INVITE_BATCH_ITEM_REASONS,
656
+ INVITE_BATCH_CHANGED,
657
+ InviteBatchStatusSchema,
658
+ InviteBatchItemStatusSchema,
659
+ InviteBatchItemReasonSchema,
660
+ InviteBatchItemSchema,
661
+ InviteBatchCountsSchema,
662
+ InviteBatchSchema,
663
+ InviteBatchResponseSchema,
664
+ CreateInviteBatchRequestSchema,
665
+ UpdateInviteBatchRequestSchema,
666
+ SendInviteBatchRequestSchema,
667
+ } from "./org/index";
668
+ export type {
669
+ InviteBatchStatus,
670
+ InviteBatchItemStatus,
671
+ InviteBatchItemReason,
672
+ InviteBatchItem,
673
+ InviteBatchCounts,
674
+ InviteBatch,
675
+ InviteBatchResponse,
676
+ CreateInviteBatchRequest,
677
+ UpdateInviteBatchRequest,
678
+ SendInviteBatchRequest,
679
+ } from "./org/index";
680
+
623
681
  // Source authority: per-field ownership — who may WRITE a given entity field.
624
682
  // Orthogonal to provenance (who reported). (ADR-CONT-083 / ADR-CTRL-182)
625
683
  export { SourceAuthoritySchema } from "./org/index";
package/src/org/README.md CHANGED
@@ -37,6 +37,14 @@ Shared type vocabulary for organization ownership, type classification, and tran
37
37
  - `AuthoritySource` _(type)_
38
38
  - `AuthoritySourceSchema` — Engine-internal authority source.
39
39
  - `AuthorizableView` _(type)_ — Type for views that can be checked against VIEWSCOPEMAP.
40
+ - `BATCH_INVITE_PERSONS_MAX` — Cap on one batch.
41
+ - `BATCH_INVITE_SKIP_REASONS` — Why one person in a batch did not receive an invite. - `already-member` — holds an account in this org…
42
+ - `BatchInviteRequest` _(type)_
43
+ - `BatchInviteRequestSchema`
44
+ - `BatchInviteResponse` _(type)_
45
+ - `BatchInviteResponseSchema`
46
+ - `BatchInviteSkipReason` _(type)_
47
+ - `BatchInviteSkipReasonSchema`
40
48
  - `COMPANY_MD_COLLAB_MAX_DOC_TEXT_BYTES` — Cap on the MATERIALIZED document text, in UTF-8 bytes of the expanded result.
41
49
  - `COMPANY_MD_COLLAB_MAX_PRESENCE_POSITION_CHARS` — Cap on ONE presence position blob (`anchor` or `head`), in base64 chars.
42
50
  - `COMPANY_MD_COLLAB_MAX_UPDATE_B64_CHARS` — Cap on ONE encoded update, measured on the base64 payload as it is sent.
@@ -101,6 +109,8 @@ Shared type vocabulary for organization ownership, type classification, and tran
101
109
  - `ConstraintViolationSchema` — One constraint the transformation engine evaluated and found violated.
102
110
  - `CreateDelegationRequest` _(type)_
103
111
  - `CreateDelegationRequestSchema`
112
+ - `CreateInviteBatchRequest` _(type)_
113
+ - `CreateInviteBatchRequestSchema`
104
114
  - `CreateInviteRequest` _(type)_ — Request payload for creating an organization invite.
105
115
  - `CreateInviteRequestPayload` _(type)_
106
116
  - `CreateInviteRequestSchema`
@@ -139,9 +149,27 @@ Shared type vocabulary for organization ownership, type classification, and tran
139
149
  - `HomeUnitRole` _(type)_
140
150
  - `HomeUnitRoleSchema` — Role the invitee takes in their home unit on acceptance.
141
151
  - `IDENTITY_TRUST_LEVEL_LABELS`
152
+ - `INVITE_BATCH_CHANGED` — The error code a stale `send` (or a mutation of an already-claimed batch) returns, as `error` on the standard…
153
+ - `INVITE_BATCH_ITEM_REASONS` — Why an item is not `sent`.
154
+ - `INVITE_BATCH_ITEM_STATUSES` — Per-recipient outcome within a batch. - `eligible` — will be invited when the batch is sent. - `excluded` —…
155
+ - `INVITE_BATCH_STATUSES` — Batch lifecycle. - `draft` — created, membership not yet evaluated.
142
156
  - `IdentityTrustLevel` _(type)_ — Identity Trust Level — org's identity posture
143
157
  - `IntegrationRequest` _(type)_ — Advisory integration request created by a member.
144
158
  - `IntegrationRequestStatus` _(type)_ — Status of an integration request from a member.
159
+ - `InviteBatch` _(type)_
160
+ - `InviteBatchCounts` _(type)_
161
+ - `InviteBatchCountsSchema` — Denormalized counters, maintained by the batch's own writers in the same transaction as the items they…
162
+ - `InviteBatchItem` _(type)_
163
+ - `InviteBatchItemReason` _(type)_
164
+ - `InviteBatchItemReasonSchema`
165
+ - `InviteBatchItemSchema`
166
+ - `InviteBatchItemStatus` _(type)_
167
+ - `InviteBatchItemStatusSchema`
168
+ - `InviteBatchResponse` _(type)_
169
+ - `InviteBatchResponseSchema`
170
+ - `InviteBatchSchema`
171
+ - `InviteBatchStatus` _(type)_
172
+ - `InviteBatchStatusSchema`
145
173
  - `InviteListResponse` _(type)_
146
174
  - `InviteListResponseSchema`
147
175
  - `InviteResponse` _(type)_
@@ -178,6 +206,10 @@ Shared type vocabulary for organization ownership, type classification, and tran
178
206
  - `OrgBudgetConfigSchema`
179
207
  - `OrgDeletionStatus` _(type)_
180
208
  - `OrgDeletionStatusSchema`
209
+ - `OrgDirectoryPerson` _(type)_
210
+ - `OrgDirectoryPersonSchema`
211
+ - `OrgDirectoryResponse` _(type)_
212
+ - `OrgDirectoryResponseSchema`
181
213
  - `OrgDivergence` _(type)_
182
214
  - `OrgDivergenceResolution` _(type)_
183
215
  - `OrgDivergenceResolutionSchema` — What resolving a divergence means today.
@@ -299,6 +331,8 @@ Shared type vocabulary for organization ownership, type classification, and tran
299
331
  - `ScopeCheckResponseSchema`
300
332
  - `SelectionRationale` _(type)_
301
333
  - `SelectionRationaleSchema`
334
+ - `SendInviteBatchRequest` _(type)_
335
+ - `SendInviteBatchRequestSchema` — `expectedRevision` is REQUIRED and has no default.
302
336
  - `SetActiveOrgResponse` _(type)_
303
337
  - `SetActiveOrgResponseSchema`
304
338
  - `SetSeatManagerResponse` _(type)_
@@ -348,6 +382,8 @@ Shared type vocabulary for organization ownership, type classification, and tran
348
382
  - `UpdateAuthPolicyRequest` _(type)_ — Request payload for updating organization auth policy.
349
383
  - `UpdateDelegationRequest` _(type)_
350
384
  - `UpdateDelegationRequestSchema` — Change a delegation's expiry without revoking it: extend, shorten, or clear (set `expiresAt: null` to make it…
385
+ - `UpdateInviteBatchRequest` _(type)_
386
+ - `UpdateInviteBatchRequestSchema` — Change what a `draft` or `ready` batch will do.
351
387
  - `UpdateOpenRoleStatusRequest` _(type)_
352
388
  - `UpdateOpenRoleStatusRequestSchema` — Only the non-terminal client transitions are accepted; `fill` is its own endpoint and `open` is the seeded…
353
389
  - `UpdateOrgUnitRequest` _(type)_
package/src/org/index.ts CHANGED
@@ -354,6 +354,29 @@ export {
354
354
  CreateInviteRequestSchema,
355
355
  InviteResponseSchema,
356
356
  InviteListResponseSchema,
357
+ // People directory + bulk invite (personId-keyed, capability-gated)
358
+ OrgDirectoryPersonSchema,
359
+ OrgDirectoryResponseSchema,
360
+ BATCH_INVITE_PERSONS_MAX,
361
+ BATCH_INVITE_SKIP_REASONS,
362
+ BatchInviteSkipReasonSchema,
363
+ BatchInviteRequestSchema,
364
+ BatchInviteResponseSchema,
365
+ // Invite batches — the persisted Prepare → Review → Send lifecycle
366
+ INVITE_BATCH_STATUSES,
367
+ INVITE_BATCH_ITEM_STATUSES,
368
+ INVITE_BATCH_ITEM_REASONS,
369
+ INVITE_BATCH_CHANGED,
370
+ InviteBatchStatusSchema,
371
+ InviteBatchItemStatusSchema,
372
+ InviteBatchItemReasonSchema,
373
+ InviteBatchItemSchema,
374
+ InviteBatchCountsSchema,
375
+ InviteBatchSchema,
376
+ InviteBatchResponseSchema,
377
+ CreateInviteBatchRequestSchema,
378
+ UpdateInviteBatchRequestSchema,
379
+ SendInviteBatchRequestSchema,
357
380
  DomainResponseSchema,
358
381
  DomainListResponseSchema,
359
382
  OrgBillingResponseSchema,
@@ -366,6 +389,21 @@ export type {
366
389
  CreateInviteRequestPayload,
367
390
  InviteResponse,
368
391
  InviteListResponse,
392
+ OrgDirectoryPerson,
393
+ OrgDirectoryResponse,
394
+ BatchInviteSkipReason,
395
+ BatchInviteRequest,
396
+ BatchInviteResponse,
397
+ InviteBatchStatus,
398
+ InviteBatchItemStatus,
399
+ InviteBatchItemReason,
400
+ InviteBatchItem,
401
+ InviteBatchCounts,
402
+ InviteBatch,
403
+ InviteBatchResponse,
404
+ CreateInviteBatchRequest,
405
+ UpdateInviteBatchRequest,
406
+ SendInviteBatchRequest,
369
407
  DomainResponse,
370
408
  DomainListResponse,
371
409
  OrgBillingResponse,
@@ -781,6 +781,434 @@ export type CreateInviteRequestPayload = z.infer<
781
781
  typeof CreateInviteRequestSchema
782
782
  >;
783
783
 
784
+ // ---------------------------------------------------------------------------
785
+ // GET /api/workspace/directory
786
+ //
787
+ // The org's PEOPLE DIRECTORY — every `persons` row, account-bearing or not.
788
+ // Distinct from the member list (`user_org_memberships`, RBAC principals) and
789
+ // from the org chart (`positions` + occupancy, topology): this is the answer to
790
+ // "who does this org know about", which after an HRIS sync is a much larger set
791
+ // than either.
792
+ //
793
+ // It exists because inviting a synced person needs their address, and the org
794
+ // chart cannot carry one: `GET /api/users/org-chart` is authenticated-only, so a
795
+ // `primaryEmail` there would let any member enumerate the whole company's email
796
+ // addresses. This surface is capability-gated instead.
797
+ // ---------------------------------------------------------------------------
798
+
799
+ export const OrgDirectoryPersonSchema = z.object({
800
+ personId: z.string().uuid(),
801
+ displayName: z.string(),
802
+ /**
803
+ * Nullable and NOT optional. A directory person with no address is a real,
804
+ * common state — an HRIS export with the email column unmapped produces a
805
+ * whole org of them — and it is the difference between "cannot be invited" and
806
+ * "not yet loaded". Callers must render the distinction rather than treat a
807
+ * missing address as absence of the person.
808
+ */
809
+ primaryEmail: z.string().nullable(),
810
+ /** The linked account, or null when this person has never signed up. */
811
+ userId: z.string().uuid().nullable(),
812
+ });
813
+
814
+ export type OrgDirectoryPerson = z.infer<typeof OrgDirectoryPersonSchema>;
815
+
816
+ export const OrgDirectoryResponseSchema = z.object({
817
+ people: z.array(OrgDirectoryPersonSchema),
818
+ });
819
+
820
+ export type OrgDirectoryResponse = z.infer<typeof OrgDirectoryResponseSchema>;
821
+
822
+ // ---------------------------------------------------------------------------
823
+ // POST /api/workspace/invites/batch — DEPRECATED (removed in contracts 58.0.0)
824
+ //
825
+ // Superseded by the invite-batch lifecycle below. This route sends real email
826
+ // the instant it is called: there is no moment at which a human sees the
827
+ // addresses that are about to be written to, and no way to cancel. Everything it
828
+ // does correctly — personId keying, per-person transactions, three honest result
829
+ // buckets — is preserved by `POST /api/workspace/invite-batches/{id}/send`, with
830
+ // a review boundary in front of it.
831
+ //
832
+ // Kept live for one major so consumers can migrate against a green cascade: a
833
+ // single major that deleted these types while the app still imported them could
834
+ // not land, because the release cascade gates on each consumer's full CI tier.
835
+ // 57.0.0 adds the lifecycle; consumers move; 58.0.0 deletes everything below.
836
+ //
837
+ // Bulk invite, keyed by `personId` rather than by email. That is a security
838
+ // property, not a convenience: the single-invite route takes a caller-supplied
839
+ // address and can therefore reach any mailbox on the internet, which is why it
840
+ // carries a strict per-minute limit. This route's recipients are resolved
841
+ // server-side from the caller's own `persons` rows, so the reachable set is
842
+ // bounded by the org's own directory and the arbitrary-recipient risk is gone.
843
+ //
844
+ // Consequence for callers: the client never needs to hold an email address to
845
+ // send an invite, and an unknown or cross-org `personId` is silently omitted
846
+ // from the response rather than reported — reporting it would make this a
847
+ // membership oracle.
848
+ // ---------------------------------------------------------------------------
849
+
850
+ /**
851
+ * Cap on one batch. Bounds worst-case request latency, since the invite emails
852
+ * are sent before the response returns (the raw token exists only in memory and
853
+ * must not be parked in a queue).
854
+ */
855
+ export const BATCH_INVITE_PERSONS_MAX = 100;
856
+
857
+ /** @deprecated Use {@link CreateInviteBatchRequestSchema}. Removed in 58.0.0. */
858
+ export const BatchInviteRequestSchema = z.object({
859
+ personIds: z.array(z.string().uuid()).min(1).max(BATCH_INVITE_PERSONS_MAX),
860
+ // Same restricted {admin, member} domain as a single invite.
861
+ role: z.enum(["admin", "member"]),
862
+ });
863
+
864
+ export type BatchInviteRequest = z.infer<typeof BatchInviteRequestSchema>;
865
+
866
+ /**
867
+ * Why one person in a batch did not receive an invite.
868
+ *
869
+ * - `already-member` — holds an account in this org already.
870
+ * - `invite-pending` — an unexpired invite is already outstanding.
871
+ * - `no-email` — the directory row carries no address to send to.
872
+ * - `send-failed` — the invite row COMMITTED but the email did not leave.
873
+ * Recoverable by resending; never a reason to roll the
874
+ * invite back.
875
+ */
876
+ export const BATCH_INVITE_SKIP_REASONS = [
877
+ "already-member",
878
+ "invite-pending",
879
+ "no-email",
880
+ "send-failed",
881
+ ] as const;
882
+
883
+ /** @deprecated Use {@link InviteBatchItemReasonSchema}. Removed in 58.0.0. */
884
+ export const BatchInviteSkipReasonSchema = z.enum(BATCH_INVITE_SKIP_REASONS);
885
+
886
+ export type BatchInviteSkipReason = z.infer<typeof BatchInviteSkipReasonSchema>;
887
+
888
+ const BatchInviteOutcomeSchema = z.object({
889
+ personId: z.string().uuid(),
890
+ reason: BatchInviteSkipReasonSchema,
891
+ });
892
+
893
+ /** @deprecated Use {@link InviteBatchResponseSchema}. Removed in 58.0.0. */
894
+ export const BatchInviteResponseSchema = z.object({
895
+ /** Person ids that now hold a pending invite AND were emailed. */
896
+ invited: z.array(z.string().uuid()),
897
+ /** Never attempted — see {@link BatchInviteSkipReason}. */
898
+ skipped: z.array(BatchInviteOutcomeSchema),
899
+ /**
900
+ * Invite row exists, delivery did not. Separate from `skipped` because the
901
+ * remedy differs: a skip needs the underlying state to change, a failure needs
902
+ * a resend.
903
+ */
904
+ failed: z.array(BatchInviteOutcomeSchema),
905
+ });
906
+
907
+ export type BatchInviteResponse = z.infer<typeof BatchInviteResponseSchema>;
908
+
909
+ // ---------------------------------------------------------------------------
910
+ // Invite batches — the Prepare → Review → Send lifecycle
911
+ //
912
+ // POST /api/workspace/invite-batches create a draft
913
+ // GET /api/workspace/invite-batches/{id} review it
914
+ // PATCH /api/workspace/invite-batches/{id} change selection/role (bumps revision)
915
+ // POST /api/workspace/invite-batches/{id}/prepare evaluate eligibility, draft → ready
916
+ // POST /api/workspace/invite-batches/{id}/send send, gated on expectedRevision
917
+ // POST /api/workspace/invite-batches/{id}/cancel cancel what has not been sent
918
+ //
919
+ // WHY A PERSISTED BATCH AT ALL. The one-shot bulk route (`POST
920
+ // /api/workspace/invites/batch`, deprecated by this vocabulary) sends real email
921
+ // the instant it is called — there is no moment at which a human can see the 88
922
+ // addresses that are about to be written to. This lifecycle inserts exactly one:
923
+ // `prepare` decides who is eligible, the review surface shows the recipients and
924
+ // their addresses, and `send` acts on what was reviewed or refuses.
925
+ //
926
+ // THE BATCH HOLDS INTENT, NEVER CREDENTIALS. There is no token field on a batch
927
+ // or an item, in any form, and there never may be. An invite token exists only
928
+ // between generation and the send, in memory; only its SHA-256 is stored, on the
929
+ // invite row. A batch that parked a live token at rest would undo that whole
930
+ // property in exchange for nothing — the item carries `inviteId`, which is the
931
+ // durable handle to the credential without being one.
932
+ //
933
+ // CONFIRMATION AUTHORIZES THE ADDRESSES SHOWN AT REVIEW TIME. `emailSnapshot` is
934
+ // what the reviewer saw. At send, an item whose person now holds a different
935
+ // address is `skipped` with `address-changed` — never mailed at the new one. The
936
+ // same is true of someone who became a member, or gained an invite by another
937
+ // route, between review and send. Approving a list is not approving whatever
938
+ // that list becomes.
939
+ // ---------------------------------------------------------------------------
940
+
941
+ /**
942
+ * Batch lifecycle.
943
+ *
944
+ * - `draft` — created, membership not yet evaluated. Sends nothing.
945
+ * - `ready` — `prepare` ran; the item set and its exclusions are the review.
946
+ * - `sending` — CLAIMED by a send. See {@link SendInviteBatchRequestSchema}.
947
+ * - `completed` — the send finished. Per-person outcomes live on the items.
948
+ * - `cancelled` — cancelled before or between sends; unsent items stay unsent.
949
+ *
950
+ * There is deliberately no `partially_completed`: it is `failedCount > 0` on a
951
+ * `completed` batch, and a second spelling of one fact is a second thing to keep
952
+ * true. There is deliberately no `expired` either — nothing sweeps batches, and
953
+ * a status no writer ever writes is a promise the API does not keep.
954
+ */
955
+ export const INVITE_BATCH_STATUSES = [
956
+ "draft",
957
+ "ready",
958
+ "sending",
959
+ "completed",
960
+ "cancelled",
961
+ ] as const;
962
+
963
+ export const InviteBatchStatusSchema = z.enum(INVITE_BATCH_STATUSES);
964
+
965
+ export type InviteBatchStatus = z.infer<typeof InviteBatchStatusSchema>;
966
+
967
+ /**
968
+ * Per-recipient outcome within a batch.
969
+ *
970
+ * - `eligible` — will be invited when the batch is sent.
971
+ * - `excluded` — will not, decided at PREPARE time. See {@link InviteBatchItemReason}.
972
+ * - `sent` — an invite row exists AND the email left. Carries `inviteId`.
973
+ * - `failed` — an invite row exists, the email did NOT leave. Also carries
974
+ * `inviteId`: the remedy is a reissue, never a re-send, because
975
+ * the credential in that row was already minted.
976
+ * - `skipped` — eligible at review, refused at SEND because the world moved.
977
+ *
978
+ * `eligible` items on a batch stuck in `sending` are exactly the ones a crashed
979
+ * send never reached — which is why there is no separate `pending_send`.
980
+ */
981
+ export const INVITE_BATCH_ITEM_STATUSES = [
982
+ "eligible",
983
+ "excluded",
984
+ "sent",
985
+ "failed",
986
+ "skipped",
987
+ ] as const;
988
+
989
+ export const InviteBatchItemStatusSchema = z.enum(INVITE_BATCH_ITEM_STATUSES);
990
+
991
+ export type InviteBatchItemStatus = z.infer<typeof InviteBatchItemStatusSchema>;
992
+
993
+ /**
994
+ * Why an item is not `sent`. One vocabulary across `excluded` (decided at
995
+ * prepare) and `skipped` (decided at send) — the reader wants "why did this
996
+ * person not get an invitation", and splitting the answer by which stage
997
+ * noticed would make that two questions.
998
+ *
999
+ * - `already-member` — holds an account in this org.
1000
+ * - `invite-pending` — an unexpired invite is already outstanding.
1001
+ * - `no-email` — the directory row carries no address.
1002
+ * - `invalid-email` — it carries one that is not a deliverable address.
1003
+ * An HRIS export with a mangled column produces these;
1004
+ * committing an invite row that can never be mailed is
1005
+ * the failure this reason exists to prevent.
1006
+ * - `removed-by-reviewer` — a human un-ticked them. Persisted rather than
1007
+ * deleted so the choice is reversible AND legible:
1008
+ * "who was deliberately left out" is a question the
1009
+ * batch must be able to answer later.
1010
+ * - `address-changed` — SEND-time only. The person's address is no longer
1011
+ * the one shown at review. Confirmation authorized the
1012
+ * address, not the person.
1013
+ * - `person-removed` — SEND-time only. The directory row is gone.
1014
+ * - `send-failed` — pairs with `failed`, not with `excluded`/`skipped`.
1015
+ * - `inactive-in-hris` — RESERVED, and NOTHING WRITES IT TODAY. `persons`
1016
+ * carries no employment-status column, the BambooHR
1017
+ * client never reads one, and a "Sync now" full resync
1018
+ * reports no departures at all — so a terminated
1019
+ * employee is currently indistinguishable from a
1020
+ * current one. The name is held here so the eventual
1021
+ * implementation does not have to change the wire
1022
+ * vocabulary; treating its ABSENCE as evidence that
1023
+ * everyone listed is still employed would be wrong.
1024
+ */
1025
+ export const INVITE_BATCH_ITEM_REASONS = [
1026
+ "already-member",
1027
+ "invite-pending",
1028
+ "no-email",
1029
+ "invalid-email",
1030
+ "removed-by-reviewer",
1031
+ "address-changed",
1032
+ "person-removed",
1033
+ "send-failed",
1034
+ "inactive-in-hris",
1035
+ ] as const;
1036
+
1037
+ export const InviteBatchItemReasonSchema = z.enum(INVITE_BATCH_ITEM_REASONS);
1038
+
1039
+ export type InviteBatchItemReason = z.infer<typeof InviteBatchItemReasonSchema>;
1040
+
1041
+ export const InviteBatchItemSchema = z.object({
1042
+ personId: z.string().uuid(),
1043
+ /**
1044
+ * Read live from the directory, NOT snapshotted. A name is for recognising the
1045
+ * human; only the ADDRESS is the thing confirmation authorizes, so only the
1046
+ * address is frozen. Null when the directory row has since been deleted.
1047
+ */
1048
+ displayName: z.string().nullable(),
1049
+ /**
1050
+ * The address as it stood when `prepare` ran — what the reviewer approved.
1051
+ * Null only when the person had none. Compared against the live address at
1052
+ * send; a mismatch is `address-changed`, never a send to the new one.
1053
+ */
1054
+ emailSnapshot: z.string().nullable(),
1055
+ role: z.enum(["admin", "member"]),
1056
+ status: InviteBatchItemStatusSchema,
1057
+ /** Null exactly when `status` is `eligible` or `sent`. */
1058
+ reason: InviteBatchItemReasonSchema.nullable(),
1059
+ /**
1060
+ * The invite row this item produced. Present for `sent` AND for `failed` — a
1061
+ * failed item's credential was minted and committed before the send was
1062
+ * attempted, so the row exists and reissue is the remedy.
1063
+ */
1064
+ inviteId: z.string().uuid().nullable(),
1065
+ /**
1066
+ * The snapshot address is not under a domain this org has VERIFIED. Computed
1067
+ * at read time from `org_domains` (status `verified`), never stored: a domain
1068
+ * verified after review must stop warning, and one revoked after review must
1069
+ * start. Advisory — it does not exclude anyone. Sending outside your own
1070
+ * domains is legitimate (contractors, personal addresses); doing it eighty-
1071
+ * eight times without noticing is the accident worth surfacing.
1072
+ */
1073
+ emailOutsideVerifiedDomain: z.boolean(),
1074
+ });
1075
+
1076
+ export type InviteBatchItem = z.infer<typeof InviteBatchItemSchema>;
1077
+
1078
+ /**
1079
+ * Denormalized counters, maintained by the batch's own writers in the same
1080
+ * transaction as the items they describe.
1081
+ *
1082
+ * DELIBERATELY NOT DERIVED CLIENT-SIDE. The confirm control reads
1083
+ * "Send {eligible} invitations", and that number must be the number the SERVER
1084
+ * will act on. A count computed from a page of items can disagree with the set
1085
+ * `send` iterates; a count the server wrote cannot.
1086
+ */
1087
+ export const InviteBatchCountsSchema = z.object({
1088
+ /** Every person the batch was created for. Never changes after create. */
1089
+ candidate: z.number().int().nonnegative(),
1090
+ eligible: z.number().int().nonnegative(),
1091
+ excluded: z.number().int().nonnegative(),
1092
+ sent: z.number().int().nonnegative(),
1093
+ failed: z.number().int().nonnegative(),
1094
+ skipped: z.number().int().nonnegative(),
1095
+ });
1096
+
1097
+ export type InviteBatchCounts = z.infer<typeof InviteBatchCountsSchema>;
1098
+
1099
+ export const InviteBatchSchema = z.object({
1100
+ id: z.string().uuid(),
1101
+ status: InviteBatchStatusSchema,
1102
+ /**
1103
+ * Where the candidate set came from. OPEN string, `directory` today — the org
1104
+ * people directory. Not an enum: a future CSV or SCIM origin should not need a
1105
+ * migration to be nameable.
1106
+ */
1107
+ source: z.string(),
1108
+ /**
1109
+ * Bumped by every mutation of the reviewable set (selection or role). `send`
1110
+ * carries the revision the reviewer saw; a mismatch is
1111
+ * {@link INVITE_BATCH_CHANGED} rather than a send of something else.
1112
+ */
1113
+ revision: z.number().int().nonnegative(),
1114
+ counts: InviteBatchCountsSchema,
1115
+ createdByUserId: z.string().uuid(),
1116
+ /** Who ran `send`, and when they did. Null until then. */
1117
+ confirmedByUserId: z.string().uuid().nullable(),
1118
+ confirmedAt: z.string().nullable(),
1119
+ createdAt: z.string(),
1120
+ updatedAt: z.string(),
1121
+ });
1122
+
1123
+ export type InviteBatch = z.infer<typeof InviteBatchSchema>;
1124
+
1125
+ export const InviteBatchResponseSchema = z.object({
1126
+ batch: InviteBatchSchema,
1127
+ items: z.array(InviteBatchItemSchema),
1128
+ /**
1129
+ * The org's verified domains, so the review surface can NAME them in the
1130
+ * out-of-domain warning rather than just flagging rows. Empty is the common
1131
+ * case and means every address is out-of-domain by this definition — render
1132
+ * the warning accordingly rather than as eighty-eight alarms.
1133
+ */
1134
+ verifiedDomains: z.array(z.string()),
1135
+ });
1136
+
1137
+ export type InviteBatchResponse = z.infer<typeof InviteBatchResponseSchema>;
1138
+
1139
+ export const CreateInviteBatchRequestSchema = z.object({
1140
+ personIds: z.array(z.string().uuid()).min(1).max(BATCH_INVITE_PERSONS_MAX),
1141
+ role: z.enum(["admin", "member"]),
1142
+ });
1143
+
1144
+ export type CreateInviteBatchRequest = z.infer<
1145
+ typeof CreateInviteBatchRequestSchema
1146
+ >;
1147
+
1148
+ /**
1149
+ * Change what a `draft` or `ready` batch will do. Every accepted call bumps
1150
+ * `revision`, which is what invalidates a review already on someone's screen.
1151
+ *
1152
+ * Deselecting does NOT delete the item — it becomes `excluded` with
1153
+ * `removed-by-reviewer`, so reselecting is a status flip rather than a
1154
+ * re-preparation, and the batch can still answer who was deliberately omitted.
1155
+ * Reselecting a person excluded by POLICY (already a member, no address) is
1156
+ * refused: `reselect` reverses a human's decision, not the server's.
1157
+ */
1158
+ export const UpdateInviteBatchRequestSchema = z
1159
+ .object({
1160
+ deselectPersonIds: z.array(z.string().uuid()).optional(),
1161
+ reselectPersonIds: z.array(z.string().uuid()).optional(),
1162
+ role: z.enum(["admin", "member"]).optional(),
1163
+ })
1164
+ .refine(
1165
+ (v) =>
1166
+ v.deselectPersonIds !== undefined ||
1167
+ v.reselectPersonIds !== undefined ||
1168
+ v.role !== undefined,
1169
+ {
1170
+ message:
1171
+ "Provide at least one of deselectPersonIds, reselectPersonIds, role",
1172
+ },
1173
+ );
1174
+
1175
+ export type UpdateInviteBatchRequest = z.infer<
1176
+ typeof UpdateInviteBatchRequestSchema
1177
+ >;
1178
+
1179
+ /**
1180
+ * `expectedRevision` is REQUIRED and has no default. Sending is the only
1181
+ * irreversible verb in this lifecycle — email cannot be recalled — so the one
1182
+ * call that performs it must state what it believes it is sending.
1183
+ *
1184
+ * There is no `Idempotency-Key` here, deliberately. The send claim
1185
+ * (`UPDATE ... SET status='sending' WHERE status='ready' AND revision = ?`) is
1186
+ * an atomic compare-and-set: the first caller wins and every later one sees zero
1187
+ * rows and gets {@link INVITE_BATCH_CHANGED}. That is exactly-once permanently
1188
+ * and across every task in the cluster, where an idempotency key would be
1189
+ * exactly-once for its retention window and only where the key store is shared.
1190
+ * Routing this through a key ALSO means wrapping check + mutation + store in one
1191
+ * transaction, which would roll back the record of mail that had already left.
1192
+ */
1193
+ export const SendInviteBatchRequestSchema = z.object({
1194
+ expectedRevision: z.number().int().nonnegative(),
1195
+ });
1196
+
1197
+ export type SendInviteBatchRequest = z.infer<
1198
+ typeof SendInviteBatchRequestSchema
1199
+ >;
1200
+
1201
+ /**
1202
+ * The error code a stale `send` (or a mutation of an already-claimed batch)
1203
+ * returns, as `error` on the standard 409 envelope.
1204
+ *
1205
+ * It means: what you reviewed is not what is here now. The client's obligation
1206
+ * is to re-fetch and put the CHANGES in front of the human again before the
1207
+ * confirm control re-arms — not to retry with the new revision, which would
1208
+ * authorize a list nobody read.
1209
+ */
1210
+ export const INVITE_BATCH_CHANGED = "BATCH_CHANGED" as const;
1211
+
784
1212
  // ---------------------------------------------------------------------------
785
1213
  // OrgDomain sub-schema
786
1214
  // ---------------------------------------------------------------------------
@@ -20,6 +20,9 @@ export type ResourceKey =
20
20
  | { type: "teams"; orgId: string }
21
21
  | { type: "integrations"; orgId: string }
22
22
  | { type: "invites"; orgId: string }
23
+ // The people directory (`persons`) — every person the org knows about,
24
+ // account-bearing or not. Distinct from `members`, which is accounts only.
25
+ | { type: "orgDirectory"; orgId: string }
23
26
  | { type: "auditEvents"; orgId: string }
24
27
  | { type: "timeline"; orgId: string }
25
28
  // Identities (singular) — single entities
@@ -129,6 +129,7 @@ const ORG_SCOPED_TYPES = [
129
129
  "teams",
130
130
  "integrations",
131
131
  "invites",
132
+ "orgDirectory",
132
133
  "auditEvents",
133
134
  "timeline",
134
135
  "workspace",
@@ -266,6 +267,7 @@ export function toQueryKey(key: ResourceKey): readonly string[] {
266
267
  case "teams":
267
268
  case "integrations":
268
269
  case "invites":
270
+ case "orgDirectory":
269
271
  case "auditEvents":
270
272
  case "timeline":
271
273
  case "workspace":