@pushary/server 1.5.0 → 2.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 +14 -4
- package/dist/adapters.cjs +81 -60
- package/dist/adapters.cjs.map +1 -1
- package/dist/adapters.d.cts +7 -5
- package/dist/adapters.d.ts +7 -5
- package/dist/adapters.js +6 -3
- package/dist/adapters.js.map +1 -1
- package/dist/{chunk-E6DWMKLZ.js → chunk-2B6ABKTM.js} +77 -59
- package/dist/chunk-2B6ABKTM.js.map +1 -0
- package/dist/index.cjs +76 -58
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/{util-DeGBh5at.d.cts → util-BvCioL1a.d.cts} +4 -0
- package/dist/{util-DeGBh5at.d.ts → util-BvCioL1a.d.ts} +4 -0
- package/package.json +1 -1
- package/dist/chunk-E6DWMKLZ.js.map +0 -1
package/README.md
CHANGED
|
@@ -140,8 +140,8 @@ const { approved, value, status } = await pushary.decisions.ask({
|
|
|
140
140
|
if (approved) await issueRefund()
|
|
141
141
|
```
|
|
142
142
|
|
|
143
|
-
`ask()` creates
|
|
144
|
-
durably until the human answers or the deadline passes (default 55s
|
|
143
|
+
`ask()` creates a fresh decision per call and polls
|
|
144
|
+
durably until the human answers or the deadline passes (default 55s for a positive local wait budget).
|
|
145
145
|
`approved` is true only when the person actually said yes, so a declined, expired, or
|
|
146
146
|
unanswered decision safely blocks the action.
|
|
147
147
|
|
|
@@ -195,8 +195,8 @@ import {
|
|
|
195
195
|
const idempotencyKey = deterministicKey(['run-abc', 'step-3', 'user-123'])
|
|
196
196
|
|
|
197
197
|
// isApproved(decision: { status, type?, value }): boolean
|
|
198
|
-
//
|
|
199
|
-
//
|
|
198
|
+
// For confirm: true only for an affirmative answer. For select/input: true
|
|
199
|
+
// means answered, not permission to act. Check the actual value for those types.
|
|
200
200
|
isApproved({ status: 'answered', type: 'confirm', value: 'yes' }) // true
|
|
201
201
|
isApproved({ status: 'answered', type: 'confirm', value: 'no' }) // false
|
|
202
202
|
isApproved({ status: 'pending', type: 'confirm', value: null }) // false
|
|
@@ -394,3 +394,13 @@ import {
|
|
|
394
394
|
## License
|
|
395
395
|
|
|
396
396
|
MIT. See `LICENSE`.
|
|
397
|
+
|
|
398
|
+
## Retries and deadlines
|
|
399
|
+
|
|
400
|
+
For retries of one business operation, pass `idempotencyKey: deterministicKey([runId, stepId, userId])`. Independent asks get fresh keys, even with identical questions. Shared adapter durable creates require this key; missing keys fail before a request is sent. Keep a run ID unique per operation and stable across its retries.
|
|
401
|
+
|
|
402
|
+
A positive `timeoutMs` covers creation and polling. `timeoutMs: 0` retains create-only behavior. A timeout during creation throws; retry with the same explicit key to recover the decision. A timeout during polling returns the known pending decision ID for later `get()` or a webhook. `signal` cancels the local wait and throws; it does not cancel the remote decision. Other HTTP calls use `requestTimeoutMs` (65 seconds by default).
|
|
403
|
+
|
|
404
|
+
### Upgrading to 2.0
|
|
405
|
+
|
|
406
|
+
Upgrade the framework adapter alongside this SDK. Durable helpers now require an explicit `idempotencyKey` tied to the run and step. The 0.3 LangGraph, Mastra and OpenAI adapters support this contract. Earlier adapters must keep the 1.x SDK until migrated. Independent blocking calls no longer deduplicate by question text.
|
package/dist/adapters.cjs
CHANGED
|
@@ -42,6 +42,7 @@ __export(adapters_exports, {
|
|
|
42
42
|
verifyWebhookSignature: () => verifyWebhookSignature
|
|
43
43
|
});
|
|
44
44
|
module.exports = __toCommonJS(adapters_exports);
|
|
45
|
+
var import_node_crypto = require("crypto");
|
|
45
46
|
|
|
46
47
|
// src/error.ts
|
|
47
48
|
var PusharyApiError = class _PusharyApiError extends Error {
|
|
@@ -92,11 +93,12 @@ var handleResponse = async (response) => {
|
|
|
92
93
|
}
|
|
93
94
|
return response.json();
|
|
94
95
|
};
|
|
95
|
-
var createRequest = (ctx) => async (method, path, body) => {
|
|
96
|
+
var createRequest = (ctx) => async (method, path, body, options) => {
|
|
96
97
|
const isGet = method === "GET";
|
|
97
98
|
const url = isGet && body ? buildUrl(ctx.baseUrl, path, body) : `${ctx.baseUrl}${path}`;
|
|
98
99
|
const response = await fetch(url, {
|
|
99
100
|
method,
|
|
101
|
+
signal: options?.signal ?? AbortSignal.timeout(ctx.timeoutMs ?? 65e3),
|
|
100
102
|
headers: ctx.headers,
|
|
101
103
|
body: !isGet && body ? JSON.stringify(body) : void 0
|
|
102
104
|
});
|
|
@@ -179,64 +181,80 @@ var DEFAULT_ASK_TIMEOUT_MS = 55e3;
|
|
|
179
181
|
var createDecisionsResource = (request) => {
|
|
180
182
|
const ask = async (data) => {
|
|
181
183
|
const idempotencyKey = data.idempotencyKey ?? (0, import_crypto2.randomUUID)();
|
|
182
|
-
const
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
184
|
+
const timeoutMs = Math.max(0, data.timeoutMs ?? DEFAULT_ASK_TIMEOUT_MS);
|
|
185
|
+
const deadline = Date.now() + timeoutMs;
|
|
186
|
+
const controller = new AbortController();
|
|
187
|
+
const abort = () => controller.abort(data.signal?.reason);
|
|
188
|
+
if (data.signal?.aborted) abort();
|
|
189
|
+
else data.signal?.addEventListener("abort", abort, { once: true });
|
|
190
|
+
const timer = setTimeout(() => controller.abort(new DOMException("Pushary ask timed out", "TimeoutError")), timeoutMs || 65e3);
|
|
191
|
+
try {
|
|
192
|
+
const created = await request("POST", "/decisions", {
|
|
193
|
+
question: data.question,
|
|
194
|
+
type: data.type,
|
|
195
|
+
options: data.options,
|
|
196
|
+
externalId: data.externalId,
|
|
197
|
+
email: data.email,
|
|
198
|
+
agentName: data.agentName,
|
|
199
|
+
context: data.context,
|
|
200
|
+
placeholder: data.placeholder,
|
|
201
|
+
toolName: data.toolName,
|
|
202
|
+
toolTarget: data.toolTarget,
|
|
203
|
+
actor: data.actor,
|
|
204
|
+
environment: data.environment,
|
|
205
|
+
parameters: data.parameters,
|
|
206
|
+
// Its changes name parameters rather than carrying values, so what the
|
|
207
|
+
// person sees is read from the same numbers a rule compares.
|
|
208
|
+
presentation: data.presentation,
|
|
209
|
+
approvalUrl: data.approvalUrl,
|
|
210
|
+
expiresInSeconds: data.expiresInSeconds,
|
|
211
|
+
poweredBy: data.poweredBy,
|
|
212
|
+
callbackUrl: data.callbackUrl,
|
|
213
|
+
requireReachable: data.requireReachable,
|
|
214
|
+
// Read only by a key issued on your workspace's sandbox site, where the
|
|
215
|
+
// simulator answers instead of a person. Ignored in production.
|
|
216
|
+
sandboxOutcome: data.sandboxOutcome,
|
|
217
|
+
idempotencyKey,
|
|
218
|
+
wait: false
|
|
219
|
+
}, { signal: controller.signal });
|
|
220
|
+
const type = created.type ?? data.type ?? "confirm";
|
|
221
|
+
let status = created.status;
|
|
222
|
+
let answered = created.answered;
|
|
223
|
+
let value = created.value ?? null;
|
|
224
|
+
while (status === "pending" && Date.now() < deadline) {
|
|
225
|
+
const waitSeconds = Math.max(
|
|
226
|
+
1,
|
|
227
|
+
Math.min(POLL_WINDOW_SECONDS, Math.ceil((deadline - Date.now()) / 1e3))
|
|
228
|
+
);
|
|
229
|
+
try {
|
|
230
|
+
const polled = await request("GET", `/decisions/${created.decisionId}`, {
|
|
231
|
+
wait: waitSeconds
|
|
232
|
+
}, { signal: controller.signal });
|
|
233
|
+
status = polled.status;
|
|
234
|
+
answered = polled.answered;
|
|
235
|
+
value = polled.value;
|
|
236
|
+
} catch (error) {
|
|
237
|
+
if (controller.signal.aborted && !data.signal?.aborted) break;
|
|
238
|
+
throw error;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
return {
|
|
242
|
+
decisionId: created.decisionId,
|
|
243
|
+
status,
|
|
244
|
+
answered,
|
|
245
|
+
value,
|
|
246
|
+
type,
|
|
247
|
+
approved: isApproved({ status, type, value }),
|
|
248
|
+
// Surfaced from the create response so a caller can tell an unanswered
|
|
249
|
+
// decision that reached nobody (reachable:false) apart from a real decline.
|
|
250
|
+
reachable: created.reachable,
|
|
251
|
+
reachableChannels: created.reachableChannels,
|
|
252
|
+
deviceCount: created.deviceCount
|
|
253
|
+
};
|
|
254
|
+
} finally {
|
|
255
|
+
clearTimeout(timer);
|
|
256
|
+
data.signal?.removeEventListener("abort", abort);
|
|
226
257
|
}
|
|
227
|
-
return {
|
|
228
|
-
decisionId: created.decisionId,
|
|
229
|
-
status,
|
|
230
|
-
answered,
|
|
231
|
-
value,
|
|
232
|
-
type,
|
|
233
|
-
approved: isApproved({ status, type, value }),
|
|
234
|
-
// Surfaced from the create response so a caller can tell an unanswered
|
|
235
|
-
// decision that reached nobody (reachable:false) apart from a real decline.
|
|
236
|
-
reachable: created.reachable,
|
|
237
|
-
reachableChannels: created.reachableChannels,
|
|
238
|
-
deviceCount: created.deviceCount
|
|
239
|
-
};
|
|
240
258
|
};
|
|
241
259
|
return Object.freeze({
|
|
242
260
|
create: (data) => request("POST", "/decisions", data),
|
|
@@ -366,6 +384,7 @@ var createContext = (config) => {
|
|
|
366
384
|
validateApiKey(config.apiKey);
|
|
367
385
|
return Object.freeze({
|
|
368
386
|
baseUrl: config.baseUrl ?? DEFAULT_BASE_URL,
|
|
387
|
+
timeoutMs: config.requestTimeoutMs ?? 65e3,
|
|
369
388
|
headers: Object.freeze({
|
|
370
389
|
"Content-Type": "application/json",
|
|
371
390
|
"Authorization": `Bearer ${config.apiKey}`
|
|
@@ -424,9 +443,8 @@ var parseDecisionCallback = (rawBody) => {
|
|
|
424
443
|
};
|
|
425
444
|
|
|
426
445
|
// src/adapters.ts
|
|
427
|
-
var DEFAULT_NODE = "ask-human";
|
|
428
446
|
var defaultProtectQuestion = (action, target) => `Approve ${action}${target ? ` ${target}` : ""}?`;
|
|
429
|
-
var idempotencyKeyFor = (input) =>
|
|
447
|
+
var idempotencyKeyFor = (input) => input.idempotencyKey ?? (0, import_node_crypto.randomUUID)();
|
|
430
448
|
var isAffirmative = (answer) => isApproved({ status: "answered", type: "confirm", value: answer ?? null });
|
|
431
449
|
var describeAnswer = (type, result) => {
|
|
432
450
|
if (!result.answered) {
|
|
@@ -633,6 +651,9 @@ var createAdapterKernel = (helpersLabel) => {
|
|
|
633
651
|
idempotencyKey: idempotencyKeyFor(input)
|
|
634
652
|
});
|
|
635
653
|
const createDurableDecision = async (config, input) => {
|
|
654
|
+
if (!input.idempotencyKey?.trim()) {
|
|
655
|
+
throw new Error("Pushary: durable decisions require an idempotencyKey tied to the run and step.");
|
|
656
|
+
}
|
|
636
657
|
const created = await client(config).decisions.create({
|
|
637
658
|
question: input.question,
|
|
638
659
|
type: input.type,
|