@pushary/server 1.3.2 → 1.4.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 CHANGED
@@ -36,7 +36,7 @@ The server SDK requires your full API key (`pk_xxx.sk_xxx`) which includes the s
36
36
 
37
37
  **Never expose this in client-side code.**
38
38
 
39
- Get your API key from your [Pushary Dashboard](https://pushary.com/dashboard/settings).
39
+ Get your API key by following [Get your API key](https://pushary.com/docs/agents/api-key).
40
40
 
41
41
  ## Resources
42
42
 
@@ -174,6 +174,136 @@ const { webhookSecret } = await pushary.decisions.getWebhookSecret()
174
174
  const ok = verifyWebhookSignature(rawBody, signatureHeader, webhookSecret)
175
175
  ```
176
176
 
177
+ ### Helpers
178
+
179
+ Four exports that the flows above rely on. Signatures are given because guessing
180
+ them is easy to get wrong: `deterministicKey` takes an **array**, and `isApproved`
181
+ reads a decision's `status`/`value`, not an `answer` field.
182
+
183
+ ```typescript
184
+ import {
185
+ deterministicKey,
186
+ isApproved,
187
+ parseDecisionCallback,
188
+ SIGNATURE_HEADER,
189
+ } from '@pushary/server'
190
+
191
+ // deterministicKey(parts: readonly string[]): string
192
+ // A stable idempotency key from the parts that identify one logical step. Same
193
+ // parts in, same key out, so a retried run reuses its decision instead of asking
194
+ // the human twice.
195
+ const idempotencyKey = deterministicKey(['run-abc', 'step-3', 'user-123'])
196
+
197
+ // isApproved(decision: { status, type?, value }): boolean
198
+ // Fail-closed: true ONLY for an answered confirm whose value is yes. Pending,
199
+ // expired, declined and free-text answers all return false.
200
+ isApproved({ status: 'answered', type: 'confirm', value: 'yes' }) // true
201
+ isApproved({ status: 'answered', type: 'confirm', value: 'no' }) // false
202
+ isApproved({ status: 'pending', type: 'confirm', value: null }) // false
203
+
204
+ // SIGNATURE_HEADER: 'x-pushary-signature', the header carrying the signature.
205
+ // parseDecisionCallback(rawBody: string): DecisionCallback | null
206
+ // Takes the RAW body string, not a parsed object, and returns null if the
207
+ // payload is not a well-formed callback. Verify the signature first.
208
+ app.post('/webhooks/pushary', async (req, res) => {
209
+ const rawBody = req.rawBody.toString('utf8')
210
+ if (!verifyWebhookSignature(rawBody, req.header(SIGNATURE_HEADER), webhookSecret)) {
211
+ return res.sendStatus(401)
212
+ }
213
+
214
+ const callback = parseDecisionCallback(rawBody)
215
+ if (!callback) return res.sendStatus(400)
216
+
217
+ // A callback carries { correlationId, answer, value, answeredAt }. It has no
218
+ // `status` field, so it is not an isApproved() argument. Reaching this point
219
+ // already means the decision was answered, so judge the value directly, or
220
+ // re-read the decision if you want isApproved to do it for you.
221
+ if (callback.value === 'yes') await issueRefund()
222
+
223
+ res.sendStatus(200)
224
+ })
225
+ ```
226
+
227
+ ## Writing a framework adapter: `@pushary/server/adapters`
228
+
229
+ Every official Pushary adapter (`@pushary/eve`, `@pushary/ai-sdk`,
230
+ `@pushary/langgraph`, `@pushary/mastra`, `@pushary/openai-agents`) is a thin binding
231
+ over one shared kernel, and that kernel is public. If you run an in-house harness, or
232
+ a framework we have not shipped for, this is the same surface they are built on.
233
+
234
+ ```typescript
235
+ import { createAdapterKernel, renderApprovalQuestion } from '@pushary/server/adapters'
236
+
237
+ const kernel = createAdapterKernel('the Acme helpers')
238
+
239
+ // blocking ask; idempotency and the fail-closed result are already handled
240
+ export const askHuman = kernel.askExternalUser
241
+
242
+ // durable create, for a framework that parks its own run and resumes on a webhook
243
+ export const openDecision = kernel.createDurableDecision
244
+
245
+ // an enforced gate: build it once, call it per tool call
246
+ const gate = kernel.createGate({ apiKey: process.env.PUSHARY_API_KEY! })
247
+
248
+ export const approve = async (toolName: string, callId: string, input: unknown) => {
249
+ const decision = await gate({
250
+ toolName,
251
+ callId,
252
+ sessionId: currentRunId,
253
+ question: renderApprovalQuestion(toolName, input),
254
+ externalId: kernel.requireExternalId(currentUserId),
255
+ })
256
+ return decision // { approved: true } | { approved: false, reason }
257
+ }
258
+ ```
259
+
260
+ The label passed to `createAdapterKernel` is what appears in the error when a key or
261
+ an end-user is missing, so it names your helpers rather than ours.
262
+
263
+ Also exported: `describeAnswer` (turn an outcome into an instruction the model cannot
264
+ misread), `resolvePusharyCallback` (verify and parse a webhook in one call),
265
+ `idempotencyKeyFor`, and `isAffirmative`.
266
+
267
+ Python has the same surface as `pushary.adapters`:
268
+
269
+ ```python
270
+ from pushary.adapters import AdapterKernel, render_approval_question
271
+
272
+ kernel = AdapterKernel("the Acme helpers")
273
+ ask_human = kernel.ask_human
274
+ gate = kernel.create_gate()
275
+ ```
276
+
277
+ Both give you what the shipped adapters have: idempotency keyed on the call so a
278
+ replay never asks twice, a denial reason the model can read, and silence treated as a
279
+ no.
280
+
281
+ ## Handling errors
282
+
283
+ Every non-2xx response throws a `PusharyApiError`. It extends `Error`, so existing
284
+ `catch (e) { e.message }` code is unaffected, and it carries the status so you can
285
+ tell "stop and re-authenticate" from "back off and retry" without matching on
286
+ message text.
287
+
288
+ ```typescript
289
+ import { PusharyApiError } from '@pushary/server'
290
+
291
+ try {
292
+ await pushary.notifications.send({ title: 'Deploy finished' })
293
+ } catch (err) {
294
+ if (err instanceof PusharyApiError) {
295
+ if (err.isAuthError) throw err // 401/403: key is bad or revoked, do not retry
296
+ if (err.isRateLimited) return backOff() // 429
297
+ if (err.isServerError) return retry() // 5xx
298
+ console.error(err.status, err.code, err.message) // 4xx: your request, fix the call
299
+ }
300
+ throw err
301
+ }
302
+ ```
303
+
304
+ `status`, `statusText`, `code` and `body` are all readable; `code` and `body` are
305
+ present only when the API sent them.
306
+
177
307
  ## TypeScript
178
308
 
179
309
  Full TypeScript support with exported types:
@@ -0,0 +1,452 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/adapters.ts
21
+ var adapters_exports = {};
22
+ __export(adapters_exports, {
23
+ DENIED_REFUSED: () => DENIED_REFUSED,
24
+ DENIED_UNANSWERED: () => DENIED_UNANSWERED,
25
+ SIGNATURE_HEADER: () => SIGNATURE_HEADER,
26
+ createAdapterKernel: () => createAdapterKernel,
27
+ describeAnswer: () => describeAnswer,
28
+ deterministicKey: () => deterministicKey,
29
+ idempotencyKeyFor: () => idempotencyKeyFor,
30
+ isAffirmative: () => isAffirmative,
31
+ isApproved: () => isApproved,
32
+ parseDecisionCallback: () => parseDecisionCallback,
33
+ renderApprovalQuestion: () => renderApprovalQuestion,
34
+ resolvePusharyCallback: () => resolvePusharyCallback,
35
+ verifyWebhookSignature: () => verifyWebhookSignature
36
+ });
37
+ module.exports = __toCommonJS(adapters_exports);
38
+
39
+ // src/error.ts
40
+ var PusharyApiError = class _PusharyApiError extends Error {
41
+ constructor(message, init) {
42
+ super(message);
43
+ this.name = "PusharyApiError";
44
+ this.status = init.status;
45
+ this.statusText = init.statusText ?? "";
46
+ this.code = init.code;
47
+ this.body = init.body;
48
+ Object.setPrototypeOf(this, _PusharyApiError.prototype);
49
+ }
50
+ /** The key is missing, malformed, or revoked. Re-authenticate; do not retry. */
51
+ get isAuthError() {
52
+ return this.status === 401 || this.status === 403;
53
+ }
54
+ /** Rate limited. Back off and retry. */
55
+ get isRateLimited() {
56
+ return this.status === 429;
57
+ }
58
+ /** Server-side fault. Safe to retry an idempotent call. */
59
+ get isServerError() {
60
+ return this.status >= 500;
61
+ }
62
+ };
63
+
64
+ // src/request.ts
65
+ var buildUrl = (baseUrl, path, params) => {
66
+ const url = new URL(`${baseUrl}${path}`);
67
+ if (params) {
68
+ Object.entries(params).forEach(([key, value]) => {
69
+ if (value !== void 0 && value !== null) {
70
+ url.searchParams.set(key, String(value));
71
+ }
72
+ });
73
+ }
74
+ return url.toString();
75
+ };
76
+ var handleResponse = async (response) => {
77
+ if (!response.ok) {
78
+ const body = await response.json().catch(() => ({}));
79
+ throw new PusharyApiError(body.message || body.error || `HTTP ${response.status}`, {
80
+ status: response.status,
81
+ statusText: response.statusText,
82
+ code: body.code,
83
+ body
84
+ });
85
+ }
86
+ return response.json();
87
+ };
88
+ var createRequest = (ctx) => async (method, path, body) => {
89
+ const isGet = method === "GET";
90
+ const url = isGet && body ? buildUrl(ctx.baseUrl, path, body) : `${ctx.baseUrl}${path}`;
91
+ const response = await fetch(url, {
92
+ method,
93
+ headers: ctx.headers,
94
+ body: !isGet && body ? JSON.stringify(body) : void 0
95
+ });
96
+ return handleResponse(response);
97
+ };
98
+
99
+ // src/resources/subscribers.ts
100
+ var createSubscribersResource = (request) => Object.freeze({
101
+ list: (params) => request("GET", "/subscribers", params),
102
+ get: (id) => request("GET", `/subscribers/${id}`),
103
+ update: (id, data) => request("PATCH", `/subscribers/${id}`, data),
104
+ delete: (id) => request("DELETE", `/subscribers/${id}`),
105
+ count: () => request("GET", "/subscribers/count")
106
+ });
107
+
108
+ // src/resources/campaigns.ts
109
+ var createCampaignsResource = (request) => Object.freeze({
110
+ list: (params) => request("GET", "/campaigns", params),
111
+ create: (data) => request("POST", "/campaigns", data),
112
+ get: (id) => request("GET", `/campaigns/${id}`),
113
+ update: (id, data) => request("PATCH", `/campaigns/${id}`, data),
114
+ delete: (id) => request("DELETE", `/campaigns/${id}`),
115
+ send: (id) => request("POST", `/campaigns/${id}/send`),
116
+ pause: (id) => request("POST", `/campaigns/${id}/pause`),
117
+ resume: (id) => request("POST", `/campaigns/${id}/resume`),
118
+ stats: (id) => request("GET", `/campaigns/${id}/stats`)
119
+ });
120
+
121
+ // src/resources/templates.ts
122
+ var createTemplatesResource = (request) => Object.freeze({
123
+ list: (params) => request("GET", "/templates", params),
124
+ create: (data) => request("POST", "/templates", data),
125
+ get: (id) => request("GET", `/templates/${id}`),
126
+ update: (id, data) => request("PATCH", `/templates/${id}`, data),
127
+ delete: (id) => request("DELETE", `/templates/${id}`)
128
+ });
129
+
130
+ // src/resources/notifications.ts
131
+ var createNotificationsResource = (request) => Object.freeze({
132
+ send: (data) => request("POST", "/send", data)
133
+ });
134
+
135
+ // src/resources/flows.ts
136
+ var createFlowsResource = (request) => Object.freeze({
137
+ list: (params) => request("GET", "/flows", params),
138
+ create: (data) => request("POST", "/flows", data),
139
+ get: (id) => request("GET", `/flows/${id}`),
140
+ update: (id, data) => request("PATCH", `/flows/${id}`, data),
141
+ delete: (id) => request("DELETE", `/flows/${id}`),
142
+ activate: (id) => request("POST", `/flows/${id}/activate`),
143
+ pause: (id) => request("POST", `/flows/${id}/pause`)
144
+ });
145
+
146
+ // src/resources/decisions.ts
147
+ var import_crypto2 = require("crypto");
148
+
149
+ // src/util.ts
150
+ var import_crypto = require("crypto");
151
+ var deterministicKey = (parts) => (0, import_crypto.createHash)("sha256").update(parts.join("\0")).digest("hex").slice(0, 40);
152
+ var AFFIRMATIVE = /* @__PURE__ */ new Set([
153
+ "yes",
154
+ "y",
155
+ "approve",
156
+ "approved",
157
+ "ok",
158
+ "okay",
159
+ "confirm",
160
+ "accept",
161
+ "true"
162
+ ]);
163
+ var isApproved = (decision) => {
164
+ if (decision.status !== "answered") return false;
165
+ if (decision.type && decision.type !== "confirm") return true;
166
+ return decision.value != null && AFFIRMATIVE.has(decision.value.trim().toLowerCase());
167
+ };
168
+
169
+ // src/resources/decisions.ts
170
+ var POLL_WINDOW_SECONDS = 50;
171
+ var DEFAULT_ASK_TIMEOUT_MS = 55e3;
172
+ var createDecisionsResource = (request) => {
173
+ const ask = async (data) => {
174
+ const idempotencyKey = data.idempotencyKey ?? (0, import_crypto2.randomUUID)();
175
+ const created = await request("POST", "/decisions", {
176
+ question: data.question,
177
+ type: data.type,
178
+ options: data.options,
179
+ externalId: data.externalId,
180
+ email: data.email,
181
+ agentName: data.agentName,
182
+ context: data.context,
183
+ placeholder: data.placeholder,
184
+ expiresInSeconds: data.expiresInSeconds,
185
+ poweredBy: data.poweredBy,
186
+ callbackUrl: data.callbackUrl,
187
+ requireReachable: data.requireReachable,
188
+ idempotencyKey,
189
+ wait: false
190
+ });
191
+ const type = created.type ?? data.type ?? "confirm";
192
+ let status = created.status;
193
+ let answered = created.answered;
194
+ let value = created.value ?? null;
195
+ const deadline = Date.now() + Math.max(0, data.timeoutMs ?? DEFAULT_ASK_TIMEOUT_MS);
196
+ while (status === "pending" && Date.now() < deadline) {
197
+ const waitSeconds = Math.max(
198
+ 1,
199
+ Math.min(POLL_WINDOW_SECONDS, Math.ceil((deadline - Date.now()) / 1e3))
200
+ );
201
+ const polled = await request("GET", `/decisions/${created.decisionId}`, {
202
+ wait: waitSeconds
203
+ });
204
+ status = polled.status;
205
+ answered = polled.answered;
206
+ value = polled.value;
207
+ }
208
+ return {
209
+ decisionId: created.decisionId,
210
+ status,
211
+ answered,
212
+ value,
213
+ type,
214
+ approved: isApproved({ status, type, value }),
215
+ // Surfaced from the create response so a caller can tell an unanswered
216
+ // decision that reached nobody (reachable:false) apart from a real decline.
217
+ reachable: created.reachable,
218
+ reachableChannels: created.reachableChannels,
219
+ deviceCount: created.deviceCount
220
+ };
221
+ };
222
+ return Object.freeze({
223
+ create: (data) => request("POST", "/decisions", data),
224
+ get: (id, opts) => request("GET", `/decisions/${id}`, opts?.wait ? { wait: opts.wait } : void 0),
225
+ ask,
226
+ answer: (id, answer) => request("POST", `/decisions/${id}`, { answer }),
227
+ cancel: (id) => request("DELETE", `/decisions/${id}`),
228
+ getWebhookSecret: () => request("GET", "/webhook-secret"),
229
+ rotateWebhookSecret: () => request("POST", "/webhook-secret")
230
+ });
231
+ };
232
+
233
+ // src/resources/keys.ts
234
+ var createKeysResource = (request) => Object.freeze({
235
+ issue: (data) => request("POST", "/keys", data),
236
+ list: () => request("GET", "/keys").then((r) => r.keys),
237
+ revoke: (keyPrefix) => request("DELETE", `/keys/${encodeURIComponent(keyPrefix)}`)
238
+ });
239
+
240
+ // src/client.ts
241
+ var DEFAULT_BASE_URL = "https://pushary.com/api/v1/server";
242
+ var validateApiKey = (apiKey) => {
243
+ if (!apiKey) {
244
+ throw new Error("API key is required. Get your API key from https://pushary.com/dashboard/settings");
245
+ }
246
+ if (!apiKey.includes(".")) {
247
+ throw new Error("Invalid API key format. Use the full API key (pk_xxx.sk_xxx)");
248
+ }
249
+ };
250
+ var createContext = (config) => {
251
+ validateApiKey(config.apiKey);
252
+ return Object.freeze({
253
+ baseUrl: config.baseUrl ?? DEFAULT_BASE_URL,
254
+ headers: Object.freeze({
255
+ "Content-Type": "application/json",
256
+ "Authorization": `Bearer ${config.apiKey}`
257
+ })
258
+ });
259
+ };
260
+ var createPusharyServer = (config) => {
261
+ const ctx = createContext(config);
262
+ const request = createRequest(ctx);
263
+ return Object.freeze({
264
+ subscribers: createSubscribersResource(request),
265
+ campaigns: createCampaignsResource(request),
266
+ templates: createTemplatesResource(request),
267
+ notifications: createNotificationsResource(request),
268
+ flows: createFlowsResource(request),
269
+ decisions: createDecisionsResource(request),
270
+ keys: createKeysResource(request),
271
+ enroll: (externalId) => request("POST", "/enroll", { externalId })
272
+ });
273
+ };
274
+
275
+ // src/webhook.ts
276
+ var import_crypto3 = require("crypto");
277
+ var SIGNATURE_HEADER = "x-pushary-signature";
278
+ var verifyWebhookSignature = (rawBody, signature, secret) => {
279
+ if (!signature || !secret) return false;
280
+ const expected = (0, import_crypto3.createHmac)("sha256", secret).update(rawBody).digest("hex");
281
+ const a = Buffer.from(expected);
282
+ const b = Buffer.from(signature);
283
+ if (a.length !== b.length) return false;
284
+ return (0, import_crypto3.timingSafeEqual)(a, b);
285
+ };
286
+ var parseDecisionCallback = (rawBody) => {
287
+ let parsed;
288
+ try {
289
+ parsed = JSON.parse(rawBody);
290
+ } catch {
291
+ return null;
292
+ }
293
+ if (!parsed || typeof parsed !== "object") return null;
294
+ const p = parsed;
295
+ if (typeof p.correlationId !== "string" || typeof p.answer !== "string") return null;
296
+ return {
297
+ correlationId: p.correlationId,
298
+ answer: p.answer,
299
+ value: typeof p.value === "string" ? p.value : p.answer,
300
+ answeredAt: typeof p.answeredAt === "string" ? p.answeredAt : "",
301
+ ...typeof p.context === "string" ? { context: p.context } : {}
302
+ };
303
+ };
304
+
305
+ // src/adapters.ts
306
+ var DEFAULT_NODE = "ask-human";
307
+ var idempotencyKeyFor = (input) => deterministicKey([input.externalId, input.node ?? DEFAULT_NODE, input.question]);
308
+ var isAffirmative = (answer) => isApproved({ status: "answered", type: "confirm", value: answer ?? null });
309
+ var describeAnswer = (type, result) => {
310
+ if (!result.answered) {
311
+ return `No answer (status: ${result.status}). Treat this as NOT approved and do not proceed.`;
312
+ }
313
+ if (type === "confirm") {
314
+ return result.approved ? "The human approved. You may proceed." : "The human declined. Do not proceed.";
315
+ }
316
+ return `The human answered: ${result.value ?? ""}`;
317
+ };
318
+ var resolvePusharyCallback = (rawBody, signature, secret) => {
319
+ if (!verifyWebhookSignature(rawBody, signature, secret)) return null;
320
+ const callback = parseDecisionCallback(rawBody);
321
+ if (!callback) return null;
322
+ return {
323
+ correlationId: callback.correlationId,
324
+ answer: callback.answer,
325
+ value: callback.value,
326
+ approved: isAffirmative(callback.answer),
327
+ context: callback.context,
328
+ answeredAt: callback.answeredAt
329
+ };
330
+ };
331
+ var MAX_INPUT_CHARS = 300;
332
+ var DENIED_UNANSWERED = "No answer from the approver, so this was denied. Do not retry the same action.";
333
+ var DENIED_REFUSED = "The approver denied this action.";
334
+ var renderApprovalQuestion = (toolName, toolInput) => {
335
+ if (toolInput === void 0 || toolInput === null) return `Approve ${toolName}?`;
336
+ const summary = summarise(toolInput);
337
+ return summary === void 0 ? `Approve ${toolName}?` : `Approve ${toolName}? ${summary}`;
338
+ };
339
+ var summarise = (toolInput) => {
340
+ let rendered;
341
+ if (typeof toolInput === "string") {
342
+ rendered = toolInput;
343
+ } else {
344
+ try {
345
+ rendered = JSON.stringify(toolInput);
346
+ } catch {
347
+ return void 0;
348
+ }
349
+ }
350
+ if (rendered === void 0) return void 0;
351
+ return rendered.length > MAX_INPUT_CHARS ? `${rendered.slice(0, MAX_INPUT_CHARS)}...` : rendered;
352
+ };
353
+ var createAdapterKernel = (helpersLabel) => {
354
+ const requireApiKey = (apiKey) => {
355
+ const resolved = apiKey ?? process.env.PUSHARY_API_KEY;
356
+ if (!resolved) {
357
+ throw new Error(`Pushary: set PUSHARY_API_KEY or pass { apiKey } to ${helpersLabel}.`);
358
+ }
359
+ return resolved;
360
+ };
361
+ const client = (config) => createPusharyServer({
362
+ apiKey: requireApiKey(config.apiKey),
363
+ ...config.baseUrl ? { baseUrl: config.baseUrl } : {}
364
+ });
365
+ const requireExternalId = (externalId) => {
366
+ if (!externalId) {
367
+ throw new Error(
368
+ `Pushary: no end-user to ask. Pass { externalId } to ${helpersLabel}, or run user-scoped auth so the framework supplies one.`
369
+ );
370
+ }
371
+ return externalId;
372
+ };
373
+ const createGate = (config) => {
374
+ const gateClient = createPusharyServer({
375
+ apiKey: requireApiKey(config.apiKey),
376
+ ...config.baseUrl ? { baseUrl: config.baseUrl } : {}
377
+ });
378
+ return async (ask) => {
379
+ const result = await gateClient.decisions.ask({
380
+ question: ask.question,
381
+ type: "confirm",
382
+ externalId: ask.externalId,
383
+ ...config.agentName ? { agentName: config.agentName } : {},
384
+ ...config.expiresInSeconds ? { expiresInSeconds: config.expiresInSeconds } : {},
385
+ ...config.timeoutMs !== void 0 ? { timeoutMs: config.timeoutMs } : {},
386
+ ...config.requireReachable ? { requireReachable: config.requireReachable } : {},
387
+ // Stable across replays, so a re-run of this call resolves to the same
388
+ // decision instead of asking twice.
389
+ idempotencyKey: deterministicKey([ask.sessionId, ask.callId, ask.toolName])
390
+ });
391
+ if (result.approved) return { approved: true };
392
+ return { approved: false, reason: result.answered ? DENIED_REFUSED : DENIED_UNANSWERED };
393
+ };
394
+ };
395
+ const askExternalUser = (config, input) => client(config).decisions.ask({
396
+ question: input.question,
397
+ type: input.type,
398
+ options: input.options,
399
+ externalId: input.externalId,
400
+ context: input.context,
401
+ agentName: config.agentName,
402
+ timeoutMs: config.timeoutMs,
403
+ idempotencyKey: idempotencyKeyFor(input)
404
+ });
405
+ const createDurableDecision = async (config, input) => {
406
+ const created = await client(config).decisions.create({
407
+ question: input.question,
408
+ type: input.type,
409
+ options: input.options,
410
+ externalId: input.externalId,
411
+ context: input.context,
412
+ callbackUrl: input.callbackUrl,
413
+ agentName: config.agentName,
414
+ idempotencyKey: idempotencyKeyFor(input),
415
+ wait: false
416
+ });
417
+ return {
418
+ decisionId: created.decisionId,
419
+ correlationId: created.decisionId,
420
+ status: created.status,
421
+ reachable: created.reachable,
422
+ reachableChannels: created.reachableChannels,
423
+ deviceCount: created.deviceCount
424
+ };
425
+ };
426
+ const connect = (config, externalId) => client(config).enroll(externalId);
427
+ return Object.freeze({
428
+ client,
429
+ askExternalUser,
430
+ createDurableDecision,
431
+ connect,
432
+ createGate,
433
+ requireExternalId
434
+ });
435
+ };
436
+ // Annotate the CommonJS export names for ESM import in node:
437
+ 0 && (module.exports = {
438
+ DENIED_REFUSED,
439
+ DENIED_UNANSWERED,
440
+ SIGNATURE_HEADER,
441
+ createAdapterKernel,
442
+ describeAnswer,
443
+ deterministicKey,
444
+ idempotencyKeyFor,
445
+ isAffirmative,
446
+ isApproved,
447
+ parseDecisionCallback,
448
+ renderApprovalQuestion,
449
+ resolvePusharyCallback,
450
+ verifyWebhookSignature
451
+ });
452
+ //# sourceMappingURL=adapters.cjs.map