@shuffl/api-contracts 0.4.0 → 0.5.1

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/dist/schemas.js CHANGED
@@ -156,7 +156,8 @@ export const matchSchema = z
156
156
  maxGroupSize: z.number().int().min(2).max(8),
157
157
  strategy: strategySchema,
158
158
  unitIds: z.array(z.string().trim().min(1).max(200)).max(20).optional(),
159
- unitKind: z.enum(['department', 'team']).optional(),
159
+ // #745: communities mix like teams people choose themselves.
160
+ unitKind: z.enum(['department', 'team', 'community']).optional(),
160
161
  })
161
162
  .refine((x) => !currentStrategies.includes(x.strategy) || x.iterations <= 50, 'Purpose matching supports at most 50 search attempts')
162
163
  .refine((x) => x.maxGroupSize >= x.groupSize, 'Maximum group size must be at least the target size')
@@ -168,7 +169,7 @@ export const matchSchema = z
168
169
  .refine((x) => !x.unitIds || new Set(x.unitIds).size === x.unitIds.length, 'Choose each team or department only once')
169
170
  .refine((x) => x.strategy === 'cross-unit' || x.strategy === 'within-unit'
170
171
  ? !!x.unitKind
171
- : x.unitKind === undefined, 'Choose teams or departments only when mixing across or within them')
172
+ : x.unitKind === undefined, 'Choose teams, departments or communities only when mixing across or within them')
172
173
  .refine((x) => !currentStrategies.includes(x.strategy) ||
173
174
  x.maxGroupSize <= x.groupSize + 1, 'Allow at most one extra person per group')
174
175
  .refine((x) => !x.keepApart || currentStrategies.includes(x.strategy), 'Keeping coworkers apart needs a current matching purpose')
@@ -176,6 +177,17 @@ export const matchSchema = z
176
177
  export const programInput = z.object({
177
178
  channelId: z.string().regex(/^[CG][A-Z0-9]+$/),
178
179
  channelName: z.string().trim().min(1).max(100),
180
+ // #745: one to 20 communities whose members take part instead, by those
181
+ // who show the community on their profile. Empty or omitted means the
182
+ // channel or the group; a program uses one of the three.
183
+ communityIds: z
184
+ .array(z.string().trim().min(1).max(128))
185
+ .max(20)
186
+ .optional()
187
+ .describe('Communities whose members take part, instead of the Slack channel or a group. Only people who show the community on their profile are enrolled.'),
188
+ // #1832: the group whose members take part; null or omitted means everyone
189
+ // in the Slack channel. Introductions are posted in the channel either way.
190
+ groupId: z.string().trim().min(1).max(128).nullish(),
179
191
  includeBios: z.boolean(),
180
192
  // Omission preserves an existing program's choice; new programs default on.
181
193
  includeSchedulingLink: z.boolean().optional(),
@@ -201,6 +213,8 @@ export const profileSchema = z.object({
201
213
  export const programPatch = z.strictObject({
202
214
  channelId: programInput.shape.channelId.optional(),
203
215
  channelName: programInput.shape.channelName.optional(),
216
+ communityIds: z.array(z.string().trim().min(1).max(128)).max(20).optional(),
217
+ groupId: z.string().trim().min(1).max(128).nullable().optional(),
204
218
  includeBios: z.boolean().optional(),
205
219
  includeSchedulingLink: z.boolean().optional(),
206
220
  intro: programInput.shape.intro.optional(),
@@ -218,7 +232,10 @@ export const programPatch = z.strictObject({
218
232
  maxGroupSize: z.number().int().min(2).max(8).optional(),
219
233
  strategy: strategySchema.optional(),
220
234
  unitIds: z.array(z.string().trim().min(1).max(200)).max(20).optional(),
221
- unitKind: z.enum(['department', 'team']).nullable().optional(),
235
+ unitKind: z
236
+ .enum(['department', 'team', 'community'])
237
+ .nullable()
238
+ .optional(),
222
239
  })
223
240
  .optional(),
224
241
  name: programInput.shape.name.optional(),
@@ -0,0 +1,373 @@
1
+ export declare const webhookApiVersion = "2026-10-17";
2
+ export declare const webhookEventGroups: {
3
+ readonly connections: {
4
+ readonly description: 'Connections programs, rounds and who takes part';
5
+ readonly label: 'Connections';
6
+ readonly scope: 'programs:read';
7
+ };
8
+ readonly hr: {
9
+ readonly description: 'Who answers HR requests';
10
+ readonly label: 'HR team';
11
+ readonly scope: 'hr:read';
12
+ };
13
+ readonly knowledge: {
14
+ readonly description: 'Proposals, drafts, published pages and connected sources';
15
+ readonly label: 'Knowledge';
16
+ readonly scope: 'knowledge:read';
17
+ };
18
+ readonly people: {
19
+ readonly description: 'People, roles, access, identities and departments';
20
+ readonly label: 'People';
21
+ readonly scope: 'users:read';
22
+ };
23
+ readonly playbook: {
24
+ readonly description: 'Playbook enrollments, steps, buddies and check-ins';
25
+ readonly label: 'Playbooks';
26
+ readonly scope: 'playbooks:read';
27
+ };
28
+ readonly webhooks: {
29
+ readonly description: 'Test events and endpoints Shuffl turned off';
30
+ readonly label: 'Webhooks';
31
+ readonly scope: 'webhooks:read';
32
+ };
33
+ readonly workspace: {
34
+ readonly description: 'The workspace itself and its Slack connection';
35
+ readonly label: 'Workspace';
36
+ readonly scope: 'team:read';
37
+ };
38
+ };
39
+ export type WebhookEventGroup = keyof typeof webhookEventGroups;
40
+ export declare const webhookEventGroupIds: WebhookEventGroup[];
41
+ export declare const webhookEventCatalog: {
42
+ readonly 'hr.responder.granted': {
43
+ readonly description: 'Someone joined the HR team that answers requests';
44
+ readonly fields: readonly ["person_id", "revision"];
45
+ readonly group: 'hr';
46
+ };
47
+ readonly 'hr.responder.revoked': {
48
+ readonly description: 'Someone left the HR team';
49
+ readonly fields: readonly ["person_id", "revision"];
50
+ readonly group: 'hr';
51
+ };
52
+ readonly 'hr.team.renamed': {
53
+ readonly description: 'The HR team’s name changed';
54
+ readonly fields: readonly ["revision"];
55
+ readonly group: 'hr';
56
+ };
57
+ readonly 'knowledge.document.archived': {
58
+ readonly description: 'A page was archived';
59
+ readonly fields: readonly ["document_id"];
60
+ readonly group: 'knowledge';
61
+ };
62
+ readonly 'knowledge.document.audience_changed': {
63
+ readonly description: 'Who can read a published page changed';
64
+ readonly fields: readonly ["document_id", "version_id", "audience", "previous_audience"];
65
+ readonly group: 'knowledge';
66
+ };
67
+ readonly 'knowledge.document.published': {
68
+ readonly description: 'A page was published';
69
+ readonly fields: readonly ["document_id", "version_id", "audience"];
70
+ readonly group: 'knowledge';
71
+ };
72
+ readonly 'knowledge.document.restored': {
73
+ readonly description: 'An archived page was restored';
74
+ readonly fields: readonly ["document_id"];
75
+ readonly group: 'knowledge';
76
+ };
77
+ readonly 'knowledge.document.withdrawn': {
78
+ readonly description: 'A published page was withdrawn';
79
+ readonly fields: readonly ["document_id"];
80
+ readonly group: 'knowledge';
81
+ };
82
+ readonly 'knowledge.draft.saved': {
83
+ readonly description: 'A draft of a page was saved';
84
+ readonly fields: readonly ["document_id", "version_id"];
85
+ readonly group: 'knowledge';
86
+ };
87
+ readonly 'knowledge.proposal.created': {
88
+ readonly description: 'Mello proposed new or changed Knowledge for review';
89
+ readonly fields: readonly ["proposal_id", "revision"];
90
+ readonly group: 'knowledge';
91
+ };
92
+ readonly 'knowledge.proposal.published': {
93
+ readonly description: 'A verifier approved a proposal, as written or edited';
94
+ readonly fields: readonly ["proposal_id", "revision", "document_id", "edited"];
95
+ readonly group: 'knowledge';
96
+ };
97
+ readonly 'knowledge.proposal.rejected': {
98
+ readonly description: 'A verifier turned a proposal down';
99
+ readonly fields: readonly ["proposal_id", "revision"];
100
+ readonly group: 'knowledge';
101
+ };
102
+ readonly 'knowledge.section.removed': {
103
+ readonly description: 'A section was removed from a handbook page';
104
+ readonly fields: readonly ["document_id"];
105
+ readonly group: 'knowledge';
106
+ };
107
+ readonly 'knowledge.source.access_lost': {
108
+ readonly description: 'Shuffl can no longer read a connected document';
109
+ readonly fields: readonly ["document_id"];
110
+ readonly group: 'knowledge';
111
+ };
112
+ readonly 'knowledge.source.connected': {
113
+ readonly description: 'A Knowledge source such as Google Drive was connected';
114
+ readonly fields: readonly ["connection_id"];
115
+ readonly group: 'knowledge';
116
+ };
117
+ readonly 'knowledge.source.disconnected': {
118
+ readonly description: 'A Knowledge source was disconnected';
119
+ readonly fields: readonly ["connection_id"];
120
+ readonly group: 'knowledge';
121
+ };
122
+ readonly 'knowledge.verifier.granted': {
123
+ readonly description: 'Someone can now verify Knowledge';
124
+ readonly fields: readonly ["person_id", "revision"];
125
+ readonly group: 'knowledge';
126
+ };
127
+ readonly 'knowledge.verifier.revoked': {
128
+ readonly description: 'Someone can no longer verify Knowledge';
129
+ readonly fields: readonly ["person_id", "revision"];
130
+ readonly group: 'knowledge';
131
+ };
132
+ readonly 'participant.opted_in': {
133
+ readonly description: 'A person joined a Connections program';
134
+ readonly fields: readonly ["person_id", "program_id", "revision"];
135
+ readonly group: 'connections';
136
+ };
137
+ readonly 'participant.opted_out': {
138
+ readonly description: 'A person left a Connections program';
139
+ readonly fields: readonly ["person_id", "program_id", "revision"];
140
+ readonly group: 'connections';
141
+ };
142
+ readonly 'participant.profile_updated': {
143
+ readonly description: 'A person updated their profile';
144
+ readonly fields: readonly ["person_id"];
145
+ readonly group: 'connections';
146
+ };
147
+ readonly 'person.access_changed': {
148
+ readonly description: 'A person’s app access or employment changed';
149
+ readonly fields: readonly ["person_id", "status", "previous_status", "employment", "previous_employment", "revision"];
150
+ readonly group: 'people';
151
+ };
152
+ readonly 'person.access_suspended': {
153
+ readonly description: 'A person’s access was suspended';
154
+ readonly fields: readonly ["person_id"];
155
+ readonly group: 'people';
156
+ };
157
+ readonly 'person.created': {
158
+ readonly description: 'A person was added to the directory';
159
+ readonly fields: readonly ["person_id", "revision"];
160
+ readonly group: 'people';
161
+ };
162
+ readonly 'person.identity_linked': {
163
+ readonly description: 'A Slack or single sign-on identity was linked to a person';
164
+ readonly fields: readonly ["person_id", "identity_id", "provider"];
165
+ readonly group: 'people';
166
+ };
167
+ readonly 'person.identity_revoked': {
168
+ readonly description: 'A linked identity was removed from a person';
169
+ readonly fields: readonly ["person_id", "identity_id", "provider"];
170
+ readonly group: 'people';
171
+ };
172
+ readonly 'person.manager_changed': {
173
+ readonly description: 'A person’s manager changed';
174
+ readonly fields: readonly ["person_id"];
175
+ readonly group: 'people';
176
+ };
177
+ readonly 'person.role_changed': {
178
+ readonly description: 'A person’s workspace role changed';
179
+ readonly fields: readonly ["person_id", "role", "previous_role"];
180
+ readonly group: 'people';
181
+ };
182
+ readonly 'person.updated': {
183
+ readonly description: 'A person’s directory record changed';
184
+ readonly fields: readonly ["person_id", "revision"];
185
+ readonly group: 'people';
186
+ };
187
+ readonly 'playbooks.approved': {
188
+ readonly description: 'HR approved a person’s playbook steps';
189
+ readonly fields: readonly ["enrollment_id"];
190
+ readonly group: 'playbook';
191
+ };
192
+ readonly 'playbooks.buddy.accepted': {
193
+ readonly description: 'A buddy said yes';
194
+ readonly fields: readonly ["enrollment_id"];
195
+ readonly group: 'playbook';
196
+ };
197
+ readonly 'playbooks.buddy.assigned': {
198
+ readonly description: 'A buddy was asked to help a new hire';
199
+ readonly fields: readonly ["enrollment_id", "buddy_person_id"];
200
+ readonly group: 'playbook';
201
+ };
202
+ readonly 'playbooks.buddy.declined': {
203
+ readonly description: 'A buddy said no';
204
+ readonly fields: readonly ["enrollment_id"];
205
+ readonly group: 'playbook';
206
+ };
207
+ readonly 'playbooks.check_in.answered': {
208
+ readonly description: 'A new hire answered a check-in: all_good, or need_help when they asked HR for help';
209
+ readonly fields: readonly ["step_id", "enrollment_id", "answer"];
210
+ readonly group: 'playbook';
211
+ };
212
+ readonly 'playbooks.enrolled': {
213
+ readonly description: 'A person started a playbook';
214
+ readonly fields: readonly ["enrollment_id", "person_id", "program_id", "with_buddy"];
215
+ readonly group: 'playbook';
216
+ };
217
+ readonly 'playbooks.enrollment_removed': {
218
+ readonly description: 'A person was taken off a playbook';
219
+ readonly fields: readonly ["enrollment_id", "reason"];
220
+ readonly group: 'playbook';
221
+ };
222
+ readonly 'playbooks.schedule_changed': {
223
+ readonly description: 'A person’s playbook schedule changed';
224
+ readonly fields: readonly ["enrollment_id"];
225
+ readonly group: 'playbook';
226
+ };
227
+ readonly 'playbooks.step.failed': {
228
+ readonly description: 'A playbook step could not be delivered';
229
+ readonly fields: readonly ["step_id", "reason"];
230
+ readonly group: 'playbook';
231
+ };
232
+ readonly 'playbooks.step.needs_review': {
233
+ readonly description: 'A playbook step waits for HR before it sends';
234
+ readonly fields: readonly ["step_id"];
235
+ readonly group: 'playbook';
236
+ };
237
+ readonly 'playbooks.step.sent': {
238
+ readonly description: 'A playbook step reached its person';
239
+ readonly fields: readonly ["step_id"];
240
+ readonly group: 'playbook';
241
+ };
242
+ readonly 'program.archived': {
243
+ readonly description: 'A program was archived';
244
+ readonly fields: readonly ["program_id"];
245
+ readonly group: 'connections';
246
+ };
247
+ readonly 'program.created': {
248
+ readonly description: 'A Connections program or playbook was created';
249
+ readonly fields: readonly ["program_id", "kind"];
250
+ readonly group: 'connections';
251
+ };
252
+ readonly 'program.paused': {
253
+ readonly description: 'A program was paused';
254
+ readonly fields: readonly ["program_id"];
255
+ readonly group: 'connections';
256
+ };
257
+ readonly 'program.resumed': {
258
+ readonly description: 'A program was switched on';
259
+ readonly fields: readonly ["program_id"];
260
+ readonly group: 'connections';
261
+ };
262
+ readonly 'program.updated': {
263
+ readonly description: 'A program’s settings changed';
264
+ readonly fields: readonly ["program_id", "kind"];
265
+ readonly group: 'connections';
266
+ };
267
+ readonly 'round.planned': {
268
+ readonly description: 'Shuffl planned a round of introductions';
269
+ readonly fields: readonly ["round_id", "groups", "unmatched"];
270
+ readonly group: 'connections';
271
+ };
272
+ readonly 'round.requested': {
273
+ readonly description: 'Someone asked for a round of introductions';
274
+ readonly fields: readonly ["round_id", "retry"];
275
+ readonly group: 'connections';
276
+ };
277
+ readonly 'slack.connected': {
278
+ readonly description: 'Slack was connected';
279
+ readonly fields: readonly [];
280
+ readonly group: 'workspace';
281
+ };
282
+ readonly 'slack.connection_expired': {
283
+ readonly description: 'Slack stopped accepting Shuffl’s connection';
284
+ readonly fields: readonly ["reason"];
285
+ readonly group: 'workspace';
286
+ };
287
+ readonly 'slack.uninstalled': {
288
+ readonly description: 'Shuffl was removed from Slack';
289
+ readonly fields: readonly ["reason"];
290
+ readonly group: 'workspace';
291
+ };
292
+ readonly 'webhook.endpoint.disabled': {
293
+ readonly description: 'Shuffl turned an endpoint off after it kept failing';
294
+ readonly fields: readonly ["endpoint_id", "reason"];
295
+ readonly group: 'webhooks';
296
+ };
297
+ readonly 'webhook.test': {
298
+ readonly description: 'A test event someone sent from Settings or the API';
299
+ readonly fields: readonly ["endpoint_id"];
300
+ readonly group: 'webhooks';
301
+ };
302
+ readonly 'workspace.deletion_requested': {
303
+ readonly description: 'An owner asked to delete the workspace';
304
+ readonly fields: readonly ["delete_after"];
305
+ readonly group: 'workspace';
306
+ };
307
+ readonly 'workspace.exported': {
308
+ readonly description: 'Someone exported the workspace’s data';
309
+ readonly fields: readonly [];
310
+ readonly group: 'workspace';
311
+ };
312
+ readonly 'workspace.renamed': {
313
+ readonly description: 'The workspace was renamed';
314
+ readonly fields: readonly [];
315
+ readonly group: 'workspace';
316
+ };
317
+ readonly 'workspace.restored': {
318
+ readonly description: 'The workspace was restored';
319
+ readonly fields: readonly ["deletion_canceled"];
320
+ readonly group: 'workspace';
321
+ };
322
+ readonly 'workspace.slack_join_access_changed': {
323
+ readonly description: 'Who can join from Slack changed';
324
+ readonly fields: readonly [];
325
+ readonly group: 'workspace';
326
+ };
327
+ };
328
+ export type WebhookEventType = keyof typeof webhookEventCatalog;
329
+ export declare const webhookEventTypes: WebhookEventType[];
330
+ export declare const isWebhookEventType: (value: string) => value is WebhookEventType;
331
+ /** Keys no payload may ever carry. A test walks the catalog against it. */
332
+ export declare const webhookNeverSendFields: readonly ['answer_text', 'body', 'comment', 'bio', 'email', 'feedback', 'message', 'name', 'note', 'previous_name', 'question', 'slack_team_id', 'slack_user_id', 'subject', 'text', 'title', 'token', 'secret'];
333
+ type Fields<T extends WebhookEventType> = (typeof webhookEventCatalog)[T]['fields'][number];
334
+ export type WebhookEventData<T extends WebhookEventType = WebhookEventType> = {
335
+ [K in Fields<T>]?: string | number | boolean | null;
336
+ };
337
+ export type WebhookActor = {
338
+ type: 'user' | 'system';
339
+ id: string | null;
340
+ };
341
+ export type WebhookEvent<T extends WebhookEventType = WebhookEventType> = {
342
+ [K in T]: {
343
+ id: string;
344
+ type: K;
345
+ api_version: string;
346
+ created_at: string;
347
+ workspace_id: string;
348
+ actor: WebhookActor;
349
+ data: WebhookEventData<K>;
350
+ };
351
+ }[T];
352
+ export declare const webhookSecretPrefix = "whsec_";
353
+ export declare const webhookToleranceSeconds = 300;
354
+ /** The `v1,<signature>` value for one secret: base64 HMAC-SHA256 of
355
+ * `id.timestamp.body`, as Standard Webhooks specifies. */
356
+ export declare function signWebhook(secret: string, id: string, timestamp: number, body: string): Promise<string>;
357
+ export declare class WebhookVerificationError extends Error {
358
+ name: string;
359
+ }
360
+ type HeaderSource = Headers | Record<string, string | string[] | undefined | null>;
361
+ /** Checks a delivery's Standard Webhooks headers against the endpoint's
362
+ * signing secret (or several while one is rolled) and returns the parsed
363
+ * event. Pass the raw request body, before any JSON parsing. Throws
364
+ * WebhookVerificationError for a missing header, a timestamp more than
365
+ * five minutes away or a signature that matches no secret. */
366
+ export declare function verifyWebhook<T extends WebhookEventType = WebhookEventType>(input: {
367
+ body: string;
368
+ headers: HeaderSource;
369
+ secret: string | readonly string[];
370
+ toleranceSeconds?: number;
371
+ now?: Date;
372
+ }): Promise<WebhookEvent<T>>;
373
+ export {};