@company-semantics/contracts 56.1.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.1.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 = '373ff35b2847' as const;
3
- export const SPEC_HASH_FULL = '373ff35b284714e9030fd0c611b3d4c1be255cd12df7f6adec2b6f7e6ba9179f' as const;
2
+ export const SPEC_HASH = 'ffeb6e367b38' as const;
3
+ export const SPEC_HASH_FULL = 'ffeb6e367b385ad32f7b46e4e74d276786d6cdf175352e57d0fc229dc8f506af' as const;
@@ -943,6 +943,23 @@ export interface paths {
943
943
  patch?: never;
944
944
  trace?: never;
945
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
+ };
946
963
  "/api/workspace/invites/accept": {
947
964
  parameters: {
948
965
  query?: never;
@@ -977,6 +994,92 @@ export interface paths {
977
994
  patch?: never;
978
995
  trace?: never;
979
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
+ };
980
1083
  "/api/workspace/invites/validate": {
981
1084
  parameters: {
982
1085
  query?: never;
@@ -4631,6 +4734,12 @@ export interface components {
4631
4734
  /** @enum {string} */
4632
4735
  role: "admin" | "member";
4633
4736
  };
4737
+ ReissueInviteResponse: {
4738
+ /** Format: uuid */
4739
+ inviteId: string;
4740
+ /** Format: uuid */
4741
+ previousInviteId: string;
4742
+ };
4634
4743
  InviteListResponse: {
4635
4744
  id: string;
4636
4745
  orgId: string;
@@ -4674,6 +4783,58 @@ export interface components {
4674
4783
  AcceptInviteRequest: {
4675
4784
  token: string;
4676
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
+ };
4677
4838
  InviteValidationResult: {
4678
4839
  valid: boolean;
4679
4840
  status: string;
@@ -8317,6 +8478,28 @@ export interface operations {
8317
8478
  };
8318
8479
  };
8319
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
+ };
8320
8503
  acceptInvite: {
8321
8504
  parameters: {
8322
8505
  query?: never;
@@ -8361,6 +8544,148 @@ export interface operations {
8361
8544
  };
8362
8545
  };
8363
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
+ };
8364
8689
  validateInviteToken: {
8365
8690
  parameters: {
8366
8691
  query: {
@@ -190,11 +190,17 @@ export const openApiRoutes = {
190
190
  '/api/workspace/domains/{id}': ['DELETE'],
191
191
  '/api/workspace/domains/{id}/verify': ['POST'],
192
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'],
193
198
  '/api/workspace/invites': ['GET', 'POST'],
194
199
  '/api/workspace/invites/accept': ['POST'],
195
200
  '/api/workspace/invites/batch': ['POST'],
196
201
  '/api/workspace/invites/validate': ['GET'],
197
202
  '/api/workspace/invites/{id}': ['DELETE'],
203
+ '/api/workspace/invites/{id}/reissue': ['POST'],
198
204
  '/api/workspace/members': ['GET'],
199
205
  '/api/workspace/members/{id}': ['DELETE', 'GET'],
200
206
  '/api/workspace/members/{id}/role': ['PATCH'],
package/src/index.ts CHANGED
@@ -645,6 +645,39 @@ export type {
645
645
  BatchInviteResponse,
646
646
  } from "./org/index";
647
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
+
648
681
  // Source authority: per-field ownership — who may WRITE a given entity field.
649
682
  // Orthogonal to provenance (who reported). (ADR-CONT-083 / ADR-CTRL-182)
650
683
  export { SourceAuthoritySchema } from "./org/index";
package/src/org/README.md CHANGED
@@ -109,6 +109,8 @@ Shared type vocabulary for organization ownership, type classification, and tran
109
109
  - `ConstraintViolationSchema` — One constraint the transformation engine evaluated and found violated.
110
110
  - `CreateDelegationRequest` _(type)_
111
111
  - `CreateDelegationRequestSchema`
112
+ - `CreateInviteBatchRequest` _(type)_
113
+ - `CreateInviteBatchRequestSchema`
112
114
  - `CreateInviteRequest` _(type)_ — Request payload for creating an organization invite.
113
115
  - `CreateInviteRequestPayload` _(type)_
114
116
  - `CreateInviteRequestSchema`
@@ -147,9 +149,27 @@ Shared type vocabulary for organization ownership, type classification, and tran
147
149
  - `HomeUnitRole` _(type)_
148
150
  - `HomeUnitRoleSchema` — Role the invitee takes in their home unit on acceptance.
149
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.
150
156
  - `IdentityTrustLevel` _(type)_ — Identity Trust Level — org's identity posture
151
157
  - `IntegrationRequest` _(type)_ — Advisory integration request created by a member.
152
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`
153
173
  - `InviteListResponse` _(type)_
154
174
  - `InviteListResponseSchema`
155
175
  - `InviteResponse` _(type)_
@@ -311,6 +331,8 @@ Shared type vocabulary for organization ownership, type classification, and tran
311
331
  - `ScopeCheckResponseSchema`
312
332
  - `SelectionRationale` _(type)_
313
333
  - `SelectionRationaleSchema`
334
+ - `SendInviteBatchRequest` _(type)_
335
+ - `SendInviteBatchRequestSchema` — `expectedRevision` is REQUIRED and has no default.
314
336
  - `SetActiveOrgResponse` _(type)_
315
337
  - `SetActiveOrgResponseSchema`
316
338
  - `SetSeatManagerResponse` _(type)_
@@ -360,6 +382,8 @@ Shared type vocabulary for organization ownership, type classification, and tran
360
382
  - `UpdateAuthPolicyRequest` _(type)_ — Request payload for updating organization auth policy.
361
383
  - `UpdateDelegationRequest` _(type)_
362
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.
363
387
  - `UpdateOpenRoleStatusRequest` _(type)_
364
388
  - `UpdateOpenRoleStatusRequestSchema` — Only the non-terminal client transitions are accepted; `fill` is its own endpoint and `open` is the seeded…
365
389
  - `UpdateOrgUnitRequest` _(type)_
package/src/org/index.ts CHANGED
@@ -362,6 +362,21 @@ export {
362
362
  BatchInviteSkipReasonSchema,
363
363
  BatchInviteRequestSchema,
364
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,
365
380
  DomainResponseSchema,
366
381
  DomainListResponseSchema,
367
382
  OrgBillingResponseSchema,
@@ -379,6 +394,16 @@ export type {
379
394
  BatchInviteSkipReason,
380
395
  BatchInviteRequest,
381
396
  BatchInviteResponse,
397
+ InviteBatchStatus,
398
+ InviteBatchItemStatus,
399
+ InviteBatchItemReason,
400
+ InviteBatchItem,
401
+ InviteBatchCounts,
402
+ InviteBatch,
403
+ InviteBatchResponse,
404
+ CreateInviteBatchRequest,
405
+ UpdateInviteBatchRequest,
406
+ SendInviteBatchRequest,
382
407
  DomainResponse,
383
408
  DomainListResponse,
384
409
  OrgBillingResponse,
@@ -820,7 +820,19 @@ export const OrgDirectoryResponseSchema = z.object({
820
820
  export type OrgDirectoryResponse = z.infer<typeof OrgDirectoryResponseSchema>;
821
821
 
822
822
  // ---------------------------------------------------------------------------
823
- // POST /api/workspace/invites/batch
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.
824
836
  //
825
837
  // Bulk invite, keyed by `personId` rather than by email. That is a security
826
838
  // property, not a convenience: the single-invite route takes a caller-supplied
@@ -842,6 +854,7 @@ export type OrgDirectoryResponse = z.infer<typeof OrgDirectoryResponseSchema>;
842
854
  */
843
855
  export const BATCH_INVITE_PERSONS_MAX = 100;
844
856
 
857
+ /** @deprecated Use {@link CreateInviteBatchRequestSchema}. Removed in 58.0.0. */
845
858
  export const BatchInviteRequestSchema = z.object({
846
859
  personIds: z.array(z.string().uuid()).min(1).max(BATCH_INVITE_PERSONS_MAX),
847
860
  // Same restricted {admin, member} domain as a single invite.
@@ -867,6 +880,7 @@ export const BATCH_INVITE_SKIP_REASONS = [
867
880
  "send-failed",
868
881
  ] as const;
869
882
 
883
+ /** @deprecated Use {@link InviteBatchItemReasonSchema}. Removed in 58.0.0. */
870
884
  export const BatchInviteSkipReasonSchema = z.enum(BATCH_INVITE_SKIP_REASONS);
871
885
 
872
886
  export type BatchInviteSkipReason = z.infer<typeof BatchInviteSkipReasonSchema>;
@@ -876,6 +890,7 @@ const BatchInviteOutcomeSchema = z.object({
876
890
  reason: BatchInviteSkipReasonSchema,
877
891
  });
878
892
 
893
+ /** @deprecated Use {@link InviteBatchResponseSchema}. Removed in 58.0.0. */
879
894
  export const BatchInviteResponseSchema = z.object({
880
895
  /** Person ids that now hold a pending invite AND were emailed. */
881
896
  invited: z.array(z.string().uuid()),
@@ -891,6 +906,309 @@ export const BatchInviteResponseSchema = z.object({
891
906
 
892
907
  export type BatchInviteResponse = z.infer<typeof BatchInviteResponseSchema>;
893
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
+
894
1212
  // ---------------------------------------------------------------------------
895
1213
  // OrgDomain sub-schema
896
1214
  // ---------------------------------------------------------------------------