@letsping/sdk 0.2.0 → 0.2.1

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
@@ -8,6 +8,7 @@ LetsPing is a behavioral firewall and Human-in-the-Loop (HITL) infrastructure la
8
8
  - **The Behavioral Shield:** Silently profiles your agent's execution paths via Markov Chains. Automatically intercepts 0-probability reasoning anomalies (hallucinations/prompt injections).
9
9
  - **Cryo-Sleep State Parking:** Pauses execution and securely uploads massive agent states directly to storage using Signed URLs, entirely bypassing serverless timeouts and webhook payload limits.
10
10
  - **Smart-Accept Drift Adaptation:** Approval decisions mathematically alter the baseline. Old unused reasoning paths decay automatically via Exponential Moving Average (EMA).
11
+ - **Agent Identity & Escrow Helpers:** Optional HMAC-based helpers (`signAgentCall`, `verifyEscrow`, `chainHandoff`) for cryptographically linking agent calls and handoffs to LetsPing requests.
11
12
 
12
13
  ## Requirements
13
14
  - Node.js 18+
@@ -22,6 +23,27 @@ npm install @letsping/sdk
22
23
 
23
24
  ## Usage
24
25
 
26
+ ### Minimal drop-in example
27
+
28
+ The fastest way to see your first approval in the dashboard:
29
+
30
+ ```ts
31
+ import { LetsPing } from "@letsping/sdk";
32
+
33
+ const apiKey = process.env.LETSPING_API_KEY;
34
+ if (!apiKey) throw new Error("Missing LETSPING_API_KEY env var.");
35
+
36
+ const lp = new LetsPing(apiKey);
37
+
38
+ const decision = await lp.ask({
39
+ service: "billing-agent",
40
+ action: "refund_user",
41
+ payload: { user_id: "u_123", amount: 100 },
42
+ });
43
+ ```
44
+
45
+ Every example in this README follows the same pattern: **either pass the key explicitly or rely on `LETSPING_API_KEY` via env**.
46
+
25
47
  ### Blocking Request (`ask`)
26
48
 
27
49
  Execution suspends until the request is approved, rejected, or times out.
@@ -34,7 +56,7 @@ const lp = new LetsPing(process.env.LETSPING_API_KEY!);
34
56
  async function processRefund(userId: string, amount: number) {
35
57
  try {
36
58
  const decision = await lp.ask({
37
- service: "billing-service",
59
+ service: "billing-agent",
38
60
  action: "refund_user",
39
61
  priority: "high",
40
62
  payload: { userId, amount },
@@ -199,7 +221,7 @@ export async function POST(req: NextRequest) {
199
221
  }
200
222
  ```
201
223
 
202
- In your agent runner, you simply include `thread_id` and `state_snapshot` when you first call LetsPing from inside a LangGraph node. The checkpointer and webhook then keep the thread resumable across restarts.
224
+ In your agent runner, you simply include `thread_id` and `state_snapshot` when you first call LetsPing from inside a LangGraph node. The checkpointer and webhook then keep the thread resumable across restarts. If the human edited the payload in the dashboard, `data.patched_payload` (or `data.payload`) is available in the webhook payload — use your framework’s normal state-update or channel overwrite semantics to inject the approved payload into the resumed graph so the run sees the correct values.
203
225
 
204
226
  ## API Reference
205
227
 
@@ -219,7 +241,7 @@ Blocks until resolved (approve / reject / timeout).
219
241
  | `payload` | `Record<string, any>` | Context passed to human operator (and returned in Decision) |
220
242
  | `priority` | `"low" \| "medium" \| "high" \| "critical"` | Routing priority in dashboard |
221
243
  | `schema` | `object` | JSON Schema (draft 07) — generates editable form in dashboard |
222
- | `timeoutMs` | `number` | Max wait time (default: 86_400_000 ms = 24 hours) |
244
+ | `timeoutMs` | `number` | Max wait time in **milliseconds** (default: 86_400_000 ms = 24 hours) |
223
245
 
224
246
  ### `lp.defer(options): Promise<{ id: string }>`
225
247
 
@@ -229,12 +251,14 @@ Fire-and-forget: queues request and returns request ID immediately. Same options
229
251
 
230
252
  ```typescript
231
253
  interface Decision {
232
- status: "APPROVED" | "REJECTED";
254
+ status: "APPROVED" | "REJECTED" | "APPROVED_WITH_MODIFICATIONS";
233
255
  payload: Record<string, any>; // Original payload sent by agent
234
256
  patched_payload?: Record<string, any>; // Human-edited values (if modified)
235
- metadata: {
257
+ diff_summary?: any; // Field-level diff between payload and patched_payload
258
+ metadata?: {
236
259
  actor_id: string; // ID/email of the approving/rejecting human
237
260
  resolved_at: string; // ISO 8601 timestamp
261
+ method?: string; // Optional resolution method (e.g. "dashboard")
238
262
  };
239
263
  }
240
264
  ```
@@ -242,6 +266,17 @@ interface Decision {
242
266
  For full documentation, request schema examples, error codes, and dashboard integration see:
243
267
  https://letsping.co/docs#sdk
244
268
 
269
+ ### Agent-to-Agent Escrow (optional)
270
+
271
+ For multi-agent systems that want cryptographic guarantees around handoffs, the SDK exposes:
272
+
273
+ - `signAgentCall(agentId, secret, call)` to attach `agent_id` and `agent_signature` to `/ingest` calls.
274
+ - `signIngestBody(agentId, secret, body)` to take an existing ingest body (`{ project_id, service, action, payload }`) and return it with `agent_id` and `agent_signature` attached.
275
+ - `verifyEscrow(event, secret)` to validate LetsPing escrow webhooks.
276
+ - `chainHandoff(previous, nextData, secret)` to safely construct downstream handoffs tied to the original request id.
277
+
278
+ See the one-page spec at `/docs/agent-escrow-spec` in the LetsPing web app for the exact wire format and interoperability rules.
279
+
245
280
  Deploy agents with confidence.
246
281
 
247
282
  ## 2-Minute Demo (Node/TypeScript)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@letsping/sdk",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "Behavioral Firewall and Cryo-Sleep State Parking for Autonomous Agents",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",
package/src/index.d.ts CHANGED
@@ -8,15 +8,67 @@ export interface RequestOptions {
8
8
  timeoutMs?: number;
9
9
  }
10
10
  export interface Decision {
11
- status: "APPROVED" | "REJECTED";
11
+ status: "APPROVED" | "REJECTED" | "APPROVED_WITH_MODIFICATIONS";
12
12
  payload: any;
13
13
  patched_payload?: any;
14
+ diff_summary?: any;
14
15
  metadata?: {
15
16
  resolved_at: string;
16
17
  actor_id: string;
17
18
  method?: string;
18
19
  };
19
20
  }
21
+ export interface EscrowEnvelope {
22
+ id: string;
23
+ event: string;
24
+ data: any;
25
+ escrow?: {
26
+ mode: "none" | "handoff" | "finalized";
27
+ handoff_signature: string | null;
28
+ upstream_agent_id: string | null;
29
+ downstream_agent_id: string | null;
30
+ };
31
+ }
32
+ export declare function verifyEscrow(event: EscrowEnvelope, secret: string): boolean;
33
+ export interface AgentCallPayload {
34
+ project_id: string;
35
+ service: string;
36
+ action: string;
37
+ payload: any;
38
+ }
39
+ export declare function signAgentCall(agentId: string, secret: string, call: AgentCallPayload): {
40
+ agent_id: string;
41
+ agent_signature: string;
42
+ };
43
+ export declare function signIngestBody(agentId: string, secret: string, body: {
44
+ project_id: string;
45
+ service: string;
46
+ action: string;
47
+ payload: any;
48
+ }): {
49
+ project_id: string;
50
+ service: string;
51
+ action: string;
52
+ payload: any;
53
+ agent_id: string;
54
+ agent_signature: string;
55
+ };
56
+ export declare function verifyAgentSignature(agentId: string, secret: string, call: AgentCallPayload, signature: string): boolean;
57
+ export declare function chainHandoff(previous: EscrowEnvelope, nextData: {
58
+ service: string;
59
+ action: string;
60
+ payload: any;
61
+ upstream_agent_id: string;
62
+ downstream_agent_id: string;
63
+ }, secret: string): {
64
+ payload: any;
65
+ escrow: {
66
+ mode: "handoff";
67
+ upstream_agent_id: string;
68
+ downstream_agent_id: string;
69
+ handoff_signature: string;
70
+ };
71
+ };
20
72
  export declare class LetsPingError extends Error {
21
73
  status?: number | undefined;
22
74
  constructor(message: string, status?: number | undefined);
@@ -26,10 +78,15 @@ export declare class LetsPing {
26
78
  private readonly baseUrl;
27
79
  constructor(apiKey?: string, options?: {
28
80
  baseUrl?: string;
81
+ encryptionKey?: string;
29
82
  });
30
83
  ask(options: RequestOptions): Promise<Decision>;
31
84
  defer(options: RequestOptions): Promise<{
32
85
  id: string;
33
86
  }>;
87
+ waitForDecision(id: string, options?: {
88
+ originalPayload?: Record<string, any>;
89
+ timeoutMs?: number;
90
+ }): Promise<Decision>;
34
91
  private request;
35
92
  }
package/src/index.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { createCipheriv, createDecipheriv, randomBytes, createHmac } from "node:crypto";
2
2
 
3
- let SDK_VERSION = "0.2.0";
3
+ let SDK_VERSION = "0.2.1";
4
4
  try {
5
5
 
6
6
  SDK_VERSION = require("../package.json").version;
@@ -153,6 +153,129 @@ function computeDiff(original: any, patched: any): any {
153
153
  return hasChanges ? changes : null;
154
154
  }
155
155
 
156
+ export interface EscrowEnvelope {
157
+ id: string;
158
+ event: string;
159
+ data: any;
160
+ escrow?: {
161
+ mode: "none" | "handoff" | "finalized";
162
+ handoff_signature: string | null;
163
+ upstream_agent_id: string | null;
164
+ downstream_agent_id: string | null;
165
+ };
166
+ }
167
+
168
+ export function verifyEscrow(event: EscrowEnvelope, secret: string): boolean {
169
+ if (!event.escrow || !event.escrow.handoff_signature) return false;
170
+ const base = {
171
+ id: event.id,
172
+ event: event.event,
173
+ data: event.data,
174
+ upstream_agent_id: event.escrow.upstream_agent_id,
175
+ downstream_agent_id: event.escrow.downstream_agent_id,
176
+ };
177
+ const expected = createHmac("sha256", secret).update(JSON.stringify(base)).digest("hex");
178
+ return expected === event.escrow.handoff_signature;
179
+ }
180
+
181
+ export interface AgentCallPayload {
182
+ project_id: string;
183
+ service: string;
184
+ action: string;
185
+ payload: any;
186
+ }
187
+
188
+ export function signAgentCall(agentId: string, secret: string, call: AgentCallPayload): {
189
+ agent_id: string;
190
+ agent_signature: string;
191
+ } {
192
+ const canonical = JSON.stringify({
193
+ project_id: call.project_id,
194
+ service: call.service,
195
+ action: call.action,
196
+ payload: call.payload,
197
+ });
198
+ const signature = createHmac("sha256", secret).update(canonical).digest("hex");
199
+ return {
200
+ agent_id: agentId,
201
+ agent_signature: signature,
202
+ };
203
+ }
204
+
205
+ export function signIngestBody(
206
+ agentId: string,
207
+ secret: string,
208
+ body: {
209
+ project_id: string;
210
+ service: string;
211
+ action: string;
212
+ payload: any;
213
+ }
214
+ ): {
215
+ project_id: string;
216
+ service: string;
217
+ action: string;
218
+ payload: any;
219
+ agent_id: string;
220
+ agent_signature: string;
221
+ } {
222
+ const { agent_id, agent_signature } = signAgentCall(agentId, secret, {
223
+ project_id: body.project_id,
224
+ service: body.service,
225
+ action: body.action,
226
+ payload: body.payload,
227
+ });
228
+ return {
229
+ ...body,
230
+ agent_id,
231
+ agent_signature,
232
+ };
233
+ }
234
+
235
+ export function verifyAgentSignature(
236
+ agentId: string,
237
+ secret: string,
238
+ call: AgentCallPayload,
239
+ signature: string
240
+ ): boolean {
241
+ const { agent_signature } = signAgentCall(agentId, secret, call);
242
+ return agent_signature === signature;
243
+ }
244
+
245
+ export function chainHandoff(previous: EscrowEnvelope, nextData: {
246
+ service: string;
247
+ action: string;
248
+ payload: any;
249
+ upstream_agent_id: string;
250
+ downstream_agent_id: string;
251
+ }, secret: string): {
252
+ payload: any;
253
+ escrow: {
254
+ mode: "handoff";
255
+ upstream_agent_id: string;
256
+ downstream_agent_id: string;
257
+ handoff_signature: string;
258
+ };
259
+ } {
260
+ const base = {
261
+ id: previous.id,
262
+ event: previous.event,
263
+ data: nextData.payload,
264
+ upstream_agent_id: nextData.upstream_agent_id,
265
+ downstream_agent_id: nextData.downstream_agent_id,
266
+ };
267
+ const handoff_signature = createHmac("sha256", secret).update(JSON.stringify(base)).digest("hex");
268
+ return {
269
+ payload: nextData.payload,
270
+ escrow: {
271
+ mode: "handoff",
272
+ upstream_agent_id: nextData.upstream_agent_id,
273
+ downstream_agent_id: nextData.downstream_agent_id,
274
+ handoff_signature,
275
+ },
276
+ };
277
+ }
278
+
156
279
  export class LetsPing {
157
280
  private readonly apiKey: string;
158
281
  private readonly baseUrl: string;
@@ -434,6 +557,56 @@ export class LetsPing {
434
557
  }
435
558
  }
436
559
 
560
+ async waitForDecision(
561
+ id: string,
562
+ options?: { originalPayload?: Record<string, any>; timeoutMs?: number }
563
+ ): Promise<Decision> {
564
+ const basePayload = options?.originalPayload || {};
565
+ const timeout = options?.timeoutMs || 24 * 60 * 60 * 1000;
566
+ const start = Date.now();
567
+ let delay = 1000;
568
+ const maxDelay = 10000;
569
+
570
+ while (Date.now() - start < timeout) {
571
+ try {
572
+ const check = await this.request<any>("GET", `/status/${id}`);
573
+
574
+ if (check.status === "APPROVED" || check.status === "REJECTED") {
575
+ const decryptedPayload = this._decrypt(check.payload) ?? basePayload;
576
+ const decryptedPatched = check.patched_payload ? this._decrypt(check.patched_payload) : undefined;
577
+
578
+ let diff_summary;
579
+ let finalStatus: Decision["status"] = check.status;
580
+ if (check.status === "APPROVED" && decryptedPatched !== undefined) {
581
+ finalStatus = "APPROVED_WITH_MODIFICATIONS";
582
+ const diff = computeDiff(decryptedPayload, decryptedPatched);
583
+ diff_summary = diff ? { changes: diff } : { changes: "Unknown structure changes" };
584
+ }
585
+
586
+ return {
587
+ status: finalStatus,
588
+ payload: decryptedPayload,
589
+ patched_payload: decryptedPatched,
590
+ diff_summary,
591
+ metadata: {
592
+ resolved_at: check.resolved_at,
593
+ actor_id: check.actor_id,
594
+ }
595
+ };
596
+ }
597
+ } catch (e: any) {
598
+ const s = e.status;
599
+ if (s && s >= 400 && s < 500 && s !== 404 && s !== 429) throw e;
600
+ }
601
+
602
+ const jitter = Math.random() * 200;
603
+ await new Promise(r => setTimeout(r, delay + jitter));
604
+ delay = Math.min(delay * 1.5, maxDelay);
605
+ }
606
+
607
+ throw new LetsPingError(`Request ${id} timed out waiting for approval.`);
608
+ }
609
+
437
610
  tool(service: string, action: string, priority: Priority = "medium"): (context: string | Record<string, any>) => Promise<string> {
438
611
  return async (context: string | Record<string, any>): Promise<string> => {
439
612
  let payload: Record<string, any>;
package/dist/index.d.mts DELETED
@@ -1,37 +0,0 @@
1
- type Priority = "low" | "medium" | "high" | "critical";
2
- interface RequestOptions {
3
- service: string;
4
- action: string;
5
- payload: Record<string, any>;
6
- priority?: Priority;
7
- schema?: Record<string, any>;
8
- timeoutMs?: number;
9
- }
10
- interface Decision {
11
- status: "APPROVED" | "REJECTED";
12
- payload: any;
13
- patched_payload?: any;
14
- metadata?: {
15
- resolved_at: string;
16
- actor_id: string;
17
- method?: string;
18
- };
19
- }
20
- declare class LetsPingError extends Error {
21
- status?: number | undefined;
22
- constructor(message: string, status?: number | undefined);
23
- }
24
- declare class LetsPing {
25
- private readonly apiKey;
26
- private readonly baseUrl;
27
- constructor(apiKey?: string, options?: {
28
- baseUrl?: string;
29
- });
30
- ask(options: RequestOptions): Promise<Decision>;
31
- defer(options: RequestOptions): Promise<{
32
- id: string;
33
- }>;
34
- private request;
35
- }
36
-
37
- export { type Decision, LetsPing, LetsPingError, type Priority, type RequestOptions };
package/dist/index.d.ts DELETED
@@ -1,59 +0,0 @@
1
- export type Priority = "low" | "medium" | "high" | "critical";
2
- export interface RequestOptions {
3
- service: string;
4
- action: string;
5
- payload: Record<string, any>;
6
- priority?: Priority;
7
- schema?: Record<string, any>;
8
- state_snapshot?: Record<string, any>;
9
- timeoutMs?: number;
10
- role?: string;
11
- /**
12
- * Optional distributed tracing identifiers. If provided, these will be
13
- * attached to the request envelope so downstream frameworks can stitch
14
- * together multi-agent flows.
15
- */
16
- trace_id?: string;
17
- parent_request_id?: string;
18
- }
19
- export interface Decision {
20
- status: "APPROVED" | "REJECTED" | "APPROVED_WITH_MODIFICATIONS";
21
- payload: any;
22
- patched_payload?: any;
23
- diff_summary?: any;
24
- metadata?: {
25
- resolved_at: string;
26
- actor_id: string;
27
- method?: string;
28
- };
29
- }
30
- export declare class LetsPingError extends Error {
31
- status?: number | undefined;
32
- constructor(message: string, status?: number | undefined);
33
- }
34
- declare function computeDiff(original: any, patched: any): any;
35
- export declare class LetsPing {
36
- private readonly apiKey;
37
- private readonly baseUrl;
38
- private readonly encryptionKey;
39
- constructor(apiKey?: string, options?: {
40
- baseUrl?: string;
41
- encryptionKey?: string;
42
- });
43
- private _encrypt;
44
- private _decrypt;
45
- private _prepareStateUpload;
46
- ask(options: RequestOptions): Promise<Decision>;
47
- defer(options: RequestOptions): Promise<{
48
- id: string;
49
- }>;
50
- private request;
51
- tool(service: string, action: string, priority?: Priority): (context: string | Record<string, any>) => Promise<string>;
52
- webhookHandler(payloadStr: string, signatureHeader: string, webhookSecret: string): Promise<{
53
- id: string;
54
- event: string;
55
- data: Decision;
56
- state_snapshot?: Record<string, any>;
57
- }>;
58
- }
59
- export { computeDiff };