@whooshbang/sdk 3.1.0 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +115 -106
- package/dist/cli/commands.js +54 -0
- package/dist/cli/commands.js.map +1 -1
- package/dist/cli/main.js +11 -2
- package/dist/cli/main.js.map +1 -1
- package/dist/client.d.ts +55 -42
- package/dist/client.js +223 -52
- package/dist/client.js.map +1 -1
- package/dist/interactions.d.ts +9 -5
- package/dist/interactions.js +15 -9
- package/dist/interactions.js.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,49 +1,57 @@
|
|
|
1
1
|
# `@whooshbang/sdk`
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
wait — see [awaiting an answer](#awaiting-an-answer).
|
|
3
|
+
Use the JavaScript SDK to send messages through Telegram, Slack and email,
|
|
4
|
+
with an optional question. Poll delivery and answers without a public URL,
|
|
5
|
+
tunnel or webhook server, or receive signed webhooks. The recipient-settings
|
|
6
|
+
web component works in React, Vue or plain HTML.
|
|
8
7
|
|
|
9
|
-
|
|
8
|
+
The SDK is ESM and takes your `fetch` implementation. It retries safe reads
|
|
9
|
+
while [waiting for an answer](#awaiting-an-answer), but never retries a send
|
|
10
|
+
automatically.
|
|
11
|
+
|
|
12
|
+
Install the current release from the public npm registry:
|
|
10
13
|
|
|
11
14
|
```sh
|
|
12
15
|
npm install @whooshbang/sdk
|
|
13
16
|
```
|
|
14
17
|
|
|
15
|
-
|
|
18
|
+
The package includes the `wb` CLI. The [agent skill](https://docs.whooshbang.com/skill.md)
|
|
19
|
+
explains how an agent can build WhooshBang into your app.
|
|
20
|
+
|
|
21
|
+
For organization teammate messages, `whoAmI(projectId, environmentId)` reports the
|
|
16
22
|
actual principal and personal `me` identity. Use
|
|
17
|
-
`sendOperatorMessage(projectId, environmentId, {
|
|
23
|
+
`sendOperatorMessage(projectId, environmentId, { recipient: "Alex Smith", content })`
|
|
18
24
|
to message the authenticated person, or add `recipient: "Alex Smith"` for a
|
|
19
25
|
teammate. Omitted recipient and `me` require personal authentication; shared
|
|
20
26
|
credentials return an explicit-recipient-required result and never guess their
|
|
21
|
-
creator or the
|
|
27
|
+
creator or the organization owner. `listOperators` and `broadcastOperators` use only
|
|
22
28
|
current membership and project access. Shared service email is the default;
|
|
23
29
|
Notification preferences selects the person's connected channel. Marketing
|
|
24
30
|
cannot use the membership-derived service binding.
|
|
25
31
|
|
|
26
32
|
```ts
|
|
27
|
-
import { WhooshBangClient
|
|
33
|
+
import { WhooshBangClient } from "@whooshbang/sdk";
|
|
34
|
+
|
|
35
|
+
const credential = process.env.WHOOSHBANG_PROJECT_CREDENTIAL;
|
|
36
|
+
if (!credential) {
|
|
37
|
+
throw new Error("Set WHOOSHBANG_PROJECT_CREDENTIAL.");
|
|
38
|
+
}
|
|
28
39
|
|
|
29
40
|
const whooshBang = new WhooshBangClient({
|
|
30
|
-
|
|
31
|
-
credential: "synthetic-local-credential",
|
|
41
|
+
credential,
|
|
32
42
|
fetch,
|
|
33
43
|
});
|
|
34
44
|
|
|
35
45
|
const sent = await whooshBang.createMessage(
|
|
36
46
|
{
|
|
37
|
-
to:
|
|
38
|
-
|
|
39
|
-
interaction: confirmInteraction({
|
|
47
|
+
to: "customer_123",
|
|
48
|
+
interaction: {
|
|
40
49
|
prompt: "Approve the synthetic deployment?",
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
}
|
|
50
|
+
confirm_label: "Approve",
|
|
51
|
+
deny_label: "Deny",
|
|
52
|
+
expires_in_seconds: 900,
|
|
53
|
+
},
|
|
45
54
|
},
|
|
46
|
-
{ idempotencyKey: "deploy-approval-987", timeoutMs: 10_000 },
|
|
47
55
|
);
|
|
48
56
|
switch (sent.outcome) {
|
|
49
57
|
case "accepted":
|
|
@@ -60,7 +68,20 @@ switch (sent.outcome) {
|
|
|
60
68
|
}
|
|
61
69
|
```
|
|
62
70
|
|
|
63
|
-
|
|
71
|
+
The client defaults to the WhooshBang production API. Supply a project credential
|
|
72
|
+
for the intended environment; set `baseUrl` to use another API origin. Keep it on your server, outside source control. Connect
|
|
73
|
+
the subscriber first using the [quickstart](../../docs/onboarding/quickstart.mdx).
|
|
74
|
+
For a local test without a recipient, use the
|
|
75
|
+
[contract mock](../../apps/contract-mock/README.md) and its synthetic credentials.
|
|
76
|
+
|
|
77
|
+
## Optional questions
|
|
78
|
+
|
|
79
|
+
<span id="interaction-builders"></span>
|
|
80
|
+
|
|
81
|
+
Use `confirmInteraction` for Yes or no, `selectInteraction` for Pick one
|
|
82
|
+
(two to six choices), or `inputInteraction` for Type a reply (up to 4,000
|
|
83
|
+
characters). Answers don't change. Your app or agent gets the first one and later taps
|
|
84
|
+
are ignored, so it won't flip after you act.
|
|
64
85
|
|
|
65
86
|
`confirmInteraction`, `selectInteraction`, and `inputInteraction` produce
|
|
66
87
|
exactly the C0 tagged interaction union and enforce every documented bound —
|
|
@@ -82,33 +103,33 @@ clock-skew grace is applied and values are never clamped. The only upper bound
|
|
|
82
103
|
is a representable timestamp through 9999-12-31T23:59:59.999Z.
|
|
83
104
|
|
|
84
105
|
Existing absolute requests and already-created interactions keep their deadlines.
|
|
85
|
-
An idempotent retry preserves the accepted deadline.
|
|
86
|
-
least seven days and
|
|
106
|
+
An idempotent retry preserves the accepted deadline. Retries with the same key
|
|
107
|
+
are recognized for at least seven days, and longer when the answer window is longer. REST, CLI and MCP use the
|
|
87
108
|
same contract (`expires_in_seconds` / `expires_at` on the interaction).
|
|
88
109
|
|
|
89
110
|
```ts
|
|
90
111
|
const question = confirmInteraction({ prompt: "Approve this week's plan?", expiresInSeconds: 604800 });
|
|
91
112
|
```
|
|
92
113
|
|
|
93
|
-
The message's outer `expires_at` is its separate delivery deadline (
|
|
94
|
-
24 hours), not the answer deadline. Delivery acceptance does not end answerability.
|
|
114
|
+
The message's outer `expires_at` is its separate delivery deadline (default 24 hours, extendable through year 9999), not the answer deadline. Delivery acceptance does not end answerability.
|
|
95
115
|
Notification history can lose its retained body while an open question keeps
|
|
96
|
-
|
|
116
|
+
the prompt and options needed to answer until the question ends.
|
|
117
|
+
|
|
118
|
+
## Poll for delivery and answers
|
|
97
119
|
|
|
98
|
-
|
|
120
|
+
<span id="bounded-response-inspection"></span>
|
|
99
121
|
|
|
100
|
-
`getMessage`
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
122
|
+
`getMessage` polls delivery; `getInteraction` polls an answer. Each request
|
|
123
|
+
waits up to 30 seconds for news, so there's no tight loop. Your app can run
|
|
124
|
+
without a public URL or webhook server. `wait` accepts whole seconds from 0
|
|
125
|
+
through 30 and defaults to 30. Use 0 for an immediate read. Reading doesn't
|
|
126
|
+
remove an answer or acknowledge an event.
|
|
105
127
|
|
|
106
128
|
`interactionAnswer` reads a summary as a tagged outcome the caller branches
|
|
107
129
|
on, never as a value that might be missing. `outcome` is the interaction's own
|
|
108
130
|
state — `open`, `expired`, `cancelled`, `retired` — except that `answered`
|
|
109
|
-
splits in two: `answered` carries the
|
|
110
|
-
|
|
111
|
-
the person answered but the typed answer has left its retention window —
|
|
131
|
+
splits in two: `answered` carries the answer in `answer.type` and `answer.value`, and `answer_not_retained` says
|
|
132
|
+
the person answered but the answer is no longer in message history —
|
|
112
133
|
`answered` and `answered_at` survive that deletion, the answer does not. A
|
|
113
134
|
summary whose `answer` disagrees with its declared `type`, or whose
|
|
114
135
|
`answer_retained` contradicts the presence of `answer`, is a
|
|
@@ -123,10 +144,8 @@ state, or the link's Problem Details, and never claims provider-side deletion.
|
|
|
123
144
|
|
|
124
145
|
## Awaiting an answer
|
|
125
146
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
hours, so waiting on a person means reading again while the interaction is
|
|
129
|
-
`open`. `awaitInteraction` is that loop, shipped once:
|
|
147
|
+
`awaitInteraction` repeats the polling until the person answers or the
|
|
148
|
+
question ends. Set `patienceMs` to stop waiting sooner:
|
|
130
149
|
|
|
131
150
|
```ts
|
|
132
151
|
const read = await whooshBang.awaitInteraction(interactionId, {
|
|
@@ -152,8 +171,7 @@ It resolves with the same tagged outcome `interactionAnswer` produces, with
|
|
|
152
171
|
one reading added: `open` means the patience bound elapsed first. `patienceMs`
|
|
153
172
|
(0 through 86400000) bounds the whole wait and is deliberately not
|
|
154
173
|
`timeoutMs`, which bounds one HTTP call and cannot express a fifteen-minute
|
|
155
|
-
wait; each read inside the wait derives its own deadline from `wait` (
|
|
156
|
-
per-hold seconds, default 30) plus a margin, so a stalled connection fails one
|
|
174
|
+
wait; each read inside the wait derives its own deadline from `wait` (default 30 seconds) plus a margin, so a stalled connection fails one
|
|
157
175
|
read rather than the wait. Without `patienceMs` the wait ends when the
|
|
158
176
|
interaction does. Nothing ordinary throws: nobody answering in time is a
|
|
159
177
|
result of asking a person a question, not a failure. Your own `signal` firing
|
|
@@ -165,14 +183,13 @@ your decision, not the wait's outcome.
|
|
|
165
183
|
```ts
|
|
166
184
|
const asked = await whooshBang.ask(
|
|
167
185
|
{
|
|
168
|
-
to:
|
|
169
|
-
|
|
170
|
-
interaction: confirmInteraction({
|
|
186
|
+
to: "customer_123",
|
|
187
|
+
interaction: {
|
|
171
188
|
prompt: "Approve the synthetic deployment?",
|
|
172
|
-
|
|
173
|
-
}
|
|
189
|
+
expires_in_seconds: 900,
|
|
190
|
+
},
|
|
174
191
|
},
|
|
175
|
-
{
|
|
192
|
+
{ patienceMs: 15 * 60_000 },
|
|
176
193
|
);
|
|
177
194
|
if (asked.outcome === "answered") {
|
|
178
195
|
console.log(asked.message.id, asked.answer.value);
|
|
@@ -195,7 +212,7 @@ retries; a successful read resets the count, and the sixth consecutive failure
|
|
|
195
212
|
ends the wait by throwing that error. A non-retryable problem, or any other
|
|
196
213
|
protocol or contract failure, ends the wait by throwing at once. A read that
|
|
197
214
|
comes back `open` in under a second is followed by the rest of that second, so
|
|
198
|
-
a server that
|
|
215
|
+
a server that returns immediately — `wait: 0`, or long polling switched off — is
|
|
199
216
|
polled once a second, never in a tight loop. `patienceMs` is when the wait
|
|
200
217
|
stops issuing reads: a read already in flight at the bound still runs to its
|
|
201
218
|
own deadline. `getInteraction` and `getMessage` called directly still make
|
|
@@ -215,7 +232,7 @@ is revealed exactly once, by create and by rotate; an equivalent idempotent
|
|
|
215
232
|
replay returns the same logical endpoint without it.
|
|
216
233
|
|
|
217
234
|
`verifyWhooshBangEvent` verifies the Standard Webhooks envelope over the exact
|
|
218
|
-
raw request bytes and returns the
|
|
235
|
+
raw request bytes and returns the verified customer event. Read the body as bytes:
|
|
219
236
|
framework JSON re-serialization changes the signed input. Verification enforces
|
|
220
237
|
the replay window, fails closed on an unknown signature version, and accepts
|
|
221
238
|
both the previous and the current secret so a rotation overlap has no gap. Use
|
|
@@ -296,19 +313,17 @@ provider is `provider_outcome_unknown`, and a rate-limited one is
|
|
|
296
313
|
### Shared Email
|
|
297
314
|
|
|
298
315
|
Use `createChannelConnection` with `provider: "email"`,
|
|
299
|
-
`mode: "whooshbang_shared"` and a disclosed `shared_identity_id`.
|
|
300
|
-
account or DNS setup
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
316
|
+
`mode: "whooshbang_shared"` and a disclosed `shared_identity_id`. You don't need
|
|
317
|
+
a provider account or DNS setup. Email from our address adds an email charge;
|
|
318
|
+
see [website pricing](https://whooshbang.com/pricing).
|
|
319
|
+
Use your own Resend team to send from your domain. Recipients must authorize
|
|
320
|
+
the new sender if you switch.
|
|
304
321
|
|
|
305
322
|
`createSubscriptionLinkInEnvironment` accepts an optional write-only `address`
|
|
306
323
|
for an invitation, or returns a page where the recipient enters their address.
|
|
307
324
|
A link visit alone cannot verify consent. Read `email_verification` and the
|
|
308
325
|
masked `binding` from `getSubscriptionLinkInEnvironment`, then send through
|
|
309
326
|
that exact connection and read the authoritative `message.delivery` fields.
|
|
310
|
-
Test environments use `delivery_target: "simulator"` with
|
|
311
|
-
`to: { subscriber_id: "test:accepted" }`; they never send email.
|
|
312
327
|
|
|
313
328
|
`getSubscriberChannelSettings`, `transitionSubscriberBinding` and
|
|
314
329
|
`setSubscriberPreferredBinding` manage existing bindings in one exact project
|
|
@@ -332,7 +347,7 @@ asset authorized by its hosted-session capability, verifies the PNG and
|
|
|
332
347
|
`nosniff` response headers, and bounds the bytes before returning a `Blob`.
|
|
333
348
|
|
|
334
349
|
Recipient-copy inventory, update, and reset methods use the same closed
|
|
335
|
-
|
|
350
|
+
customer-editable key enum as the service. Effective embedded-panel overrides arrive
|
|
336
351
|
through the session's sparse `panel_copy`; mandatory system-owned strings are
|
|
337
352
|
not writable SDK inputs.
|
|
338
353
|
|
|
@@ -341,27 +356,11 @@ import `@whooshbang/sdk/recipient/element` to register
|
|
|
341
356
|
`<whooshbang-recipient-settings>`, or import `mountRecipientSettings` from
|
|
342
357
|
`@whooshbang/sdk/recipient` for an imperative mount. These subpaths are kept out
|
|
343
358
|
of the package root, so a server or Worker importing `@whooshbang/sdk` never
|
|
344
|
-
evaluates DOM globals.
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
accent authoritative.
|
|
350
|
-
|
|
351
|
-
`@whooshbang/sdk@1.0.0-rc.34` carries contracts `1.0.0-rc.25`: the environment
|
|
352
|
-
recipient policy lists `connectable_channels`, the channels a recipient can
|
|
353
|
-
authorize on right now.
|
|
354
|
-
|
|
355
|
-
`@whooshbang/sdk@1.0.0-rc.33` carries contracts `1.0.0-rc.24`, which adds the
|
|
356
|
-
`internal_error` problem: a retryable 500 for a fault inside WhooshBang, never
|
|
357
|
-
a provider or a system of yours.
|
|
358
|
-
|
|
359
|
-
`@whooshbang/sdk@1.0.0-rc.32` carries contracts `1.0.0-rc.23`: a project
|
|
360
|
-
credential holding the opt-in `projects:read` / `projects:write` scopes can now
|
|
361
|
-
read and write its own project's recipient configuration, an origin may be
|
|
362
|
-
written in any spelling that means the same origin, and
|
|
363
|
-
`WhooshBangProblemError.pendingInvitation` names the rollout gate an uninvited
|
|
364
|
-
organization is waiting on.
|
|
359
|
+
evaluates DOM globals. The panel respects your configured name, logo and
|
|
360
|
+
accent. Its `connectable_channels` policy lists the channels the recipient can
|
|
361
|
+
authorize now. Read and write project recipient configuration using
|
|
362
|
+
`projects:read` and `projects:write`; a project credential can configure only
|
|
363
|
+
its own project.
|
|
365
364
|
|
|
366
365
|
## Machine clients
|
|
367
366
|
|
|
@@ -373,8 +372,9 @@ secret. `registerMachineCredential`, `revokeMachineCredential`,
|
|
|
373
372
|
complete the stream lifecycle. Rotate by registering the replacement first and
|
|
374
373
|
revoking the original second.
|
|
375
374
|
|
|
376
|
-
The machine
|
|
377
|
-
|
|
375
|
+
The [integration reference](../../docs/onboarding/reference.mdx#machine-events)
|
|
376
|
+
explains polling, durable consumption and acknowledgements. Contract-mock
|
|
377
|
+
scenarios exercise replay and contiguous cursor advancement locally.
|
|
378
378
|
|
|
379
379
|
## Outcomes and exceptions
|
|
380
380
|
|
|
@@ -397,18 +397,19 @@ every call site rather than a silent fallthrough.
|
|
|
397
397
|
|
|
398
398
|
| Method | `outcome` | Carries | Means |
|
|
399
399
|
| --------------------------------------------- | ----------------------------------------------------- | --------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
400
|
-
| `interactionAnswer` | `answered` | `answer`, `summary` | A person answered and the
|
|
401
|
-
| | `answer_not_retained` | `summary` | Answered; the
|
|
400
|
+
| `interactionAnswer` | `answered` | `answer`, `summary` | A person answered and the answer is retained. |
|
|
401
|
+
| | `answer_not_retained` | `summary` | Answered; the answer is no longer in message history. `answered_at` survives. |
|
|
402
402
|
| | `open` | `summary` | Nobody has answered yet. |
|
|
403
403
|
| | `expired` | `summary` | The interaction reached its server-side expiry unanswered. |
|
|
404
404
|
| | `cancelled` | `summary` | Cancelled before an answer. |
|
|
405
|
-
| | `retired` | `summary` |
|
|
405
|
+
| | `retired` | `summary` | The question was retired; later answers are ignored. |
|
|
406
406
|
| `createMessage`, `createMessageInEnvironment` | `accepted` | `message` | The service accepted the send; `message` is its current state. |
|
|
407
407
|
| | `subscriber_unbound` | `problem` | No active binding reaches the subscriber on this notifier. Nothing was sent; usually they have to subscribe again, sometimes your routing changed. |
|
|
408
408
|
| | `preferred_binding_unavailable` | `problem` | The channel the subscriber chose as preferred is paused or revoked, and the service never picks a fallback. Nothing was sent; they have to choose again or subscribe again. |
|
|
409
|
+
| | `funding_required` | `reason`, `problem` | Add credit or wait for a paid allowance to renew, then retry the refused request. |
|
|
409
410
|
| `awaitInteraction` | as `interactionAnswer` | `summary`, `answer` on `answered` | The same members; `open` means the patience bound elapsed and the interaction can be awaited again. |
|
|
410
411
|
| `ask` | as `awaitInteraction` | plus `message` | Every interaction member also carries the accepted message. |
|
|
411
|
-
| | `subscriber_unbound`, `preferred_binding_unavailable` | `problem` | The send's own refusals, exactly as `createMessage` returns them. Nothing was sent, so there is no message. |
|
|
412
|
+
| | `subscriber_unbound`, `preferred_binding_unavailable`, `funding_required` | `problem` | The send's own refusals, exactly as `createMessage` returns them. Nothing was sent, so there is no message. |
|
|
412
413
|
| `cancelMessage` | `cancelled` | `message` | Cancellation took effect before a worker owned the provider call. |
|
|
413
414
|
| | `cancellation_too_late` | `message` | A worker already owned the provider call. `message` is the current state; no provider-side deletion is claimed. |
|
|
414
415
|
| `cancelSubscriptionLink` | `cancelled` | `link` | The pending link is cancelled and can no longer create a binding. |
|
|
@@ -435,9 +436,9 @@ code throws from that same method. The one conditional classification is
|
|
|
435
436
|
`scope_forbidden`: its explicit `billing_wallet_insufficient` or
|
|
436
437
|
`billing_email_credit_insufficient` field error is an ordinary funding ceiling.
|
|
437
438
|
`createMessage`, `testConnection` and `ask` return `funding_required` with the
|
|
438
|
-
reason and problem; other scope refusals still throw.
|
|
439
|
+
reason and problem; other scope refusals still throw. Add credit or wait
|
|
439
440
|
for a paid allowance to renew, then explicitly retry the same refused request.
|
|
440
|
-
Bonus credit funds core sends only; shared email needs purchased or
|
|
441
|
+
Bonus credit funds core sends only; shared email needs purchased or welcome
|
|
441
442
|
credit. These ceilings do not schedule an automatic SDK retry.
|
|
442
443
|
|
|
443
444
|
| Code | Side | Why |
|
|
@@ -447,7 +448,7 @@ credit. These ceilings do not schedule an automatic SDK retry.
|
|
|
447
448
|
| `credential_invalid` | Exception | The credential is malformed, revoked, or past its rotation overlap. Only a replacement credential fixes it. |
|
|
448
449
|
| `scope_forbidden` | Exception | The credential lacks a scope, or the organization is not yet invited (`pendingInvitation`). No branch at the call site grants either. |
|
|
449
450
|
| `environment_mismatch` | Exception | A credential reached across the test/live boundary. |
|
|
450
|
-
| `idempotency_key_required` | Exception |
|
|
451
|
+
| `idempotency_key_required` | Exception | A supplied key was invalid. Omit it or use 8 to 255 visible ASCII characters. |
|
|
451
452
|
| `idempotency_conflict` | Exception | Same key, different body — including the stale row-version, claimed-identity, revoked-client, and acknowledgement cursor or disposition refusals that reuse the code. The fix is a new request, never a retry. |
|
|
452
453
|
| `project_slug_conflict` | Exception | The slug is already taken in this organization. Refused input, handled as `request_invalid` is: choose another. |
|
|
453
454
|
| `request_invalid` | Exception | The request broke a documented bound. `fieldErrors` names each field. |
|
|
@@ -548,39 +549,26 @@ node packages/sdk-js/examples/send-interactive-message.mjs
|
|
|
548
549
|
|
|
549
550
|
## Compatibility
|
|
550
551
|
|
|
551
|
-
The
|
|
552
|
-
`getAccount` and credential lifecycle calls to their organization equivalents,
|
|
553
|
-
response fields to `organization_id`, and CLI tenant options to `--organization`.
|
|
554
|
-
An older SDK can reject a response before application code reads it, including
|
|
555
|
-
project-scoped calls whose caller ignores the tenant field. Use the matching
|
|
556
|
-
contracts and mock release and follow the
|
|
557
|
-
[upgrade guide](../../docs/onboarding/organization-upgrade.mdx).
|
|
558
|
-
|
|
559
|
-
The exact runtime dependency is
|
|
560
|
-
`@whooshbang/contracts@1.0.0-rc.23`. Successful payloads are
|
|
552
|
+
The exact runtime dependency is pinned in `package.json`. Successful payloads are
|
|
561
553
|
validated with its packaged validators, while additive response fields remain
|
|
562
554
|
available on the returned object.
|
|
563
555
|
|
|
564
556
|
## Assert email consent already obtained
|
|
565
557
|
|
|
566
558
|
`assertEmailConsent` activates one customer-owned email binding and creates the
|
|
567
|
-
subscriber if needed. It requires `subscriptions:write`,
|
|
568
|
-
|
|
569
|
-
|
|
559
|
+
subscriber if needed. It requires `subscriptions:write`, healthy customer OAuth
|
|
560
|
+
and webhook authorization, and consent your application already obtained. The
|
|
561
|
+
sole customer-owned email connection supplies its domain; specify `connection_id`
|
|
562
|
+
when more than one exists. No verification message is sent.
|
|
570
563
|
|
|
571
564
|
```ts
|
|
572
565
|
const binding = await client.assertEmailConsent(
|
|
573
566
|
{
|
|
574
567
|
subscriber_id: "customer_123",
|
|
575
|
-
notifier_id: "default",
|
|
576
|
-
connection_id: "connection_customer_email",
|
|
577
|
-
domain_id: "emaildom_customer",
|
|
578
568
|
address: "Case.Sensitive+tag@example.com",
|
|
579
569
|
consented: true,
|
|
580
570
|
consented_at: "2026-09-10T12:00:00Z",
|
|
581
|
-
source: "checkout",
|
|
582
571
|
},
|
|
583
|
-
{ idempotencyKey: "consent-customer-123-v1" },
|
|
584
572
|
);
|
|
585
573
|
```
|
|
586
574
|
|
|
@@ -608,7 +596,7 @@ session. Linkless assertions emit `subscription.asserted` and
|
|
|
608
596
|
`subscription.asserted_revoked`, whose `consent_assertion_id` is the binding ID.
|
|
609
597
|
Existing link-backed subscription events retain their required link reference.
|
|
610
598
|
|
|
611
|
-
For
|
|
599
|
+
For a user whose mailbox your app has verified receiving account notices, `assertEmailConsent`
|
|
612
600
|
also accepts `basis: "service"`, required `source` and `relationship_started_at`
|
|
613
601
|
instead of `consented`/`consented_at`. The returned email binding has `basis:
|
|
614
602
|
"service"` and `service` evidence, with no consent claim. The environment-scoped
|
|
@@ -625,3 +613,24 @@ make another consent assertion or replace a withdrawn subscriber.
|
|
|
625
613
|
### Migrating to SDK 3
|
|
626
614
|
|
|
627
615
|
Interaction builders now return send-time inputs. Relative durations start when the server accepts the send; the builder does not calculate an absolute deadline locally. Read the effective `expires_at` from the accepted interaction response instead of the builder result. Existing explicit absolute deadlines remain supported.
|
|
616
|
+
|
|
617
|
+
## Short inputs and retry keys
|
|
618
|
+
|
|
619
|
+
The production API URL is the default. Override `baseUrl` for a local mock or proxy.
|
|
620
|
+
|
|
621
|
+
```ts
|
|
622
|
+
const wb = new WhooshBangClient({ credential: process.env.WHOOSHBANG_CREDENTIAL! });
|
|
623
|
+
await wb.createMessage({ to: "customer_123", content: "Order 4821 has shipped." });
|
|
624
|
+
await wb.ask({ to: "customer_123", interaction: { prompt: "Ship it?" } }, { patienceMs: 60_000 });
|
|
625
|
+
await wb.ask({ to: "customer_123", interaction: { prompt: "Choose", options: ["A", "B"] } });
|
|
626
|
+
await wb.ask({ to: "customer_123", interaction: { type: "input", prompt: "What should change?" } });
|
|
627
|
+
```
|
|
628
|
+
|
|
629
|
+
`{ text }` and `{ blocks }` infer their content type. An image needs only its
|
|
630
|
+
`asset_id`; its purpose defaults to `illustration`. A string choice, or a choice
|
|
631
|
+
with only `label`, uses that label as its value. Prompt-only questions display once.
|
|
632
|
+
|
|
633
|
+
Every call generates its own idempotency key when omitted. If you retry an
|
|
634
|
+
application job, pass `{ idempotencyKey: job.id }` on every call for that job.
|
|
635
|
+
The same key and body replay the original result; a keyless REST retry or another
|
|
636
|
+
SDK call creates a new operation. Supplied keys are validated before transport.
|
package/dist/cli/commands.js
CHANGED
|
@@ -27,6 +27,31 @@ function privacyMode(parsed) {
|
|
|
27
27
|
}
|
|
28
28
|
return value;
|
|
29
29
|
}
|
|
30
|
+
function describeEndpoint(endpoint) {
|
|
31
|
+
const reason = endpoint.pause_reason === "sustained_failure"
|
|
32
|
+
? "Paused after 24 hours without a successful delivery."
|
|
33
|
+
: endpoint.pause_reason === "probe_failed"
|
|
34
|
+
? "The resume test failed; the endpoint remains paused."
|
|
35
|
+
: endpoint.pause_reason === "customer"
|
|
36
|
+
? "Paused by a project administrator."
|
|
37
|
+
: undefined;
|
|
38
|
+
return [
|
|
39
|
+
`${endpoint.id} ${endpoint.status}`,
|
|
40
|
+
...(reason ? [reason] : []),
|
|
41
|
+
...(endpoint.recovery_pending
|
|
42
|
+
? ["Delivery is held until one probe succeeds."]
|
|
43
|
+
: []),
|
|
44
|
+
...(endpoint.next_probe_at
|
|
45
|
+
? [`Next probe: ${endpoint.next_probe_at}`]
|
|
46
|
+
: []),
|
|
47
|
+
...(endpoint.status === "paused"
|
|
48
|
+
? [
|
|
49
|
+
`Resume: wb endpoint resume ${endpoint.id}`,
|
|
50
|
+
"Held events expire seven days after creation.",
|
|
51
|
+
]
|
|
52
|
+
: []),
|
|
53
|
+
];
|
|
54
|
+
}
|
|
30
55
|
function idempotency(parsed) {
|
|
31
56
|
return { idempotencyKey: parsed.options.get("key") ?? randomUUID() };
|
|
32
57
|
}
|
|
@@ -1233,6 +1258,33 @@ const commands = {
|
|
|
1233
1258
|
: await untilAnswered(api, scope, interactionId, wait);
|
|
1234
1259
|
emit(context.writer, summary, describeInteraction(summary, where));
|
|
1235
1260
|
},
|
|
1261
|
+
async "endpoint list"(context) {
|
|
1262
|
+
const { api, flat, projectId, environmentId } = await messageScope(context);
|
|
1263
|
+
const result = flat
|
|
1264
|
+
? await api.listCustomerEndpointsWithCredential()
|
|
1265
|
+
: await api.listCustomerEndpoints(projectId, environmentId);
|
|
1266
|
+
emit(context.writer, result, result.items.flatMap(describeEndpoint));
|
|
1267
|
+
},
|
|
1268
|
+
async "endpoint get"(context) {
|
|
1269
|
+
const id = context.parsed.words[2];
|
|
1270
|
+
if (!id)
|
|
1271
|
+
throw new UsageError("wb endpoint get <endpoint-id>");
|
|
1272
|
+
const { api, flat, projectId, environmentId } = await messageScope(context);
|
|
1273
|
+
const endpoint = flat
|
|
1274
|
+
? await api.getCustomerEndpointWithCredential(id)
|
|
1275
|
+
: await api.getCustomerEndpoint(projectId, environmentId, id);
|
|
1276
|
+
emit(context.writer, endpoint, describeEndpoint(endpoint));
|
|
1277
|
+
},
|
|
1278
|
+
async "endpoint resume"(context) {
|
|
1279
|
+
const id = context.parsed.words[2];
|
|
1280
|
+
if (!id)
|
|
1281
|
+
throw new UsageError("wb endpoint resume <endpoint-id>");
|
|
1282
|
+
const { api, flat, projectId, environmentId } = await messageScope(context);
|
|
1283
|
+
const endpoint = flat
|
|
1284
|
+
? await api.resumeCustomerEndpointWithCredential(id)
|
|
1285
|
+
: await api.resumeCustomerEndpoint(projectId, environmentId, id);
|
|
1286
|
+
emit(context.writer, endpoint, describeEndpoint(endpoint));
|
|
1287
|
+
},
|
|
1236
1288
|
async "message get"(context) {
|
|
1237
1289
|
const messageId = context.parsed.words[2];
|
|
1238
1290
|
if (!messageId)
|
|
@@ -1269,6 +1321,8 @@ export function strayWords(words) {
|
|
|
1269
1321
|
// These commands each take one positional after their two-word name.
|
|
1270
1322
|
const takesPositional = two === "project create" ||
|
|
1271
1323
|
two === "message get" ||
|
|
1324
|
+
two === "endpoint get" ||
|
|
1325
|
+
two === "endpoint resume" ||
|
|
1272
1326
|
two === "interaction get" ||
|
|
1273
1327
|
two === "subscriber create" ||
|
|
1274
1328
|
two === "subscriber export" ||
|