@pushary/server 1.3.3 → 1.5.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.
@@ -0,0 +1,733 @@
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_BY_POLICY: () => DENIED_BY_POLICY,
24
+ DENIED_REFUSED: () => DENIED_REFUSED,
25
+ DENIED_UNANSWERED: () => DENIED_UNANSWERED,
26
+ SIGNATURE_HEADER: () => SIGNATURE_HEADER,
27
+ UNBOUND_APPROVAL: () => UNBOUND_APPROVAL,
28
+ UNCONFIRMED_PERMIT: () => UNCONFIRMED_PERMIT,
29
+ createAdapterKernel: () => createAdapterKernel,
30
+ defaultProtectQuestion: () => defaultProtectQuestion,
31
+ deniedByPolicy: () => deniedByPolicy,
32
+ deriveParameters: () => deriveParameters,
33
+ describeAnswer: () => describeAnswer,
34
+ deterministicKey: () => deterministicKey,
35
+ idempotencyKeyFor: () => idempotencyKeyFor,
36
+ isAffirmative: () => isAffirmative,
37
+ isApproved: () => isApproved,
38
+ parseDecisionCallback: () => parseDecisionCallback,
39
+ refusedPermit: () => refusedPermit,
40
+ renderApprovalQuestion: () => renderApprovalQuestion,
41
+ resolvePusharyCallback: () => resolvePusharyCallback,
42
+ verifyWebhookSignature: () => verifyWebhookSignature
43
+ });
44
+ module.exports = __toCommonJS(adapters_exports);
45
+
46
+ // src/error.ts
47
+ var PusharyApiError = class _PusharyApiError extends Error {
48
+ constructor(message, init) {
49
+ super(message);
50
+ this.name = "PusharyApiError";
51
+ this.status = init.status;
52
+ this.statusText = init.statusText ?? "";
53
+ this.code = init.code;
54
+ this.body = init.body;
55
+ Object.setPrototypeOf(this, _PusharyApiError.prototype);
56
+ }
57
+ /** The key is missing, malformed, or revoked. Re-authenticate; do not retry. */
58
+ get isAuthError() {
59
+ return this.status === 401 || this.status === 403;
60
+ }
61
+ /** Rate limited. Back off and retry. */
62
+ get isRateLimited() {
63
+ return this.status === 429;
64
+ }
65
+ /** Server-side fault. Safe to retry an idempotent call. */
66
+ get isServerError() {
67
+ return this.status >= 500;
68
+ }
69
+ };
70
+
71
+ // src/request.ts
72
+ var buildUrl = (baseUrl, path, params) => {
73
+ const url = new URL(`${baseUrl}${path}`);
74
+ if (params) {
75
+ Object.entries(params).forEach(([key, value]) => {
76
+ if (value !== void 0 && value !== null) {
77
+ url.searchParams.set(key, String(value));
78
+ }
79
+ });
80
+ }
81
+ return url.toString();
82
+ };
83
+ var handleResponse = async (response) => {
84
+ if (!response.ok) {
85
+ const body = await response.json().catch(() => ({}));
86
+ throw new PusharyApiError(body.message || body.error || `HTTP ${response.status}`, {
87
+ status: response.status,
88
+ statusText: response.statusText,
89
+ code: body.code,
90
+ body
91
+ });
92
+ }
93
+ return response.json();
94
+ };
95
+ var createRequest = (ctx) => async (method, path, body) => {
96
+ const isGet = method === "GET";
97
+ const url = isGet && body ? buildUrl(ctx.baseUrl, path, body) : `${ctx.baseUrl}${path}`;
98
+ const response = await fetch(url, {
99
+ method,
100
+ headers: ctx.headers,
101
+ body: !isGet && body ? JSON.stringify(body) : void 0
102
+ });
103
+ return handleResponse(response);
104
+ };
105
+
106
+ // src/resources/subscribers.ts
107
+ var createSubscribersResource = (request) => Object.freeze({
108
+ list: (params) => request("GET", "/subscribers", params),
109
+ get: (id) => request("GET", `/subscribers/${id}`),
110
+ update: (id, data) => request("PATCH", `/subscribers/${id}`, data),
111
+ delete: (id) => request("DELETE", `/subscribers/${id}`),
112
+ count: () => request("GET", "/subscribers/count")
113
+ });
114
+
115
+ // src/resources/campaigns.ts
116
+ var createCampaignsResource = (request) => Object.freeze({
117
+ list: (params) => request("GET", "/campaigns", params),
118
+ create: (data) => request("POST", "/campaigns", data),
119
+ get: (id) => request("GET", `/campaigns/${id}`),
120
+ update: (id, data) => request("PATCH", `/campaigns/${id}`, data),
121
+ delete: (id) => request("DELETE", `/campaigns/${id}`),
122
+ send: (id) => request("POST", `/campaigns/${id}/send`),
123
+ pause: (id) => request("POST", `/campaigns/${id}/pause`),
124
+ resume: (id) => request("POST", `/campaigns/${id}/resume`),
125
+ stats: (id) => request("GET", `/campaigns/${id}/stats`)
126
+ });
127
+
128
+ // src/resources/templates.ts
129
+ var createTemplatesResource = (request) => Object.freeze({
130
+ list: (params) => request("GET", "/templates", params),
131
+ create: (data) => request("POST", "/templates", data),
132
+ get: (id) => request("GET", `/templates/${id}`),
133
+ update: (id, data) => request("PATCH", `/templates/${id}`, data),
134
+ delete: (id) => request("DELETE", `/templates/${id}`)
135
+ });
136
+
137
+ // src/resources/notifications.ts
138
+ var createNotificationsResource = (request) => Object.freeze({
139
+ send: (data) => request("POST", "/send", data)
140
+ });
141
+
142
+ // src/resources/flows.ts
143
+ var createFlowsResource = (request) => Object.freeze({
144
+ list: (params) => request("GET", "/flows", params),
145
+ create: (data) => request("POST", "/flows", data),
146
+ get: (id) => request("GET", `/flows/${id}`),
147
+ update: (id, data) => request("PATCH", `/flows/${id}`, data),
148
+ delete: (id) => request("DELETE", `/flows/${id}`),
149
+ activate: (id) => request("POST", `/flows/${id}/activate`),
150
+ pause: (id) => request("POST", `/flows/${id}/pause`)
151
+ });
152
+
153
+ // src/resources/decisions.ts
154
+ var import_crypto2 = require("crypto");
155
+
156
+ // src/util.ts
157
+ var import_crypto = require("crypto");
158
+ var deterministicKey = (parts) => (0, import_crypto.createHash)("sha256").update(parts.join("\0")).digest("hex").slice(0, 40);
159
+ var AFFIRMATIVE = /* @__PURE__ */ new Set([
160
+ "yes",
161
+ "y",
162
+ "approve",
163
+ "approved",
164
+ "ok",
165
+ "okay",
166
+ "confirm",
167
+ "accept",
168
+ "true"
169
+ ]);
170
+ var isApproved = (decision) => {
171
+ if (decision.status !== "answered") return false;
172
+ if (decision.type && decision.type !== "confirm") return true;
173
+ return decision.value != null && AFFIRMATIVE.has(decision.value.trim().toLowerCase());
174
+ };
175
+
176
+ // src/resources/decisions.ts
177
+ var POLL_WINDOW_SECONDS = 50;
178
+ var DEFAULT_ASK_TIMEOUT_MS = 55e3;
179
+ var createDecisionsResource = (request) => {
180
+ const ask = async (data) => {
181
+ const idempotencyKey = data.idempotencyKey ?? (0, import_crypto2.randomUUID)();
182
+ const created = await request("POST", "/decisions", {
183
+ question: data.question,
184
+ type: data.type,
185
+ options: data.options,
186
+ externalId: data.externalId,
187
+ email: data.email,
188
+ agentName: data.agentName,
189
+ context: data.context,
190
+ placeholder: data.placeholder,
191
+ toolName: data.toolName,
192
+ toolTarget: data.toolTarget,
193
+ actor: data.actor,
194
+ environment: data.environment,
195
+ parameters: data.parameters,
196
+ // Its changes name parameters rather than carrying values, so what the
197
+ // person sees is read from the same numbers a rule compares.
198
+ presentation: data.presentation,
199
+ approvalUrl: data.approvalUrl,
200
+ expiresInSeconds: data.expiresInSeconds,
201
+ poweredBy: data.poweredBy,
202
+ callbackUrl: data.callbackUrl,
203
+ requireReachable: data.requireReachable,
204
+ // Read only by a key issued on your workspace's sandbox site, where the
205
+ // simulator answers instead of a person. Ignored in production.
206
+ sandboxOutcome: data.sandboxOutcome,
207
+ idempotencyKey,
208
+ wait: false
209
+ });
210
+ const type = created.type ?? data.type ?? "confirm";
211
+ let status = created.status;
212
+ let answered = created.answered;
213
+ let value = created.value ?? null;
214
+ const deadline = Date.now() + Math.max(0, data.timeoutMs ?? DEFAULT_ASK_TIMEOUT_MS);
215
+ while (status === "pending" && Date.now() < deadline) {
216
+ const waitSeconds = Math.max(
217
+ 1,
218
+ Math.min(POLL_WINDOW_SECONDS, Math.ceil((deadline - Date.now()) / 1e3))
219
+ );
220
+ const polled = await request("GET", `/decisions/${created.decisionId}`, {
221
+ wait: waitSeconds
222
+ });
223
+ status = polled.status;
224
+ answered = polled.answered;
225
+ value = polled.value;
226
+ }
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
+ };
241
+ return Object.freeze({
242
+ create: (data) => request("POST", "/decisions", data),
243
+ get: (id, opts) => request("GET", `/decisions/${id}`, opts?.wait ? { wait: opts.wait } : void 0),
244
+ ask,
245
+ answer: (id, answer) => request("POST", `/decisions/${id}`, { answer }),
246
+ cancel: (id) => request("DELETE", `/decisions/${id}`),
247
+ getWebhookSecret: () => request("GET", "/webhook-secret"),
248
+ rotateWebhookSecret: () => request("POST", "/webhook-secret")
249
+ });
250
+ };
251
+
252
+ // src/resources/keys.ts
253
+ var createKeysResource = (request) => Object.freeze({
254
+ issue: (data) => request("POST", "/keys", data),
255
+ list: () => request("GET", "/keys").then((r) => r.keys),
256
+ revoke: (keyPrefix) => request("DELETE", `/keys/${encodeURIComponent(keyPrefix)}`)
257
+ });
258
+
259
+ // src/authorize.ts
260
+ var defaultQuestion = (toolName, toolTarget) => `Approve ${toolName}${toolTarget ? ` ${toolTarget}` : ""}?`;
261
+ var authorizationSubject = (input) => ({
262
+ toolName: input.toolName,
263
+ toolTarget: input.toolTarget,
264
+ actor: input.actor,
265
+ environment: input.environment,
266
+ parameters: input.parameters
267
+ });
268
+ var createEvaluateAuthorization = (request) => (input) => request("POST", "/authorize", {
269
+ ...authorizationSubject(input),
270
+ externalId: input.externalId,
271
+ question: input.question,
272
+ agentName: input.agentName
273
+ });
274
+ var createAuthorize = (request, decisions) => {
275
+ const evaluate = createEvaluateAuthorization(request);
276
+ return async (input) => {
277
+ const subject = authorizationSubject(input);
278
+ const evaluation = await evaluate(input);
279
+ if (evaluation.verdict !== "requires_human") {
280
+ return {
281
+ approved: evaluation.verdict === "allow",
282
+ resolvedBy: "policy",
283
+ policy: evaluation.policy,
284
+ reason: evaluation.reason,
285
+ decisionId: null,
286
+ authorizationId: evaluation.authorizationId
287
+ };
288
+ }
289
+ const answer = await decisions.ask({
290
+ question: input.question ?? defaultQuestion(input.toolName, input.toolTarget),
291
+ type: "confirm",
292
+ externalId: input.externalId,
293
+ agentName: input.agentName,
294
+ ...subject,
295
+ timeoutMs: input.timeoutMs,
296
+ expiresInSeconds: input.expiresInSeconds,
297
+ idempotencyKey: input.idempotencyKey,
298
+ requireReachable: input.requireReachable
299
+ });
300
+ return {
301
+ approved: answer.approved,
302
+ resolvedBy: "human",
303
+ policy: evaluation.policy,
304
+ reason: answer.approved ? "A person approved it." : answer.answered ? "A person denied it." : "Nobody answered, so this was not approved.",
305
+ decisionId: answer.decisionId,
306
+ authorizationId: null
307
+ };
308
+ };
309
+ };
310
+
311
+ // src/permits.ts
312
+ var REFUSALS = [
313
+ "not_authorized",
314
+ "action_mismatch",
315
+ "expired",
316
+ "already_consumed"
317
+ ];
318
+ var EXECUTION_STATES = ["running", "succeeded", "failed"];
319
+ var refusalOf = (error) => {
320
+ if (!(error instanceof PusharyApiError)) return null;
321
+ const body = error.body;
322
+ const refusal = body?.refusal;
323
+ if (typeof refusal !== "string" || !REFUSALS.includes(refusal)) return null;
324
+ const state = body?.executionState;
325
+ return {
326
+ consumed: false,
327
+ refusal,
328
+ reason: error.message,
329
+ ...typeof state === "string" && EXECUTION_STATES.includes(state) ? { executionState: state } : {}
330
+ };
331
+ };
332
+ var createConsumeAuthorization = (request) => async (binding) => {
333
+ try {
334
+ const permit = await request("POST", "/authorizations/consume", {
335
+ authorizationId: binding.authorizationId,
336
+ toolName: binding.toolName,
337
+ toolTarget: binding.toolTarget,
338
+ actor: binding.actor,
339
+ environment: binding.environment,
340
+ externalId: binding.externalId,
341
+ parameters: binding.parameters
342
+ });
343
+ return { consumed: true, permit };
344
+ } catch (error) {
345
+ const refused = refusalOf(error);
346
+ if (refused) return refused;
347
+ throw error;
348
+ }
349
+ };
350
+ var createRecordExecution = (request) => (receipt) => request("POST", `/authorizations/${encodeURIComponent(receipt.permitId)}/receipt`, {
351
+ outcome: receipt.outcome,
352
+ summary: receipt.summary
353
+ });
354
+
355
+ // src/client.ts
356
+ var DEFAULT_BASE_URL = "https://pushary.com/api/v1/server";
357
+ var validateApiKey = (apiKey) => {
358
+ if (!apiKey) {
359
+ throw new Error("API key is required. Get your API key from https://pushary.com/dashboard/settings");
360
+ }
361
+ if (!apiKey.includes(".")) {
362
+ throw new Error("Invalid API key format. Use the full API key (pk_xxx.sk_xxx)");
363
+ }
364
+ };
365
+ var createContext = (config) => {
366
+ validateApiKey(config.apiKey);
367
+ return Object.freeze({
368
+ baseUrl: config.baseUrl ?? DEFAULT_BASE_URL,
369
+ headers: Object.freeze({
370
+ "Content-Type": "application/json",
371
+ "Authorization": `Bearer ${config.apiKey}`
372
+ })
373
+ });
374
+ };
375
+ var createPusharyServer = (config) => {
376
+ const ctx = createContext(config);
377
+ const request = createRequest(ctx);
378
+ const decisions = createDecisionsResource(request);
379
+ return Object.freeze({
380
+ subscribers: createSubscribersResource(request),
381
+ campaigns: createCampaignsResource(request),
382
+ templates: createTemplatesResource(request),
383
+ notifications: createNotificationsResource(request),
384
+ flows: createFlowsResource(request),
385
+ decisions,
386
+ keys: createKeysResource(request),
387
+ authorize: createAuthorize(request, decisions),
388
+ evaluateAuthorization: createEvaluateAuthorization(request),
389
+ consumeAuthorization: createConsumeAuthorization(request),
390
+ recordExecution: createRecordExecution(request),
391
+ enroll: (externalId) => request("POST", "/enroll", { externalId }),
392
+ reachability: (externalId) => request("GET", "/reachability", { externalId })
393
+ });
394
+ };
395
+
396
+ // src/webhook.ts
397
+ var import_crypto3 = require("crypto");
398
+ var SIGNATURE_HEADER = "x-pushary-signature";
399
+ var verifyWebhookSignature = (rawBody, signature, secret) => {
400
+ if (!signature || !secret) return false;
401
+ const expected = (0, import_crypto3.createHmac)("sha256", secret).update(rawBody).digest("hex");
402
+ const a = Buffer.from(expected);
403
+ const b = Buffer.from(signature);
404
+ if (a.length !== b.length) return false;
405
+ return (0, import_crypto3.timingSafeEqual)(a, b);
406
+ };
407
+ var parseDecisionCallback = (rawBody) => {
408
+ let parsed;
409
+ try {
410
+ parsed = JSON.parse(rawBody);
411
+ } catch {
412
+ return null;
413
+ }
414
+ if (!parsed || typeof parsed !== "object") return null;
415
+ const p = parsed;
416
+ if (typeof p.correlationId !== "string" || typeof p.answer !== "string") return null;
417
+ return {
418
+ correlationId: p.correlationId,
419
+ answer: p.answer,
420
+ value: typeof p.value === "string" ? p.value : p.answer,
421
+ answeredAt: typeof p.answeredAt === "string" ? p.answeredAt : "",
422
+ ...typeof p.context === "string" ? { context: p.context } : {}
423
+ };
424
+ };
425
+
426
+ // src/adapters.ts
427
+ var DEFAULT_NODE = "ask-human";
428
+ var defaultProtectQuestion = (action, target) => `Approve ${action}${target ? ` ${target}` : ""}?`;
429
+ var idempotencyKeyFor = (input) => deterministicKey([input.externalId, input.node ?? DEFAULT_NODE, input.question]);
430
+ var isAffirmative = (answer) => isApproved({ status: "answered", type: "confirm", value: answer ?? null });
431
+ var describeAnswer = (type, result) => {
432
+ if (!result.answered) {
433
+ return `No answer (status: ${result.status}). Treat this as NOT approved and do not proceed.`;
434
+ }
435
+ if (type === "confirm") {
436
+ return result.approved ? "The human approved. You may proceed." : "The human declined. Do not proceed.";
437
+ }
438
+ return `The human answered: ${result.value ?? ""}`;
439
+ };
440
+ var resolvePusharyCallback = (rawBody, signature, secret) => {
441
+ if (!verifyWebhookSignature(rawBody, signature, secret)) return null;
442
+ const callback = parseDecisionCallback(rawBody);
443
+ if (!callback) return null;
444
+ return {
445
+ correlationId: callback.correlationId,
446
+ answer: callback.answer,
447
+ value: callback.value,
448
+ approved: isAffirmative(callback.answer),
449
+ context: callback.context,
450
+ answeredAt: callback.answeredAt
451
+ };
452
+ };
453
+ var MAX_INPUT_CHARS = 300;
454
+ var DENIED_UNANSWERED = "No answer from the approver, so this was denied. Do not retry the same action.";
455
+ var DENIED_REFUSED = "The approver denied this action.";
456
+ var DENIED_BY_POLICY = "Denied by policy. Do not retry the same action.";
457
+ var UNBOUND_APPROVAL = "This was approved, but the approval could not be bound to a single execution, so it was not run. Do not retry the same action.";
458
+ var UNCONFIRMED_PERMIT = "This was approved, but it could not be confirmed as still unspent, so it was not run. Ask again rather than retrying this one.";
459
+ var REFUSED_REPLAY = "This authorization was already used. The action has run once and must not run again.";
460
+ var refusedPermit = (refused) => refused.refusal === "already_consumed" ? REFUSED_REPLAY : `${refused.reason} Do not retry the same action.`;
461
+ var deniedByPolicy = (evaluation) => evaluation.reason ? `${evaluation.reason} Do not retry the same action.` : DENIED_BY_POLICY;
462
+ var PARAMETERS_MAX_KEYS = 32;
463
+ var PARAMETER_KEY_MAX_LENGTH = 64;
464
+ var PARAMETER_VALUE_MAX_LENGTH = 200;
465
+ var deriveParameters = (input) => {
466
+ if (input === null || typeof input !== "object" || Array.isArray(input)) return void 0;
467
+ const entries = Object.entries(input);
468
+ if (entries.length === 0 || entries.length > PARAMETERS_MAX_KEYS) return void 0;
469
+ const parameters = {};
470
+ for (const [key, value] of entries) {
471
+ if (key.length === 0 || key.length > PARAMETER_KEY_MAX_LENGTH) return void 0;
472
+ if (typeof value === "boolean") {
473
+ parameters[key] = value;
474
+ } else if (typeof value === "number") {
475
+ if (!Number.isFinite(value)) return void 0;
476
+ parameters[key] = value;
477
+ } else if (typeof value === "string") {
478
+ if (value.length > PARAMETER_VALUE_MAX_LENGTH) return void 0;
479
+ parameters[key] = value;
480
+ } else {
481
+ return void 0;
482
+ }
483
+ }
484
+ return parameters;
485
+ };
486
+ var renderApprovalQuestion = (toolName, toolInput) => {
487
+ if (toolInput === void 0 || toolInput === null) return `Approve ${toolName}?`;
488
+ const summary = summarise(toolInput);
489
+ return summary === void 0 ? `Approve ${toolName}?` : `Approve ${toolName}? ${summary}`;
490
+ };
491
+ var summarise = (toolInput) => {
492
+ let rendered;
493
+ if (typeof toolInput === "string") {
494
+ rendered = toolInput;
495
+ } else {
496
+ try {
497
+ rendered = JSON.stringify(toolInput);
498
+ } catch {
499
+ return void 0;
500
+ }
501
+ }
502
+ if (rendered === void 0) return void 0;
503
+ return rendered.length > MAX_INPUT_CHARS ? `${rendered.slice(0, MAX_INPUT_CHARS)}...` : rendered;
504
+ };
505
+ var createAdapterKernel = (helpersLabel) => {
506
+ const requireApiKey = (apiKey) => {
507
+ const resolved = apiKey ?? process.env.PUSHARY_API_KEY;
508
+ if (!resolved) {
509
+ throw new Error(`Pushary: set PUSHARY_API_KEY or pass { apiKey } to ${helpersLabel}.`);
510
+ }
511
+ return resolved;
512
+ };
513
+ const client = (config) => createPusharyServer({
514
+ apiKey: requireApiKey(config.apiKey),
515
+ ...config.baseUrl ? { baseUrl: config.baseUrl } : {}
516
+ });
517
+ const requireExternalId = (externalId) => {
518
+ if (!externalId) {
519
+ throw new Error(
520
+ `Pushary: no end-user to ask. Pass { externalId } to ${helpersLabel}, or run user-scoped auth so the framework supplies one.`
521
+ );
522
+ }
523
+ return externalId;
524
+ };
525
+ const createGate = (config) => {
526
+ const gateClient = createPusharyServer({
527
+ apiKey: requireApiKey(config.apiKey),
528
+ ...config.baseUrl ? { baseUrl: config.baseUrl } : {}
529
+ });
530
+ const usePolicy = config.policy ?? true;
531
+ const evaluate = async (ask, facts) => {
532
+ if (!usePolicy) return null;
533
+ try {
534
+ return await gateClient.evaluateAuthorization({
535
+ toolName: ask.toolName,
536
+ toolTarget: ask.toolTarget,
537
+ actor: ask.actor,
538
+ environment: ask.environment,
539
+ parameters: facts,
540
+ externalId: ask.externalId,
541
+ question: ask.question,
542
+ ...config.agentName ? { agentName: config.agentName } : {}
543
+ });
544
+ } catch {
545
+ return null;
546
+ }
547
+ };
548
+ return async (ask) => {
549
+ const facts = ask.parameters ?? deriveParameters(ask.input);
550
+ const evaluation = await evaluate(ask, facts);
551
+ if (evaluation?.verdict === "allow") {
552
+ return {
553
+ approved: true,
554
+ // Exactly the subject POST /authorize recorded, which is the whole of
555
+ // what it was sent.
556
+ ...evaluation.authorizationId ? {
557
+ authorization: {
558
+ authorizationId: evaluation.authorizationId,
559
+ toolName: ask.toolName,
560
+ toolTarget: ask.toolTarget,
561
+ actor: ask.actor,
562
+ environment: ask.environment,
563
+ externalId: ask.externalId,
564
+ parameters: facts
565
+ }
566
+ } : {}
567
+ };
568
+ }
569
+ if (evaluation?.verdict === "deny") {
570
+ return { approved: false, reason: deniedByPolicy(evaluation) };
571
+ }
572
+ const result = await gateClient.decisions.ask({
573
+ question: ask.question,
574
+ type: "confirm",
575
+ externalId: ask.externalId,
576
+ ...config.agentName ? { agentName: config.agentName } : {},
577
+ ...config.expiresInSeconds ? { expiresInSeconds: config.expiresInSeconds } : {},
578
+ ...config.timeoutMs !== void 0 ? { timeoutMs: config.timeoutMs } : {},
579
+ ...config.requireReachable ? { requireReachable: config.requireReachable } : {},
580
+ // The gated tool, recorded on the decision. The gate already knows it
581
+ // and keyed on it below; sending it is what lets the decision be grouped
582
+ // and matched later instead of surviving only as prose in the question.
583
+ toolName: ask.toolName,
584
+ // The rest of the subject only when the evaluation carried it and the
585
+ // server accepted it. An evaluation that failed proves nothing about what
586
+ // this ask would accept, and this path has to stay byte-for-byte the call
587
+ // that worked before policy existed.
588
+ ...evaluation ? {
589
+ toolTarget: ask.toolTarget,
590
+ actor: ask.actor,
591
+ environment: ask.environment,
592
+ parameters: facts
593
+ } : {},
594
+ // Stable across replays, so a re-run of this call resolves to the same
595
+ // decision instead of asking twice.
596
+ idempotencyKey: deterministicKey([ask.sessionId, ask.callId, ask.toolName])
597
+ });
598
+ if (result.approved) {
599
+ return {
600
+ approved: true,
601
+ authorization: {
602
+ authorizationId: result.decisionId,
603
+ toolName: ask.toolName,
604
+ externalId: ask.externalId,
605
+ // Mirrors the ask above exactly. The decision recorded the rest of
606
+ // the subject only when the evaluation carried it, so claiming it
607
+ // here regardless would describe an action the record does not hold.
608
+ ...evaluation ? {
609
+ toolTarget: ask.toolTarget,
610
+ actor: ask.actor,
611
+ environment: ask.environment,
612
+ parameters: facts
613
+ } : {}
614
+ }
615
+ };
616
+ }
617
+ return { approved: false, reason: result.answered ? DENIED_REFUSED : DENIED_UNANSWERED };
618
+ };
619
+ };
620
+ const askExternalUser = (config, input) => client(config).decisions.ask({
621
+ question: input.question,
622
+ type: input.type,
623
+ options: input.options,
624
+ externalId: input.externalId,
625
+ context: input.context,
626
+ agentName: config.agentName,
627
+ timeoutMs: config.timeoutMs,
628
+ // The node/step name doubles as the action label. The default is not
629
+ // sent: grouping every unnamed ask across every framework under one
630
+ // bucket would mine into a suggestion to auto-approve "ask-human", which
631
+ // spans unrelated questions.
632
+ toolName: input.node,
633
+ idempotencyKey: idempotencyKeyFor(input)
634
+ });
635
+ const createDurableDecision = async (config, input) => {
636
+ const created = await client(config).decisions.create({
637
+ question: input.question,
638
+ type: input.type,
639
+ options: input.options,
640
+ externalId: input.externalId,
641
+ context: input.context,
642
+ callbackUrl: input.callbackUrl,
643
+ agentName: config.agentName,
644
+ toolName: input.node,
645
+ idempotencyKey: idempotencyKeyFor(input),
646
+ wait: false
647
+ });
648
+ return {
649
+ decisionId: created.decisionId,
650
+ correlationId: created.decisionId,
651
+ status: created.status,
652
+ reachable: created.reachable,
653
+ reachableChannels: created.reachableChannels,
654
+ deviceCount: created.deviceCount
655
+ };
656
+ };
657
+ const connect = (config, externalId) => client(config).enroll(externalId);
658
+ const protect = (config) => {
659
+ const gate = createGate(config);
660
+ const permitClient = client(config);
661
+ const report = async (permitId, outcome, summary) => {
662
+ try {
663
+ await permitClient.recordExecution({ permitId, outcome, summary });
664
+ } catch {
665
+ }
666
+ };
667
+ return async (action) => {
668
+ const decision = await gate({
669
+ toolName: action.action,
670
+ toolTarget: action.target,
671
+ callId: action.callId,
672
+ sessionId: action.runId,
673
+ question: action.question ?? defaultProtectQuestion(action.action, action.target),
674
+ externalId: action.externalId,
675
+ actor: action.actor,
676
+ environment: action.environment,
677
+ parameters: action.facts
678
+ });
679
+ if (!decision.approved) return { ok: false, reason: decision.reason };
680
+ if (!decision.authorization) return { ok: false, reason: UNBOUND_APPROVAL };
681
+ let consumed;
682
+ try {
683
+ consumed = await permitClient.consumeAuthorization(decision.authorization);
684
+ } catch {
685
+ return { ok: false, reason: UNCONFIRMED_PERMIT };
686
+ }
687
+ if (!consumed.consumed) return { ok: false, reason: refusedPermit(consumed) };
688
+ const permitId = consumed.permit.permitId;
689
+ let result;
690
+ try {
691
+ result = await action.run();
692
+ } catch (error) {
693
+ await report(permitId, "failed", error instanceof Error ? error.message : "unknown error");
694
+ throw error;
695
+ }
696
+ await report(permitId, "succeeded");
697
+ return { ok: true, result };
698
+ };
699
+ };
700
+ return Object.freeze({
701
+ client,
702
+ askExternalUser,
703
+ createDurableDecision,
704
+ connect,
705
+ createGate,
706
+ protect,
707
+ requireExternalId
708
+ });
709
+ };
710
+ // Annotate the CommonJS export names for ESM import in node:
711
+ 0 && (module.exports = {
712
+ DENIED_BY_POLICY,
713
+ DENIED_REFUSED,
714
+ DENIED_UNANSWERED,
715
+ SIGNATURE_HEADER,
716
+ UNBOUND_APPROVAL,
717
+ UNCONFIRMED_PERMIT,
718
+ createAdapterKernel,
719
+ defaultProtectQuestion,
720
+ deniedByPolicy,
721
+ deriveParameters,
722
+ describeAnswer,
723
+ deterministicKey,
724
+ idempotencyKeyFor,
725
+ isAffirmative,
726
+ isApproved,
727
+ parseDecisionCallback,
728
+ refusedPermit,
729
+ renderApprovalQuestion,
730
+ resolvePusharyCallback,
731
+ verifyWebhookSignature
732
+ });
733
+ //# sourceMappingURL=adapters.cjs.map