@pushary/server 2.2.0 → 2.3.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 +57 -37
- package/dist/adapters.cjs +27 -12
- package/dist/adapters.cjs.map +1 -1
- package/dist/adapters.d.cts +3 -3
- package/dist/adapters.d.ts +3 -3
- package/dist/adapters.js +1 -1
- package/dist/adapters.js.map +1 -1
- package/dist/{chunk-DLJ3L77X.js → chunk-5MUTDMR5.js} +28 -13
- package/dist/chunk-5MUTDMR5.js.map +1 -0
- package/dist/index.cjs +27 -12
- 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-M0kCZWd-.d.cts → util-CP-PiRcz.d.cts} +7 -4
- package/dist/{util-M0kCZWd-.d.ts → util-CP-PiRcz.d.ts} +7 -4
- package/package.json +6 -4
- package/dist/chunk-DLJ3L77X.js.map +0 -1
package/README.md
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
# @pushary/server
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The decision layer for AI agents. Your agent asks, your user decides on their phone.
|
|
4
|
+
|
|
5
|
+
This is the server SDK for [Pushary](https://pushary.com). It also sends push notifications.
|
|
6
|
+
|
|
7
|
+
## What you need
|
|
8
|
+
|
|
9
|
+
- A Pushary Partner plan, from $99 a month, to ask your own users. [Start the trial](https://pushary.com/sign-up?from=agent&plan=partner).
|
|
10
|
+
- An API key from [Partner onboarding](https://pushary.com/onboarding/partner), set as `PUSHARY_API_KEY`.
|
|
11
|
+
- Your users install the free Pushary app ([iPhone](https://apps.apple.com/us/app/pushary/id6785677563), [Android](https://play.google.com/store/apps/details?id=com.pushary.app)). They never sign up or pay.
|
|
4
12
|
|
|
5
13
|
## Installation
|
|
6
14
|
|
|
@@ -19,17 +27,25 @@ bun add @pushary/server
|
|
|
19
27
|
```typescript
|
|
20
28
|
import { createPusharyServer } from '@pushary/server'
|
|
21
29
|
|
|
22
|
-
const pushary = createPusharyServer({
|
|
23
|
-
apiKey: process.env.PUSHARY_API_KEY,
|
|
24
|
-
})
|
|
30
|
+
const pushary = createPusharyServer({ apiKey: process.env.PUSHARY_API_KEY })
|
|
25
31
|
|
|
26
|
-
await pushary.
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
32
|
+
const { universalLink } = await pushary.enroll('user-123')
|
|
33
|
+
// Once per user: show universalLink as a button or QR code.
|
|
34
|
+
|
|
35
|
+
const { approved } = await pushary.decisions.ask({
|
|
36
|
+
externalId: 'user-123',
|
|
37
|
+
question: 'Issue a $50 refund?',
|
|
38
|
+
type: 'confirm', // confirm | select | input
|
|
30
39
|
})
|
|
40
|
+
if (approved) await issueRefund()
|
|
31
41
|
```
|
|
32
42
|
|
|
43
|
+
That is the whole integration: connect a user's phone once, then ask them whenever
|
|
44
|
+
your agent needs a yes. `ask()` creates a fresh decision per call and waits until the
|
|
45
|
+
person answers or the deadline passes (55 seconds by default). `approved` is true only
|
|
46
|
+
when the person said yes, so a declined, expired or unanswered decision blocks the
|
|
47
|
+
action. For longer waits, see [Decisions (lower-level)](#decisions-lower-level).
|
|
48
|
+
|
|
33
49
|
## API Key
|
|
34
50
|
|
|
35
51
|
The server SDK requires your full API key (`pk_xxx.sk_xxx`) which includes the secret portion.
|
|
@@ -38,6 +54,22 @@ The server SDK requires your full API key (`pk_xxx.sk_xxx`) which includes the s
|
|
|
38
54
|
|
|
39
55
|
Get your API key by following [Get your API key](https://pushary.com/docs/agents/api-key).
|
|
40
56
|
|
|
57
|
+
## Push notifications quick start
|
|
58
|
+
|
|
59
|
+
```typescript
|
|
60
|
+
import { createPusharyServer } from '@pushary/server'
|
|
61
|
+
|
|
62
|
+
const pushary = createPusharyServer({
|
|
63
|
+
apiKey: process.env.PUSHARY_API_KEY,
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
await pushary.notifications.send({
|
|
67
|
+
title: 'Hello!',
|
|
68
|
+
body: 'Your order has shipped',
|
|
69
|
+
subscriberIds: ['sub_123'],
|
|
70
|
+
})
|
|
71
|
+
```
|
|
72
|
+
|
|
41
73
|
## Resources
|
|
42
74
|
|
|
43
75
|
### Subscribers
|
|
@@ -121,30 +153,6 @@ await pushary.notifications.send({
|
|
|
121
153
|
})
|
|
122
154
|
```
|
|
123
155
|
|
|
124
|
-
### Human-in-the-loop for agents (the two-call contract)
|
|
125
|
-
|
|
126
|
-
The whole integration is two calls. Connect an end-user's phone once, then ask them
|
|
127
|
-
whenever your agent needs a human. Requires the [Partner plan](https://pushary.com/agent-notifications-integration).
|
|
128
|
-
|
|
129
|
-
```typescript
|
|
130
|
-
// 1. Connect an end-user's phone (keyless, no account for them). Show the link.
|
|
131
|
-
const { universalLink } = await pushary.enroll('user-123')
|
|
132
|
-
// Render universalLink as a button or QR. One tap turns on approvals.
|
|
133
|
-
|
|
134
|
-
// 2. Ask that person and block until they answer. Fail-closed `approved` flag.
|
|
135
|
-
const { approved, value, status } = await pushary.decisions.ask({
|
|
136
|
-
externalId: 'user-123',
|
|
137
|
-
question: 'Issue a $50 refund?',
|
|
138
|
-
type: 'confirm', // confirm | select | input
|
|
139
|
-
})
|
|
140
|
-
if (approved) await issueRefund()
|
|
141
|
-
```
|
|
142
|
-
|
|
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
|
-
`approved` is true only when the person actually said yes, so a declined, expired, or
|
|
146
|
-
unanswered decision safely blocks the action.
|
|
147
|
-
|
|
148
156
|
### Decisions (lower-level)
|
|
149
157
|
|
|
150
158
|
For long waits or your own resume logic, use `create` + a webhook or `get`, and
|
|
@@ -248,7 +256,8 @@ For an explicit human gate, set `policy: false`. The gate keeps the full review
|
|
|
248
256
|
**Upgrade handling:** version 2.1 changes human gate keys. Finish already-started operations on the SDK version that created them, or migrate their persisted bindings explicitly. Do not replay previously executed operations through the new key scheme. A fingerprint identifies the proposed action; the application's execution record and framework persistence still own recovery after an uncertain tool execution.
|
|
249
257
|
|
|
250
258
|
Every official Pushary adapter (`@pushary/eve`, `@pushary/ai-sdk`,
|
|
251
|
-
`@pushary/langgraph`, `@pushary/mastra`, `@pushary/openai-agents
|
|
259
|
+
`@pushary/langgraph`, `@pushary/mastra`, `@pushary/openai-agents`,
|
|
260
|
+
`@pushary/claude-agent-sdk`) is a thin binding
|
|
252
261
|
over one shared kernel, and that kernel is public. If you run an in-house harness, or
|
|
253
262
|
a framework we have not shipped for, this is the same surface they are built on.
|
|
254
263
|
|
|
@@ -316,15 +325,15 @@ names asks a person. `run` is called only after the action is authorized, and an
|
|
|
316
325
|
error it throws is not caught: reporting it as `ok: false` would be
|
|
317
326
|
indistinguishable from a refusal.
|
|
318
327
|
|
|
319
|
-
`protect()` is **at most once**. The decision is idempotent
|
|
320
|
-
`runId` + `callId` + action lands on the same approval instead of asking twice
|
|
328
|
+
`protect()` is **at most once**. The decision is idempotent: a replay of the same
|
|
329
|
+
`runId` + `callId` + action lands on the same approval instead of asking twice: and
|
|
321
330
|
the approval is then spent against a durable permit before `run` is called. A retry, a
|
|
322
331
|
concurrent worker and a resumed run all reach the same permit and exactly one of them
|
|
323
332
|
proceeds; the rest come back `ok: false` with a reason worded to stop the model rather
|
|
324
333
|
than invite another attempt.
|
|
325
334
|
|
|
326
|
-
The permit is bound to the exact subject that was authorized
|
|
327
|
-
the actor, the environment, the end-user and every fact you sent
|
|
335
|
+
The permit is bound to the exact subject that was authorized: the action, its target,
|
|
336
|
+
the actor, the environment, the end-user and every fact you sent: so changing the
|
|
328
337
|
amount between the approval and the execution leaves you with no permit for the action
|
|
329
338
|
you now want to run. `protect()` then records `succeeded` or `failed` against it; a
|
|
330
339
|
process that dies mid-action leaves the permit unresolved, which is visible and
|
|
@@ -425,3 +434,14 @@ A positive `timeoutMs` covers creation and polling. `timeoutMs: 0` retains creat
|
|
|
425
434
|
### Upgrading to 2.0
|
|
426
435
|
|
|
427
436
|
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.
|
|
437
|
+
|
|
438
|
+
### Cancel decision HTTP work
|
|
439
|
+
|
|
440
|
+
`decisions.create(data, { signal })`, `decisions.get(id, { wait, signal })`, and
|
|
441
|
+
`decisions.cancel(id, { signal })` accept an optional `AbortSignal`. The HTTP
|
|
442
|
+
`requestTimeoutMs` still applies when a caller signal is present, including while
|
|
443
|
+
reading the response body. Caller cancellation preserves its abort reason;
|
|
444
|
+
HTTP deadline expiry raises `TimeoutError`. These calls do not retry internally.
|
|
445
|
+
Aborting HTTP work does not withdraw a server-side decision: cancel its known ID
|
|
446
|
+
with a separate cleanup signal. A lost create response may leave a decision until
|
|
447
|
+
its configured expiration.
|
package/dist/adapters.cjs
CHANGED
|
@@ -97,13 +97,28 @@ var handleResponse = async (response) => {
|
|
|
97
97
|
var createRequest = (ctx) => async (method, path, body, options) => {
|
|
98
98
|
const isGet = method === "GET";
|
|
99
99
|
const url = isGet && body ? buildUrl(ctx.baseUrl, path, body) : `${ctx.baseUrl}${path}`;
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
100
|
+
options?.signal?.throwIfAborted();
|
|
101
|
+
const controller = new AbortController();
|
|
102
|
+
const abort = () => controller.abort(options?.signal?.reason);
|
|
103
|
+
options?.signal?.addEventListener("abort", abort, { once: true });
|
|
104
|
+
const timer = setTimeout(() => controller.abort(new DOMException("Pushary request timed out", "TimeoutError")), ctx.timeoutMs ?? 65e3);
|
|
105
|
+
try {
|
|
106
|
+
const response = await fetch(url, {
|
|
107
|
+
method,
|
|
108
|
+
signal: controller.signal,
|
|
109
|
+
headers: ctx.headers,
|
|
110
|
+
body: !isGet && body ? JSON.stringify(body) : void 0
|
|
111
|
+
});
|
|
112
|
+
const result = await handleResponse(response);
|
|
113
|
+
controller.signal.throwIfAborted();
|
|
114
|
+
return result;
|
|
115
|
+
} catch (error) {
|
|
116
|
+
controller.signal.throwIfAborted();
|
|
117
|
+
throw error;
|
|
118
|
+
} finally {
|
|
119
|
+
clearTimeout(timer);
|
|
120
|
+
options?.signal?.removeEventListener("abort", abort);
|
|
121
|
+
}
|
|
107
122
|
};
|
|
108
123
|
|
|
109
124
|
// src/resources/subscribers.ts
|
|
@@ -277,12 +292,12 @@ var createDecisionsResource = (request) => {
|
|
|
277
292
|
}
|
|
278
293
|
};
|
|
279
294
|
return Object.freeze({
|
|
280
|
-
create: (data) => request("POST", "/decisions", data),
|
|
281
|
-
get: (id, opts) => request("GET", `/decisions/${id}`, opts?.wait ? { wait: opts.wait } : void 0),
|
|
295
|
+
create: (data, options) => request("POST", "/decisions", data, options),
|
|
296
|
+
get: (id, opts) => request("GET", `/decisions/${id}`, opts?.wait ? { wait: opts.wait } : void 0, opts),
|
|
282
297
|
list: (opts) => request("GET", "/decisions", opts ? { ...opts } : void 0),
|
|
283
298
|
ask,
|
|
284
299
|
answer: (id, answer) => request("POST", `/decisions/${id}`, { answer }),
|
|
285
|
-
cancel: (id) => request("DELETE", `/decisions/${id}
|
|
300
|
+
cancel: (id, options) => request("DELETE", `/decisions/${id}`, void 0, options),
|
|
286
301
|
getWebhookSecret: () => request("GET", "/webhook-secret"),
|
|
287
302
|
rotateWebhookSecret: () => request("POST", "/webhook-secret")
|
|
288
303
|
});
|
|
@@ -395,10 +410,10 @@ var createRecordExecution = (request) => (receipt) => request("POST", `/authoriz
|
|
|
395
410
|
var DEFAULT_BASE_URL = "https://pushary.com/api/v1/server";
|
|
396
411
|
var validateApiKey = (apiKey) => {
|
|
397
412
|
if (!apiKey) {
|
|
398
|
-
throw new Error("API key is required. Get your API key from https://pushary.com/dashboard/settings");
|
|
413
|
+
throw new Error("API key is required. Get your API key from https://pushary.com/dashboard/agent/settings");
|
|
399
414
|
}
|
|
400
415
|
if (!apiKey.includes(".")) {
|
|
401
|
-
throw new Error("Invalid API key format. Use the full API key (pk_xxx.
|
|
416
|
+
throw new Error("Invalid API key format. Use the full API key (pk_xxx.xxx)");
|
|
402
417
|
}
|
|
403
418
|
};
|
|
404
419
|
var createContext = (config) => {
|