@atlasent/sdk 1.5.0 → 2.10.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
@@ -13,32 +13,69 @@ import { AtlaSentClient } from "@atlasent/sdk";
13
13
 
14
14
  const client = new AtlaSentClient({ apiKey: process.env.ATLASENT_API_KEY! });
15
15
 
16
- const result = await client.evaluate({
17
- agent: "clinical-data-agent",
18
- action: "modify_patient_record",
19
- context: { user: "dr_smith", environment: "production" },
16
+ const gate = await client.deployGate({
17
+ context: { repo: "atlasent/api", commit: process.env.GIT_SHA },
20
18
  });
21
19
 
22
- if (result.decision === "ALLOW") {
23
- // execute the action
24
- } else {
25
- console.warn("Blocked:", result.reason);
20
+ if (!gate.allowed) {
21
+ console.error("Deploy blocked:", gate.reason);
22
+ process.exit(1);
26
23
  }
24
+
25
+ // runDeploy();
27
26
  ```
28
27
 
29
- That's it. `evaluate()` calls the AtlaSent policy engine, generates a hash-chained audit entry (21 CFR Part 11 / GxP-ready), and returns a result you branch on. A clean `DENY` is **not** thrown — network / server / auth failures are.
28
+ That's it. `deployGate()` performs the V1 Deploy Gate sequence against `production.deploy`: `evaluate()` calls `POST /v1-evaluate`, receives a permit when allowed, then `verifyPermit()` calls `POST /v1-verify-permit` before your deployment can run. A clean `deny` is returned as a block result — network / server / auth failures are thrown.
30
29
 
31
- ## Two methods, that's the whole surface
30
+ ## Simple V1 surface
32
31
 
33
32
  ```ts
34
33
  client.evaluate({ agent, action, context? })
35
- // → { decision: "ALLOW" | "DENY", permitId, reason, auditHash, timestamp }
34
+ // → { decision: "allow" | "deny" | "hold" | "escalate", permitId, reason, auditHash, timestamp }
36
35
 
37
36
  client.verifyPermit({ permitId, agent?, action?, context? })
38
37
  // → { verified, outcome, permitHash, timestamp }
38
+
39
+ client.deployGate({ agent?, action?, context? })
40
+ // defaults action to "production.deploy" and returns { allowed, reason, evidence }
41
+ ```
42
+
43
+ `verifyPermit()` confirms a previously-issued permit server-side. Signed/offline permit artifacts never imply deployment authorization by themselves.
44
+
45
+ ## Decision replay
46
+
47
+ Re-evaluate a recorded decision against its originally-pinned policy bundle and engine version. **Side-effect-free**: no audit row is written, no permit is minted (ADR-016 `mode: "replay"` sentinel). Useful for compliance review, regression-testing bundle changes, and post-incident investigation.
48
+
49
+ Two surfaces exist; pick the one that matches your call site:
50
+
51
+ ```ts
52
+ // SDK-canonical (preferred for new code) — wire DECISION_CHANGED is normalized
53
+ // to POLICY_DRIFT; 409 replay_not_eligible returns ENGINE_DRIFT or BUNDLE_MISSING
54
+ // instead of throwing. You can always `switch` on the variance kind.
55
+ const r = await client.replay({ evaluationId: "dec_abc123" });
56
+ switch (r.varianceKind) {
57
+ case "NONE": /* replay agrees */ break;
58
+ case "POLICY_DRIFT": /* same envelope/bundle, different decision */ break;
59
+ case "ENVELOPE_DRIFT": /* recorded envelope no longer hashes */ break;
60
+ case "ENGINE_DRIFT": /* original engine retired beyond archival */ break;
61
+ case "BUNDLE_MISSING": /* original eval had no bundle pinned */ break;
62
+ case "CHAIN_TAMPER": /* audit-chain v5 detector tripped */ break;
63
+ }
39
64
  ```
40
65
 
41
- `verifyPermit()` confirms a previously-issued permit end-to-end — use it as a second-factor gate (e.g., in a CI deploy pipeline before side-effects run).
66
+ ```ts
67
+ // Raw-wire surface — variance values pass through verbatim
68
+ // (NONE / DECISION_CHANGED / ENVELOPE_DRIFT); 409 throws AtlaSentError
69
+ const result = await client.replayDecision("dec_abc123");
70
+ if (result.variance === "DECISION_CHANGED") {
71
+ console.warn(
72
+ `Decision ${result.decision_id} drifted: ` +
73
+ `${result.original_decision} → ${result.replay_decision}`,
74
+ );
75
+ }
76
+ ```
77
+
78
+ `/v1/decisions/:id/replay` is alpha per `atlasent-api/docs/STABLE_V2_PROMOTION.md` — wire shapes can shift without a deprecation cycle until it graduates to stable v1.
42
79
 
43
80
  ## CI deploy-gate pattern
44
81
 
@@ -49,11 +86,11 @@ const client = new AtlaSentClient({ apiKey: process.env.ATLASENT_API_KEY! });
49
86
 
50
87
  const evaluation = await client.evaluate({
51
88
  agent: "ci-deploy-bot",
52
- action: "deploy_to_production",
89
+ action: "production.deploy",
53
90
  context: { service: "billing-api", commit: process.env.GIT_SHA },
54
91
  });
55
92
 
56
- if (evaluation.decision !== "ALLOW") {
93
+ if (evaluation.decision !== "allow") {
57
94
  console.error("Deploy blocked:", evaluation.reason);
58
95
  process.exit(1);
59
96
  }
@@ -76,10 +113,10 @@ See [`examples/deploy-gate.ts`](./examples/deploy-gate.ts) for a complete CI-sha
76
113
 
77
114
  ```ts
78
115
  new AtlaSentClient({
79
- apiKey: "ask_live_...", // required
116
+ apiKey: "ask_live_...", // required
80
117
  baseUrl: "https://api.atlasent.io", // default
81
- timeoutMs: 10_000, // default — per-request
82
- fetch: customFetch, // default: globalThis.fetch
118
+ timeoutMs: 10_000, // default — per-request
119
+ fetch: customFetch, // default: globalThis.fetch
83
120
  });
84
121
  ```
85
122
 
@@ -99,16 +136,16 @@ try {
99
136
  }
100
137
  ```
101
138
 
102
- | `err.code` | When it's thrown |
103
- |--------------------|---------------------------------------------------------|
104
- | `invalid_api_key` | HTTP 401 |
105
- | `forbidden` | HTTP 403 |
106
- | `rate_limited` | HTTP 429 (check `err.retryAfterMs`) |
107
- | `bad_request` | HTTP 4xx (other than 401/403/429) |
108
- | `server_error` | HTTP 5xx |
109
- | `timeout` | `timeoutMs` exceeded |
110
- | `network` | DNS / connection failure, fetch threw |
111
- | `bad_response` | non-JSON body or missing required fields |
139
+ | `err.code` | When it's thrown |
140
+ | ----------------- | ---------------------------------------- |
141
+ | `invalid_api_key` | HTTP 401 |
142
+ | `forbidden` | HTTP 403 |
143
+ | `rate_limited` | HTTP 429 (check `err.retryAfterMs`) |
144
+ | `bad_request` | HTTP 4xx (other than 401/403/429) |
145
+ | `server_error` | HTTP 5xx |
146
+ | `timeout` | `timeoutMs` exceeded |
147
+ | `network` | DNS / connection failure, fetch threw |
148
+ | `bad_response` | non-JSON body or missing required fields |
112
149
 
113
150
  Every `AtlaSentError` carries `err.requestId` — the UUID the SDK sent as `X-Request-ID`, correlatable in your server logs.
114
151
 
@@ -122,8 +159,85 @@ Every `AtlaSentError` carries `err.requestId` — the UUID the SDK sent as `X-Re
122
159
  ## Requirements
123
160
 
124
161
  - Node.js **20** or newer (native `fetch`, `AbortSignal.timeout`, `crypto.randomUUID`).
162
+ - **Browser:** Chrome 103+, Firefox 100+, Safari 16+, Edge 103+. The SDK uses
163
+ `AbortSignal.timeout` for per-request deadlines — the constructor throws a
164
+ clear `AtlaSentError(code: "network")` on runtimes that lack it so the failure
165
+ is loud rather than silent.
125
166
  - TypeScript **5.0+** for best type-inference ergonomics (older is fine — types are plain interfaces).
126
167
 
168
+ ## Hono middleware
169
+
170
+ Drop-in protection for [Hono](https://hono.dev) routes via the
171
+ `@atlasent/sdk/hono` subpath export (requires `hono` as a peer dep):
172
+
173
+ ```ts
174
+ import { Hono } from "hono";
175
+ import { atlaSentGuard, atlaSentErrorHandler } from "@atlasent/sdk/hono";
176
+
177
+ const app = new Hono();
178
+ app.onError(atlaSentErrorHandler());
179
+
180
+ app.post(
181
+ "/deploy/:service",
182
+ atlaSentGuard({
183
+ action: (c) => `deploy_${c.req.param("service")}`,
184
+ agent: (c) => c.req.header("x-agent-id") ?? "anonymous",
185
+ context: async (c) => ({ commit: (await c.req.json()).commit }),
186
+ }),
187
+ (c) => c.json({ ok: true, permit: c.get("atlasent") }),
188
+ );
189
+ ```
190
+
191
+ `atlaSentGuard` calls `protect()` under the hood — fail-closed
192
+ semantics. On allow it stashes a `Permit` on the context (key:
193
+ `"atlasent"`, override via `options.key`). On deny or transport error
194
+ it throws; `atlaSentErrorHandler` maps those to 403 / 503 responses
195
+ so every guarded route shares one error-handling path.
196
+
197
+ > **Upcoming migration:** after `@atlasent/enforce` reaches GA the
198
+ > guard API will change to accept a pre-constructed `Enforce` instance
199
+ > instead of per-route `action/agent/context` options. The current API
200
+ > is **not deprecated** until that ships. See the
201
+ > [CHANGELOG](./CHANGELOG.md) for the full before/after and
202
+ > [`contract/ENFORCE_PACK.md`](../contract/ENFORCE_PACK.md) for
203
+ > migration details.
204
+
205
+ ## Browser support
206
+
207
+ The SDK is universal and works in modern browsers with no build-time changes:
208
+
209
+ ```ts
210
+ import { AtlaSentClient } from "@atlasent/sdk";
211
+
212
+ const client = new AtlaSentClient({
213
+ apiKey: import.meta.env.VITE_ATLASENT_API_KEY,
214
+ baseUrl: import.meta.env.VITE_ATLASENT_API_URL,
215
+ });
216
+
217
+ const result = await client.evaluate({
218
+ agent: currentUser.id,
219
+ action: "view_sensitive_report",
220
+ });
221
+ ```
222
+
223
+ **Auth model in browser contexts.** Shipping a long-lived API key in a browser
224
+ bundle exposes it in DevTools and makes it replayable if exfiltrated. The
225
+ recommended options in increasing security order are:
226
+
227
+ - **Option B — browser-scoped keys (short term):** Create a read-only,
228
+ scope-restricted, IP-allowlisted key class from the AtlaSent console.
229
+ Safe for internal dashboards where you control the network. Not suitable
230
+ for public-facing apps.
231
+ - **Option A — session-token mode (recommended for atlasent-hosted surfaces):**
232
+ After SSO sign-in, the frontend obtains a short-lived (15-min) Bearer token
233
+ from `GET /v1-session/token` bound to the user's scopes and tenant. The SDK
234
+ handles token refresh transparently. See
235
+ [atlasent-api#144](https://github.com/AtlaSent-Systems-Inc/atlasent-api/issues/144).
236
+
237
+ The `User-Agent` header is set to `@atlasent/sdk/<version> browser` in browser
238
+ runtimes (browsers strip this header anyway — it's harmless) and
239
+ `@atlasent/sdk/<version> node/<node-version>` in Node.
240
+
127
241
  ## Related
128
242
 
129
243
  - **Python SDK:** same repo, [`../python/`](../python/README.md). Wire-compatible.