@whooshbang/contracts 1.0.0-rc.19 → 1.0.0-rc.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/contract-manifest.json +46 -32
  3. package/dist/generated/artifact-index.d.ts +34 -34
  4. package/dist/generated/artifact-index.js +46 -32
  5. package/dist/generated/artifact-index.js.map +1 -1
  6. package/dist/generated/validators-standalone.d.ts +384 -254
  7. package/dist/generated/validators-standalone.js +25421 -15666
  8. package/dist/generated/validators-standalone.js.map +1 -1
  9. package/dist/generated/validators.d.ts +11 -1
  10. package/dist/generated/validators.js +132 -31
  11. package/dist/generated/validators.js.map +1 -1
  12. package/dist/generated/whooshbang-v1.d.ts +836 -9
  13. package/dist/index.d.ts +6 -2
  14. package/dist/index.js +1 -1
  15. package/dist/index.js.map +1 -1
  16. package/fixtures/core-v1/fixture-index.json +110 -1
  17. package/fixtures/core-v1/problems/credential-account-mismatch.json +9 -0
  18. package/fixtures/core-v1/requests/create-account-credential.json +4 -0
  19. package/fixtures/core-v1/requests/create-connection-test.json +3 -0
  20. package/fixtures/core-v1/requests/rotate-account-credential.json +3 -0
  21. package/fixtures/core-v1/scenarios/api-credential-invalid-indistinguishable.json +1 -1
  22. package/fixtures/core-v1/scenarios/api-credential-one-time-reveal.json +1 -1
  23. package/fixtures/core-v1/scenarios/api-credential-revocation-replay.json +1 -1
  24. package/fixtures/core-v1/scenarios/api-credential-rotation-overlap.json +1 -1
  25. package/fixtures/core-v1/scenarios/cancellation-too-late.json +1 -1
  26. package/fixtures/core-v1/scenarios/cross-account-project.json +1 -1
  27. package/fixtures/core-v1/scenarios/cross-project-resource.json +1 -1
  28. package/fixtures/core-v1/scenarios/environment-mismatch.json +1 -1
  29. package/fixtures/core-v1/scenarios/idempotency-conflict.json +1 -1
  30. package/fixtures/core-v1/scenarios/idempotency-equivalent-replay.json +1 -1
  31. package/fixtures/core-v1/scenarios/malformed-bearer.json +1 -1
  32. package/fixtures/core-v1/scenarios/metadata-overrun.json +1 -1
  33. package/fixtures/core-v1/scenarios/missing-scope.json +1 -1
  34. package/fixtures/core-v1/scenarios/project-create-equivalent-replay.json +1 -1
  35. package/fixtures/core-v1/scenarios/project-slug-conflict.json +1 -1
  36. package/fixtures/core-v1/scenarios/provider-outcome-unknown.json +1 -1
  37. package/fixtures/core-v1/scenarios/subscription-activation-replay.json +1 -1
  38. package/fixtures/core-v1/scenarios/subscription-authorization-capability-opacity.json +1 -1
  39. package/fixtures/core-v1/scenarios/subscription-cross-connection-callback-rejected.json +1 -1
  40. package/fixtures/core-v1/scenarios/subscription-expired-replay.json +1 -1
  41. package/fixtures/core-v1/scenarios/subscription-selected-connection-replay.json +1 -1
  42. package/fixtures/core-v1/success/account-credential-collection.json +17 -0
  43. package/fixtures/core-v1/success/account-credential-created.json +12 -0
  44. package/fixtures/core-v1/success/account-credential-revoked.json +11 -0
  45. package/fixtures/core-v1/success/account-credential-rotated.json +25 -0
  46. package/fixtures/core-v1/success/channel-connection-slack-customer-active.json +31 -0
  47. package/fixtures/core-v1/success/channel-connection-slack-customer-pending.json +36 -0
  48. package/fixtures/core-v1/success/connection-test-collection.json +39 -0
  49. package/fixtures/core-v1/success/connection-test-pending.json +16 -0
  50. package/fixtures/core-v1/success/connection-test-succeeded.json +20 -0
  51. package/fixtures/core-v1/success/customer-owned-slack-app-manifest.json +53 -0
  52. package/fixtures/interaction-machine-v1/scenarios/ack-conflict.json +1 -1
  53. package/fixtures/interaction-machine-v1/scenarios/ack-cross-machine.json +1 -1
  54. package/fixtures/interaction-machine-v1/scenarios/ack-out-of-order.json +1 -1
  55. package/fixtures/interaction-machine-v1/scenarios/ack-replay.json +1 -1
  56. package/fixtures/interaction-machine-v1/scenarios/ambiguous-free-text.json +1 -1
  57. package/fixtures/interaction-machine-v1/scenarios/callback-token-opacity.json +1 -1
  58. package/fixtures/interaction-machine-v1/scenarios/credential-registration-conflict.json +1 -1
  59. package/fixtures/interaction-machine-v1/scenarios/credential-registration-replay.json +1 -1
  60. package/fixtures/interaction-machine-v1/scenarios/credential-revocation-replay.json +1 -1
  61. package/fixtures/interaction-machine-v1/scenarios/credential-rotation.json +1 -1
  62. package/fixtures/interaction-machine-v1/scenarios/first-credential-activates.json +1 -1
  63. package/fixtures/interaction-machine-v1/scenarios/interaction-duplicate.json +1 -1
  64. package/fixtures/interaction-machine-v1/scenarios/interaction-expired.json +1 -1
  65. package/fixtures/interaction-machine-v1/scenarios/interaction-mismatch.json +1 -1
  66. package/fixtures/interaction-machine-v1/scenarios/interaction-retired.json +1 -1
  67. package/fixtures/interaction-machine-v1/scenarios/interaction-stale.json +1 -1
  68. package/fixtures/interaction-machine-v1/scenarios/machine-admin-denied.json +1 -1
  69. package/fixtures/interaction-machine-v1/scenarios/machine-bound-message-create.json +1 -1
  70. package/fixtures/interaction-machine-v1/scenarios/machine-cancel-denied.json +1 -1
  71. package/fixtures/interaction-machine-v1/scenarios/machine-crash-before-ack.json +1 -1
  72. package/fixtures/interaction-machine-v1/scenarios/machine-notifier-mismatch.json +1 -1
  73. package/fixtures/interaction-machine-v1/scenarios/machine-other-message-read.json +1 -1
  74. package/fixtures/interaction-machine-v1/scenarios/machine-owned-message-read.json +1 -1
  75. package/fixtures/interaction-machine-v1/scenarios/machine-subscriber-mismatch.json +1 -1
  76. package/fixtures/interaction-machine-v1/scenarios/poll-uncommitted-cursor-rejected.json +1 -1
  77. package/fixtures/interaction-machine-v1/scenarios/project-credential-machine-endpoint-denied.json +1 -1
  78. package/fixtures/interaction-machine-v1/scenarios/quarantine-integrity-rejected.json +1 -1
  79. package/fixtures/interaction-machine-v1/scenarios/quarantine-signature-rejected.json +1 -1
  80. package/fixtures/interaction-machine-v1/scenarios/quarantine-valid.json +1 -1
  81. package/fixtures/interaction-machine-v1/scenarios/revoked-machine-credential.json +1 -1
  82. package/fixtures/recipient-experience-v1/scenarios/membership-broadcast-snapshot.json +1 -1
  83. package/fixtures/recipient-experience-v1/scenarios/preferred-binding-resolution.json +1 -1
  84. package/fixtures/recipient-experience-v1/scenarios/recipient-session-security.json +1 -1
  85. package/fixtures/recipient-experience-v1/scenarios/subscriber-initial-groups-transaction.json +1 -1
  86. package/openapi/generated/whooshbang-v1.bundle.json +1623 -99
  87. package/openapi/whooshbang-v1.yaml +687 -14
  88. package/package.json +1 -1
  89. package/schemas/core-scenario.schema.json +1 -1
  90. package/schemas/events/agent-binding-degraded-v1.schema.json +1 -1
  91. package/schemas/events/interaction-expired-v1.schema.json +1 -1
  92. package/schemas/events/interaction-received-v1.schema.json +1 -1
  93. package/schemas/events/message-delivered-v1.schema.json +1 -1
  94. package/schemas/events/message-expired-v1.schema.json +1 -1
  95. package/schemas/events/message-failed-v1.schema.json +1 -1
  96. package/schemas/events/message-provider-accepted-v1.schema.json +1 -1
  97. package/schemas/events/subscription-activated-v1.schema.json +1 -1
  98. package/schemas/events/subscription-revoked-v1.schema.json +1 -1
  99. package/schemas/events/whooshbang-event-v1.schema.json +1 -1
  100. package/schemas/fixture-index.schema.json +1 -1
  101. package/schemas/interaction-machine-scenario.schema.json +1 -1
  102. package/schemas/interaction-v1.schema.json +1 -1
  103. package/schemas/machine-v1.schema.json +1 -1
  104. package/schemas/provider-capabilities-v1.schema.json +1 -1
  105. package/schemas/webhooks/webhook-delivery-case-v1.schema.json +1 -1
  106. package/schemas/webhooks/webhook-signing-vector-v1.schema.json +1 -1
  107. package/schemas/whooshbang-v1.schema.json +798 -8
@@ -2,7 +2,7 @@
2
2
  "openapi": "3.1.1",
3
3
  "info": {
4
4
  "title": "WhooshBang API",
5
- "version": "1.0.0-rc.19",
5
+ "version": "1.0.0-rc.21",
6
6
  "description": "The V1 contract for human account/project administration, project-scoped\nsubscription links, and asynchronous text messages. A Clerk session selects\none account for administration. A project credential selects one account,\nproject, and `test` or `live` environment for product operations; request\nbodies cannot override either authenticated scope.\n\nA person's OAuth grant selects one account and carries scopes, but no\nproject. It names the project and environment it acts in through the URL\npath, on the environment-scoped form of each operation; the service checks\nthat path against the granting person's membership on every call. The flat\nforms remain credential-only and are unchanged.\n\nResource identifiers are opaque and are not authorization. Reads outside the\ncredential scope return the same `404` shape as absent resources. Provider\ncredentials, raw Telegram identity, and protected provider responses are\nnever part of this API.\n",
7
7
  "termsOfService": "https://flowxo.com/terms",
8
8
  "contact": {
@@ -32,6 +32,10 @@
32
32
  "name": "API credentials",
33
33
  "description": "Human-administered, environment-scoped bearer credentials with one-time secret reveal."
34
34
  },
35
+ {
36
+ "name": "Account credentials",
37
+ "description": "Human-administered, account-scoped bearer credentials that name their project and environment per call."
38
+ },
35
39
  {
36
40
  "name": "Subscription links",
37
41
  "description": "Single-use, expiring Telegram authorization links."
@@ -119,13 +123,299 @@
119
123
  }
120
124
  }
121
125
  },
126
+ "/v1/account/credentials": {
127
+ "get": {
128
+ "operationId": "listAccountCredentials",
129
+ "summary": "List safe account credential metadata",
130
+ "description": "Returns only credentials owned by the account selected by the verified\nClerk organization session. Secrets and digest material are never\nreturned.\n",
131
+ "tags": ["Account credentials"],
132
+ "security": [{ "clerkSession": [] }],
133
+ "x-account-membership": "required",
134
+ "parameters": [
135
+ { "$ref": "#/components/parameters/ApiCredentialAfterCursor" },
136
+ { "$ref": "#/components/parameters/ApiCredentialPageLimit" },
137
+ { "$ref": "#/components/parameters/PageNumber" }
138
+ ],
139
+ "responses": {
140
+ "200": {
141
+ "description": "A stable page of safe account credential metadata.",
142
+ "headers": {
143
+ "WhooshBang-Diagnostic-Id": {
144
+ "$ref": "#/components/headers/DiagnosticId"
145
+ }
146
+ },
147
+ "content": {
148
+ "application/json": {
149
+ "schema": {
150
+ "$ref": "#/components/schemas/AccountCredentialCollection"
151
+ },
152
+ "examples": {
153
+ "accountCredentials": {
154
+ "value": {
155
+ "items": [
156
+ {
157
+ "id": "acred_AAAAAAAAAAAAAAAAAAAAAA",
158
+ "prefix": "wb_ac1.acred_AAAAAAAA",
159
+ "label": "Synthetic release automation",
160
+ "scopes": [
161
+ "account:read",
162
+ "messages:write",
163
+ "messages:read"
164
+ ],
165
+ "created_by": "member_synthetic_001",
166
+ "created_at": "2026-07-25T17:45:00Z",
167
+ "last_used_at": "2026-07-25T17:50:00Z",
168
+ "status": "active"
169
+ }
170
+ ],
171
+ "page": 1,
172
+ "page_size": 50,
173
+ "total": 1
174
+ }
175
+ }
176
+ }
177
+ }
178
+ }
179
+ },
180
+ "400": { "$ref": "#/components/responses/BadRequest" },
181
+ "401": { "$ref": "#/components/responses/Unauthorized" },
182
+ "403": { "$ref": "#/components/responses/Forbidden" },
183
+ "404": { "$ref": "#/components/responses/NotFound" },
184
+ "429": { "$ref": "#/components/responses/RateLimited" }
185
+ }
186
+ },
187
+ "post": {
188
+ "operationId": "createAccountCredential",
189
+ "summary": "Create an account-scoped API credential",
190
+ "description": "Creates one credential against the whole account. It names no project\nand no environment: a request names them in its path, and the\ncredential's authority on that path is the intersection of its scopes\nwith the project-credential vocabulary, so it is never wider than a\nproject credential would have been for the same project environment.\nThe complete bearer secret appears only in this creation response. It\ncannot be read later and is never replayed from an idempotency cache.\n",
191
+ "tags": ["Account credentials"],
192
+ "security": [{ "clerkSession": [] }],
193
+ "x-account-membership": "required",
194
+ "x-secret-reveal": {
195
+ "responses": 1,
196
+ "replayable": false,
197
+ "later_reads": "metadata_only"
198
+ },
199
+ "requestBody": {
200
+ "required": true,
201
+ "content": {
202
+ "application/json": {
203
+ "schema": {
204
+ "$ref": "#/components/schemas/CreateAccountCredentialRequest"
205
+ },
206
+ "examples": {
207
+ "createAccountCredential": {
208
+ "value": {
209
+ "label": "Release automation",
210
+ "scopes": [
211
+ "account:read",
212
+ "messages:write",
213
+ "messages:read"
214
+ ]
215
+ }
216
+ }
217
+ }
218
+ }
219
+ }
220
+ },
221
+ "responses": {
222
+ "201": {
223
+ "description": "Creation-only secret reveal and safe credential metadata.",
224
+ "headers": {
225
+ "WhooshBang-Diagnostic-Id": {
226
+ "$ref": "#/components/headers/DiagnosticId"
227
+ }
228
+ },
229
+ "content": {
230
+ "application/json": {
231
+ "schema": {
232
+ "$ref": "#/components/schemas/AccountCredentialSecretReveal"
233
+ },
234
+ "examples": {
235
+ "createdAccountCredential": {
236
+ "value": {
237
+ "credential": {
238
+ "id": "acred_AAAAAAAAAAAAAAAAAAAAAA",
239
+ "prefix": "wb_ac1.acred_AAAAAAAA",
240
+ "label": "Synthetic release automation",
241
+ "scopes": [
242
+ "account:read",
243
+ "messages:write",
244
+ "messages:read"
245
+ ],
246
+ "created_by": "member_synthetic_001",
247
+ "created_at": "2026-07-25T17:45:00Z",
248
+ "status": "active"
249
+ },
250
+ "secret": "wb_ac1.acred_AAAAAAAAAAAAAAAAAAAAAA.AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
251
+ }
252
+ }
253
+ }
254
+ }
255
+ }
256
+ },
257
+ "400": { "$ref": "#/components/responses/BadRequest" },
258
+ "401": { "$ref": "#/components/responses/Unauthorized" },
259
+ "403": { "$ref": "#/components/responses/Forbidden" },
260
+ "404": { "$ref": "#/components/responses/NotFound" },
261
+ "429": { "$ref": "#/components/responses/RateLimited" }
262
+ }
263
+ }
264
+ },
265
+ "/v1/account/credentials/{account_credential_id}/rotate": {
266
+ "parameters": [{ "$ref": "#/components/parameters/AccountCredentialId" }],
267
+ "post": {
268
+ "operationId": "rotateAccountCredential",
269
+ "summary": "Rotate an account-scoped API credential",
270
+ "description": "Creates one replacement with the same label and scopes. The complete new\nbearer secret appears only in this response. The replaced credential\nremains active for the explicitly declared overlap, from zero through\n86400 seconds, then becomes terminally revoked. A revoked credential\ncannot be rotated. Missing and out-of-account credentials use the same\nnon-enumerating `404`.\n",
271
+ "tags": ["Account credentials"],
272
+ "security": [{ "clerkSession": [] }],
273
+ "x-account-membership": "required",
274
+ "x-secret-reveal": {
275
+ "responses": 1,
276
+ "replayable": false,
277
+ "later_reads": "metadata_only"
278
+ },
279
+ "requestBody": {
280
+ "required": true,
281
+ "content": {
282
+ "application/json": {
283
+ "schema": {
284
+ "$ref": "#/components/schemas/RotateAccountCredentialRequest"
285
+ },
286
+ "examples": {
287
+ "rotateAccountCredential": {
288
+ "value": { "overlap_seconds": 3600 }
289
+ }
290
+ }
291
+ }
292
+ }
293
+ },
294
+ "responses": {
295
+ "201": {
296
+ "description": "Creation-only replacement secret and safe metadata for both credentials.",
297
+ "headers": {
298
+ "WhooshBang-Diagnostic-Id": {
299
+ "$ref": "#/components/headers/DiagnosticId"
300
+ }
301
+ },
302
+ "content": {
303
+ "application/json": {
304
+ "schema": {
305
+ "$ref": "#/components/schemas/AccountCredentialRotation"
306
+ },
307
+ "examples": {
308
+ "rotatedAccountCredential": {
309
+ "value": {
310
+ "credential": {
311
+ "credential": {
312
+ "id": "acred_BBBBBBBBBBBBBBBBBBBBBA",
313
+ "prefix": "wb_ac1.acred_BBBBBBBB",
314
+ "label": "Synthetic release automation",
315
+ "scopes": [
316
+ "account:read",
317
+ "messages:write",
318
+ "messages:read"
319
+ ],
320
+ "created_by": "member_synthetic_001",
321
+ "created_at": "2026-07-25T17:45:00Z",
322
+ "status": "active"
323
+ },
324
+ "secret": "wb_ac1.acred_BBBBBBBBBBBBBBBBBBBBBA.BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBA"
325
+ },
326
+ "replaced_credential": {
327
+ "id": "acred_AAAAAAAAAAAAAAAAAAAAAA",
328
+ "prefix": "wb_ac1.acred_AAAAAAAA",
329
+ "label": "Synthetic release automation",
330
+ "scopes": [
331
+ "account:read",
332
+ "messages:write",
333
+ "messages:read"
334
+ ],
335
+ "created_by": "member_synthetic_001",
336
+ "created_at": "2026-07-25T17:45:00Z",
337
+ "last_used_at": "2026-07-25T17:50:00Z",
338
+ "scheduled_revocation_at": "2026-07-25T18:45:00Z",
339
+ "status": "active"
340
+ }
341
+ }
342
+ }
343
+ }
344
+ }
345
+ }
346
+ },
347
+ "400": { "$ref": "#/components/responses/BadRequest" },
348
+ "401": { "$ref": "#/components/responses/Unauthorized" },
349
+ "403": { "$ref": "#/components/responses/Forbidden" },
350
+ "404": { "$ref": "#/components/responses/NotFound" },
351
+ "429": { "$ref": "#/components/responses/RateLimited" }
352
+ }
353
+ }
354
+ },
355
+ "/v1/account/credentials/{account_credential_id}/revoke": {
356
+ "parameters": [{ "$ref": "#/components/parameters/AccountCredentialId" }],
357
+ "post": {
358
+ "operationId": "revokeAccountCredential",
359
+ "summary": "Revoke an account-scoped API credential",
360
+ "description": "Immediately and terminally revokes the selected credential. Repeating\nthe request for the same visible credential is safe and returns the\nsame revoked metadata without revealing secret or digest material.\nMissing and out-of-account credentials use the same non-enumerating\n`404`.\n",
361
+ "tags": ["Account credentials"],
362
+ "security": [{ "clerkSession": [] }],
363
+ "x-account-membership": "required",
364
+ "x-repeat-semantics": {
365
+ "same_target": "idempotent",
366
+ "secret_returned": false
367
+ },
368
+ "responses": {
369
+ "200": {
370
+ "description": "Safe terminal credential metadata.",
371
+ "headers": {
372
+ "WhooshBang-Diagnostic-Id": {
373
+ "$ref": "#/components/headers/DiagnosticId"
374
+ }
375
+ },
376
+ "content": {
377
+ "application/json": {
378
+ "schema": { "$ref": "#/components/schemas/AccountCredential" },
379
+ "examples": {
380
+ "revokedAccountCredential": {
381
+ "value": {
382
+ "id": "acred_AAAAAAAAAAAAAAAAAAAAAA",
383
+ "prefix": "wb_ac1.acred_AAAAAAAA",
384
+ "label": "Synthetic release automation",
385
+ "scopes": [
386
+ "account:read",
387
+ "messages:write",
388
+ "messages:read"
389
+ ],
390
+ "created_by": "member_synthetic_001",
391
+ "created_at": "2026-07-25T17:45:00Z",
392
+ "last_used_at": "2026-07-25T17:50:00Z",
393
+ "status": "revoked",
394
+ "revoked_at": "2026-07-25T17:55:00Z"
395
+ }
396
+ }
397
+ }
398
+ }
399
+ }
400
+ },
401
+ "401": { "$ref": "#/components/responses/Unauthorized" },
402
+ "403": { "$ref": "#/components/responses/Forbidden" },
403
+ "404": { "$ref": "#/components/responses/NotFound" },
404
+ "429": { "$ref": "#/components/responses/RateLimited" }
405
+ }
406
+ }
407
+ },
122
408
  "/v1/projects": {
123
409
  "get": {
124
410
  "operationId": "listProjects",
125
411
  "summary": "List projects in the current account",
126
412
  "description": "Returns only projects owned by the account selected by the verified\nClerk organization session. Every project includes its complete test/live\nenvironment and default-notifier bootstrap.\n",
127
413
  "tags": ["Projects"],
128
- "security": [{ "clerkSession": [] }, { "bearerGrant": [] }],
414
+ "security": [
415
+ { "clerkSession": [] },
416
+ { "bearerGrant": [] },
417
+ { "bearerAccountCredential": [] }
418
+ ],
129
419
  "x-required-scopes": ["projects:read"],
130
420
  "x-account-membership": "required",
131
421
  "parameters": [
@@ -308,7 +598,11 @@
308
598
  "summary": "Read one project in the current account",
309
599
  "description": "Returns the complete project bootstrap. An absent project and a project\noutside the authenticated account use the same non-enumerating `404`.\n",
310
600
  "tags": ["Projects"],
311
- "security": [{ "clerkSession": [] }, { "bearerGrant": [] }],
601
+ "security": [
602
+ { "clerkSession": [] },
603
+ { "bearerGrant": [] },
604
+ { "bearerAccountCredential": [] }
605
+ ],
312
606
  "x-required-scopes": ["projects:read"],
313
607
  "x-account-membership": "required",
314
608
  "responses": {
@@ -382,7 +676,11 @@
382
676
  "summary": "List sparse recipient-copy overrides for one project",
383
677
  "description": "Returns only customer-authored overrides for the project in the\nauthenticated account. Missing values are intentionally absent and use\nWhooshBang's complete shipped catalog at render time. An optional\ncanonical language filter preserves the project + language + string\naddress without applying locale negotiation.\n",
384
678
  "tags": ["Projects"],
385
- "security": [{ "clerkSession": [] }, { "bearerGrant": [] }],
679
+ "security": [
680
+ { "clerkSession": [] },
681
+ { "bearerGrant": [] },
682
+ { "bearerCredential": [] }
683
+ ],
386
684
  "x-required-scopes": ["projects:read"],
387
685
  "x-account-membership": "required",
388
686
  "parameters": [
@@ -441,7 +739,11 @@
441
739
  "summary": "Set one project recipient-copy override",
442
740
  "description": "Replaces the customer-authored plain-text value at one exact project +\nlanguage + string address. Key-specific safety validation rejects\nmarkup, links, controls, impersonation, blank text, and oversized text.\n",
443
741
  "tags": ["Projects"],
444
- "security": [{ "clerkSession": [] }, { "bearerGrant": [] }],
742
+ "security": [
743
+ { "clerkSession": [] },
744
+ { "bearerGrant": [] },
745
+ { "bearerCredential": [] }
746
+ ],
445
747
  "x-required-scopes": ["projects:write"],
446
748
  "x-account-membership": "required",
447
749
  "requestBody": {
@@ -482,7 +784,11 @@
482
784
  "summary": "Reset one project recipient-copy override",
483
785
  "description": "Deletes the sparse value at one exact address. Repeating the reset is\nsafe; the shipped catalog is effective immediately whether or not an\noverride existed.\n",
484
786
  "tags": ["Projects"],
485
- "security": [{ "clerkSession": [] }, { "bearerGrant": [] }],
787
+ "security": [
788
+ { "clerkSession": [] },
789
+ { "bearerGrant": [] },
790
+ { "bearerCredential": [] }
791
+ ],
486
792
  "x-required-scopes": ["projects:write"],
487
793
  "x-account-membership": "required",
488
794
  "responses": {
@@ -1047,7 +1353,11 @@
1047
1353
  "summary": "Normalize and retain one environment-owned notification image",
1048
1354
  "description": "Accepts one bounded raw PNG, validates and re-encodes it as canonical\nRGBA PNG bytes, and retains the immutable asset for a 30-day admission\nwindow. The service never fetches a customer URL. An equivalent replay\nreturns the original asset with `200`; a new asset returns `201`.\n",
1049
1355
  "tags": ["Messages"],
1050
- "security": [{ "bearerCredential": [] }, { "bearerGrant": [] }],
1356
+ "security": [
1357
+ { "bearerCredential": [] },
1358
+ { "bearerGrant": [] },
1359
+ { "bearerAccountCredential": [] }
1360
+ ],
1051
1361
  "x-required-scopes": ["messages:write"],
1052
1362
  "x-idempotency": {
1053
1363
  "required": true,
@@ -1134,7 +1444,11 @@
1134
1444
  "summary": "Delete one environment-owned notification image",
1135
1445
  "description": "Immediately prevents new notification references. Repeating deletion\nof an asset already deleted in this exact scope remains successful;\nunknown and foreign identifiers return the same non-enumerating `404`.\nAccepted notifications retain their existing delivery lease.\n",
1136
1446
  "tags": ["Messages"],
1137
- "security": [{ "bearerCredential": [] }, { "bearerGrant": [] }],
1447
+ "security": [
1448
+ { "bearerCredential": [] },
1449
+ { "bearerGrant": [] },
1450
+ { "bearerAccountCredential": [] }
1451
+ ],
1138
1452
  "x-required-scopes": ["messages:write"],
1139
1453
  "x-repeat-semantics": {
1140
1454
  "same_target": "idempotent",
@@ -2270,7 +2584,8 @@
2270
2584
  "security": [
2271
2585
  { "clerkSession": [] },
2272
2586
  { "bearerCredential": [] },
2273
- { "bearerGrant": [] }
2587
+ { "bearerGrant": [] },
2588
+ { "bearerAccountCredential": [] }
2274
2589
  ],
2275
2590
  "x-required-scopes": ["messages:read"],
2276
2591
  "parameters": [
@@ -2343,7 +2658,8 @@
2343
2658
  "security": [
2344
2659
  { "clerkSession": [] },
2345
2660
  { "bearerCredential": [] },
2346
- { "bearerGrant": [] }
2661
+ { "bearerGrant": [] },
2662
+ { "bearerAccountCredential": [] }
2347
2663
  ],
2348
2664
  "x-required-scopes": ["messages:write"],
2349
2665
  "x-idempotency": {
@@ -2476,7 +2792,8 @@
2476
2792
  "security": [
2477
2793
  { "clerkSession": [] },
2478
2794
  { "bearerCredential": [] },
2479
- { "bearerGrant": [] }
2795
+ { "bearerGrant": [] },
2796
+ { "bearerAccountCredential": [] }
2480
2797
  ],
2481
2798
  "x-required-scopes": ["messages:read"],
2482
2799
  "responses": {
@@ -2507,7 +2824,8 @@
2507
2824
  "security": [
2508
2825
  { "clerkSession": [] },
2509
2826
  { "bearerCredential": [] },
2510
- { "bearerGrant": [] }
2827
+ { "bearerGrant": [] },
2828
+ { "bearerAccountCredential": [] }
2511
2829
  ],
2512
2830
  "x-required-scopes": ["messages:write"],
2513
2831
  "responses": {
@@ -2540,7 +2858,8 @@
2540
2858
  "security": [
2541
2859
  { "clerkSession": [] },
2542
2860
  { "bearerCredential": [] },
2543
- { "bearerGrant": [] }
2861
+ { "bearerGrant": [] },
2862
+ { "bearerAccountCredential": [] }
2544
2863
  ],
2545
2864
  "x-required-scopes": ["messages:write"],
2546
2865
  "x-idempotency": {
@@ -2643,7 +2962,8 @@
2643
2962
  "security": [
2644
2963
  { "clerkSession": [] },
2645
2964
  { "bearerCredential": [] },
2646
- { "bearerGrant": [] }
2965
+ { "bearerGrant": [] },
2966
+ { "bearerAccountCredential": [] }
2647
2967
  ],
2648
2968
  "x-required-scopes": ["messages:write"],
2649
2969
  "x-repeat-semantics": {
@@ -2679,7 +2999,8 @@
2679
2999
  "security": [
2680
3000
  { "clerkSession": [] },
2681
3001
  { "bearerCredential": [] },
2682
- { "bearerGrant": [] }
3002
+ { "bearerGrant": [] },
3003
+ { "bearerAccountCredential": [] }
2683
3004
  ],
2684
3005
  "x-required-scopes": ["messages:read"],
2685
3006
  "parameters": [
@@ -2739,7 +3060,8 @@
2739
3060
  "security": [
2740
3061
  { "clerkSession": [] },
2741
3062
  { "bearerCredential": [] },
2742
- { "bearerGrant": [] }
3063
+ { "bearerGrant": [] },
3064
+ { "bearerAccountCredential": [] }
2743
3065
  ],
2744
3066
  "x-required-scopes": ["connections:read"],
2745
3067
  "responses": {
@@ -2813,7 +3135,8 @@
2813
3135
  "security": [
2814
3136
  { "clerkSession": [] },
2815
3137
  { "bearerCredential": [] },
2816
- { "bearerGrant": [] }
3138
+ { "bearerGrant": [] },
3139
+ { "bearerAccountCredential": [] }
2817
3140
  ],
2818
3141
  "x-required-scopes": ["connections:read"],
2819
3142
  "parameters": [
@@ -2883,7 +3206,8 @@
2883
3206
  "security": [
2884
3207
  { "clerkSession": [] },
2885
3208
  { "bearerCredential": [] },
2886
- { "bearerGrant": [] }
3209
+ { "bearerGrant": [] },
3210
+ { "bearerAccountCredential": [] }
2887
3211
  ],
2888
3212
  "x-required-scopes": ["connections:write"],
2889
3213
  "x-idempotency": {
@@ -3072,7 +3396,8 @@
3072
3396
  "security": [
3073
3397
  { "clerkSession": [] },
3074
3398
  { "bearerCredential": [] },
3075
- { "bearerGrant": [] }
3399
+ { "bearerGrant": [] },
3400
+ { "bearerAccountCredential": [] }
3076
3401
  ],
3077
3402
  "x-required-scopes": ["connections:read"],
3078
3403
  "responses": {
@@ -3150,7 +3475,8 @@
3150
3475
  "security": [
3151
3476
  { "clerkSession": [] },
3152
3477
  { "bearerCredential": [] },
3153
- { "bearerGrant": [] }
3478
+ { "bearerGrant": [] },
3479
+ { "bearerAccountCredential": [] }
3154
3480
  ],
3155
3481
  "x-required-scopes": ["connections:write"],
3156
3482
  "requestBody": {
@@ -3202,7 +3528,8 @@
3202
3528
  "security": [
3203
3529
  { "clerkSession": [] },
3204
3530
  { "bearerCredential": [] },
3205
- { "bearerGrant": [] }
3531
+ { "bearerGrant": [] },
3532
+ { "bearerAccountCredential": [] }
3206
3533
  ],
3207
3534
  "x-required-scopes": ["connections:write"],
3208
3535
  "responses": {
@@ -3262,7 +3589,8 @@
3262
3589
  "security": [
3263
3590
  { "clerkSession": [] },
3264
3591
  { "bearerCredential": [] },
3265
- { "bearerGrant": [] }
3592
+ { "bearerGrant": [] },
3593
+ { "bearerAccountCredential": [] }
3266
3594
  ],
3267
3595
  "x-required-scopes": ["connections:write"],
3268
3596
  "responses": {
@@ -3330,7 +3658,8 @@
3330
3658
  "security": [
3331
3659
  { "clerkSession": [] },
3332
3660
  { "bearerCredential": [] },
3333
- { "bearerGrant": [] }
3661
+ { "bearerGrant": [] },
3662
+ { "bearerAccountCredential": [] }
3334
3663
  ],
3335
3664
  "x-required-scopes": ["connections:write"],
3336
3665
  "x-idempotency": {
@@ -3373,13 +3702,14 @@
3373
3702
  ],
3374
3703
  "post": {
3375
3704
  "operationId": "restartChannelConnectionSetup",
3376
- "summary": "Start a fresh managed setup on a connection that never finished one",
3377
- "description": "Issues a new handoff for a `customer_managed` connection whose guided\nsetup expired, failed, or was simply abandoned, and returns the same\nconnection carrying a fresh `setup`. The connection is reused rather\nthan replaced because it is still `pending`: nothing was ever\nprovisioned under it, no identity was claimed, and no subscriber ever\nconsented to anything through it.\n\nRestart takes no body. The suggested identity recorded when the\nconnection was created is what the new attempt proposes again, so a\ncaller resuming a half-finished setup cannot accidentally rename a bot\nan administrator was already looking at. A different suggestion means\na different connection.\n\nRepeating the call is safe. A setup that is still live is returned as\nit stands rather than being replaced, so two dashboard tabs converge\non one session instead of racing to strand each other's link, and only\na session that can no longer be used is exchanged for a new one.\n\nA bot the previous attempt already created is not withdrawn and cannot\nbe: Telegram exposes no way to delete a bot. The administrator keeps it\nin their own account until they retire it in BotFather, and a restart\nwalks them through creating another rather than pretending the first\nnever happened.\n\nRefusals reuse the closed Problem vocabulary and differ by\n`field_errors[].code`:\n\n- the connection did not come from guided setup, so there is no setup\n to restart — `idempotency_conflict` 409, `setup_not_managed` at `/`;\n- the connection already finished setup, or is archived, so restarting\n would offer a second identity for something subscribers may already\n be bound to — `idempotency_conflict` 409,\n `invalid_connection_transition` at `/`;\n- the provider could not be reached, so whether a new session exists is\n genuinely unknown — `provider_outcome_unknown` 502; read the\n connection before calling again;\n- the provider rate-limited the call — `provider_retryable` 503 with\n `retry_at`.\n",
3705
+ "summary": "Start a fresh setup on a connection that never finished one",
3706
+ "description": "Issues a new guided setup for a `customer_managed` connection whose\nsetup expired, failed, or was simply abandoned. Nothing was ever\nprovisioned under it, no identity was claimed, and no subscriber ever\nconsented to anything through it, so nothing is lost.\n\n**Telegram reuses the connection and answers `200`.** A fresh `setup`\nappears on the same resource.\n\n**Customer-owned Slack replaces the connection and answers `201`.** The\nabandoned connection is archived and a new one is returned, carrying a\nnew `app_setup` and a new creation link. The connection cannot be\nreused, because Slack bakes this deployment's request URLs into the app\nat the moment it is created and they are not ours to change afterwards:\na second setup on one connection would mean two Slack apps answering to\none address. **Read `id` on the response** — the connection you were\npolling is now archived. A Slack app the previous attempt already\ncreated is not withdrawn and cannot be; it is the customer's, and they\ndelete it in Slack when they are ready.\n\nEntering the three app-specific values again is not a restart, and does\nnot need one. `rotate-credential` replaces them on the connection that\nalready exists.\n\nRestart takes no body. The suggested identity recorded when the\nconnection was created is what the new attempt proposes again, so a\ncaller resuming a half-finished setup cannot accidentally rename a bot\nan administrator was already looking at. A different suggestion means\na different connection.\n\nRepeating the call is safe. A setup that is still live is returned as\nit stands rather than being replaced, so two dashboard tabs converge\non one session instead of racing to strand each other's link, and only\na session that can no longer be used is exchanged for a new one.\n\nA bot the previous attempt already created is not withdrawn and cannot\nbe: Telegram exposes no way to delete a bot. The administrator keeps it\nin their own account until they retire it in BotFather, and a restart\nwalks them through creating another rather than pretending the first\nnever happened.\n\nRefusals reuse the closed Problem vocabulary and differ by\n`field_errors[].code`:\n\n- the connection did not come from guided setup, so there is no setup\n to restart — `idempotency_conflict` 409, `setup_not_managed` at `/`;\n- the connection already finished setup, or is archived, so restarting\n would offer a second identity for something subscribers may already\n be bound to — `idempotency_conflict` 409,\n `invalid_connection_transition` at `/`;\n- the provider could not be reached, so whether a new session exists is\n genuinely unknown — `provider_outcome_unknown` 502; read the\n connection before calling again;\n- the provider rate-limited the call — `provider_retryable` 503 with\n `retry_at`.\n",
3378
3707
  "tags": ["Channel connections"],
3379
3708
  "security": [
3380
3709
  { "clerkSession": [] },
3381
3710
  { "bearerCredential": [] },
3382
- { "bearerGrant": [] }
3711
+ { "bearerGrant": [] },
3712
+ { "bearerAccountCredential": [] }
3383
3713
  ],
3384
3714
  "x-required-scopes": ["connections:write"],
3385
3715
  "x-repeat-semantics": {
@@ -3427,17 +3757,70 @@
3427
3757
  }
3428
3758
  }
3429
3759
  },
3430
- "401": { "$ref": "#/components/responses/Unauthorized" },
3431
- "403": { "$ref": "#/components/responses/Forbidden" },
3432
- "404": { "$ref": "#/components/responses/NotFound" },
3433
- "409": { "$ref": "#/components/responses/Conflict" },
3434
- "502": { "$ref": "#/components/responses/ProviderOutcomeUnknown" },
3435
- "503": { "$ref": "#/components/responses/ProviderRetryable" }
3436
- }
3437
- }
3438
- },
3439
- "/v1/projects/{project_id}/environments/{environment_id}/channel-connections/{connection_id}/rotate-credential": {
3440
- "parameters": [
3760
+ "201": {
3761
+ "description": "A replacement connection carrying a fresh setup, for a\ncustomer-owned Slack app whose request URLs cannot be re-pointed.\nThe connection named in the request is now archived.\n",
3762
+ "headers": {
3763
+ "WhooshBang-Diagnostic-Id": {
3764
+ "$ref": "#/components/headers/DiagnosticId"
3765
+ }
3766
+ },
3767
+ "content": {
3768
+ "application/json": {
3769
+ "schema": { "$ref": "#/components/schemas/ChannelConnection" },
3770
+ "examples": {
3771
+ "setupReplaced": {
3772
+ "value": {
3773
+ "id": "chcon_77777777777777777777777777777777",
3774
+ "project_id": "project_synthetic_alpha",
3775
+ "environment_id": "environment_test_synthetic_alpha",
3776
+ "environment": "test",
3777
+ "provider": "slack",
3778
+ "mode": "customer_managed",
3779
+ "display_name": "Acme Slack",
3780
+ "status": "pending",
3781
+ "health": "unknown",
3782
+ "installation": {
3783
+ "display_name": "Slack app not yet created",
3784
+ "health": "unknown",
3785
+ "status": "pending"
3786
+ },
3787
+ "capabilities": [],
3788
+ "recovery_action": "complete_authorization",
3789
+ "app_setup": {
3790
+ "status": "awaiting_authorities",
3791
+ "creation_url": "https://api.slack.com/apps?new_app=1&manifest_json=%7B%22_metadata%22%3A%7B%22major_version%22%3A1%7D%7D",
3792
+ "expires_at": "2026-09-01T12:30:00.000Z",
3793
+ "app_name": "Acme Notifications",
3794
+ "bot_display_name": "Acme Notify",
3795
+ "outstanding_authorities": [
3796
+ "client_identity",
3797
+ "client_secret",
3798
+ "signing_secret"
3799
+ ]
3800
+ },
3801
+ "row_version": 1,
3802
+ "created_at": "2026-09-01T12:00:00.000Z",
3803
+ "updated_at": "2026-09-01T12:00:00.000Z",
3804
+ "last_transition_at": "2026-09-01T12:00:00.000Z",
3805
+ "last_transition_reason": "slack_setup_awaiting_authorities",
3806
+ "diagnostic_id": "diag_slack_customer_pending_001"
3807
+ }
3808
+ }
3809
+ }
3810
+ }
3811
+ }
3812
+ },
3813
+ "401": { "$ref": "#/components/responses/Unauthorized" },
3814
+ "403": { "$ref": "#/components/responses/Forbidden" },
3815
+ "404": { "$ref": "#/components/responses/NotFound" },
3816
+ "409": { "$ref": "#/components/responses/Conflict" },
3817
+ "502": { "$ref": "#/components/responses/ProviderOutcomeUnknown" },
3818
+ "503": { "$ref": "#/components/responses/ProviderRetryable" }
3819
+ }
3820
+ }
3821
+ },
3822
+ "/v1/projects/{project_id}/environments/{environment_id}/channel-connections/{connection_id}/rotate-credential": {
3823
+ "parameters": [
3441
3824
  { "$ref": "#/components/parameters/ProjectId" },
3442
3825
  { "$ref": "#/components/parameters/EnvironmentId" },
3443
3826
  { "$ref": "#/components/parameters/ChannelConnectionId" }
@@ -3450,7 +3833,8 @@
3450
3833
  "security": [
3451
3834
  { "clerkSession": [] },
3452
3835
  { "bearerCredential": [] },
3453
- { "bearerGrant": [] }
3836
+ { "bearerGrant": [] },
3837
+ { "bearerAccountCredential": [] }
3454
3838
  ],
3455
3839
  "x-required-scopes": ["connections:write"],
3456
3840
  "requestBody": {
@@ -3561,7 +3945,8 @@
3561
3945
  "security": [
3562
3946
  { "clerkSession": [] },
3563
3947
  { "bearerCredential": [] },
3564
- { "bearerGrant": [] }
3948
+ { "bearerGrant": [] },
3949
+ { "bearerAccountCredential": [] }
3565
3950
  ],
3566
3951
  "x-required-scopes": ["connections:write"],
3567
3952
  "responses": {
@@ -3627,7 +4012,8 @@
3627
4012
  "security": [
3628
4013
  { "clerkSession": [] },
3629
4014
  { "bearerCredential": [] },
3630
- { "bearerGrant": [] }
4015
+ { "bearerGrant": [] },
4016
+ { "bearerAccountCredential": [] }
3631
4017
  ],
3632
4018
  "x-required-scopes": ["connections:write"],
3633
4019
  "responses": {
@@ -3679,6 +4065,352 @@
3679
4065
  }
3680
4066
  }
3681
4067
  },
4068
+ "/v1/projects/{project_id}/environments/{environment_id}/channel-connections/{connection_id}/app-manifest": {
4069
+ "parameters": [
4070
+ { "$ref": "#/components/parameters/ProjectId" },
4071
+ { "$ref": "#/components/parameters/EnvironmentId" },
4072
+ { "$ref": "#/components/parameters/ChannelConnectionId" }
4073
+ ],
4074
+ "get": {
4075
+ "operationId": "getChannelConnectionAppManifest",
4076
+ "summary": "Read the generated app configuration a customer-owned setup will create",
4077
+ "description": "Returns the exact configuration WhooshBang generated for this\ncustomer-owned Slack setup — the scopes it will ask for, the request\nURLs it will point at, the literal manifest document, and the same\nprefilled creation link the setup already carries.\n\nIt exists so the owner can read what they are about to approve, and a\nreviewer can check it, without opening Slack or decoding a URL. It\ncarries configuration only: no account, project, or environment, no\ncredential, and no provider-native identifier.\n\nOnly a `customer_managed` Slack connection has one. Every other\nprovider and mode answers `404`, because a manifest they do not have is\nnot a manifest that is empty.\n",
4078
+ "tags": ["Channel connections"],
4079
+ "security": [
4080
+ { "clerkSession": [] },
4081
+ { "bearerCredential": [] },
4082
+ { "bearerGrant": [] },
4083
+ { "bearerAccountCredential": [] }
4084
+ ],
4085
+ "x-required-scopes": ["connections:read"],
4086
+ "responses": {
4087
+ "200": {
4088
+ "description": "The generated app configuration for exactly this setup.",
4089
+ "headers": {
4090
+ "WhooshBang-Diagnostic-Id": {
4091
+ "$ref": "#/components/headers/DiagnosticId"
4092
+ }
4093
+ },
4094
+ "content": {
4095
+ "application/json": {
4096
+ "schema": {
4097
+ "$ref": "#/components/schemas/CustomerOwnedSlackAppManifest"
4098
+ },
4099
+ "examples": {
4100
+ "manifest": {
4101
+ "value": {
4102
+ "creation_url": "https://api.slack.com/apps?new_app=1&manifest_json=%7B%22_metadata%22%3A%7B%22major_version%22%3A1%7D%7D",
4103
+ "app_name": "Acme Notifications",
4104
+ "bot_display_name": "Acme Notify",
4105
+ "description": "Delivery notifications from Acme.",
4106
+ "bot_scopes": ["chat:write"],
4107
+ "user_scopes": ["openid"],
4108
+ "request_urls": {
4109
+ "events": "https://api.whooshbang.com/internal/webhooks/slack/apps/CTBQNURSpqxDgLcqvbEDcA/events",
4110
+ "interactions": "https://api.whooshbang.com/internal/webhooks/slack/apps/CTBQNURSpqxDgLcqvbEDcA/interactions",
4111
+ "installation_redirect": "https://api.whooshbang.com/v1/provider-authorizations/slack/apps/callback",
4112
+ "recipient_redirect": "https://api.whooshbang.com/v1/provider-authorizations/slack/apps/recipient/callback"
4113
+ },
4114
+ "manifest": {
4115
+ "_metadata": { "major_version": 1, "minor_version": 1 },
4116
+ "display_information": {
4117
+ "name": "Acme Notifications",
4118
+ "description": "Delivery notifications from Acme."
4119
+ },
4120
+ "features": {
4121
+ "app_home": {
4122
+ "home_tab_enabled": true,
4123
+ "messages_tab_enabled": true,
4124
+ "messages_tab_read_only_enabled": true
4125
+ },
4126
+ "bot_user": {
4127
+ "display_name": "Acme Notify",
4128
+ "always_online": false
4129
+ }
4130
+ },
4131
+ "oauth_config": {
4132
+ "pkce_enabled": false,
4133
+ "redirect_urls": [
4134
+ "https://api.whooshbang.com/v1/provider-authorizations/slack/apps/callback",
4135
+ "https://api.whooshbang.com/v1/provider-authorizations/slack/apps/recipient/callback"
4136
+ ],
4137
+ "scopes": {
4138
+ "bot": ["chat:write"],
4139
+ "user": ["openid"]
4140
+ }
4141
+ },
4142
+ "settings": {
4143
+ "event_subscriptions": {
4144
+ "request_url": "https://api.whooshbang.com/internal/webhooks/slack/apps/CTBQNURSpqxDgLcqvbEDcA/events",
4145
+ "bot_events": [
4146
+ "app_home_opened",
4147
+ "app_uninstalled",
4148
+ "tokens_revoked"
4149
+ ]
4150
+ },
4151
+ "interactivity": {
4152
+ "is_enabled": true,
4153
+ "request_url": "https://api.whooshbang.com/internal/webhooks/slack/apps/CTBQNURSpqxDgLcqvbEDcA/interactions"
4154
+ },
4155
+ "is_hosted": false,
4156
+ "is_mcp_enabled": false,
4157
+ "org_deploy_enabled": false,
4158
+ "socket_mode_enabled": false,
4159
+ "token_rotation_enabled": true
4160
+ }
4161
+ },
4162
+ "diagnostic_id": "diag_slack_app_manifest_001"
4163
+ }
4164
+ }
4165
+ }
4166
+ }
4167
+ }
4168
+ },
4169
+ "401": { "$ref": "#/components/responses/Unauthorized" },
4170
+ "403": { "$ref": "#/components/responses/Forbidden" },
4171
+ "404": { "$ref": "#/components/responses/NotFound" }
4172
+ }
4173
+ }
4174
+ },
4175
+ "/v1/projects/{project_id}/environments/{environment_id}/channel-connections/{connection_id}/tests": {
4176
+ "parameters": [
4177
+ { "$ref": "#/components/parameters/ProjectId" },
4178
+ { "$ref": "#/components/parameters/EnvironmentId" },
4179
+ { "$ref": "#/components/parameters/ChannelConnectionId" }
4180
+ ],
4181
+ "get": {
4182
+ "operationId": "listConnectionTests",
4183
+ "summary": "List this connection's recent owner-triggered tests",
4184
+ "description": "Newest first. This is the audit-safe failure history for one\nconnection: what was tried, when, what came back, and what to do about\nit. Every entry is safe to render, log, and paste into an issue.\n",
4185
+ "tags": ["Channel connections"],
4186
+ "security": [
4187
+ { "clerkSession": [] },
4188
+ { "bearerCredential": [] },
4189
+ { "bearerGrant": [] },
4190
+ { "bearerAccountCredential": [] }
4191
+ ],
4192
+ "x-required-scopes": ["connections:read"],
4193
+ "parameters": [
4194
+ { "$ref": "#/components/parameters/ListAfterCursor" },
4195
+ { "$ref": "#/components/parameters/ListPageLimit" },
4196
+ { "$ref": "#/components/parameters/PageNumber" }
4197
+ ],
4198
+ "responses": {
4199
+ "200": {
4200
+ "description": "Tests placed against exactly this connection.",
4201
+ "headers": {
4202
+ "WhooshBang-Diagnostic-Id": {
4203
+ "$ref": "#/components/headers/DiagnosticId"
4204
+ }
4205
+ },
4206
+ "content": {
4207
+ "application/json": {
4208
+ "schema": {
4209
+ "$ref": "#/components/schemas/ConnectionTestCollection"
4210
+ },
4211
+ "examples": {
4212
+ "tests": {
4213
+ "value": {
4214
+ "items": [
4215
+ {
4216
+ "id": "cntest_11111111111111111111111111111111",
4217
+ "connection_id": "chcon_77777777777777777777777777777777",
4218
+ "kind": "signed_interaction",
4219
+ "status": "succeeded",
4220
+ "requested_at": "2026-09-02T09:00:00.000Z",
4221
+ "completed_at": "2026-09-02T09:00:22.000Z",
4222
+ "expires_at": "2026-09-02T09:15:00.000Z",
4223
+ "evidence": {
4224
+ "identity_handle": "Acme Notify",
4225
+ "workspace_display_name": "Acme",
4226
+ "accepted_at": "2026-09-02T09:00:01.000Z",
4227
+ "observed_at": "2026-09-02T09:00:22.000Z",
4228
+ "round_trip_ms": 22000,
4229
+ "signature_verified": true,
4230
+ "ingress_host": "api.whooshbang.com"
4231
+ },
4232
+ "recovery_action": "none",
4233
+ "diagnostic_id": "diag_connection_test_succeeded_001"
4234
+ },
4235
+ {
4236
+ "id": "cntest_22222222222222222222222222222222",
4237
+ "connection_id": "chcon_77777777777777777777777777777777",
4238
+ "kind": "outbound_notification",
4239
+ "status": "failed",
4240
+ "requested_at": "2026-09-02T08:40:00.000Z",
4241
+ "completed_at": "2026-09-02T08:40:02.000Z",
4242
+ "expires_at": "2026-09-02T08:55:00.000Z",
4243
+ "failure_code": "credential_unavailable",
4244
+ "recovery_action": "reinstall",
4245
+ "diagnostic_id": "diag_connection_test_failed_001"
4246
+ }
4247
+ ],
4248
+ "page": 1,
4249
+ "page_size": 25,
4250
+ "total": 2
4251
+ }
4252
+ }
4253
+ }
4254
+ }
4255
+ }
4256
+ },
4257
+ "401": { "$ref": "#/components/responses/Unauthorized" },
4258
+ "403": { "$ref": "#/components/responses/Forbidden" },
4259
+ "404": { "$ref": "#/components/responses/NotFound" }
4260
+ }
4261
+ },
4262
+ "post": {
4263
+ "operationId": "createConnectionTest",
4264
+ "summary": "Prove this exact connection still works",
4265
+ "description": "Runs one test against one connection, on demand, and answers with what\nit observed rather than with a claim.\n\nAvailable on any Slack connection, whether the app is one the customer\nowns or the one WhooshBang operates. Every other provider and mode is\nrefused rather than reporting a pass it never ran.\n\n**A test addresses the workspace member who authorized this exact\nconnection, and nobody else.** For an app the customer owns that is the\nperson who installed it, once, for one connection. The\nWhooshBang-operated app's installation is shared across tenants, so\n\"whoever installed the app\" is not necessarily anybody the calling\ntenant named; what is recorded instead is the member who approved this\nenvironment's own connection, during its own authorization, and each\nlater authorization replaces it. One tenant therefore cannot cause a\nmessage to somebody another tenant authorized.\n\n`outbound_notification` sends one visible private message through this\nexact identity to that member, and settles when the provider accepts\nit. Nobody else can be addressed: a test cannot reach a channel, and it\ncannot reach a subscriber who has not authorized anything.\n\n`signed_interaction` sends that person a message with one button and\nthen waits. It settles when the provider calls this deployment's\ninteractivity URL and the signature over the untouched body verifies\nagainst the secret this connection custodies — the only proof that the\nsigning secret, the request URL, and public reachability are all\ncorrect at once. A window that closes with nothing arriving settles as\n`expired`, which says nobody pressed it, and a request that arrives and\ndoes not verify settles as `failed` with `unverified_request_received`.\n\nThe response is the placed test. An `outbound_notification` is usually\nalready settled when it returns; a `signed_interaction` is `pending`\nuntil somebody presses the button, so read it again or list this\nconnection's tests.\n\n**A provider failure is an answer, not a refusal.** If the connection\nhas not finished setup, or its grant is unavailable, or Slack refuses,\nrate-limits, or cannot be reached, the test is **recorded** and answers\n`201` carrying `status: failed`, a `failure_code` and a\n`recovery_action`. There is no `502` or `503` on this operation, and\nthat is deliberate: a diagnostic that leaves no trace is the one thing\nan audit-safe failure history cannot have, so the failure goes in the\nhistory rather than into a problem document nobody keeps.\n\nThree things are refused outright, because none describes a test that\nran: the connection is archived, it is not a Slack connection, or it is\na WhooshBang-operated Slack connection whose authorization has been\nrevoked — `idempotency_conflict` 409, `invalid_connection_transition`\nat `/`. A connection that has never recorded who authorized it is not\nrefused: it is a recorded failure with\n`verification_target_unknown`, because there is nobody a test could\nhonestly address.\n\nA test never carries customer notification content, never counts as a\ndelivery to a subscriber, and never changes what any subscriber\nreceives.\n",
4266
+ "tags": ["Channel connections"],
4267
+ "security": [
4268
+ { "clerkSession": [] },
4269
+ { "bearerCredential": [] },
4270
+ { "bearerGrant": [] },
4271
+ { "bearerAccountCredential": [] }
4272
+ ],
4273
+ "x-required-scopes": ["connections:write"],
4274
+ "x-idempotency": {
4275
+ "required": true,
4276
+ "retention_seconds": 604800,
4277
+ "scope": ["authenticated_environment", "operation_id", "key"],
4278
+ "equivalence": "exact_request_body",
4279
+ "replay_retention_extends": false
4280
+ },
4281
+ "requestBody": {
4282
+ "required": true,
4283
+ "content": {
4284
+ "application/json": {
4285
+ "schema": {
4286
+ "$ref": "#/components/schemas/CreateConnectionTestRequest"
4287
+ },
4288
+ "examples": {
4289
+ "signedInteraction": {
4290
+ "value": { "kind": "signed_interaction" }
4291
+ }
4292
+ }
4293
+ }
4294
+ }
4295
+ },
4296
+ "responses": {
4297
+ "200": {
4298
+ "description": "The equivalent test an idempotent replay already placed.",
4299
+ "headers": {
4300
+ "WhooshBang-Diagnostic-Id": {
4301
+ "$ref": "#/components/headers/DiagnosticId"
4302
+ }
4303
+ },
4304
+ "content": {
4305
+ "application/json": {
4306
+ "schema": { "$ref": "#/components/schemas/ConnectionTest" }
4307
+ }
4308
+ }
4309
+ },
4310
+ "201": {
4311
+ "description": "The placed test, settled if it could settle immediately.",
4312
+ "headers": {
4313
+ "WhooshBang-Diagnostic-Id": {
4314
+ "$ref": "#/components/headers/DiagnosticId"
4315
+ }
4316
+ },
4317
+ "content": {
4318
+ "application/json": {
4319
+ "schema": { "$ref": "#/components/schemas/ConnectionTest" },
4320
+ "examples": {
4321
+ "pending": {
4322
+ "value": {
4323
+ "id": "cntest_11111111111111111111111111111111",
4324
+ "connection_id": "chcon_77777777777777777777777777777777",
4325
+ "kind": "signed_interaction",
4326
+ "status": "pending",
4327
+ "requested_at": "2026-09-02T09:00:00.000Z",
4328
+ "expires_at": "2026-09-02T09:15:00.000Z",
4329
+ "evidence": {
4330
+ "identity_handle": "Acme Notify",
4331
+ "workspace_display_name": "Acme",
4332
+ "accepted_at": "2026-09-02T09:00:01.000Z",
4333
+ "ingress_host": "api.whooshbang.com"
4334
+ },
4335
+ "recovery_action": "none",
4336
+ "diagnostic_id": "diag_connection_test_pending_001"
4337
+ }
4338
+ }
4339
+ }
4340
+ }
4341
+ }
4342
+ },
4343
+ "400": { "$ref": "#/components/responses/BadRequest" },
4344
+ "401": { "$ref": "#/components/responses/Unauthorized" },
4345
+ "403": { "$ref": "#/components/responses/Forbidden" },
4346
+ "404": { "$ref": "#/components/responses/NotFound" },
4347
+ "409": { "$ref": "#/components/responses/Conflict" }
4348
+ }
4349
+ }
4350
+ },
4351
+ "/v1/projects/{project_id}/environments/{environment_id}/channel-connections/{connection_id}/tests/{test_id}": {
4352
+ "parameters": [
4353
+ { "$ref": "#/components/parameters/ProjectId" },
4354
+ { "$ref": "#/components/parameters/EnvironmentId" },
4355
+ { "$ref": "#/components/parameters/ChannelConnectionId" },
4356
+ { "$ref": "#/components/parameters/ConnectionTestId" }
4357
+ ],
4358
+ "get": {
4359
+ "operationId": "getConnectionTest",
4360
+ "summary": "Read one owner-triggered connection test",
4361
+ "description": "The durable answer to one test. A `signed_interaction` is read again\nhere until it settles; a pending test whose window has closed reads as\n`expired` on the way out rather than waiting for a sweep, so the status\nthe owner sees is the one they actually have.\n",
4362
+ "tags": ["Channel connections"],
4363
+ "security": [
4364
+ { "clerkSession": [] },
4365
+ { "bearerCredential": [] },
4366
+ { "bearerGrant": [] },
4367
+ { "bearerAccountCredential": [] }
4368
+ ],
4369
+ "x-required-scopes": ["connections:read"],
4370
+ "responses": {
4371
+ "200": {
4372
+ "description": "The exact test.",
4373
+ "headers": {
4374
+ "WhooshBang-Diagnostic-Id": {
4375
+ "$ref": "#/components/headers/DiagnosticId"
4376
+ }
4377
+ },
4378
+ "content": {
4379
+ "application/json": {
4380
+ "schema": { "$ref": "#/components/schemas/ConnectionTest" },
4381
+ "examples": {
4382
+ "succeeded": {
4383
+ "value": {
4384
+ "id": "cntest_11111111111111111111111111111111",
4385
+ "connection_id": "chcon_77777777777777777777777777777777",
4386
+ "kind": "signed_interaction",
4387
+ "status": "succeeded",
4388
+ "requested_at": "2026-09-02T09:00:00.000Z",
4389
+ "completed_at": "2026-09-02T09:00:22.000Z",
4390
+ "expires_at": "2026-09-02T09:15:00.000Z",
4391
+ "evidence": {
4392
+ "identity_handle": "Acme Notify",
4393
+ "workspace_display_name": "Acme",
4394
+ "accepted_at": "2026-09-02T09:00:01.000Z",
4395
+ "observed_at": "2026-09-02T09:00:22.000Z",
4396
+ "round_trip_ms": 22000,
4397
+ "signature_verified": true,
4398
+ "ingress_host": "api.whooshbang.com"
4399
+ },
4400
+ "recovery_action": "none",
4401
+ "diagnostic_id": "diag_connection_test_succeeded_001"
4402
+ }
4403
+ }
4404
+ }
4405
+ }
4406
+ }
4407
+ },
4408
+ "401": { "$ref": "#/components/responses/Unauthorized" },
4409
+ "403": { "$ref": "#/components/responses/Forbidden" },
4410
+ "404": { "$ref": "#/components/responses/NotFound" }
4411
+ }
4412
+ }
4413
+ },
3682
4414
  "/v1/projects/{project_id}/environments/{environment_id}/notifier-connections": {
3683
4415
  "parameters": [
3684
4416
  { "$ref": "#/components/parameters/ProjectId" },
@@ -3692,7 +4424,8 @@
3692
4424
  "security": [
3693
4425
  { "clerkSession": [] },
3694
4426
  { "bearerCredential": [] },
3695
- { "bearerGrant": [] }
4427
+ { "bearerGrant": [] },
4428
+ { "bearerAccountCredential": [] }
3696
4429
  ],
3697
4430
  "x-required-scopes": ["connections:read"],
3698
4431
  "parameters": [
@@ -3750,7 +4483,8 @@
3750
4483
  "security": [
3751
4484
  { "clerkSession": [] },
3752
4485
  { "bearerCredential": [] },
3753
- { "bearerGrant": [] }
4486
+ { "bearerGrant": [] },
4487
+ { "bearerAccountCredential": [] }
3754
4488
  ],
3755
4489
  "x-required-scopes": ["connections:write"],
3756
4490
  "requestBody": {
@@ -3831,7 +4565,8 @@
3831
4565
  "security": [
3832
4566
  { "clerkSession": [] },
3833
4567
  { "bearerCredential": [] },
3834
- { "bearerGrant": [] }
4568
+ { "bearerGrant": [] },
4569
+ { "bearerAccountCredential": [] }
3835
4570
  ],
3836
4571
  "x-required-scopes": ["connections:write"],
3837
4572
  "responses": {
@@ -3883,7 +4618,11 @@
3883
4618
  "operationId": "getProjectRecipientExperience",
3884
4619
  "summary": "Read project-owned recipient brand and fallback locale",
3885
4620
  "tags": ["Recipient experience"],
3886
- "security": [{ "clerkSession": [] }, { "bearerGrant": [] }],
4621
+ "security": [
4622
+ { "clerkSession": [] },
4623
+ { "bearerGrant": [] },
4624
+ { "bearerCredential": [] }
4625
+ ],
3887
4626
  "x-required-scopes": ["projects:read"],
3888
4627
  "x-account-membership": "required",
3889
4628
  "responses": {
@@ -3923,7 +4662,11 @@
3923
4662
  "operationId": "updateProjectRecipientExperience",
3924
4663
  "summary": "Replace project-owned recipient brand and fallback locale",
3925
4664
  "tags": ["Recipient experience"],
3926
- "security": [{ "clerkSession": [] }, { "bearerGrant": [] }],
4665
+ "security": [
4666
+ { "clerkSession": [] },
4667
+ { "bearerGrant": [] },
4668
+ { "bearerCredential": [] }
4669
+ ],
3927
4670
  "x-required-scopes": ["projects:write"],
3928
4671
  "x-account-membership": "required",
3929
4672
  "requestBody": {
@@ -3976,7 +4719,11 @@
3976
4719
  "summary": "Normalize, host, and configure one project recipient logo",
3977
4720
  "description": "Accepts only a bounded PNG payload. WhooshBang decodes and re-encodes it, stores only the normalized result, and atomically replaces the project's prior logo. The service never fetches a customer URL.",
3978
4721
  "tags": ["Recipient experience"],
3979
- "security": [{ "clerkSession": [] }, { "bearerGrant": [] }],
4722
+ "security": [
4723
+ { "clerkSession": [] },
4724
+ { "bearerGrant": [] },
4725
+ { "bearerCredential": [] }
4726
+ ],
3980
4727
  "x-required-scopes": ["projects:write"],
3981
4728
  "x-account-membership": "required",
3982
4729
  "parameters": [
@@ -4028,7 +4775,13 @@
4028
4775
  "operationId": "getEnvironmentRecipientExperience",
4029
4776
  "summary": "Read one environment's exact recipient browser policy",
4030
4777
  "tags": ["Recipient experience"],
4031
- "security": [{ "clerkSession": [] }],
4778
+ "security": [
4779
+ { "clerkSession": [] },
4780
+ { "bearerGrant": [] },
4781
+ { "bearerCredential": [] },
4782
+ { "bearerAccountCredential": [] }
4783
+ ],
4784
+ "x-required-scopes": ["projects:read"],
4032
4785
  "x-account-membership": "required",
4033
4786
  "responses": {
4034
4787
  "200": {
@@ -4069,9 +4822,15 @@
4069
4822
  "put": {
4070
4823
  "operationId": "updateEnvironmentRecipientExperience",
4071
4824
  "summary": "Replace one environment's enabled channels and exact browser policy",
4072
- "description": "Origins are exact scheme/host/port values. Return destinations are\nregistered developer keys resolving to exact URLs. Wildcards,\ntemplates, arbitrary recipient-time URLs, and channels not backed by\nan offered compatible connection are refused.\n",
4825
+ "description": "Origins are exact scheme/host/port values, written in any spelling that\nmeans that same origin: a trailing slash, an uppercase host, and an\nexplicitly written default port are accepted and stored in canonical\nform. Return destinations are registered developer keys resolving to\nexact URLs. Wildcards, templates, arbitrary recipient-time URLs, and\nchannels not backed by an offered compatible connection are refused.\n",
4073
4826
  "tags": ["Recipient experience"],
4074
- "security": [{ "clerkSession": [] }],
4827
+ "security": [
4828
+ { "clerkSession": [] },
4829
+ { "bearerGrant": [] },
4830
+ { "bearerCredential": [] },
4831
+ { "bearerAccountCredential": [] }
4832
+ ],
4833
+ "x-required-scopes": ["projects:write"],
4075
4834
  "x-account-membership": "required",
4076
4835
  "requestBody": {
4077
4836
  "required": true,
@@ -5245,7 +6004,7 @@
5245
6004
  "summary": "Create a subscription link in a named project environment",
5246
6005
  "description": "The environment-scoped form of `createSubscriptionLink`, for a caller\nwhose authority selects an account rather than a project. Behavior,\nrequest, and response are identical; the path names the scope the\ncredential form takes from its bearer, and the service checks it\nagainst the granting person's membership before acting.\n",
5247
6006
  "tags": ["Subscription links"],
5248
- "security": [{ "bearerGrant": [] }],
6007
+ "security": [{ "bearerGrant": [] }, { "bearerAccountCredential": [] }],
5249
6008
  "x-required-scopes": ["subscriptions:write"],
5250
6009
  "x-idempotency": {
5251
6010
  "required": true,
@@ -5327,7 +6086,7 @@
5327
6086
  "summary": "Inspect a subscription link in a named project environment",
5328
6087
  "description": "The environment-scoped form of `getSubscriptionLink`. A link outside the\nnamed environment is absent, exactly as it is for a credential.\n",
5329
6088
  "tags": ["Subscription links"],
5330
- "security": [{ "bearerGrant": [] }],
6089
+ "security": [{ "bearerGrant": [] }, { "bearerAccountCredential": [] }],
5331
6090
  "x-required-scopes": ["subscriptions:read"],
5332
6091
  "responses": {
5333
6092
  "200": {
@@ -5397,7 +6156,7 @@
5397
6156
  "summary": "Create an asynchronous message in a named project environment",
5398
6157
  "description": "The environment-scoped form of `createMessage`. The path names the\naccount, project, and environment the credential form takes from its\nbearer; test and live remain a property of the named environment and are\nnever selected by the request body.\n",
5399
6158
  "tags": ["Messages"],
5400
- "security": [{ "bearerGrant": [] }],
6159
+ "security": [{ "bearerGrant": [] }, { "bearerAccountCredential": [] }],
5401
6160
  "x-required-scopes": ["messages:write"],
5402
6161
  "x-idempotency": {
5403
6162
  "required": true,
@@ -5486,7 +6245,7 @@
5486
6245
  { "$ref": "#/components/parameters/ResponseInspectionWait" }
5487
6246
  ],
5488
6247
  "tags": ["Messages"],
5489
- "security": [{ "bearerGrant": [] }],
6248
+ "security": [{ "bearerGrant": [] }, { "bearerAccountCredential": [] }],
5490
6249
  "x-required-scopes": ["messages:read"],
5491
6250
  "responses": {
5492
6251
  "200": {
@@ -5557,7 +6316,7 @@
5557
6316
  { "$ref": "#/components/parameters/ResponseInspectionWait" }
5558
6317
  ],
5559
6318
  "tags": ["Messages"],
5560
- "security": [{ "bearerGrant": [] }],
6319
+ "security": [{ "bearerGrant": [] }, { "bearerAccountCredential": [] }],
5561
6320
  "x-required-scopes": ["messages:read"],
5562
6321
  "responses": {
5563
6322
  "200": {
@@ -5605,7 +6364,7 @@
5605
6364
  "summary": "Create a destination-bound machine client in a named project environment",
5606
6365
  "description": "The environment-scoped form of `createMachineClient`. Scope remains the\nexact bound destination; the path only names where the client is created.\n",
5607
6366
  "tags": ["Machine clients"],
5608
- "security": [{ "bearerGrant": [] }],
6367
+ "security": [{ "bearerGrant": [] }, { "bearerAccountCredential": [] }],
5609
6368
  "x-required-scopes": ["machine-clients:write"],
5610
6369
  "x-idempotency": {
5611
6370
  "required": true,
@@ -5667,7 +6426,74 @@
5667
6426
  "403": { "$ref": "#/components/responses/Forbidden" },
5668
6427
  "404": { "$ref": "#/components/responses/NotFound" },
5669
6428
  "409": { "$ref": "#/components/responses/Conflict" },
5670
- "422": { "$ref": "#/components/responses/Unprocessable" },
6429
+ "422": { "$ref": "#/components/responses/Unprocessable" },
6430
+ "429": { "$ref": "#/components/responses/RateLimited" }
6431
+ }
6432
+ }
6433
+ },
6434
+ "/v1/projects/{project_id}/environments/{environment_id}/machine-clients/{machine_client_id}/credentials": {
6435
+ "parameters": [
6436
+ { "$ref": "#/components/parameters/ProjectId" },
6437
+ { "$ref": "#/components/parameters/EnvironmentId" },
6438
+ { "$ref": "#/components/parameters/MachineClientId" }
6439
+ ],
6440
+ "post": {
6441
+ "operationId": "registerMachineCredentialInEnvironment",
6442
+ "summary": "Register a machine-credential digest in a named project environment",
6443
+ "description": "The environment-scoped form of `registerMachineCredential`. Registers\nonly the credential ID and SHA-256 digest; the path only names where the\nowning machine client lives. Request and response shapes are that\noperation's exactly, including its published examples.\n",
6444
+ "tags": ["Machine clients"],
6445
+ "security": [{ "bearerGrant": [] }, { "bearerAccountCredential": [] }],
6446
+ "x-required-scopes": ["machine-clients:write"],
6447
+ "x-idempotency": {
6448
+ "required": true,
6449
+ "retention_seconds": 604800,
6450
+ "scope": ["authenticated_environment", "operation_id", "key"],
6451
+ "equivalence": "validated_request_after_documented_defaults",
6452
+ "replay_retention_extends": false
6453
+ },
6454
+ "parameters": [{ "$ref": "#/components/parameters/IdempotencyKey" }],
6455
+ "requestBody": {
6456
+ "required": true,
6457
+ "content": {
6458
+ "application/json": {
6459
+ "schema": {
6460
+ "$ref": "#/components/schemas/RegisterMachineCredentialRequest"
6461
+ }
6462
+ }
6463
+ }
6464
+ },
6465
+ "responses": {
6466
+ "200": {
6467
+ "description": "Existing safe credential metadata on an equivalent registration replay; never secret or digest.",
6468
+ "headers": {
6469
+ "WhooshBang-Diagnostic-Id": {
6470
+ "$ref": "#/components/headers/DiagnosticId"
6471
+ }
6472
+ },
6473
+ "content": {
6474
+ "application/json": {
6475
+ "schema": { "$ref": "#/components/schemas/MachineCredential" }
6476
+ }
6477
+ }
6478
+ },
6479
+ "201": {
6480
+ "description": "Newly registered safe credential metadata; never secret or digest.",
6481
+ "headers": {
6482
+ "WhooshBang-Diagnostic-Id": {
6483
+ "$ref": "#/components/headers/DiagnosticId"
6484
+ }
6485
+ },
6486
+ "content": {
6487
+ "application/json": {
6488
+ "schema": { "$ref": "#/components/schemas/MachineCredential" }
6489
+ }
6490
+ }
6491
+ },
6492
+ "400": { "$ref": "#/components/responses/BadRequest" },
6493
+ "401": { "$ref": "#/components/responses/Unauthorized" },
6494
+ "403": { "$ref": "#/components/responses/Forbidden" },
6495
+ "404": { "$ref": "#/components/responses/NotFound" },
6496
+ "409": { "$ref": "#/components/responses/Conflict" },
5671
6497
  "429": { "$ref": "#/components/responses/RateLimited" }
5672
6498
  }
5673
6499
  }
@@ -5682,7 +6508,7 @@
5682
6508
  "summary": "Inspect provider-neutral capabilities for a named project environment",
5683
6509
  "description": "The environment-scoped form of `getCapabilities`. The vocabulary is\nprovider-neutral and identical; the path names the environment whose\nactive degradation is reported.\n",
5684
6510
  "tags": ["Capabilities"],
5685
- "security": [{ "bearerGrant": [] }],
6511
+ "security": [{ "bearerGrant": [] }, { "bearerAccountCredential": [] }],
5686
6512
  "x-required-scopes": ["capabilities:read"],
5687
6513
  "responses": {
5688
6514
  "200": {
@@ -5769,7 +6595,8 @@
5769
6595
  "security": [
5770
6596
  { "clerkSession": [] },
5771
6597
  { "bearerCredential": [] },
5772
- { "bearerGrant": [] }
6598
+ { "bearerGrant": [] },
6599
+ { "bearerAccountCredential": [] }
5773
6600
  ],
5774
6601
  "x-required-scopes": ["subscriptions:write"],
5775
6602
  "x-idempotency": {
@@ -5846,7 +6673,8 @@
5846
6673
  "security": [
5847
6674
  { "clerkSession": [] },
5848
6675
  { "bearerCredential": [] },
5849
- { "bearerGrant": [] }
6676
+ { "bearerGrant": [] },
6677
+ { "bearerAccountCredential": [] }
5850
6678
  ],
5851
6679
  "x-required-scopes": ["subscriptions:read"],
5852
6680
  "responses": {
@@ -5877,7 +6705,8 @@
5877
6705
  "security": [
5878
6706
  { "clerkSession": [] },
5879
6707
  { "bearerCredential": [] },
5880
- { "bearerGrant": [] }
6708
+ { "bearerGrant": [] },
6709
+ { "bearerAccountCredential": [] }
5881
6710
  ],
5882
6711
  "x-required-scopes": ["subscriptions:read"],
5883
6712
  "parameters": [
@@ -5929,7 +6758,8 @@
5929
6758
  "security": [
5930
6759
  { "clerkSession": [] },
5931
6760
  { "bearerCredential": [] },
5932
- { "bearerGrant": [] }
6761
+ { "bearerGrant": [] },
6762
+ { "bearerAccountCredential": [] }
5933
6763
  ],
5934
6764
  "x-required-scopes": ["subscriptions:write"],
5935
6765
  "x-idempotency": {
@@ -6017,7 +6847,8 @@
6017
6847
  "security": [
6018
6848
  { "clerkSession": [] },
6019
6849
  { "bearerCredential": [] },
6020
- { "bearerGrant": [] }
6850
+ { "bearerGrant": [] },
6851
+ { "bearerAccountCredential": [] }
6021
6852
  ],
6022
6853
  "x-required-scopes": ["subscriptions:read"],
6023
6854
  "responses": {
@@ -6042,7 +6873,8 @@
6042
6873
  "security": [
6043
6874
  { "clerkSession": [] },
6044
6875
  { "bearerCredential": [] },
6045
- { "bearerGrant": [] }
6876
+ { "bearerGrant": [] },
6877
+ { "bearerAccountCredential": [] }
6046
6878
  ],
6047
6879
  "x-required-scopes": ["subscriptions:write"],
6048
6880
  "requestBody": {
@@ -6079,7 +6911,8 @@
6079
6911
  "security": [
6080
6912
  { "clerkSession": [] },
6081
6913
  { "bearerCredential": [] },
6082
- { "bearerGrant": [] }
6914
+ { "bearerGrant": [] },
6915
+ { "bearerAccountCredential": [] }
6083
6916
  ],
6084
6917
  "x-required-scopes": ["subscriptions:write"],
6085
6918
  "responses": {
@@ -6121,7 +6954,8 @@
6121
6954
  "security": [
6122
6955
  { "clerkSession": [] },
6123
6956
  { "bearerCredential": [] },
6124
- { "bearerGrant": [] }
6957
+ { "bearerGrant": [] },
6958
+ { "bearerAccountCredential": [] }
6125
6959
  ],
6126
6960
  "x-required-scopes": ["subscriptions:read"],
6127
6961
  "parameters": [
@@ -6221,7 +7055,8 @@
6221
7055
  "security": [
6222
7056
  { "clerkSession": [] },
6223
7057
  { "bearerCredential": [] },
6224
- { "bearerGrant": [] }
7058
+ { "bearerGrant": [] },
7059
+ { "bearerAccountCredential": [] }
6225
7060
  ],
6226
7061
  "x-required-scopes": ["subscriptions:write"],
6227
7062
  "responses": {
@@ -6264,7 +7099,8 @@
6264
7099
  "security": [
6265
7100
  { "clerkSession": [] },
6266
7101
  { "bearerCredential": [] },
6267
- { "bearerGrant": [] }
7102
+ { "bearerGrant": [] },
7103
+ { "bearerAccountCredential": [] }
6268
7104
  ],
6269
7105
  "x-required-scopes": ["subscriptions:write"],
6270
7106
  "responses": {
@@ -6302,7 +7138,8 @@
6302
7138
  "security": [
6303
7139
  { "clerkSession": [] },
6304
7140
  { "bearerCredential": [] },
6305
- { "bearerGrant": [] }
7141
+ { "bearerGrant": [] },
7142
+ { "bearerAccountCredential": [] }
6306
7143
  ],
6307
7144
  "x-required-scopes": ["messages:write"],
6308
7145
  "x-idempotency": {
@@ -6400,7 +7237,8 @@
6400
7237
  "security": [
6401
7238
  { "clerkSession": [] },
6402
7239
  { "bearerCredential": [] },
6403
- { "bearerGrant": [] }
7240
+ { "bearerGrant": [] },
7241
+ { "bearerAccountCredential": [] }
6404
7242
  ],
6405
7243
  "x-required-scopes": ["messages:read"],
6406
7244
  "responses": {
@@ -6459,7 +7297,13 @@
6459
7297
  "type": "http",
6460
7298
  "scheme": "bearer",
6461
7299
  "bearerFormat": "OAuth 2.0 access token",
6462
- "description": "An access token issued by the WhooshBang authorization server for one\nconsenting account member. It selects exactly one account and carries\nthe scopes that member consented to, and never a project: an operation\nnames its project and environment in the path, and the service checks\nboth against the granting person's membership before acting. It can\nhold account-level scopes, which no credential can.\n"
7300
+ "description": "An access token issued by the WhooshBang authorization server for one\nconsenting account member. It selects exactly one account and carries\nthe scopes that member consented to, and never a project: an operation\nnames its project and environment in the path, and the service checks\nboth against the granting person's membership before acting. A project\ncredential can name no project but its own; an account credential names\none per call the same way this does.\n"
7301
+ },
7302
+ "bearerAccountCredential": {
7303
+ "type": "http",
7304
+ "scheme": "bearer",
7305
+ "bearerFormat": "wb_ac1.acred_<16-byte-base64url>.<32-byte-base64url-secret>",
7306
+ "description": "Selects exactly one account and nothing below it. It names no project\nand no environment, so it authenticates only operations whose path does,\nand its authority there is the intersection of its scopes with the\nproject-credential vocabulary — never wider than a project credential\nwould have been for the same project environment. A path naming a\nproject outside its account is refused with `credential_account_mismatch`\nrather than a bare scope failure. `account:read` is the one scope it can\nhold that a project credential cannot, and it authorizes only the\naccount's project directory: listing the account's projects and reading\none, which is the account-wide question a project credential cannot ask\nand the reason a `--project <slug>` selector can work at all. It is\nrequired *in addition to* each of those operations' own\n`x-required-scopes`, never instead of them.\nCredentials cannot be read after creation.\n"
6463
7307
  },
6464
7308
  "bearerCredential": {
6465
7309
  "type": "http",
@@ -6516,6 +7360,13 @@
6516
7360
  "description": "Canonical opaque project credential identity; never a readable secret.",
6517
7361
  "schema": { "$ref": "#/components/schemas/ApiCredentialId" }
6518
7362
  },
7363
+ "AccountCredentialId": {
7364
+ "name": "account_credential_id",
7365
+ "in": "path",
7366
+ "required": true,
7367
+ "description": "Canonical opaque account credential identity; never a readable secret.",
7368
+ "schema": { "$ref": "#/components/schemas/AccountCredentialId" }
7369
+ },
6519
7370
  "ApiCredentialAfterCursor": {
6520
7371
  "name": "after",
6521
7372
  "in": "query",
@@ -6644,6 +7495,13 @@
6644
7495
  "description": "Opaque connection identity; never authorization.",
6645
7496
  "schema": { "$ref": "#/components/schemas/OpaqueIdentifier" }
6646
7497
  },
7498
+ "ConnectionTestId": {
7499
+ "name": "test_id",
7500
+ "in": "path",
7501
+ "required": true,
7502
+ "description": "Opaque identity of one owner-triggered connection test; never authorization.",
7503
+ "schema": { "$ref": "#/components/schemas/OpaqueIdentifier" }
7504
+ },
6647
7505
  "NotifierId": {
6648
7506
  "name": "notifier_id",
6649
7507
  "in": "path",
@@ -6846,7 +7704,10 @@
6846
7704
  },
6847
7705
  "setup_modes": {
6848
7706
  "type": "array",
6849
- "const": ["whooshbang_hosted"]
7707
+ "minItems": 1,
7708
+ "maxItems": 2,
7709
+ "uniqueItems": true,
7710
+ "items": { "enum": ["whooshbang_hosted", "customer_managed"] }
6850
7711
  },
6851
7712
  "lifecycle_operations": {
6852
7713
  "type": "array",
@@ -6880,7 +7741,8 @@
6880
7741
  "description": "One environment's provider-discriminated connection. Credentials and provider-native authority identifiers are structurally absent.",
6881
7742
  "oneOf": [
6882
7743
  { "$ref": "#/components/schemas/TelegramChannelConnection" },
6883
- { "$ref": "#/components/schemas/HostedChannelConnection" }
7744
+ { "$ref": "#/components/schemas/HostedChannelConnection" },
7745
+ { "$ref": "#/components/schemas/CustomerOwnedSlackChannelConnection" }
6884
7746
  ]
6885
7747
  },
6886
7748
  "ChannelConnectionCollection": {
@@ -6990,6 +7852,47 @@
6990
7852
  },
6991
7853
  "additionalProperties": false
6992
7854
  },
7855
+ {
7856
+ "type": "object",
7857
+ "description": "Starts a guided setup in which the customer creates and owns their own Slack app. The connection comes back `pending` with an `app_setup` carrying Slack's own creation link, already filled in with this app's manifest.\n\nThis arm carries no credential field, because at this point no credential exists: Slack issues the app's secrets when the owner presses Create. They arrive later through the write-only rotation boundary, and the bot grant never arrives from the caller at all — WhooshBang obtains it from Slack's installation exchange. The three names here are what the owner's whole workspace will see, which is why the caller chooses them rather than WhooshBang: a generated name would permanently brand a customer's own app with ours, and that is the one thing customer-owned modes exist to prevent.",
7858
+ "required": [
7859
+ "mode",
7860
+ "provider",
7861
+ "display_name",
7862
+ "app_name",
7863
+ "bot_display_name",
7864
+ "app_description"
7865
+ ],
7866
+ "properties": {
7867
+ "mode": { "type": "string", "const": "customer_managed" },
7868
+ "provider": { "type": "string", "const": "slack" },
7869
+ "display_name": {
7870
+ "type": "string",
7871
+ "minLength": 1,
7872
+ "maxLength": 100,
7873
+ "description": "What this connection is called inside WhooshBang. Unrelated to what Slack shows the workspace."
7874
+ },
7875
+ "app_name": {
7876
+ "type": "string",
7877
+ "minLength": 1,
7878
+ "maxLength": 35,
7879
+ "description": "The app name Slack's creation dialog is prefilled with, constrained to Slack's own 35-character limit. The owner can still change it in Slack; this is a suggestion, not a reservation."
7880
+ },
7881
+ "bot_display_name": {
7882
+ "type": "string",
7883
+ "minLength": 1,
7884
+ "maxLength": 80,
7885
+ "description": "The bot display name in the same dialog — what a recipient reads at the top of the conversation. Slack's own 80-character limit."
7886
+ },
7887
+ "app_description": {
7888
+ "type": "string",
7889
+ "minLength": 1,
7890
+ "maxLength": 140,
7891
+ "description": "The short description Slack shows beside the app, within Slack's own 140-character limit."
7892
+ }
7893
+ },
7894
+ "additionalProperties": false
7895
+ },
6993
7896
  {
6994
7897
  "type": "object",
6995
7898
  "description": "Starts the hosted authorization for a WhooshBang-owned distributed app installed independently at the provider authority Slack derives during OAuth. The caller supplies neither installation identity nor credential material.",
@@ -7023,8 +7926,43 @@
7023
7926
  "additionalProperties": false
7024
7927
  },
7025
7928
  "RotateChannelConnectionCredentialRequest": {
7026
- "description": "Replaces the secret behind a connection, discriminated by where a replacement can come from at all. A customer-owned bot has one only its owner can produce and therefore sends it; a managed bot has one only Telegram can issue and therefore sends nothing. Either way the connection keeps its provider identity, so a subscriber stays bound to the identity it consented to and only the secret changes.",
7929
+ "description": "Replaces the secret behind a connection, discriminated by where a replacement can come from at all. A customer-owned bot has one only its owner can produce and therefore sends it; a managed bot has one only Telegram can issue and therefore sends nothing. A customer-owned Slack app has three separate authorities and names which of them it is replacing. Either way the connection keeps its provider identity, so a subscriber stays bound to the identity it consented to and only the secret changes.\n\nThe first submission during setup uses this same boundary. Handing a secret over and replacing it are the same operation — a write that never reads the old value back — and giving initial setup its own endpoint would have meant two ways to do one thing, differing only in what the connection's status happened to be.",
7027
7930
  "oneOf": [
7931
+ {
7932
+ "type": "object",
7933
+ "description": "Supplies or replaces the app-specific authorities of a Slack app the customer owns. All three are sent together, both for the first submission during setup and for every later replacement.\n\nSending only the one that changed would read as the kinder API, and it is the wrong shape: the three are sealed as one versioned set, so replacing one alone would mean decrypting the other two and writing them back — reading secrets at a boundary whose whole purpose is that nothing reads them. Slack shows all three on one screen, so re-entering them costs the owner a copy each and costs the system no read at all.\n\n`client_identity` is safe metadata and is echoed back; the other two are write-only and are never readable afterwards by any surface.\n\nThere is no bot-token field here, and the absence is the point: WhooshBang obtains the bot grant from Slack's installation exchange, so it never passes through a browser, a clipboard, this request, or any response.",
7934
+ "required": [
7935
+ "provider",
7936
+ "client_identity",
7937
+ "client_secret",
7938
+ "signing_secret"
7939
+ ],
7940
+ "properties": {
7941
+ "provider": { "type": "string", "const": "slack" },
7942
+ "client_identity": {
7943
+ "type": "string",
7944
+ "minLength": 7,
7945
+ "maxLength": 66,
7946
+ "pattern": "^[0-9]{3,32}\\.[0-9]{3,32}$",
7947
+ "description": "The app's Client ID, in Slack's own `<digits>.<digits>` shape. Safe app metadata rather than an authority: it identifies the app in an OAuth redirect the workspace can already see, so unlike the other two it is echoed back."
7948
+ },
7949
+ "client_secret": {
7950
+ "type": "string",
7951
+ "minLength": 1,
7952
+ "maxLength": 512,
7953
+ "writeOnly": true,
7954
+ "description": "The app's Client Secret. Write-only: sealed on arrival, never echoed here or by any later read, and reachable only by the installation and identity exchanges — delivery cannot obtain it, and holding it sends no message."
7955
+ },
7956
+ "signing_secret": {
7957
+ "type": "string",
7958
+ "minLength": 1,
7959
+ "maxLength": 512,
7960
+ "writeOnly": true,
7961
+ "description": "The app's Signing Secret, which verifies that an inbound request really came from Slack. Write-only on the same terms, and reachable only by ingress verification."
7962
+ }
7963
+ },
7964
+ "additionalProperties": false
7965
+ },
7028
7966
  {
7029
7967
  "type": "object",
7030
7968
  "description": "Replaces the customer-owned credential behind a connection. The connection keeps its provider identity, so a subscriber stays bound to the identity it consented to and only the secret changes; a credential for a different identity is refused.",
@@ -7055,6 +7993,245 @@
7055
7993
  }
7056
7994
  ]
7057
7995
  },
7996
+ "ConnectionTestKind": {
7997
+ "type": "string",
7998
+ "enum": ["outbound_notification", "signed_interaction"],
7999
+ "description": "Which half of a connection a test exercises. `outbound_notification` proves this exact identity can put a message in front of the person who set it up. `signed_interaction` proves the opposite direction: that the provider can reach this deployment's interactivity URL and that the request it signs verifies against the secret we hold. The two fail for entirely different reasons, so they are never collapsed into one result.",
8000
+ "$comment": "Provider-neutral vocabulary. Slack is the only provider that implements either today; a provider that cannot run one refuses it rather than reporting a pass it never proved."
8001
+ },
8002
+ "ConnectionTestRecoveryAction": {
8003
+ "type": "string",
8004
+ "enum": [
8005
+ "none",
8006
+ "complete_setup",
8007
+ "replace_credentials",
8008
+ "reinstall",
8009
+ "retry_later",
8010
+ "check_request_url",
8011
+ "contact_support"
8012
+ ],
8013
+ "description": "The exact safe next action for one test result, in product vocabulary.\n\nIt is a different vocabulary from a connection's own recovery action because the actions genuinely differ: a test that nothing answered is fixed by checking the request URL the provider was given, and a request that arrived and did not verify is fixed by re-entering the app's secrets — neither of which is a lifecycle transition on the connection.\n\n`check_request_url` is also the honest answer when nobody simply pressed the button: the window closed with nothing arriving, and that has exactly two causes."
8014
+ },
8015
+ "ConnectionTestStatus": {
8016
+ "type": "string",
8017
+ "enum": ["pending", "succeeded", "failed", "expired"],
8018
+ "description": "`pending` means the test is placed and its answer has not arrived; a signed-interaction test stays there until somebody presses the button it sent. `expired` is the honest reading of a pending test whose window closed: nothing failed, nobody answered."
8019
+ },
8020
+ "ConnectionTestFailureCode": {
8021
+ "type": "string",
8022
+ "enum": [
8023
+ "setup_incomplete",
8024
+ "verification_target_unknown",
8025
+ "credential_unavailable",
8026
+ "provider_rejected",
8027
+ "provider_unavailable",
8028
+ "provider_rate_limited",
8029
+ "unverified_request_received"
8030
+ ],
8031
+ "description": "Why a test did not pass, in a vocabulary the owner can act on rather than the provider's wire error.\n\n`verification_target_unknown` means nobody has installed the app from this deployment yet, so there is no owner to send a test to. `unverified_request_received` is a statement of fact and not a diagnosis: a request carrying this test's own challenge reached the interactivity URL and its signature did not verify against the secret this connection holds.\n\nA window that closed with nothing arriving is not in this list, because it is not a failure: it reads as `expired`, which says nobody answered rather than that anything broke."
8032
+ },
8033
+ "ConnectionTestEvidence": {
8034
+ "type": "object",
8035
+ "description": "What a finished test actually observed. Every field is safe to render, log, and paste into an issue: no credential, no provider-native app, workspace, channel, or user identifier, and no message content.",
8036
+ "properties": {
8037
+ "identity_handle": {
8038
+ "type": "string",
8039
+ "minLength": 1,
8040
+ "maxLength": 128,
8041
+ "description": "The visible handle the test went out as — the same identity a subscriber would see."
8042
+ },
8043
+ "workspace_display_name": {
8044
+ "type": "string",
8045
+ "minLength": 1,
8046
+ "maxLength": 100,
8047
+ "description": "The provider workspace the test ran in, as its own members see it named."
8048
+ },
8049
+ "accepted_at": {
8050
+ "$ref": "#/components/schemas/Timestamp",
8051
+ "description": "When the provider accepted the outbound message. Acceptance is the provider's own answer, not our belief."
8052
+ },
8053
+ "observed_at": {
8054
+ "$ref": "#/components/schemas/Timestamp",
8055
+ "description": "When the signed request the test was waiting for arrived and verified."
8056
+ },
8057
+ "round_trip_ms": {
8058
+ "type": "integer",
8059
+ "minimum": 0,
8060
+ "maximum": 86400000,
8061
+ "description": "Milliseconds from placing the test to observing its answer."
8062
+ },
8063
+ "signature_verified": {
8064
+ "type": "boolean",
8065
+ "description": "Whether the provider's signature over the untouched request body verified against the secret this connection custodies. Present only once a request has arrived."
8066
+ },
8067
+ "ingress_host": {
8068
+ "type": "string",
8069
+ "minLength": 1,
8070
+ "maxLength": 253,
8071
+ "description": "The public host the provider actually called, which is what a misconfigured or stale request URL gets wrong. It is this deployment's own hostname and names no tenant."
8072
+ }
8073
+ },
8074
+ "additionalProperties": false
8075
+ },
8076
+ "ConnectionTest": {
8077
+ "type": "object",
8078
+ "description": "One owner-triggered proof that an exact connection still works, run on demand and answered with evidence rather than with a claim.\n\nIt is deliberately incapable of carrying a secret: nothing here is a credential, a provider-native identifier, or message content, and a test that failed says why in a closed vocabulary rather than by quoting a provider.",
8079
+ "required": [
8080
+ "id",
8081
+ "connection_id",
8082
+ "kind",
8083
+ "status",
8084
+ "requested_at",
8085
+ "expires_at",
8086
+ "recovery_action",
8087
+ "diagnostic_id"
8088
+ ],
8089
+ "properties": {
8090
+ "id": { "$ref": "#/components/schemas/OpaqueIdentifier" },
8091
+ "connection_id": { "$ref": "#/components/schemas/OpaqueIdentifier" },
8092
+ "kind": { "$ref": "#/components/schemas/ConnectionTestKind" },
8093
+ "status": { "$ref": "#/components/schemas/ConnectionTestStatus" },
8094
+ "requested_at": { "$ref": "#/components/schemas/Timestamp" },
8095
+ "completed_at": {
8096
+ "$ref": "#/components/schemas/Timestamp",
8097
+ "description": "When the test settled. Absent while it is still pending."
8098
+ },
8099
+ "expires_at": {
8100
+ "$ref": "#/components/schemas/Timestamp",
8101
+ "description": "When a pending test stops waiting. A test that reaches this without an answer becomes `expired`, which is not a failure of the connection."
8102
+ },
8103
+ "evidence": { "$ref": "#/components/schemas/ConnectionTestEvidence" },
8104
+ "failure_code": {
8105
+ "$ref": "#/components/schemas/ConnectionTestFailureCode"
8106
+ },
8107
+ "recovery_action": {
8108
+ "$ref": "#/components/schemas/ConnectionTestRecoveryAction",
8109
+ "description": "The exact safe next action for this result. `none` on a pass."
8110
+ },
8111
+ "diagnostic_id": { "$ref": "#/components/schemas/DiagnosticId" }
8112
+ },
8113
+ "additionalProperties": false
8114
+ },
8115
+ "ConnectionTestCollection": {
8116
+ "type": "object",
8117
+ "description": "This connection's recent tests, newest first. It is the audit-safe failure history for a connection: what was tried, when, and what came back.",
8118
+ "required": ["items"],
8119
+ "properties": {
8120
+ "items": {
8121
+ "type": "array",
8122
+ "maxItems": 100,
8123
+ "items": { "$ref": "#/components/schemas/ConnectionTest" }
8124
+ },
8125
+ "next_cursor": {
8126
+ "type": "string",
8127
+ "minLength": 1,
8128
+ "maxLength": 256,
8129
+ "pattern": "^[A-Za-z0-9_-]+$"
8130
+ },
8131
+ "page": { "$ref": "#/components/schemas/ListPageNumber" },
8132
+ "page_size": { "$ref": "#/components/schemas/ListPageSize" },
8133
+ "total": { "$ref": "#/components/schemas/ListTotal" }
8134
+ },
8135
+ "additionalProperties": false
8136
+ },
8137
+ "CreateConnectionTestRequest": {
8138
+ "type": "object",
8139
+ "description": "Places one test against an exact connection. It carries nothing but which test to run: a test needs no target, because the only person it may address is the one who installed the app.",
8140
+ "required": ["kind"],
8141
+ "properties": {
8142
+ "kind": { "$ref": "#/components/schemas/ConnectionTestKind" }
8143
+ },
8144
+ "additionalProperties": false
8145
+ },
8146
+ "CustomerOwnedSlackAppManifest": {
8147
+ "type": "object",
8148
+ "description": "The exact configuration a customer-owned Slack app is created from, as data rather than as a URL-encoded blob.\n\nIt exists so the owner can read what they are about to approve, and so a reviewer can check it, without opening Slack. It carries configuration only: no account, project, or environment, no credential, and no provider-native identifier. The opaque handle inside the request URLs selects candidate setup state and authorizes nothing — a request still has to carry Slack's signature over the untouched body before anything inside it is believed.",
8149
+ "required": [
8150
+ "creation_url",
8151
+ "app_name",
8152
+ "bot_display_name",
8153
+ "description",
8154
+ "bot_scopes",
8155
+ "user_scopes",
8156
+ "request_urls",
8157
+ "manifest",
8158
+ "diagnostic_id"
8159
+ ],
8160
+ "properties": {
8161
+ "creation_url": {
8162
+ "type": "string",
8163
+ "format": "uri",
8164
+ "pattern": "^https://api\\.slack\\.com/apps\\?[^\\s]+$",
8165
+ "maxLength": 8192,
8166
+ "description": "The same prefilled creation link the setup carries, repeated here so one read answers both questions."
8167
+ },
8168
+ "app_name": { "type": "string", "minLength": 1, "maxLength": 35 },
8169
+ "bot_display_name": {
8170
+ "type": "string",
8171
+ "minLength": 1,
8172
+ "maxLength": 80
8173
+ },
8174
+ "description": { "type": "string", "minLength": 1, "maxLength": 140 },
8175
+ "bot_scopes": {
8176
+ "type": "array",
8177
+ "maxItems": 20,
8178
+ "uniqueItems": true,
8179
+ "items": { "type": "string", "minLength": 1, "maxLength": 64 },
8180
+ "description": "Every provider scope the bot will ask for, exactly as Slack will show them. Raw provider vocabulary on purpose: this is the screen the owner is about to approve, and translating it would make it unverifiable."
8181
+ },
8182
+ "user_scopes": {
8183
+ "type": "array",
8184
+ "maxItems": 20,
8185
+ "uniqueItems": true,
8186
+ "items": { "type": "string", "minLength": 1, "maxLength": 64 },
8187
+ "description": "Every provider scope a recipient will be asked for when they sign in. Separate from the bot scopes because they are approved by different people at different moments."
8188
+ },
8189
+ "request_urls": {
8190
+ "type": "object",
8191
+ "description": "Where this deployment answers the app. A stale or edited value here is the failure a signed-interaction test detects.",
8192
+ "required": [
8193
+ "events",
8194
+ "interactions",
8195
+ "installation_redirect",
8196
+ "recipient_redirect"
8197
+ ],
8198
+ "properties": {
8199
+ "events": {
8200
+ "type": "string",
8201
+ "format": "uri",
8202
+ "pattern": "^https://[^\\s]+$",
8203
+ "maxLength": 2048
8204
+ },
8205
+ "interactions": {
8206
+ "type": "string",
8207
+ "format": "uri",
8208
+ "pattern": "^https://[^\\s]+$",
8209
+ "maxLength": 2048
8210
+ },
8211
+ "installation_redirect": {
8212
+ "type": "string",
8213
+ "format": "uri",
8214
+ "pattern": "^https://[^\\s]+$",
8215
+ "maxLength": 2048
8216
+ },
8217
+ "recipient_redirect": {
8218
+ "type": "string",
8219
+ "format": "uri",
8220
+ "pattern": "^https://[^\\s]+$",
8221
+ "maxLength": 2048
8222
+ }
8223
+ },
8224
+ "additionalProperties": false
8225
+ },
8226
+ "manifest": {
8227
+ "type": "object",
8228
+ "description": "The literal manifest document Slack is handed. Reproduced whole so what the owner reviews and what Slack receives cannot drift apart.",
8229
+ "additionalProperties": true
8230
+ },
8231
+ "diagnostic_id": { "$ref": "#/components/schemas/DiagnosticId" }
8232
+ },
8233
+ "additionalProperties": false
8234
+ },
7058
8235
  "NotifierConnectionRoute": {
7059
8236
  "type": "object",
7060
8237
  "description": "One notifier's use of one connection in the same environment. This is the record that decides which identity a send actually leaves through: a connection nothing routes to is configured and inert, and a subscriber cannot authorize against it either. Routing is deliberately separate from creating the connection so that adding a second identity never moves traffic on its own.",
@@ -7421,13 +8598,133 @@
7421
8598
  },
7422
8599
  "additionalProperties": false
7423
8600
  },
7424
- "supported_languages": {
8601
+ "supported_languages": {
8602
+ "type": "array",
8603
+ "description": "Every canonical recipient language independently addressable by the update endpoint.",
8604
+ "minItems": 16,
8605
+ "maxItems": 16,
8606
+ "uniqueItems": true,
8607
+ "items": { "$ref": "#/components/schemas/RecipientLanguage" }
8608
+ },
8609
+ "next_cursor": {
8610
+ "type": "string",
8611
+ "minLength": 1,
8612
+ "maxLength": 256,
8613
+ "pattern": "^[A-Za-z0-9_-]+$"
8614
+ },
8615
+ "page": { "$ref": "#/components/schemas/ListPageNumber" },
8616
+ "page_size": { "$ref": "#/components/schemas/ListPageSize" },
8617
+ "total": { "$ref": "#/components/schemas/ListTotal" }
8618
+ },
8619
+ "additionalProperties": false
8620
+ },
8621
+ "UpdateRecipientCopyOverrideRequest": {
8622
+ "type": "object",
8623
+ "description": "Sets one exact project + language + string value. Delete the same address to restore the shipped default.",
8624
+ "required": ["value"],
8625
+ "properties": {
8626
+ "value": { "$ref": "#/components/schemas/RecipientCopyOverrideValue" }
8627
+ },
8628
+ "additionalProperties": false,
8629
+ "x-max-json-bytes": 2048
8630
+ },
8631
+ "CreateApiCredentialRequest": {
8632
+ "type": "object",
8633
+ "description": "Creates one credential inside the project environment selected by the authenticated administration path.",
8634
+ "required": ["label", "scopes"],
8635
+ "properties": {
8636
+ "label": { "type": "string", "minLength": 1, "maxLength": 100 },
8637
+ "scopes": { "$ref": "#/components/schemas/ApiCredentialScopeSet" }
8638
+ },
8639
+ "additionalProperties": false
8640
+ },
8641
+ "CreateAccountCredentialRequest": {
8642
+ "type": "object",
8643
+ "description": "Creates one credential against the whole account rather than one project environment.",
8644
+ "required": ["label", "scopes"],
8645
+ "properties": {
8646
+ "label": { "type": "string", "minLength": 1, "maxLength": 100 },
8647
+ "scopes": { "$ref": "#/components/schemas/AccountCredentialScopeSet" }
8648
+ },
8649
+ "additionalProperties": false
8650
+ },
8651
+ "RotateAccountCredentialRequest": {
8652
+ "type": "object",
8653
+ "description": "Creates a replacement with the same label and scopes and schedules terminal revocation of the replaced credential after the declared overlap.",
8654
+ "required": ["overlap_seconds"],
8655
+ "properties": {
8656
+ "overlap_seconds": {
8657
+ "type": "integer",
8658
+ "minimum": 0,
8659
+ "maximum": 86400
8660
+ }
8661
+ },
8662
+ "additionalProperties": false
8663
+ },
8664
+ "AccountCredentialId": {
8665
+ "type": "string",
8666
+ "pattern": "^acred_[A-Za-z0-9_-]{21}[AQgw]$",
8667
+ "description": "acred_ followed by the canonical unpadded base64url encoding of exactly 16 random bytes."
8668
+ },
8669
+ "AccountCredential": {
8670
+ "type": "object",
8671
+ "description": "Safe account-scoped API credential metadata. It carries no project or environment, because it names neither: a request does, in its path. It never contains a bearer secret, digest, Clerk identifier, or digest-key reference.",
8672
+ "required": [
8673
+ "id",
8674
+ "prefix",
8675
+ "label",
8676
+ "scopes",
8677
+ "created_by",
8678
+ "created_at",
8679
+ "status"
8680
+ ],
8681
+ "properties": {
8682
+ "id": { "$ref": "#/components/schemas/AccountCredentialId" },
8683
+ "prefix": { "$ref": "#/components/schemas/AccountCredentialPrefix" },
8684
+ "label": { "type": "string", "minLength": 1, "maxLength": 100 },
8685
+ "scopes": {
8686
+ "$ref": "#/components/schemas/AccountCredentialScopeSet"
8687
+ },
8688
+ "created_by": {
8689
+ "$ref": "#/components/schemas/OpaqueIdentifier",
8690
+ "description": "An opaque WhooshBang member identifier. It is never a Clerk user identifier."
8691
+ },
8692
+ "created_at": { "$ref": "#/components/schemas/Timestamp" },
8693
+ "last_used_at": { "$ref": "#/components/schemas/Timestamp" },
8694
+ "scheduled_revocation_at": {
8695
+ "$ref": "#/components/schemas/Timestamp",
8696
+ "description": "When an active credential in a rotation overlap becomes terminally revoked."
8697
+ },
8698
+ "status": { "type": "string", "enum": ["active", "revoked"] },
8699
+ "revoked_at": { "$ref": "#/components/schemas/Timestamp" }
8700
+ },
8701
+ "allOf": [
8702
+ {
8703
+ "if": {
8704
+ "properties": { "status": { "const": "revoked" } },
8705
+ "required": ["status"]
8706
+ },
8707
+ "then": {
8708
+ "required": ["revoked_at"],
8709
+ "properties": {
8710
+ "revoked_at": {},
8711
+ "scheduled_revocation_at": false
8712
+ }
8713
+ },
8714
+ "else": { "properties": { "revoked_at": false } }
8715
+ }
8716
+ ],
8717
+ "additionalProperties": false
8718
+ },
8719
+ "AccountCredentialCollection": {
8720
+ "type": "object",
8721
+ "description": "A stable page of safe account-credential metadata for one account.",
8722
+ "required": ["items"],
8723
+ "properties": {
8724
+ "items": {
7425
8725
  "type": "array",
7426
- "description": "Every canonical recipient language independently addressable by the update endpoint.",
7427
- "minItems": 16,
7428
- "maxItems": 16,
7429
- "uniqueItems": true,
7430
- "items": { "$ref": "#/components/schemas/RecipientLanguage" }
8726
+ "maxItems": 100,
8727
+ "items": { "$ref": "#/components/schemas/AccountCredential" }
7431
8728
  },
7432
8729
  "next_cursor": {
7433
8730
  "type": "string",
@@ -7441,23 +8738,27 @@
7441
8738
  },
7442
8739
  "additionalProperties": false
7443
8740
  },
7444
- "UpdateRecipientCopyOverrideRequest": {
8741
+ "AccountCredentialSecretReveal": {
7445
8742
  "type": "object",
7446
- "description": "Sets one exact project + language + string value. Delete the same address to restore the shipped default.",
7447
- "required": ["value"],
8743
+ "description": "The creation-only response. The secret is emitted in this response exactly once and is never available from list, rotate-target, revoke, or later read responses.",
8744
+ "required": ["credential", "secret"],
7448
8745
  "properties": {
7449
- "value": { "$ref": "#/components/schemas/RecipientCopyOverrideValue" }
8746
+ "credential": { "$ref": "#/components/schemas/AccountCredential" },
8747
+ "secret": { "$ref": "#/components/schemas/AccountCredentialSecret" }
7450
8748
  },
7451
- "additionalProperties": false,
7452
- "x-max-json-bytes": 2048
8749
+ "additionalProperties": false
7453
8750
  },
7454
- "CreateApiCredentialRequest": {
8751
+ "AccountCredentialRotation": {
7455
8752
  "type": "object",
7456
- "description": "Creates one credential inside the project environment selected by the authenticated administration path.",
7457
- "required": ["label", "scopes"],
8753
+ "description": "A creation-only replacement secret plus safe metadata for the credential it replaces.",
8754
+ "required": ["credential", "replaced_credential"],
7458
8755
  "properties": {
7459
- "label": { "type": "string", "minLength": 1, "maxLength": 100 },
7460
- "scopes": { "$ref": "#/components/schemas/ApiCredentialScopeSet" }
8756
+ "credential": {
8757
+ "$ref": "#/components/schemas/AccountCredentialSecretReveal"
8758
+ },
8759
+ "replaced_credential": {
8760
+ "$ref": "#/components/schemas/AccountCredential"
8761
+ }
7461
8762
  },
7462
8763
  "additionalProperties": false
7463
8764
  },
@@ -8703,6 +10004,18 @@
8703
10004
  "retryable": { "const": false }
8704
10005
  }
8705
10006
  },
10007
+ {
10008
+ "type": "object",
10009
+ "properties": {
10010
+ "type": {
10011
+ "const": "https://whooshbang.flowxo.com/problems/credential_account_mismatch"
10012
+ },
10013
+ "title": { "const": "Credential account mismatch" },
10014
+ "status": { "const": 403 },
10015
+ "code": { "const": "credential_account_mismatch" },
10016
+ "retryable": { "const": false }
10017
+ }
10018
+ },
8706
10019
  {
8707
10020
  "type": "object",
8708
10021
  "properties": {
@@ -9923,6 +11236,7 @@
9923
11236
  "resource_not_found",
9924
11237
  "resource_expired",
9925
11238
  "cancellation_too_late",
11239
+ "credential_account_mismatch",
9926
11240
  "endpoint_rotation_conflict",
9927
11241
  "rate_limited",
9928
11242
  "provider_retryable",
@@ -10041,10 +11355,35 @@
10041
11355
  }
10042
11356
  ]
10043
11357
  },
10044
- "Environment": {
11358
+ "AccountCredentialPrefix": {
10045
11359
  "type": "string",
10046
- "enum": ["test", "live"],
10047
- "description": "The single environment selected by the authenticated credential."
11360
+ "pattern": "^wb_ac1\\.acred_[A-Za-z0-9_-]{8}$",
11361
+ "description": "A safe display prefix containing the account-credential format marker and first eight base64url identity characters, never secret bytes."
11362
+ },
11363
+ "AccountCredentialScope": {
11364
+ "type": "string",
11365
+ "enum": [
11366
+ "account:read",
11367
+ "projects:write",
11368
+ "projects:read",
11369
+ "messages:write",
11370
+ "messages:read",
11371
+ "subscriptions:write",
11372
+ "subscriptions:read",
11373
+ "machine-clients:write",
11374
+ "machine-clients:read",
11375
+ "capabilities:read",
11376
+ "connections:write",
11377
+ "connections:read"
11378
+ ],
11379
+ "description": "One account-credential capability. `account:read` is the account's project directory — listing the account's projects and reading one — and is the only scope here a project credential can never carry; it authorizes nothing below the account. Every other name is the project-credential vocabulary unchanged, and is exercised only on the environment-scoped routes, where the project and environment come from the path and the credential's authority is the intersection of these scopes with the project-credential vocabulary. An account credential is therefore never wider than a project credential would have been for the same project environment."
11380
+ },
11381
+ "AccountCredentialScopeSet": {
11382
+ "type": "array",
11383
+ "minItems": 1,
11384
+ "maxItems": 12,
11385
+ "uniqueItems": true,
11386
+ "items": { "$ref": "#/components/schemas/AccountCredentialScope" }
10048
11387
  },
10049
11388
  "ListPageSize": {
10050
11389
  "type": "integer",
@@ -10057,6 +11396,16 @@
10057
11396
  "minimum": 0,
10058
11397
  "description": "How many records the list holds in total, not only on this page. Counted while the page was read, so a write landing between the count and the page can leave it a record or two out of step with `items`. That is noise on an inspection surface; it never changes which records a page addresses."
10059
11398
  },
11399
+ "AccountCredentialSecret": {
11400
+ "type": "string",
11401
+ "pattern": "^wb_ac1\\.acred_[A-Za-z0-9_-]{21}[AQgw]\\.[A-Za-z0-9_-]{42}[AEIMQUYcgkosw048]$",
11402
+ "description": "The exact one-time runtime bearer syntax. The final segment is canonical unpadded base64url for exactly 32 random secret bytes."
11403
+ },
11404
+ "Environment": {
11405
+ "type": "string",
11406
+ "enum": ["test", "live"],
11407
+ "description": "The single environment selected by the authenticated credential."
11408
+ },
10060
11409
  "RecipientCopyOverrideValue": {
10061
11410
  "type": "string",
10062
11411
  "minLength": 1,
@@ -10071,6 +11420,8 @@
10071
11420
  "ApiCredentialScope": {
10072
11421
  "type": "string",
10073
11422
  "enum": [
11423
+ "projects:write",
11424
+ "projects:read",
10074
11425
  "messages:write",
10075
11426
  "messages:read",
10076
11427
  "subscriptions:write",
@@ -10081,12 +11432,12 @@
10081
11432
  "connections:write",
10082
11433
  "connections:read"
10083
11434
  ],
10084
- "description": "One project-credential capability. Environment authority always comes from the credential record, never a scope or request field."
11435
+ "description": "One project-credential capability. Environment authority always comes from the credential record, never a scope or request field. `projects:write` configures the credential's own project and environment; it never creates a project, which remains an account authority. The project-wide singletons — brand, default locale, logo and recipient copy — are what a live recipient sees, so writing them requires a live credential; a test credential may read them and may write its own environment's policy."
10085
11436
  },
10086
11437
  "ApiCredentialScopeSet": {
10087
11438
  "type": "array",
10088
11439
  "minItems": 1,
10089
- "maxItems": 9,
11440
+ "maxItems": 11,
10090
11441
  "uniqueItems": true,
10091
11442
  "items": { "$ref": "#/components/schemas/ApiCredentialScope" }
10092
11443
  },
@@ -10794,6 +12145,7 @@
10794
12145
  },
10795
12146
  "setup": { "$ref": "#/components/schemas/ChannelConnectionSetup" },
10796
12147
  "authorization_setup": false,
12148
+ "app_setup": false,
10797
12149
  "installation": false,
10798
12150
  "row_version": {
10799
12151
  "type": "integer",
@@ -10955,6 +12307,7 @@
10955
12307
  "$ref": "#/components/schemas/SlackConnectionIdentity"
10956
12308
  },
10957
12309
  "setup": false,
12310
+ "app_setup": false,
10958
12311
  "authorization_setup": {
10959
12312
  "$ref": "#/components/schemas/HostedChannelConnectionSetup",
10960
12313
  "description": "The additive hosted authorization state. Older tolerant readers discard this unknown field rather than interpreting it as Telegram setup."
@@ -10998,6 +12351,177 @@
10998
12351
  },
10999
12352
  "additionalProperties": false
11000
12353
  },
12354
+ "CustomerOwnedSlackSetupStatus": {
12355
+ "type": "string",
12356
+ "enum": [
12357
+ "awaiting_app_creation",
12358
+ "awaiting_authorities",
12359
+ "awaiting_authorization",
12360
+ "awaiting_ingress_proof",
12361
+ "provisioning",
12362
+ "failed",
12363
+ "expired"
12364
+ ],
12365
+ "description": "How far a customer-owned Slack setup has reached, which is the only thing a caller can act on while the connection is still `pending`. Each verification state names a different authority, because Slack hands the app's secrets over together but exercises them at different moments: `awaiting_authorization` has the three values and is waiting for the install, `awaiting_ingress_proof` has a bot grant Slack confirmed and is waiting for the first request Slack itself signs. Collapsing them would let a connection report a verified ingress it has never verified."
12366
+ },
12367
+ "CustomerOwnedSlackAppAuthority": {
12368
+ "type": "string",
12369
+ "enum": ["client_identity", "client_secret", "signing_secret"],
12370
+ "description": "One app-specific authority the owner hands over. Product vocabulary for what each value is for, never the value: `client_identity` is safe metadata, and the other two are write-only."
12371
+ },
12372
+ "CustomerOwnedSlackConnectionSetup": {
12373
+ "type": "object",
12374
+ "description": "The unfinished half of a customer-owned Slack connection: what the owner still has to do in Slack, and by when.\n\nIt is deliberately incapable of carrying a secret. The creation link contains configuration and nothing else, the two secrets the owner supplies are write-only and have no field here or on the connection, and the bot grant never travels through a browser at all — WhooshBang obtains it from Slack's own installation exchange. Everything here is therefore safe to poll, render, and hand to whoever is doing the setup.",
12375
+ "required": [
12376
+ "status",
12377
+ "creation_url",
12378
+ "expires_at",
12379
+ "app_name",
12380
+ "bot_display_name",
12381
+ "outstanding_authorities"
12382
+ ],
12383
+ "properties": {
12384
+ "status": {
12385
+ "$ref": "#/components/schemas/CustomerOwnedSlackSetupStatus"
12386
+ },
12387
+ "creation_url": {
12388
+ "type": "string",
12389
+ "format": "uri",
12390
+ "pattern": "^https://api\\.slack\\.com/apps\\?[^\\s]+$",
12391
+ "maxLength": 8192,
12392
+ "description": "Slack's own app-creation flow, opened with this app's manifest already filled in, so the owner reviews and presses Create rather than writing or pasting YAML. The URL carries configuration only: it names no account, project, or environment and contains no credential. The opaque handle it does carry selects candidate setup state and authorizes nothing."
12393
+ },
12394
+ "expires_at": {
12395
+ "$ref": "#/components/schemas/Timestamp",
12396
+ "description": "When an unfinished setup stops being usable. A setup that already produced a Slack app stays recoverable past this, because the app exists whether or not the browser came back."
12397
+ },
12398
+ "authorization_url": {
12399
+ "type": "string",
12400
+ "format": "uri",
12401
+ "pattern": "^https://[^\\s]+$",
12402
+ "maxLength": 2048,
12403
+ "description": "The short-lived handoff that installs the owner's app into their workspace, present once the three authorities have arrived and an installation has been started. It carries opaque state, never a provider credential or a raw authorization code, and the bot grant it produces is returned to WhooshBang by Slack rather than to the browser."
12404
+ },
12405
+ "app_name": {
12406
+ "type": "string",
12407
+ "minLength": 1,
12408
+ "maxLength": 35,
12409
+ "description": "The name the owner chose, echoed so a resumed session shows the same app it started with. Slack's own limit."
12410
+ },
12411
+ "bot_display_name": {
12412
+ "type": "string",
12413
+ "minLength": 1,
12414
+ "maxLength": 80,
12415
+ "description": "The bot display name the owner chose, echoed for the same reason. Slack's own limit."
12416
+ },
12417
+ "outstanding_authorities": {
12418
+ "type": "array",
12419
+ "maxItems": 3,
12420
+ "uniqueItems": true,
12421
+ "items": {
12422
+ "$ref": "#/components/schemas/CustomerOwnedSlackAppAuthority"
12423
+ },
12424
+ "description": "Which of the three app-specific values this setup is still waiting for. Empty once they have all arrived; it never says what any of them are."
12425
+ },
12426
+ "failure_reason": {
12427
+ "type": "string",
12428
+ "minLength": 1,
12429
+ "maxLength": 200,
12430
+ "description": "Why this attempt ended, in terms the owner can act on. It never echoes a submitted secret, whole or in part, and never names another customer: a Slack app already connected elsewhere says only that."
12431
+ }
12432
+ },
12433
+ "additionalProperties": false
12434
+ },
12435
+ "CustomerOwnedSlackChannelConnection": {
12436
+ "type": "object",
12437
+ "description": "A Slack app the customer created and owns, referenced by one exact environment connection.\n\nIt differs from the hosted arm in who owns the app and its lifecycle, not in what a notification can do. WhooshBang custodies the three app-specific authorities Slack makes unavoidable and none of them appears here or in any other read: absence, not redaction, is the mechanism.",
12438
+ "required": [
12439
+ "id",
12440
+ "project_id",
12441
+ "environment_id",
12442
+ "environment",
12443
+ "provider",
12444
+ "mode",
12445
+ "display_name",
12446
+ "status",
12447
+ "health",
12448
+ "capabilities",
12449
+ "recovery_action",
12450
+ "row_version",
12451
+ "created_at",
12452
+ "updated_at",
12453
+ "diagnostic_id"
12454
+ ],
12455
+ "properties": {
12456
+ "id": { "$ref": "#/components/schemas/OpaqueIdentifier" },
12457
+ "project_id": { "$ref": "#/components/schemas/OpaqueIdentifier" },
12458
+ "environment_id": { "$ref": "#/components/schemas/OpaqueIdentifier" },
12459
+ "environment": { "$ref": "#/components/schemas/Environment" },
12460
+ "provider": { "const": "slack" },
12461
+ "mode": { "const": "customer_managed" },
12462
+ "display_name": {
12463
+ "type": "string",
12464
+ "minLength": 1,
12465
+ "maxLength": 100
12466
+ },
12467
+ "status": { "$ref": "#/components/schemas/ChannelConnectionStatus" },
12468
+ "health": { "$ref": "#/components/schemas/ProviderHealthState" },
12469
+ "capabilities": {
12470
+ "type": "array",
12471
+ "maxItems": 1,
12472
+ "uniqueItems": true,
12473
+ "items": {
12474
+ "$ref": "#/components/schemas/HostedConnectionCapability"
12475
+ }
12476
+ },
12477
+ "identity": {
12478
+ "$ref": "#/components/schemas/SlackConnectionIdentity"
12479
+ },
12480
+ "setup": false,
12481
+ "authorization_setup": false,
12482
+ "app_setup": {
12483
+ "$ref": "#/components/schemas/CustomerOwnedSlackConnectionSetup",
12484
+ "description": "The guided customer-owned setup state. A distinct field from Telegram's `setup` and the hosted arm's `authorization_setup` so a tolerant reader can never mistake one journey's progress for another's."
12485
+ },
12486
+ "installation": {
12487
+ "$ref": "#/components/schemas/ProviderInstallationSummary",
12488
+ "description": "The safe installation this exact connection owns. It exposes no provider-native identifier and no credential reference."
12489
+ },
12490
+ "management_url": {
12491
+ "type": "string",
12492
+ "format": "uri",
12493
+ "pattern": "^slack://app\\?[^\\s]+$",
12494
+ "maxLength": 2048,
12495
+ "description": "A provider-owned App Home deep link for the customer's own workspace and their own app. It is not a caller-supplied return URL."
12496
+ },
12497
+ "recovery_action": {
12498
+ "$ref": "#/components/schemas/HostedConnectionRecoveryAction"
12499
+ },
12500
+ "last_successful_verification_at": {
12501
+ "$ref": "#/components/schemas/Timestamp"
12502
+ },
12503
+ "last_successful_delivery_at": {
12504
+ "$ref": "#/components/schemas/Timestamp"
12505
+ },
12506
+ "row_version": {
12507
+ "type": "integer",
12508
+ "minimum": 1,
12509
+ "description": "Send this back on an update. A stale value is refused rather than silently overwriting a change made in between."
12510
+ },
12511
+ "created_at": { "$ref": "#/components/schemas/Timestamp" },
12512
+ "updated_at": { "$ref": "#/components/schemas/Timestamp" },
12513
+ "archived_at": { "$ref": "#/components/schemas/Timestamp" },
12514
+ "health_checked_at": { "$ref": "#/components/schemas/Timestamp" },
12515
+ "last_transition_at": { "$ref": "#/components/schemas/Timestamp" },
12516
+ "last_transition_reason": {
12517
+ "type": "string",
12518
+ "minLength": 1,
12519
+ "maxLength": 200
12520
+ },
12521
+ "diagnostic_id": { "$ref": "#/components/schemas/DiagnosticId" }
12522
+ },
12523
+ "additionalProperties": false
12524
+ },
11001
12525
  "RecipientBrand": {
11002
12526
  "type": "object",
11003
12527
  "description": "Constrained recipient-facing brand tokens. Arbitrary markup, CSS, scripts, remote assets, fonts, and tracking inputs are structurally absent.",
@@ -11025,8 +12549,8 @@
11025
12549
  "format": "uri",
11026
12550
  "minLength": 8,
11027
12551
  "maxLength": 255,
11028
- "pattern": "^https?://[^/?#*\\s]+(?::[0-9]{1,5})?$",
11029
- "description": "One exact scheme, host, and optional port. Paths, queries, fragments, credentials, and wildcards are forbidden; HTTP is accepted only for localhost or synthetic .test configuration."
12552
+ "pattern": "^https?://[^/?#*\\s]+(?::[0-9]{1,5})?/?$",
12553
+ "description": "One exact scheme, host, and optional port, in any spelling that means that same origin. A trailing slash, an uppercase host, and an explicitly written default port are accepted and stored in canonical form. Paths, queries, fragments, credentials, and wildcards are forbidden; HTTP is accepted only for localhost or synthetic .test configuration."
11030
12554
  },
11031
12555
  "DeveloperKey": {
11032
12556
  "type": "string",
@@ -11259,7 +12783,7 @@
11259
12783
  "provider-capabilities-v1.schema": {
11260
12784
  "$schema": "https://json-schema.org/draft/2020-12/schema",
11261
12785
  "$id": "https://whooshbang.flowxo.com/schemas/provider-capabilities-v1.schema.json",
11262
- "x-contract-version": "1.0.0-rc.19",
12786
+ "x-contract-version": "1.0.0-rc.21",
11263
12787
  "title": "ProviderCapabilitiesV1",
11264
12788
  "description": "Provider-neutral, renderer-facing delivery and interaction capabilities with explicit degradation.",
11265
12789
  "type": "object",
@@ -11911,5 +13435,5 @@
11911
13435
  }
11912
13436
  }
11913
13437
  },
11914
- "x-contract-version": "1.0.0-rc.19"
13438
+ "x-contract-version": "1.0.0-rc.21"
11915
13439
  }