specter-sdk 0.1.0 → 0.2.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
@@ -1,73 +1,114 @@
1
1
  # specter-sdk
2
2
 
3
- Thin client for **[Specter](https://specter-ia.vercel.app)** the *detect → block → prove* firewall / decision API for **AI-agent payments**. It inspects every money-moving action an agent attempts and proves where the destination came from (the user's request, or content the agent ingested mid-task — i.e. prompt injection).
4
-
5
- The SDK is the thin "plug"; the decision API is the product. **Zero runtime dependencies.**
3
+ The thin "plug" that connects an agent's actions to the
4
+ [Specter](https://specter-ia.vercel.app) decision API — *detect → block → prove*
5
+ for AI-agent payments. The API is the product; this is ~150 lines of glue.
6
+ **Zero dependencies.**
6
7
 
7
8
  ```bash
8
9
  npm i specter-sdk
9
10
  ```
10
11
 
11
- ## Guard — check an action programmatically
12
+ ## Programmatic gate
12
13
 
13
14
  ```ts
14
15
  import { Guard } from 'specter-sdk';
15
16
 
16
17
  const guard = new Guard({
17
- apiUrl: 'https://specter-decision-api.fly.dev',
18
- apiKey: process.env.SPECTER_API_KEY!, // your tenant key
18
+ apiUrl: process.env.SPECTER_API_URL!, // e.g. https://specter-decision-api.fly.dev
19
+ apiKey: process.env.SPECTER_API_KEY!,
19
20
  });
20
21
 
21
- const result = await guard.check({
22
+ const decision = await guard.check({
22
23
  agentId: 'shop-agent',
24
+ sessionId: 'sess_42',
23
25
  action: {
24
26
  type: 'payment',
25
27
  amount: 79.99,
26
28
  currency: 'USD',
27
- destination: 'acct_attacker_x9f3',
29
+ destination: 'acct_acme_store',
28
30
  merchantClaimed: 'Acme Store',
31
+ rawInput: {},
29
32
  },
30
33
  context: {
31
- userPrompt: 'Buy the Acme mouse from Acme Store',
32
- destinationOrigin: 'ingested_content', // came from a scraped page, not the user
33
- sourceRefs: ['firecrawl:https://shop.example/acme'],
34
+ userPrompt: 'Buy the Acme mouse from Acme Store under $100.',
35
+ destinationOrigin: 'user_prompt', // or 'ingested_content' if it came from a scraped page
34
36
  establishedMerchant: 'Acme Store',
35
37
  },
36
38
  });
37
39
 
38
- if (result.decision !== 'allow') {
39
- // result.decision: 'deny' | 'review' · result.reason · result.riskScore
40
- throw new Error(`Blocked by Specter: ${result.reason}`);
41
- }
42
- // safe to execute the payment
40
+ if (decision.decision !== 'allow') throw new Error(decision.reason);
41
+ // ...only now move money.
42
+ ```
43
43
 
44
- // convenience:
45
- if (await guard.isAllowed({ action })) { /* ... */ }
44
+ ## Claude Code hook (drop-in)
45
+
46
+ ### Option A — HTTP hook straight to the deployed API (no code)
47
+
48
+ Point Claude Code's PreToolUse hook at the API's `/hooks/claude-code` route.
49
+ `.claude/settings.json`:
50
+
51
+ ```json
52
+ {
53
+ "hooks": {
54
+ "PreToolUse": [
55
+ {
56
+ "matcher": "*",
57
+ "hooks": [
58
+ {
59
+ "type": "http",
60
+ "url": "https://specter-decision-api.fly.dev/hooks/claude-code",
61
+ "headers": { "x-api-key": "$SPECTER_API_KEY" },
62
+ "allowedEnvVars": ["SPECTER_API_KEY"],
63
+ "timeout": 5
64
+ }
65
+ ]
66
+ }
67
+ ]
68
+ }
69
+ }
46
70
  ```
47
71
 
48
- ## Claude Code drop-in PreToolUse hook
72
+ > **The gotcha (do not "fix" this):** an HTTP hook does **not** block via status
73
+ > code. A 4xx/5xx only logs an error and the tool runs anyway. To actually block,
74
+ > the endpoint returns **HTTP 200** with `permissionDecision: "deny"` in the body.
75
+ > The `/hooks/claude-code` route already does this; `createClaudeCodeHook` builds
76
+ > the same body if you host it yourself. (Command-type hooks block with exit code
77
+ > 2; exit 1 only warns.)
78
+
79
+ ### Option B — host the handler yourself
49
80
 
50
81
  ```ts
51
82
  import { Guard, createClaudeCodeHook } from 'specter-sdk';
52
83
 
53
- const handle = createClaudeCodeHook(
54
- new Guard({ apiUrl: '…', apiKey: '…' }),
55
- );
84
+ const guard = new Guard({ apiUrl, apiKey });
85
+ const handle = createClaudeCodeHook(guard);
56
86
 
57
- // In your hook server, return the handler's response with HTTP 200:
58
- const response = await handle(claudeCodePayload);
59
- // { hookSpecificOutput: { permissionDecision: 'allow' | 'deny' | 'ask', ... } }
87
+ // in your tiny HTTP server, always answer 200:
88
+ const body = await handle(await req.json());
89
+ res.writeHead(200, { 'content-type': 'application/json' });
90
+ res.end(JSON.stringify(body));
60
91
  ```
61
92
 
62
- > Claude Code only blocks on an HTTP **200** whose body says
63
- > `permissionDecision: "deny"`. A non-2xx merely logs and the tool runs anyway —
64
- > so your host must answer 200 with this body. Fails safe to `ask` on any error.
93
+ ## Decision contract
65
94
 
66
- ## Types
95
+ `guard.check` → `POST /v1/evaluate` →
67
96
 
68
- `Guard`, `GuardOptions`, `CheckInput`, `createClaudeCodeHook`, `HookPayload`,
69
- `HookResponse`, and the action/decision types (`Action`, `Context`,
70
- `DecisionResult`, `Decision`, `ActionType`, `DestinationOrigin`, `SignalResult`)
71
- are all exported.
97
+ ```ts
98
+ { decision: 'allow' | 'deny' | 'review',
99
+ riskScore: number, // 0..1
100
+ reason: string,
101
+ signals: Record<string,string>,
102
+ signalDetail: { id, score, verdict }[],
103
+ audit?: { seq: number, hash: string } } // the tamper-evident chain row it was written to
104
+ ```
105
+
106
+ ## Proof (the "prove" pillar)
107
+
108
+ Every decision is appended to a tamper-evident hash chain. Read it back, or
109
+ verify it end-to-end, with the same client:
72
110
 
73
- MIT · [github.com/Eras256/Specter](https://github.com/Eras256/Specter)
111
+ ```ts
112
+ const log = await guard.audit({ limit: 10, order: 'desc' }); // newest first, each with its hash
113
+ const { valid, brokenAt } = await guard.verify(); // false + index if any row was rewritten
114
+ ```
package/index.cjs CHANGED
@@ -105,6 +105,30 @@ var Guard = class {
105
105
  async isAllowed(input) {
106
106
  return (await this.check(input)).decision === "allow";
107
107
  }
108
+ /**
109
+ * Read the tamper-evident decision log (the "prove" pillar). Pass
110
+ * `{ order: 'desc' }` for newest-first.
111
+ */
112
+ async audit(opts = {}) {
113
+ const q = new URLSearchParams();
114
+ if (opts.limit != null) q.set("limit", String(opts.limit));
115
+ if (opts.order) q.set("order", opts.order);
116
+ const qs = q.toString();
117
+ const data = await this.get(`/v1/audit${qs ? `?${qs}` : ""}`);
118
+ return data.records ?? [];
119
+ }
120
+ /** Verify the chain is intact end-to-end (proof nothing was rewritten). */
121
+ async verify() {
122
+ return this.get("/v1/audit/verify");
123
+ }
124
+ async get(path) {
125
+ const res = await fetch(`${this.opts.apiUrl.replace(/\/$/, "")}${path}`, {
126
+ headers: { authorization: `Bearer ${this.opts.apiKey}` },
127
+ signal: AbortSignal.timeout(this.opts.timeoutMs ?? 4e3)
128
+ });
129
+ if (!res.ok) throw new Error(`Specter API ${res.status}: ${await res.text()}`);
130
+ return await res.json();
131
+ }
108
132
  };
109
133
 
110
134
  exports.Guard = Guard;
package/index.cjs.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/claude-code-hook.ts","../src/guard.ts"],"names":[],"mappings":";;;AA4BO,SAAS,qBAAqB,KAAA,EAAc;AACjD,EAAA,OAAO,eAAe,OAAO,OAAA,EAA6C;AACxE,IAAA,IAAI;AACF,MAAA,MAAM,MAAA,GAAS,UAAU,OAAO,CAAA;AAChC,MAAA,MAAM,MAAA,GAAS,MAAM,KAAA,CAAM,KAAA,CAAM;AAAA,QAC/B,OAAA,EAAS,CAAA,YAAA,EAAe,OAAA,CAAQ,SAAA,IAAa,MAAM,CAAA,CAAA;AAAA,QACnD,SAAA,EAAW,QAAQ,UAAA,IAAc,YAAA;AAAA,QACjC,MAAA;AAAA,QACA,OAAA,EAAS;AAAA,UACP,UAAA,EAAY,OAAA,CAAQ,OAAA,EAAS,UAAA,IAAc,EAAA;AAAA,UAC3C,mBAAA,EAAqB,QAAQ,OAAA,EAAS,mBAAA;AAAA,UACtC,iBAAA,EAAoB,OAAA,CAAQ,OAAA,EAAS,iBAAA,IAA2C;AAAA;AAClF,OACD,CAAA;AACD,MAAA,MAAM,kBAAA,GACJ,OAAO,QAAA,KAAa,OAAA,GAAU,UAAU,MAAA,CAAO,QAAA,KAAa,SAAS,MAAA,GAAS,KAAA;AAChF,MAAA,OAAO,IAAA,CAAK,kBAAA,EAAoB,MAAA,CAAO,MAAM,CAAA;AAAA,IAC/C,SAAS,GAAA,EAAK;AAEZ,MAAA,OAAO,IAAA,CAAK,KAAA,EAAO,CAAA,iCAAA,EAAqC,GAAA,CAAc,OAAO,CAAA,CAAE,CAAA;AAAA,IACjF;AAAA,EACF,CAAA;AACF;AAEA,SAAS,IAAA,CACP,oBACA,wBAAA,EACc;AACd,EAAA,OAAO;AAAA,IACL,kBAAA,EAAoB;AAAA,MAClB,aAAA,EAAe,YAAA;AAAA,MACf,kBAAA;AAAA,MACA;AAAA;AACF,GACF;AACF;AAEA,SAAS,UAAU,CAAA,EAAwB;AACzC,EAAA,MAAM,IAAA,GAAO,EAAE,SAAA,IAAa,SAAA;AAC5B,EAAA,MAAM,EAAA,GAAK,CAAA,CAAE,UAAA,IAAc,EAAC;AAC5B,EAAA,MAAM,SAAS,OAAO,EAAA,CAAG,MAAA,KAAW,QAAA,GAAW,GAAG,MAAA,GAAS,MAAA;AAC3D,EAAA,MAAM,WAAA,GAAc,GAAA,CAAI,EAAA,CAAG,WAAW,CAAA,IAAK,GAAA,CAAI,EAAA,CAAG,EAAE,CAAA,IAAK,GAAA,CAAI,EAAA,CAAG,OAAO,CAAA,IAAK,MAAA;AAE5E,EAAA,IAAI,IAAA,GAAmB,OAAA;AACvB,EAAA,IAAI,OAAA;AACJ,EAAA,IAAI,MAAA,IAAU,IAAA,IAAQ,WAAA,EAAa,IAAA,GAAO,SAAA;AAAA,OAAA,IACjC,SAAS,MAAA,EAAQ;AACxB,IAAA,IAAA,GAAO,OAAA;AACP,IAAA,OAAA,GAAU,GAAA,CAAI,GAAG,OAAO,CAAA;AAAA,EAC1B,CAAA,MAAA,IAAW,CAAC,MAAA,EAAQ,OAAA,EAAS,QAAQ,cAAc,CAAA,CAAE,QAAA,CAAS,IAAI,CAAA,EAAG;AACnE,IAAA,IAAA,GAAO,MAAA;AACP,IAAA,OAAA,GAAU,IAAI,EAAA,CAAG,SAAS,CAAA,IAAK,GAAA,CAAI,GAAG,IAAI,CAAA;AAAA,EAC5C,CAAA,MAAO,OAAA,GAAU,IAAA,CAAK,SAAA,CAAU,EAAE,CAAA;AAElC,EAAA,OAAO;AAAA,IACL,IAAA;AAAA,IACA,MAAA;AAAA,IACA,QAAA,EAAU,GAAA,CAAI,EAAA,CAAG,QAAQ,CAAA;AAAA,IACzB,WAAA;AAAA,IACA,eAAA,EAAiB,GAAA,CAAI,EAAA,CAAG,eAAe,CAAA;AAAA,IACvC,OAAA;AAAA,IACA,QAAA,EAAU;AAAA,GACZ;AACF;AAEA,SAAS,IAAI,CAAA,EAAgC;AAC3C,EAAA,OAAO,OAAO,CAAA,KAAM,QAAA,IAAY,CAAA,CAAE,SAAS,CAAA,GAAI,MAAA;AACjD;;;ACvEO,IAAM,QAAN,MAAY;AAAA,EACjB,YAAoB,IAAA,EAAoB;AAApB,IAAA,IAAA,CAAA,IAAA,GAAA,IAAA;AAAA,EAAqB;AAAA,EAArB,IAAA;AAAA,EAEpB,MAAM,MAAM,KAAA,EAA4C;AACtD,IAAA,MAAM,IAAA,GAAO;AAAA,MACX,OAAA,EAAS,KAAA,CAAM,OAAA,IAAW,IAAA,CAAK,KAAK,OAAA,IAAW,WAAA;AAAA,MAC/C,SAAA,EAAW,MAAM,SAAA,IAAa,aAAA;AAAA,MAC9B,QAAQ,KAAA,CAAM,MAAA;AAAA,MACd,OAAA,EAAS;AAAA,QACP,UAAA,EAAY,KAAA,CAAM,OAAA,EAAS,UAAA,IAAc,EAAA;AAAA,QACzC,iBAAA,EAAmB,KAAA,CAAM,OAAA,EAAS,iBAAA,IAAqB,SAAA;AAAA,QACvD,UAAA,EAAY,KAAA,CAAM,OAAA,EAAS,UAAA,IAAc,EAAC;AAAA,QAC1C,mBAAA,EAAqB,MAAM,OAAA,EAAS;AAAA;AACtC,KACF;AAEA,IAAA,MAAM,OAAA,GAAU,IAAA,CAAK,IAAA,CAAK,OAAA,IAAW,CAAA;AACrC,IAAA,IAAI,OAAA;AACJ,IAAA,KAAA,IAAS,OAAA,GAAU,CAAA,EAAG,OAAA,IAAW,OAAA,EAAS,OAAA,EAAA,EAAW;AACnD,MAAA,IAAI;AACF,QAAA,MAAM,GAAA,GAAM,MAAM,KAAA,CAAM,CAAA,EAAG,IAAA,CAAK,IAAA,CAAK,MAAA,CAAO,OAAA,CAAQ,KAAA,EAAO,EAAE,CAAC,CAAA,YAAA,CAAA,EAAgB;AAAA,UAC5E,MAAA,EAAQ,MAAA;AAAA,UACR,OAAA,EAAS;AAAA,YACP,cAAA,EAAgB,kBAAA;AAAA,YAChB,aAAA,EAAe,CAAA,OAAA,EAAU,IAAA,CAAK,IAAA,CAAK,MAAM,CAAA;AAAA,WAC3C;AAAA,UACA,IAAA,EAAM,IAAA,CAAK,SAAA,CAAU,IAAI,CAAA;AAAA,UACzB,QAAQ,WAAA,CAAY,OAAA,CAAQ,IAAA,CAAK,IAAA,CAAK,aAAa,GAAI;AAAA,SACxD,CAAA;AACD,QAAA,IAAI,CAAC,GAAA,CAAI,EAAA,EAAI,MAAM,IAAI,KAAA,CAAM,CAAA,YAAA,EAAe,GAAA,CAAI,MAAM,CAAA,EAAA,EAAK,MAAM,GAAA,CAAI,IAAA,EAAM,CAAA,CAAE,CAAA;AAC7E,QAAA,OAAQ,MAAM,IAAI,IAAA,EAAK;AAAA,MACzB,SAAS,GAAA,EAAK;AACZ,QAAA,OAAA,GAAU,GAAA;AAAA,MACZ;AAAA,IACF;AACA,IAAA,MAAM,IAAI,KAAA;AAAA,MACR,CAAA,2BAAA,EAA8B,OAAA,GAAU,CAAC,CAAA,WAAA,EAAe,QAAkB,OAAO,CAAA;AAAA,KACnF;AAAA,EACF;AAAA;AAAA,EAGA,MAAM,UAAU,KAAA,EAAqC;AACnD,IAAA,OAAA,CAAQ,MAAM,IAAA,CAAK,KAAA,CAAM,KAAK,GAAG,QAAA,KAAa,OAAA;AAAA,EAChD;AACF","file":"index.cjs","sourcesContent":["import type { Guard } from './guard.js';\nimport type { Action, ActionType, DestinationOrigin } from './types.js';\n\n/** Claude Code PreToolUse hook payload (subset). */\nexport interface HookPayload {\n session_id?: string;\n tool_name?: string;\n tool_input?: Record<string, unknown>;\n specter?: { userPrompt?: string; establishedMerchant?: string; destinationOrigin?: string };\n}\n\n/** The body Claude Code requires to actually block. */\nexport interface HookResponse {\n hookSpecificOutput: {\n hookEventName: 'PreToolUse';\n permissionDecision: 'allow' | 'deny' | 'ask';\n permissionDecisionReason: string;\n };\n}\n\n/**\n * Build a ready-made Claude Code PreToolUse hook handler from a Guard.\n *\n * IMPORTANT: when wired as an HTTP hook, Claude Code only blocks on an HTTP 200\n * whose body says permissionDecision: \"deny\". A non-2xx status merely logs and\n * the tool runs anyway. This handler returns the correct body; the host (the\n * decision API's /hooks/claude-code route, or your own server) must answer 200.\n */\nexport function createClaudeCodeHook(guard: Guard) {\n return async function handle(payload: HookPayload): Promise<HookResponse> {\n try {\n const action = normalize(payload);\n const result = await guard.check({\n agentId: `claude-code:${payload.tool_name ?? 'tool'}`,\n sessionId: payload.session_id ?? 'cc-session',\n action,\n context: {\n userPrompt: payload.specter?.userPrompt ?? '',\n establishedMerchant: payload.specter?.establishedMerchant,\n destinationOrigin: (payload.specter?.destinationOrigin as DestinationOrigin) ?? 'unknown',\n },\n });\n const permissionDecision =\n result.decision === 'allow' ? 'allow' : result.decision === 'deny' ? 'deny' : 'ask';\n return resp(permissionDecision, result.reason);\n } catch (err) {\n // Fail safe: escalate to a human rather than silently allowing.\n return resp('ask', `Specter unavailable, escalating: ${(err as Error).message}`);\n }\n };\n}\n\nfunction resp(\n permissionDecision: 'allow' | 'deny' | 'ask',\n permissionDecisionReason: string,\n): HookResponse {\n return {\n hookSpecificOutput: {\n hookEventName: 'PreToolUse',\n permissionDecision,\n permissionDecisionReason,\n },\n };\n}\n\nfunction normalize(p: HookPayload): Action {\n const tool = p.tool_name ?? 'unknown';\n const ti = p.tool_input ?? {};\n const amount = typeof ti.amount === 'number' ? ti.amount : undefined;\n const destination = str(ti.destination) ?? str(ti.to) ?? str(ti.account) ?? undefined;\n\n let type: ActionType = 'other';\n let command: string | undefined;\n if (amount != null || destination) type = 'payment';\n else if (tool === 'Bash') {\n type = 'shell';\n command = str(ti.command);\n } else if (['Read', 'Write', 'Edit', 'NotebookEdit'].includes(tool)) {\n type = 'file';\n command = str(ti.file_path) ?? str(ti.path);\n } else command = JSON.stringify(ti);\n\n return {\n type,\n amount,\n currency: str(ti.currency),\n destination,\n merchantClaimed: str(ti.merchantClaimed),\n command,\n rawInput: ti,\n };\n}\n\nfunction str(v: unknown): string | undefined {\n return typeof v === 'string' && v.length ? v : undefined;\n}\n","import type { Action, Context, DecisionResult } from './types.js';\n\nexport interface GuardOptions {\n apiUrl: string;\n apiKey: string;\n /** Per-request timeout in ms (default 4000). The gate targets <500ms p99. */\n timeoutMs?: number;\n /** Retry attempts on network/5xx (default 1). */\n retries?: number;\n agentId?: string;\n}\n\nexport interface CheckInput {\n agentId?: string;\n sessionId?: string;\n action: Action;\n context?: Partial<Context>;\n}\n\n/**\n * Thin, typed client for the Specter decision API. The whole SDK is the \"plug\";\n * the API is the product. Use `guard.check(action, context)` programmatically,\n * or `createClaudeCodeHook(guard)` for the drop-in Claude Code hook.\n */\nexport class Guard {\n constructor(private opts: GuardOptions) {}\n\n async check(input: CheckInput): Promise<DecisionResult> {\n const body = {\n agentId: input.agentId ?? this.opts.agentId ?? 'sdk-agent',\n sessionId: input.sessionId ?? 'sdk-session',\n action: input.action,\n context: {\n userPrompt: input.context?.userPrompt ?? '',\n destinationOrigin: input.context?.destinationOrigin ?? 'unknown',\n sourceRefs: input.context?.sourceRefs ?? [],\n establishedMerchant: input.context?.establishedMerchant,\n },\n };\n\n const retries = this.opts.retries ?? 1;\n let lastErr: unknown;\n for (let attempt = 0; attempt <= retries; attempt++) {\n try {\n const res = await fetch(`${this.opts.apiUrl.replace(/\\/$/, '')}/v1/evaluate`, {\n method: 'POST',\n headers: {\n 'content-type': 'application/json',\n authorization: `Bearer ${this.opts.apiKey}`,\n },\n body: JSON.stringify(body),\n signal: AbortSignal.timeout(this.opts.timeoutMs ?? 4000),\n });\n if (!res.ok) throw new Error(`Specter API ${res.status}: ${await res.text()}`);\n return (await res.json()) as DecisionResult;\n } catch (err) {\n lastErr = err;\n }\n }\n throw new Error(\n `Specter check failed after ${retries + 1} attempts: ${(lastErr as Error).message}`,\n );\n }\n\n /** Convenience: true only when the action is explicitly allowed. */\n async isAllowed(input: CheckInput): Promise<boolean> {\n return (await this.check(input)).decision === 'allow';\n }\n}\n"]}
1
+ {"version":3,"sources":["../src/claude-code-hook.ts","../src/guard.ts"],"names":[],"mappings":";;;AA4BO,SAAS,qBAAqB,KAAA,EAAc;AACjD,EAAA,OAAO,eAAe,OAAO,OAAA,EAA6C;AACxE,IAAA,IAAI;AACF,MAAA,MAAM,MAAA,GAAS,UAAU,OAAO,CAAA;AAChC,MAAA,MAAM,MAAA,GAAS,MAAM,KAAA,CAAM,KAAA,CAAM;AAAA,QAC/B,OAAA,EAAS,CAAA,YAAA,EAAe,OAAA,CAAQ,SAAA,IAAa,MAAM,CAAA,CAAA;AAAA,QACnD,SAAA,EAAW,QAAQ,UAAA,IAAc,YAAA;AAAA,QACjC,MAAA;AAAA,QACA,OAAA,EAAS;AAAA,UACP,UAAA,EAAY,OAAA,CAAQ,OAAA,EAAS,UAAA,IAAc,EAAA;AAAA,UAC3C,mBAAA,EAAqB,QAAQ,OAAA,EAAS,mBAAA;AAAA,UACtC,iBAAA,EAAoB,OAAA,CAAQ,OAAA,EAAS,iBAAA,IAA2C;AAAA;AAClF,OACD,CAAA;AACD,MAAA,MAAM,kBAAA,GACJ,OAAO,QAAA,KAAa,OAAA,GAAU,UAAU,MAAA,CAAO,QAAA,KAAa,SAAS,MAAA,GAAS,KAAA;AAChF,MAAA,OAAO,IAAA,CAAK,kBAAA,EAAoB,MAAA,CAAO,MAAM,CAAA;AAAA,IAC/C,SAAS,GAAA,EAAK;AAEZ,MAAA,OAAO,IAAA,CAAK,KAAA,EAAO,CAAA,iCAAA,EAAqC,GAAA,CAAc,OAAO,CAAA,CAAE,CAAA;AAAA,IACjF;AAAA,EACF,CAAA;AACF;AAEA,SAAS,IAAA,CACP,oBACA,wBAAA,EACc;AACd,EAAA,OAAO;AAAA,IACL,kBAAA,EAAoB;AAAA,MAClB,aAAA,EAAe,YAAA;AAAA,MACf,kBAAA;AAAA,MACA;AAAA;AACF,GACF;AACF;AAEA,SAAS,UAAU,CAAA,EAAwB;AACzC,EAAA,MAAM,IAAA,GAAO,EAAE,SAAA,IAAa,SAAA;AAC5B,EAAA,MAAM,EAAA,GAAK,CAAA,CAAE,UAAA,IAAc,EAAC;AAC5B,EAAA,MAAM,SAAS,OAAO,EAAA,CAAG,MAAA,KAAW,QAAA,GAAW,GAAG,MAAA,GAAS,MAAA;AAC3D,EAAA,MAAM,WAAA,GAAc,GAAA,CAAI,EAAA,CAAG,WAAW,CAAA,IAAK,GAAA,CAAI,EAAA,CAAG,EAAE,CAAA,IAAK,GAAA,CAAI,EAAA,CAAG,OAAO,CAAA,IAAK,MAAA;AAE5E,EAAA,IAAI,IAAA,GAAmB,OAAA;AACvB,EAAA,IAAI,OAAA;AACJ,EAAA,IAAI,MAAA,IAAU,IAAA,IAAQ,WAAA,EAAa,IAAA,GAAO,SAAA;AAAA,OAAA,IACjC,SAAS,MAAA,EAAQ;AACxB,IAAA,IAAA,GAAO,OAAA;AACP,IAAA,OAAA,GAAU,GAAA,CAAI,GAAG,OAAO,CAAA;AAAA,EAC1B,CAAA,MAAA,IAAW,CAAC,MAAA,EAAQ,OAAA,EAAS,QAAQ,cAAc,CAAA,CAAE,QAAA,CAAS,IAAI,CAAA,EAAG;AACnE,IAAA,IAAA,GAAO,MAAA;AACP,IAAA,OAAA,GAAU,IAAI,EAAA,CAAG,SAAS,CAAA,IAAK,GAAA,CAAI,GAAG,IAAI,CAAA;AAAA,EAC5C,CAAA,MAAO,OAAA,GAAU,IAAA,CAAK,SAAA,CAAU,EAAE,CAAA;AAElC,EAAA,OAAO;AAAA,IACL,IAAA;AAAA,IACA,MAAA;AAAA,IACA,QAAA,EAAU,GAAA,CAAI,EAAA,CAAG,QAAQ,CAAA;AAAA,IACzB,WAAA;AAAA,IACA,eAAA,EAAiB,GAAA,CAAI,EAAA,CAAG,eAAe,CAAA;AAAA,IACvC,OAAA;AAAA,IACA,QAAA,EAAU;AAAA,GACZ;AACF;AAEA,SAAS,IAAI,CAAA,EAAgC;AAC3C,EAAA,OAAO,OAAO,CAAA,KAAM,QAAA,IAAY,CAAA,CAAE,SAAS,CAAA,GAAI,MAAA;AACjD;;;ACvEO,IAAM,QAAN,MAAY;AAAA,EACjB,YAAoB,IAAA,EAAoB;AAApB,IAAA,IAAA,CAAA,IAAA,GAAA,IAAA;AAAA,EAAqB;AAAA,EAArB,IAAA;AAAA,EAEpB,MAAM,MAAM,KAAA,EAA4C;AACtD,IAAA,MAAM,IAAA,GAAO;AAAA,MACX,OAAA,EAAS,KAAA,CAAM,OAAA,IAAW,IAAA,CAAK,KAAK,OAAA,IAAW,WAAA;AAAA,MAC/C,SAAA,EAAW,MAAM,SAAA,IAAa,aAAA;AAAA,MAC9B,QAAQ,KAAA,CAAM,MAAA;AAAA,MACd,OAAA,EAAS;AAAA,QACP,UAAA,EAAY,KAAA,CAAM,OAAA,EAAS,UAAA,IAAc,EAAA;AAAA,QACzC,iBAAA,EAAmB,KAAA,CAAM,OAAA,EAAS,iBAAA,IAAqB,SAAA;AAAA,QACvD,UAAA,EAAY,KAAA,CAAM,OAAA,EAAS,UAAA,IAAc,EAAC;AAAA,QAC1C,mBAAA,EAAqB,MAAM,OAAA,EAAS;AAAA;AACtC,KACF;AAEA,IAAA,MAAM,OAAA,GAAU,IAAA,CAAK,IAAA,CAAK,OAAA,IAAW,CAAA;AACrC,IAAA,IAAI,OAAA;AACJ,IAAA,KAAA,IAAS,OAAA,GAAU,CAAA,EAAG,OAAA,IAAW,OAAA,EAAS,OAAA,EAAA,EAAW;AACnD,MAAA,IAAI;AACF,QAAA,MAAM,GAAA,GAAM,MAAM,KAAA,CAAM,CAAA,EAAG,IAAA,CAAK,IAAA,CAAK,MAAA,CAAO,OAAA,CAAQ,KAAA,EAAO,EAAE,CAAC,CAAA,YAAA,CAAA,EAAgB;AAAA,UAC5E,MAAA,EAAQ,MAAA;AAAA,UACR,OAAA,EAAS;AAAA,YACP,cAAA,EAAgB,kBAAA;AAAA,YAChB,aAAA,EAAe,CAAA,OAAA,EAAU,IAAA,CAAK,IAAA,CAAK,MAAM,CAAA;AAAA,WAC3C;AAAA,UACA,IAAA,EAAM,IAAA,CAAK,SAAA,CAAU,IAAI,CAAA;AAAA,UACzB,QAAQ,WAAA,CAAY,OAAA,CAAQ,IAAA,CAAK,IAAA,CAAK,aAAa,GAAI;AAAA,SACxD,CAAA;AACD,QAAA,IAAI,CAAC,GAAA,CAAI,EAAA,EAAI,MAAM,IAAI,KAAA,CAAM,CAAA,YAAA,EAAe,GAAA,CAAI,MAAM,CAAA,EAAA,EAAK,MAAM,GAAA,CAAI,IAAA,EAAM,CAAA,CAAE,CAAA;AAC7E,QAAA,OAAQ,MAAM,IAAI,IAAA,EAAK;AAAA,MACzB,SAAS,GAAA,EAAK;AACZ,QAAA,OAAA,GAAU,GAAA;AAAA,MACZ;AAAA,IACF;AACA,IAAA,MAAM,IAAI,KAAA;AAAA,MACR,CAAA,2BAAA,EAA8B,OAAA,GAAU,CAAC,CAAA,WAAA,EAAe,QAAkB,OAAO,CAAA;AAAA,KACnF;AAAA,EACF;AAAA;AAAA,EAGA,MAAM,UAAU,KAAA,EAAqC;AACnD,IAAA,OAAA,CAAQ,MAAM,IAAA,CAAK,KAAA,CAAM,KAAK,GAAG,QAAA,KAAa,OAAA;AAAA,EAChD;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAM,KAAA,CAAM,IAAA,GAAmD,EAAC,EAA2B;AACzF,IAAA,MAAM,CAAA,GAAI,IAAI,eAAA,EAAgB;AAC9B,IAAA,IAAI,IAAA,CAAK,SAAS,IAAA,EAAM,CAAA,CAAE,IAAI,OAAA,EAAS,MAAA,CAAO,IAAA,CAAK,KAAK,CAAC,CAAA;AACzD,IAAA,IAAI,KAAK,KAAA,EAAO,CAAA,CAAE,GAAA,CAAI,OAAA,EAAS,KAAK,KAAK,CAAA;AACzC,IAAA,MAAM,EAAA,GAAK,EAAE,QAAA,EAAS;AACtB,IAAA,MAAM,IAAA,GAAO,MAAM,IAAA,CAAK,GAAA,CAAgC,CAAA,SAAA,EAAY,KAAK,CAAA,CAAA,EAAI,EAAE,CAAA,CAAA,GAAK,EAAE,CAAA,CAAE,CAAA;AACxF,IAAA,OAAO,IAAA,CAAK,WAAW,EAAC;AAAA,EAC1B;AAAA;AAAA,EAGA,MAAM,MAAA,GAAgC;AACpC,IAAA,OAAO,IAAA,CAAK,IAAkB,kBAAkB,CAAA;AAAA,EAClD;AAAA,EAEA,MAAc,IAAO,IAAA,EAA0B;AAC7C,IAAA,MAAM,GAAA,GAAM,MAAM,KAAA,CAAM,CAAA,EAAG,IAAA,CAAK,IAAA,CAAK,MAAA,CAAO,OAAA,CAAQ,KAAA,EAAO,EAAE,CAAC,CAAA,EAAG,IAAI,CAAA,CAAA,EAAI;AAAA,MACvE,SAAS,EAAE,aAAA,EAAe,UAAU,IAAA,CAAK,IAAA,CAAK,MAAM,CAAA,CAAA,EAAG;AAAA,MACvD,QAAQ,WAAA,CAAY,OAAA,CAAQ,IAAA,CAAK,IAAA,CAAK,aAAa,GAAI;AAAA,KACxD,CAAA;AACD,IAAA,IAAI,CAAC,GAAA,CAAI,EAAA,EAAI,MAAM,IAAI,KAAA,CAAM,CAAA,YAAA,EAAe,GAAA,CAAI,MAAM,CAAA,EAAA,EAAK,MAAM,GAAA,CAAI,IAAA,EAAM,CAAA,CAAE,CAAA;AAC7E,IAAA,OAAQ,MAAM,IAAI,IAAA,EAAK;AAAA,EACzB;AACF","file":"index.cjs","sourcesContent":["import type { Guard } from './guard.js';\nimport type { Action, ActionType, DestinationOrigin } from './types.js';\n\n/** Claude Code PreToolUse hook payload (subset). */\nexport interface HookPayload {\n session_id?: string;\n tool_name?: string;\n tool_input?: Record<string, unknown>;\n specter?: { userPrompt?: string; establishedMerchant?: string; destinationOrigin?: string };\n}\n\n/** The body Claude Code requires to actually block. */\nexport interface HookResponse {\n hookSpecificOutput: {\n hookEventName: 'PreToolUse';\n permissionDecision: 'allow' | 'deny' | 'ask';\n permissionDecisionReason: string;\n };\n}\n\n/**\n * Build a ready-made Claude Code PreToolUse hook handler from a Guard.\n *\n * IMPORTANT: when wired as an HTTP hook, Claude Code only blocks on an HTTP 200\n * whose body says permissionDecision: \"deny\". A non-2xx status merely logs and\n * the tool runs anyway. This handler returns the correct body; the host (the\n * decision API's /hooks/claude-code route, or your own server) must answer 200.\n */\nexport function createClaudeCodeHook(guard: Guard) {\n return async function handle(payload: HookPayload): Promise<HookResponse> {\n try {\n const action = normalize(payload);\n const result = await guard.check({\n agentId: `claude-code:${payload.tool_name ?? 'tool'}`,\n sessionId: payload.session_id ?? 'cc-session',\n action,\n context: {\n userPrompt: payload.specter?.userPrompt ?? '',\n establishedMerchant: payload.specter?.establishedMerchant,\n destinationOrigin: (payload.specter?.destinationOrigin as DestinationOrigin) ?? 'unknown',\n },\n });\n const permissionDecision =\n result.decision === 'allow' ? 'allow' : result.decision === 'deny' ? 'deny' : 'ask';\n return resp(permissionDecision, result.reason);\n } catch (err) {\n // Fail safe: escalate to a human rather than silently allowing.\n return resp('ask', `Specter unavailable, escalating: ${(err as Error).message}`);\n }\n };\n}\n\nfunction resp(\n permissionDecision: 'allow' | 'deny' | 'ask',\n permissionDecisionReason: string,\n): HookResponse {\n return {\n hookSpecificOutput: {\n hookEventName: 'PreToolUse',\n permissionDecision,\n permissionDecisionReason,\n },\n };\n}\n\nfunction normalize(p: HookPayload): Action {\n const tool = p.tool_name ?? 'unknown';\n const ti = p.tool_input ?? {};\n const amount = typeof ti.amount === 'number' ? ti.amount : undefined;\n const destination = str(ti.destination) ?? str(ti.to) ?? str(ti.account) ?? undefined;\n\n let type: ActionType = 'other';\n let command: string | undefined;\n if (amount != null || destination) type = 'payment';\n else if (tool === 'Bash') {\n type = 'shell';\n command = str(ti.command);\n } else if (['Read', 'Write', 'Edit', 'NotebookEdit'].includes(tool)) {\n type = 'file';\n command = str(ti.file_path) ?? str(ti.path);\n } else command = JSON.stringify(ti);\n\n return {\n type,\n amount,\n currency: str(ti.currency),\n destination,\n merchantClaimed: str(ti.merchantClaimed),\n command,\n rawInput: ti,\n };\n}\n\nfunction str(v: unknown): string | undefined {\n return typeof v === 'string' && v.length ? v : undefined;\n}\n","import type { Action, AuditRecord, Context, EvaluateResult, VerifyResult } from './types.js';\n\nexport interface GuardOptions {\n apiUrl: string;\n apiKey: string;\n /** Per-request timeout in ms (default 4000). The gate targets <500ms p99. */\n timeoutMs?: number;\n /** Retry attempts on network/5xx (default 1). */\n retries?: number;\n agentId?: string;\n}\n\nexport interface CheckInput {\n agentId?: string;\n sessionId?: string;\n action: Action;\n context?: Partial<Context>;\n}\n\n/**\n * Thin, typed client for the Specter decision API. The whole SDK is the \"plug\";\n * the API is the product. Use `guard.check(action, context)` programmatically,\n * or `createClaudeCodeHook(guard)` for the drop-in Claude Code hook.\n */\nexport class Guard {\n constructor(private opts: GuardOptions) {}\n\n async check(input: CheckInput): Promise<EvaluateResult> {\n const body = {\n agentId: input.agentId ?? this.opts.agentId ?? 'sdk-agent',\n sessionId: input.sessionId ?? 'sdk-session',\n action: input.action,\n context: {\n userPrompt: input.context?.userPrompt ?? '',\n destinationOrigin: input.context?.destinationOrigin ?? 'unknown',\n sourceRefs: input.context?.sourceRefs ?? [],\n establishedMerchant: input.context?.establishedMerchant,\n },\n };\n\n const retries = this.opts.retries ?? 1;\n let lastErr: unknown;\n for (let attempt = 0; attempt <= retries; attempt++) {\n try {\n const res = await fetch(`${this.opts.apiUrl.replace(/\\/$/, '')}/v1/evaluate`, {\n method: 'POST',\n headers: {\n 'content-type': 'application/json',\n authorization: `Bearer ${this.opts.apiKey}`,\n },\n body: JSON.stringify(body),\n signal: AbortSignal.timeout(this.opts.timeoutMs ?? 4000),\n });\n if (!res.ok) throw new Error(`Specter API ${res.status}: ${await res.text()}`);\n return (await res.json()) as EvaluateResult;\n } catch (err) {\n lastErr = err;\n }\n }\n throw new Error(\n `Specter check failed after ${retries + 1} attempts: ${(lastErr as Error).message}`,\n );\n }\n\n /** Convenience: true only when the action is explicitly allowed. */\n async isAllowed(input: CheckInput): Promise<boolean> {\n return (await this.check(input)).decision === 'allow';\n }\n\n /**\n * Read the tamper-evident decision log (the \"prove\" pillar). Pass\n * `{ order: 'desc' }` for newest-first.\n */\n async audit(opts: { limit?: number; order?: 'asc' | 'desc' } = {}): Promise<AuditRecord[]> {\n const q = new URLSearchParams();\n if (opts.limit != null) q.set('limit', String(opts.limit));\n if (opts.order) q.set('order', opts.order);\n const qs = q.toString();\n const data = await this.get<{ records: AuditRecord[] }>(`/v1/audit${qs ? `?${qs}` : ''}`);\n return data.records ?? [];\n }\n\n /** Verify the chain is intact end-to-end (proof nothing was rewritten). */\n async verify(): Promise<VerifyResult> {\n return this.get<VerifyResult>('/v1/audit/verify');\n }\n\n private async get<T>(path: string): Promise<T> {\n const res = await fetch(`${this.opts.apiUrl.replace(/\\/$/, '')}${path}`, {\n headers: { authorization: `Bearer ${this.opts.apiKey}` },\n signal: AbortSignal.timeout(this.opts.timeoutMs ?? 4000),\n });\n if (!res.ok) throw new Error(`Specter API ${res.status}: ${await res.text()}`);\n return (await res.json()) as T;\n }\n}\n"]}
package/index.d.cts CHANGED
@@ -38,6 +38,32 @@ interface DecisionResult {
38
38
  signals: Record<string, string>;
39
39
  signalDetail: SignalResult[];
40
40
  }
41
+ /** Pointer to the tamper-evident chain row a decision was written to. */
42
+ interface AuditRef {
43
+ /** Position of this decision in the append-only chain. */
44
+ seq: number;
45
+ /** SHA-256 hash linking this record to the previous one. */
46
+ hash: string;
47
+ }
48
+ /** What `Guard.check` returns: the decision plus the proof row it was recorded as. */
49
+ interface EvaluateResult extends DecisionResult {
50
+ /** The tamper-evident chain row this decision was recorded as (when the API persists it). */
51
+ audit?: AuditRef;
52
+ }
53
+ /** A row from the append-only audit chain (`GET /v1/audit`). */
54
+ interface AuditRecord {
55
+ seq: number;
56
+ record: Record<string, unknown>;
57
+ prev_hash: string;
58
+ hash: string;
59
+ }
60
+ /** Result of verifying the chain (`GET /v1/audit/verify`). */
61
+ interface VerifyResult {
62
+ valid: boolean;
63
+ /** First index where the chain diverges, if any. */
64
+ brokenAt?: number;
65
+ reason?: string;
66
+ }
41
67
 
42
68
  interface GuardOptions {
43
69
  apiUrl: string;
@@ -62,9 +88,20 @@ interface CheckInput {
62
88
  declare class Guard {
63
89
  private opts;
64
90
  constructor(opts: GuardOptions);
65
- check(input: CheckInput): Promise<DecisionResult>;
91
+ check(input: CheckInput): Promise<EvaluateResult>;
66
92
  /** Convenience: true only when the action is explicitly allowed. */
67
93
  isAllowed(input: CheckInput): Promise<boolean>;
94
+ /**
95
+ * Read the tamper-evident decision log (the "prove" pillar). Pass
96
+ * `{ order: 'desc' }` for newest-first.
97
+ */
98
+ audit(opts?: {
99
+ limit?: number;
100
+ order?: 'asc' | 'desc';
101
+ }): Promise<AuditRecord[]>;
102
+ /** Verify the chain is intact end-to-end (proof nothing was rewritten). */
103
+ verify(): Promise<VerifyResult>;
104
+ private get;
68
105
  }
69
106
 
70
107
  /** Claude Code PreToolUse hook payload (subset). */
@@ -96,4 +133,4 @@ interface HookResponse {
96
133
  */
97
134
  declare function createClaudeCodeHook(guard: Guard): (payload: HookPayload) => Promise<HookResponse>;
98
135
 
99
- export { type Action, type ActionType, type CheckInput, type Context, type Decision, type DecisionResult, type DestinationOrigin, Guard, type GuardOptions, type HookPayload, type HookResponse, type SignalResult, createClaudeCodeHook };
136
+ export { type Action, type ActionType, type AuditRecord, type AuditRef, type CheckInput, type Context, type Decision, type DecisionResult, type DestinationOrigin, type EvaluateResult, Guard, type GuardOptions, type HookPayload, type HookResponse, type SignalResult, type VerifyResult, createClaudeCodeHook };
package/index.d.ts CHANGED
@@ -38,6 +38,32 @@ interface DecisionResult {
38
38
  signals: Record<string, string>;
39
39
  signalDetail: SignalResult[];
40
40
  }
41
+ /** Pointer to the tamper-evident chain row a decision was written to. */
42
+ interface AuditRef {
43
+ /** Position of this decision in the append-only chain. */
44
+ seq: number;
45
+ /** SHA-256 hash linking this record to the previous one. */
46
+ hash: string;
47
+ }
48
+ /** What `Guard.check` returns: the decision plus the proof row it was recorded as. */
49
+ interface EvaluateResult extends DecisionResult {
50
+ /** The tamper-evident chain row this decision was recorded as (when the API persists it). */
51
+ audit?: AuditRef;
52
+ }
53
+ /** A row from the append-only audit chain (`GET /v1/audit`). */
54
+ interface AuditRecord {
55
+ seq: number;
56
+ record: Record<string, unknown>;
57
+ prev_hash: string;
58
+ hash: string;
59
+ }
60
+ /** Result of verifying the chain (`GET /v1/audit/verify`). */
61
+ interface VerifyResult {
62
+ valid: boolean;
63
+ /** First index where the chain diverges, if any. */
64
+ brokenAt?: number;
65
+ reason?: string;
66
+ }
41
67
 
42
68
  interface GuardOptions {
43
69
  apiUrl: string;
@@ -62,9 +88,20 @@ interface CheckInput {
62
88
  declare class Guard {
63
89
  private opts;
64
90
  constructor(opts: GuardOptions);
65
- check(input: CheckInput): Promise<DecisionResult>;
91
+ check(input: CheckInput): Promise<EvaluateResult>;
66
92
  /** Convenience: true only when the action is explicitly allowed. */
67
93
  isAllowed(input: CheckInput): Promise<boolean>;
94
+ /**
95
+ * Read the tamper-evident decision log (the "prove" pillar). Pass
96
+ * `{ order: 'desc' }` for newest-first.
97
+ */
98
+ audit(opts?: {
99
+ limit?: number;
100
+ order?: 'asc' | 'desc';
101
+ }): Promise<AuditRecord[]>;
102
+ /** Verify the chain is intact end-to-end (proof nothing was rewritten). */
103
+ verify(): Promise<VerifyResult>;
104
+ private get;
68
105
  }
69
106
 
70
107
  /** Claude Code PreToolUse hook payload (subset). */
@@ -96,4 +133,4 @@ interface HookResponse {
96
133
  */
97
134
  declare function createClaudeCodeHook(guard: Guard): (payload: HookPayload) => Promise<HookResponse>;
98
135
 
99
- export { type Action, type ActionType, type CheckInput, type Context, type Decision, type DecisionResult, type DestinationOrigin, Guard, type GuardOptions, type HookPayload, type HookResponse, type SignalResult, createClaudeCodeHook };
136
+ export { type Action, type ActionType, type AuditRecord, type AuditRef, type CheckInput, type Context, type Decision, type DecisionResult, type DestinationOrigin, type EvaluateResult, Guard, type GuardOptions, type HookPayload, type HookResponse, type SignalResult, type VerifyResult, createClaudeCodeHook };
package/index.js CHANGED
@@ -103,6 +103,30 @@ var Guard = class {
103
103
  async isAllowed(input) {
104
104
  return (await this.check(input)).decision === "allow";
105
105
  }
106
+ /**
107
+ * Read the tamper-evident decision log (the "prove" pillar). Pass
108
+ * `{ order: 'desc' }` for newest-first.
109
+ */
110
+ async audit(opts = {}) {
111
+ const q = new URLSearchParams();
112
+ if (opts.limit != null) q.set("limit", String(opts.limit));
113
+ if (opts.order) q.set("order", opts.order);
114
+ const qs = q.toString();
115
+ const data = await this.get(`/v1/audit${qs ? `?${qs}` : ""}`);
116
+ return data.records ?? [];
117
+ }
118
+ /** Verify the chain is intact end-to-end (proof nothing was rewritten). */
119
+ async verify() {
120
+ return this.get("/v1/audit/verify");
121
+ }
122
+ async get(path) {
123
+ const res = await fetch(`${this.opts.apiUrl.replace(/\/$/, "")}${path}`, {
124
+ headers: { authorization: `Bearer ${this.opts.apiKey}` },
125
+ signal: AbortSignal.timeout(this.opts.timeoutMs ?? 4e3)
126
+ });
127
+ if (!res.ok) throw new Error(`Specter API ${res.status}: ${await res.text()}`);
128
+ return await res.json();
129
+ }
106
130
  };
107
131
 
108
132
  export { Guard, createClaudeCodeHook };
package/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/claude-code-hook.ts","../src/guard.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["import type { Guard } from './guard.js';\nimport type { Action, ActionType, DestinationOrigin } from './types.js';\n\n/** Claude Code PreToolUse hook payload (subset). */\nexport interface HookPayload {\n session_id?: string;\n tool_name?: string;\n tool_input?: Record<string, unknown>;\n specter?: { userPrompt?: string; establishedMerchant?: string; destinationOrigin?: string };\n}\n\n/** The body Claude Code requires to actually block. */\nexport interface HookResponse {\n hookSpecificOutput: {\n hookEventName: 'PreToolUse';\n permissionDecision: 'allow' | 'deny' | 'ask';\n permissionDecisionReason: string;\n };\n}\n\n/**\n * Build a ready-made Claude Code PreToolUse hook handler from a Guard.\n *\n * IMPORTANT: when wired as an HTTP hook, Claude Code only blocks on an HTTP 200\n * whose body says permissionDecision: \"deny\". A non-2xx status merely logs and\n * the tool runs anyway. This handler returns the correct body; the host (the\n * decision API's /hooks/claude-code route, or your own server) must answer 200.\n */\nexport function createClaudeCodeHook(guard: Guard) {\n return async function handle(payload: HookPayload): Promise<HookResponse> {\n try {\n const action = normalize(payload);\n const result = await guard.check({\n agentId: `claude-code:${payload.tool_name ?? 'tool'}`,\n sessionId: payload.session_id ?? 'cc-session',\n action,\n context: {\n userPrompt: payload.specter?.userPrompt ?? '',\n establishedMerchant: payload.specter?.establishedMerchant,\n destinationOrigin: (payload.specter?.destinationOrigin as DestinationOrigin) ?? 'unknown',\n },\n });\n const permissionDecision =\n result.decision === 'allow' ? 'allow' : result.decision === 'deny' ? 'deny' : 'ask';\n return resp(permissionDecision, result.reason);\n } catch (err) {\n // Fail safe: escalate to a human rather than silently allowing.\n return resp('ask', `Specter unavailable, escalating: ${(err as Error).message}`);\n }\n };\n}\n\nfunction resp(\n permissionDecision: 'allow' | 'deny' | 'ask',\n permissionDecisionReason: string,\n): HookResponse {\n return {\n hookSpecificOutput: {\n hookEventName: 'PreToolUse',\n permissionDecision,\n permissionDecisionReason,\n },\n };\n}\n\nfunction normalize(p: HookPayload): Action {\n const tool = p.tool_name ?? 'unknown';\n const ti = p.tool_input ?? {};\n const amount = typeof ti.amount === 'number' ? ti.amount : undefined;\n const destination = str(ti.destination) ?? str(ti.to) ?? str(ti.account) ?? undefined;\n\n let type: ActionType = 'other';\n let command: string | undefined;\n if (amount != null || destination) type = 'payment';\n else if (tool === 'Bash') {\n type = 'shell';\n command = str(ti.command);\n } else if (['Read', 'Write', 'Edit', 'NotebookEdit'].includes(tool)) {\n type = 'file';\n command = str(ti.file_path) ?? str(ti.path);\n } else command = JSON.stringify(ti);\n\n return {\n type,\n amount,\n currency: str(ti.currency),\n destination,\n merchantClaimed: str(ti.merchantClaimed),\n command,\n rawInput: ti,\n };\n}\n\nfunction str(v: unknown): string | undefined {\n return typeof v === 'string' && v.length ? v : undefined;\n}\n","import type { Action, Context, DecisionResult } from './types.js';\n\nexport interface GuardOptions {\n apiUrl: string;\n apiKey: string;\n /** Per-request timeout in ms (default 4000). The gate targets <500ms p99. */\n timeoutMs?: number;\n /** Retry attempts on network/5xx (default 1). */\n retries?: number;\n agentId?: string;\n}\n\nexport interface CheckInput {\n agentId?: string;\n sessionId?: string;\n action: Action;\n context?: Partial<Context>;\n}\n\n/**\n * Thin, typed client for the Specter decision API. The whole SDK is the \"plug\";\n * the API is the product. Use `guard.check(action, context)` programmatically,\n * or `createClaudeCodeHook(guard)` for the drop-in Claude Code hook.\n */\nexport class Guard {\n constructor(private opts: GuardOptions) {}\n\n async check(input: CheckInput): Promise<DecisionResult> {\n const body = {\n agentId: input.agentId ?? this.opts.agentId ?? 'sdk-agent',\n sessionId: input.sessionId ?? 'sdk-session',\n action: input.action,\n context: {\n userPrompt: input.context?.userPrompt ?? '',\n destinationOrigin: input.context?.destinationOrigin ?? 'unknown',\n sourceRefs: input.context?.sourceRefs ?? [],\n establishedMerchant: input.context?.establishedMerchant,\n },\n };\n\n const retries = this.opts.retries ?? 1;\n let lastErr: unknown;\n for (let attempt = 0; attempt <= retries; attempt++) {\n try {\n const res = await fetch(`${this.opts.apiUrl.replace(/\\/$/, '')}/v1/evaluate`, {\n method: 'POST',\n headers: {\n 'content-type': 'application/json',\n authorization: `Bearer ${this.opts.apiKey}`,\n },\n body: JSON.stringify(body),\n signal: AbortSignal.timeout(this.opts.timeoutMs ?? 4000),\n });\n if (!res.ok) throw new Error(`Specter API ${res.status}: ${await res.text()}`);\n return (await res.json()) as DecisionResult;\n } catch (err) {\n lastErr = err;\n }\n }\n throw new Error(\n `Specter check failed after ${retries + 1} attempts: ${(lastErr as Error).message}`,\n );\n }\n\n /** Convenience: true only when the action is explicitly allowed. */\n async isAllowed(input: CheckInput): Promise<boolean> {\n return (await this.check(input)).decision === 'allow';\n }\n}\n"]}
1
+ {"version":3,"sources":["../src/claude-code-hook.ts","../src/guard.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["import type { Guard } from './guard.js';\nimport type { Action, ActionType, DestinationOrigin } from './types.js';\n\n/** Claude Code PreToolUse hook payload (subset). */\nexport interface HookPayload {\n session_id?: string;\n tool_name?: string;\n tool_input?: Record<string, unknown>;\n specter?: { userPrompt?: string; establishedMerchant?: string; destinationOrigin?: string };\n}\n\n/** The body Claude Code requires to actually block. */\nexport interface HookResponse {\n hookSpecificOutput: {\n hookEventName: 'PreToolUse';\n permissionDecision: 'allow' | 'deny' | 'ask';\n permissionDecisionReason: string;\n };\n}\n\n/**\n * Build a ready-made Claude Code PreToolUse hook handler from a Guard.\n *\n * IMPORTANT: when wired as an HTTP hook, Claude Code only blocks on an HTTP 200\n * whose body says permissionDecision: \"deny\". A non-2xx status merely logs and\n * the tool runs anyway. This handler returns the correct body; the host (the\n * decision API's /hooks/claude-code route, or your own server) must answer 200.\n */\nexport function createClaudeCodeHook(guard: Guard) {\n return async function handle(payload: HookPayload): Promise<HookResponse> {\n try {\n const action = normalize(payload);\n const result = await guard.check({\n agentId: `claude-code:${payload.tool_name ?? 'tool'}`,\n sessionId: payload.session_id ?? 'cc-session',\n action,\n context: {\n userPrompt: payload.specter?.userPrompt ?? '',\n establishedMerchant: payload.specter?.establishedMerchant,\n destinationOrigin: (payload.specter?.destinationOrigin as DestinationOrigin) ?? 'unknown',\n },\n });\n const permissionDecision =\n result.decision === 'allow' ? 'allow' : result.decision === 'deny' ? 'deny' : 'ask';\n return resp(permissionDecision, result.reason);\n } catch (err) {\n // Fail safe: escalate to a human rather than silently allowing.\n return resp('ask', `Specter unavailable, escalating: ${(err as Error).message}`);\n }\n };\n}\n\nfunction resp(\n permissionDecision: 'allow' | 'deny' | 'ask',\n permissionDecisionReason: string,\n): HookResponse {\n return {\n hookSpecificOutput: {\n hookEventName: 'PreToolUse',\n permissionDecision,\n permissionDecisionReason,\n },\n };\n}\n\nfunction normalize(p: HookPayload): Action {\n const tool = p.tool_name ?? 'unknown';\n const ti = p.tool_input ?? {};\n const amount = typeof ti.amount === 'number' ? ti.amount : undefined;\n const destination = str(ti.destination) ?? str(ti.to) ?? str(ti.account) ?? undefined;\n\n let type: ActionType = 'other';\n let command: string | undefined;\n if (amount != null || destination) type = 'payment';\n else if (tool === 'Bash') {\n type = 'shell';\n command = str(ti.command);\n } else if (['Read', 'Write', 'Edit', 'NotebookEdit'].includes(tool)) {\n type = 'file';\n command = str(ti.file_path) ?? str(ti.path);\n } else command = JSON.stringify(ti);\n\n return {\n type,\n amount,\n currency: str(ti.currency),\n destination,\n merchantClaimed: str(ti.merchantClaimed),\n command,\n rawInput: ti,\n };\n}\n\nfunction str(v: unknown): string | undefined {\n return typeof v === 'string' && v.length ? v : undefined;\n}\n","import type { Action, AuditRecord, Context, EvaluateResult, VerifyResult } from './types.js';\n\nexport interface GuardOptions {\n apiUrl: string;\n apiKey: string;\n /** Per-request timeout in ms (default 4000). The gate targets <500ms p99. */\n timeoutMs?: number;\n /** Retry attempts on network/5xx (default 1). */\n retries?: number;\n agentId?: string;\n}\n\nexport interface CheckInput {\n agentId?: string;\n sessionId?: string;\n action: Action;\n context?: Partial<Context>;\n}\n\n/**\n * Thin, typed client for the Specter decision API. The whole SDK is the \"plug\";\n * the API is the product. Use `guard.check(action, context)` programmatically,\n * or `createClaudeCodeHook(guard)` for the drop-in Claude Code hook.\n */\nexport class Guard {\n constructor(private opts: GuardOptions) {}\n\n async check(input: CheckInput): Promise<EvaluateResult> {\n const body = {\n agentId: input.agentId ?? this.opts.agentId ?? 'sdk-agent',\n sessionId: input.sessionId ?? 'sdk-session',\n action: input.action,\n context: {\n userPrompt: input.context?.userPrompt ?? '',\n destinationOrigin: input.context?.destinationOrigin ?? 'unknown',\n sourceRefs: input.context?.sourceRefs ?? [],\n establishedMerchant: input.context?.establishedMerchant,\n },\n };\n\n const retries = this.opts.retries ?? 1;\n let lastErr: unknown;\n for (let attempt = 0; attempt <= retries; attempt++) {\n try {\n const res = await fetch(`${this.opts.apiUrl.replace(/\\/$/, '')}/v1/evaluate`, {\n method: 'POST',\n headers: {\n 'content-type': 'application/json',\n authorization: `Bearer ${this.opts.apiKey}`,\n },\n body: JSON.stringify(body),\n signal: AbortSignal.timeout(this.opts.timeoutMs ?? 4000),\n });\n if (!res.ok) throw new Error(`Specter API ${res.status}: ${await res.text()}`);\n return (await res.json()) as EvaluateResult;\n } catch (err) {\n lastErr = err;\n }\n }\n throw new Error(\n `Specter check failed after ${retries + 1} attempts: ${(lastErr as Error).message}`,\n );\n }\n\n /** Convenience: true only when the action is explicitly allowed. */\n async isAllowed(input: CheckInput): Promise<boolean> {\n return (await this.check(input)).decision === 'allow';\n }\n\n /**\n * Read the tamper-evident decision log (the \"prove\" pillar). Pass\n * `{ order: 'desc' }` for newest-first.\n */\n async audit(opts: { limit?: number; order?: 'asc' | 'desc' } = {}): Promise<AuditRecord[]> {\n const q = new URLSearchParams();\n if (opts.limit != null) q.set('limit', String(opts.limit));\n if (opts.order) q.set('order', opts.order);\n const qs = q.toString();\n const data = await this.get<{ records: AuditRecord[] }>(`/v1/audit${qs ? `?${qs}` : ''}`);\n return data.records ?? [];\n }\n\n /** Verify the chain is intact end-to-end (proof nothing was rewritten). */\n async verify(): Promise<VerifyResult> {\n return this.get<VerifyResult>('/v1/audit/verify');\n }\n\n private async get<T>(path: string): Promise<T> {\n const res = await fetch(`${this.opts.apiUrl.replace(/\\/$/, '')}${path}`, {\n headers: { authorization: `Bearer ${this.opts.apiKey}` },\n signal: AbortSignal.timeout(this.opts.timeoutMs ?? 4000),\n });\n if (!res.ok) throw new Error(`Specter API ${res.status}: ${await res.text()}`);\n return (await res.json()) as T;\n }\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "specter-sdk",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Thin client for Specter — the detect → block → prove firewall / decision API for AI-agent payments. Guard.check() + a drop-in Claude Code hook. Zero dependencies.",
5
5
  "type": "module",
6
6
  "main": "./index.cjs",
@@ -8,8 +8,14 @@
8
8
  "types": "./index.d.ts",
9
9
  "exports": {
10
10
  ".": {
11
- "import": { "types": "./index.d.ts", "default": "./index.js" },
12
- "require": { "types": "./index.d.cts", "default": "./index.cjs" }
11
+ "import": {
12
+ "types": "./index.d.ts",
13
+ "default": "./index.js"
14
+ },
15
+ "require": {
16
+ "types": "./index.d.cts",
17
+ "default": "./index.cjs"
18
+ }
13
19
  }
14
20
  },
15
21
  "sideEffects": false,
@@ -29,6 +35,10 @@
29
35
  "claude-code",
30
36
  "specter"
31
37
  ],
32
- "engines": { "node": ">=18" },
33
- "publishConfig": { "access": "public" }
38
+ "engines": {
39
+ "node": ">=18"
40
+ },
41
+ "publishConfig": {
42
+ "access": "public"
43
+ }
34
44
  }