@atlasent/sdk 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.
package/dist/index.cjs ADDED
@@ -0,0 +1,763 @@
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/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ AtlaSentClient: () => AtlaSentClient,
24
+ AtlaSentDeniedError: () => AtlaSentDeniedError,
25
+ AtlaSentError: () => AtlaSentError,
26
+ DEFAULT_RETRY_POLICY: () => DEFAULT_RETRY_POLICY,
27
+ canonicalJSON: () => canonicalJSON,
28
+ computeBackoffMs: () => computeBackoffMs,
29
+ configure: () => configure,
30
+ default: () => index_default,
31
+ hasAttemptsLeft: () => hasAttemptsLeft,
32
+ isRetryable: () => isRetryable,
33
+ mergePolicy: () => mergePolicy,
34
+ protect: () => protect,
35
+ signedBytesFor: () => signedBytesFor,
36
+ verifyAuditBundle: () => verifyAuditBundle,
37
+ verifyBundle: () => verifyBundle
38
+ });
39
+ module.exports = __toCommonJS(index_exports);
40
+
41
+ // src/errors.ts
42
+ var AtlaSentError = class extends Error {
43
+ // Subclasses override to their own literal (e.g. "AtlaSentDeniedError");
44
+ // keep this assignable rather than pinned to a single literal.
45
+ name = "AtlaSentError";
46
+ /** HTTP status code, when the error originated from an API response. */
47
+ status;
48
+ /** Coarse category — useful for `switch` statements at call sites. */
49
+ code;
50
+ /** Correlation ID echoed from the `X-Request-ID` header the SDK sent. */
51
+ requestId;
52
+ /** Parsed `Retry-After` header value, in milliseconds. Only set for 429. */
53
+ retryAfterMs;
54
+ constructor(message, init = {}) {
55
+ super(
56
+ message,
57
+ init.cause !== void 0 ? { cause: init.cause } : void 0
58
+ );
59
+ this.status = init.status;
60
+ this.code = init.code;
61
+ this.requestId = init.requestId;
62
+ this.retryAfterMs = init.retryAfterMs;
63
+ }
64
+ };
65
+ var AtlaSentDeniedError = class extends AtlaSentError {
66
+ name = "AtlaSentDeniedError";
67
+ /** Policy decision — `"deny"` today; `"hold"` / `"escalate"` reserved. */
68
+ decision;
69
+ /** Opaque permit/decision id from `/v1-evaluate`. */
70
+ evaluationId;
71
+ /** Human-readable explanation from the policy engine, if provided. */
72
+ reason;
73
+ /** Hash-chained audit-trail entry associated with the decision. */
74
+ auditHash;
75
+ constructor(init) {
76
+ const msg = init.reason ? `AtlaSent ${init.decision}: ${init.reason}` : `AtlaSent ${init.decision}`;
77
+ const errInit = { status: 200 };
78
+ if (init.requestId !== void 0) errInit.requestId = init.requestId;
79
+ super(msg, errInit);
80
+ this.decision = init.decision;
81
+ this.evaluationId = init.evaluationId;
82
+ this.reason = init.reason;
83
+ this.auditHash = init.auditHash;
84
+ }
85
+ };
86
+
87
+ // src/client.ts
88
+ var DEFAULT_BASE_URL = "https://api.atlasent.io";
89
+ var DEFAULT_TIMEOUT_MS = 1e4;
90
+ var SDK_VERSION = "0.1.0";
91
+ var AtlaSentClient = class {
92
+ apiKey;
93
+ baseUrl;
94
+ timeoutMs;
95
+ fetchImpl;
96
+ constructor(options) {
97
+ if (!options.apiKey || typeof options.apiKey !== "string") {
98
+ throw new AtlaSentError("apiKey is required", {
99
+ code: "invalid_api_key"
100
+ });
101
+ }
102
+ this.apiKey = options.apiKey;
103
+ this.baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
104
+ this.timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
105
+ this.fetchImpl = options.fetch ?? globalThis.fetch.bind(globalThis);
106
+ }
107
+ /**
108
+ * Ask the policy engine whether an agent action is permitted.
109
+ *
110
+ * A "DENY" is **not** thrown — it is returned in
111
+ * `response.decision`. Network errors, invalid API key, rate
112
+ * limits, timeouts, and malformed responses throw
113
+ * {@link AtlaSentError}.
114
+ */
115
+ async evaluate(input) {
116
+ const body = {
117
+ action: input.action,
118
+ agent: input.agent,
119
+ context: input.context ?? {},
120
+ api_key: this.apiKey
121
+ };
122
+ const { body: wire, rateLimit } = await this.post("/v1-evaluate", body);
123
+ if (typeof wire.permitted !== "boolean" || typeof wire.decision_id !== "string") {
124
+ throw new AtlaSentError(
125
+ "Malformed response from /v1-evaluate: missing `permitted` or `decision_id`",
126
+ { code: "bad_response" }
127
+ );
128
+ }
129
+ return {
130
+ decision: wire.permitted ? "ALLOW" : "DENY",
131
+ permitId: wire.decision_id,
132
+ reason: wire.reason ?? "",
133
+ auditHash: wire.audit_hash ?? "",
134
+ timestamp: wire.timestamp ?? "",
135
+ rateLimit
136
+ };
137
+ }
138
+ /**
139
+ * Verify that a previously issued permit is still valid.
140
+ *
141
+ * A `verified: false` response is **not** thrown — inspect the
142
+ * returned object. Only transport / server errors throw.
143
+ */
144
+ async verifyPermit(input) {
145
+ const body = {
146
+ decision_id: input.permitId,
147
+ action: input.action ?? "",
148
+ agent: input.agent ?? "",
149
+ context: input.context ?? {},
150
+ api_key: this.apiKey
151
+ };
152
+ const { body: wire, rateLimit } = await this.post(
153
+ "/v1-verify-permit",
154
+ body
155
+ );
156
+ if (typeof wire.verified !== "boolean") {
157
+ throw new AtlaSentError(
158
+ "Malformed response from /v1-verify-permit: missing `verified`",
159
+ { code: "bad_response" }
160
+ );
161
+ }
162
+ return {
163
+ verified: wire.verified,
164
+ outcome: wire.outcome ?? "",
165
+ permitHash: wire.permit_hash ?? "",
166
+ timestamp: wire.timestamp ?? "",
167
+ rateLimit
168
+ };
169
+ }
170
+ /**
171
+ * Self-introspection: ask the server to describe the API key this
172
+ * client was constructed with. Returns the key's ID, organization,
173
+ * environment, scopes, IP allowlist, per-minute rate limit, the
174
+ * client IP the server observed, and the expiry (if any).
175
+ *
176
+ * Never includes the raw key or its hash. Safe to surface in operator
177
+ * dashboards. Useful for `IP_NOT_ALLOWED` debugging (the server tells
178
+ * you exactly which IP it saw) and for proactive expiry warnings.
179
+ *
180
+ * Throws {@link AtlaSentError} on transport / auth failures — same
181
+ * taxonomy as {@link AtlaSentClient.evaluate}.
182
+ */
183
+ async keySelf() {
184
+ const { body: wire, rateLimit } = await this.get(
185
+ "/v1-api-key-self"
186
+ );
187
+ if (typeof wire.key_id !== "string" || typeof wire.organization_id !== "string") {
188
+ throw new AtlaSentError(
189
+ "Malformed response from /v1-api-key-self: missing `key_id` or `organization_id`",
190
+ { code: "bad_response" }
191
+ );
192
+ }
193
+ return {
194
+ keyId: wire.key_id,
195
+ organizationId: wire.organization_id,
196
+ environment: wire.environment,
197
+ scopes: wire.scopes ?? [],
198
+ allowedCidrs: wire.allowed_cidrs ?? null,
199
+ rateLimitPerMinute: wire.rate_limit_per_minute,
200
+ clientIp: wire.client_ip ?? null,
201
+ expiresAt: wire.expires_at ?? null,
202
+ rateLimit
203
+ };
204
+ }
205
+ /**
206
+ * List persisted audit events for the authenticated organization
207
+ * (`GET /v1-audit/events`). Returned rows are wire-identical with
208
+ * the server: snake_case field names, including `previous_hash` and
209
+ * the `hash` chain, so the response can be fed straight into the
210
+ * offline verifier when paired with a signed export.
211
+ *
212
+ * `query.types` is a comma-joined list (e.g.
213
+ * `"evaluate.allow,policy.updated"`). `cursor` is the opaque
214
+ * `next_cursor` from the prior page. All fields are optional; the
215
+ * server defaults `limit` to 50 (capped at 500).
216
+ *
217
+ * Throws {@link AtlaSentError} on transport / auth failures — same
218
+ * taxonomy as {@link AtlaSentClient.evaluate}.
219
+ */
220
+ async listAuditEvents(query = {}) {
221
+ const { body: wire, rateLimit } = await this.get(
222
+ "/v1-audit/events",
223
+ buildAuditEventsQuery(query)
224
+ );
225
+ if (!Array.isArray(wire.events) || typeof wire.total !== "number") {
226
+ throw new AtlaSentError(
227
+ "Malformed response from /v1-audit/events: missing `events` or `total`",
228
+ { code: "bad_response" }
229
+ );
230
+ }
231
+ return { ...wire, rateLimit };
232
+ }
233
+ /**
234
+ * Request a signed audit export bundle
235
+ * (`POST /v1-audit/exports`). The returned object is wire-identical
236
+ * with the server — `signature`, `chain_head_hash`, `events`, and
237
+ * friends survive untouched so the bundle can be persisted to disk
238
+ * and handed to the offline verifier (`verifyBundle` /
239
+ * `verifyAuditBundle`) without any reshaping.
240
+ *
241
+ * Pass `filter.types`, `filter.from`, `filter.to`, or `filter.actor_id`
242
+ * to narrow the export; omit for a full-org bundle. `rateLimit` is
243
+ * attached alongside the wire fields for observability.
244
+ *
245
+ * Throws {@link AtlaSentError} on transport / auth failures — same
246
+ * taxonomy as {@link AtlaSentClient.evaluate}.
247
+ */
248
+ async createAuditExport(filter = {}) {
249
+ const { body: wire, rateLimit } = await this.post(
250
+ "/v1-audit/exports",
251
+ filter
252
+ );
253
+ if (typeof wire.export_id !== "string" || typeof wire.chain_head_hash !== "string" || !Array.isArray(wire.events)) {
254
+ throw new AtlaSentError(
255
+ "Malformed response from /v1-audit/exports: missing `export_id`, `chain_head_hash`, or `events`",
256
+ { code: "bad_response" }
257
+ );
258
+ }
259
+ return { ...wire, rateLimit };
260
+ }
261
+ async post(path, body) {
262
+ return this.request(path, "POST", body, void 0);
263
+ }
264
+ async get(path, query) {
265
+ return this.request(path, "GET", void 0, query);
266
+ }
267
+ async request(path, method, body, query) {
268
+ const qs = query && Array.from(query).length > 0 ? `?${query.toString()}` : "";
269
+ const url = `${this.baseUrl}${path}${qs}`;
270
+ const requestId = globalThis.crypto.randomUUID();
271
+ const headers = {
272
+ Accept: "application/json",
273
+ Authorization: `Bearer ${this.apiKey}`,
274
+ "User-Agent": `@atlasent/sdk/${SDK_VERSION} node/${process.version}`,
275
+ "X-Request-ID": requestId
276
+ };
277
+ if (method === "POST") headers["Content-Type"] = "application/json";
278
+ const init = {
279
+ method,
280
+ headers,
281
+ signal: AbortSignal.timeout(this.timeoutMs)
282
+ };
283
+ if (method === "POST") init.body = JSON.stringify(body);
284
+ let response;
285
+ try {
286
+ response = await this.fetchImpl(url, init);
287
+ } catch (err) {
288
+ throw mapFetchError(err, requestId);
289
+ }
290
+ if (!response.ok) {
291
+ throw await buildHttpError(response, requestId);
292
+ }
293
+ let parsed;
294
+ try {
295
+ parsed = await response.json();
296
+ } catch (err) {
297
+ throw new AtlaSentError("Invalid JSON response from AtlaSent API", {
298
+ code: "bad_response",
299
+ status: response.status,
300
+ requestId,
301
+ cause: err
302
+ });
303
+ }
304
+ if (parsed === null || typeof parsed !== "object") {
305
+ throw new AtlaSentError("Expected a JSON object from AtlaSent API", {
306
+ code: "bad_response",
307
+ status: response.status,
308
+ requestId
309
+ });
310
+ }
311
+ return {
312
+ body: parsed,
313
+ rateLimit: parseRateLimitHeaders(response.headers)
314
+ };
315
+ }
316
+ };
317
+ function parseRateLimitHeaders(headers) {
318
+ const rawLimit = headers.get("x-ratelimit-limit");
319
+ const rawRemaining = headers.get("x-ratelimit-remaining");
320
+ const rawReset = headers.get("x-ratelimit-reset");
321
+ if (rawLimit === null || rawRemaining === null || rawReset === null) {
322
+ return null;
323
+ }
324
+ const limit = Number(rawLimit);
325
+ const remaining = Number(rawRemaining);
326
+ if (!Number.isFinite(limit) || !Number.isFinite(remaining)) {
327
+ return null;
328
+ }
329
+ const resetAt = parseResetHeader(rawReset);
330
+ if (resetAt === null) {
331
+ return null;
332
+ }
333
+ return { limit, remaining, resetAt };
334
+ }
335
+ function parseResetHeader(raw) {
336
+ const seconds = Number(raw);
337
+ if (Number.isFinite(seconds)) {
338
+ return new Date(seconds * 1e3);
339
+ }
340
+ const ms = Date.parse(raw);
341
+ if (Number.isFinite(ms)) {
342
+ return new Date(ms);
343
+ }
344
+ return null;
345
+ }
346
+ function mapFetchError(err, requestId) {
347
+ if (err instanceof AtlaSentError) return err;
348
+ if (err instanceof DOMException && err.name === "TimeoutError") {
349
+ return new AtlaSentError("Request to AtlaSent API timed out", {
350
+ code: "timeout",
351
+ requestId,
352
+ cause: err
353
+ });
354
+ }
355
+ if (err instanceof Error && err.name === "AbortError") {
356
+ return new AtlaSentError("Request to AtlaSent API timed out", {
357
+ code: "timeout",
358
+ requestId,
359
+ cause: err
360
+ });
361
+ }
362
+ const message = err instanceof Error ? err.message : "network error";
363
+ return new AtlaSentError(`Failed to reach AtlaSent API: ${message}`, {
364
+ code: "network",
365
+ requestId,
366
+ cause: err
367
+ });
368
+ }
369
+ async function buildHttpError(response, requestId) {
370
+ const status = response.status;
371
+ const classified = await classifyHttpStatus(response);
372
+ const init = {
373
+ status,
374
+ code: classified.code,
375
+ requestId
376
+ };
377
+ if (classified.retryAfterMs !== void 0) {
378
+ init.retryAfterMs = classified.retryAfterMs;
379
+ }
380
+ return new AtlaSentError(classified.message, init);
381
+ }
382
+ async function classifyHttpStatus(response) {
383
+ const status = response.status;
384
+ const serverMessage = await readServerMessage(response);
385
+ if (status === 401) {
386
+ return {
387
+ message: serverMessage ?? "Invalid API key",
388
+ code: "invalid_api_key",
389
+ retryAfterMs: void 0
390
+ };
391
+ }
392
+ if (status === 403) {
393
+ return {
394
+ message: serverMessage ?? "Access forbidden \u2014 check your API key permissions",
395
+ code: "forbidden",
396
+ retryAfterMs: void 0
397
+ };
398
+ }
399
+ if (status === 429) {
400
+ return {
401
+ message: serverMessage ?? "Rate limited by AtlaSent API",
402
+ code: "rate_limited",
403
+ retryAfterMs: parseRetryAfter(response.headers.get("retry-after"))
404
+ };
405
+ }
406
+ if (status >= 500) {
407
+ return {
408
+ message: serverMessage ?? `AtlaSent API returned HTTP ${status}`,
409
+ code: "server_error",
410
+ retryAfterMs: void 0
411
+ };
412
+ }
413
+ return {
414
+ message: serverMessage ?? `AtlaSent API returned HTTP ${status}`,
415
+ code: "bad_request",
416
+ retryAfterMs: void 0
417
+ };
418
+ }
419
+ async function readServerMessage(response) {
420
+ try {
421
+ const text = await response.text();
422
+ if (!text) return null;
423
+ try {
424
+ const parsed = JSON.parse(text);
425
+ if (parsed && typeof parsed === "object") {
426
+ const msg = parsed.message;
427
+ const reason = parsed.reason;
428
+ if (typeof msg === "string" && msg.length > 0) return msg;
429
+ if (typeof reason === "string" && reason.length > 0) return reason;
430
+ }
431
+ } catch {
432
+ }
433
+ return text.length > 500 ? `${text.slice(0, 500)}\u2026` : text;
434
+ } catch {
435
+ return null;
436
+ }
437
+ }
438
+ function buildAuditEventsQuery(query) {
439
+ const params = new URLSearchParams();
440
+ if (query.types !== void 0 && query.types !== "") {
441
+ params.set("types", query.types);
442
+ }
443
+ if (query.actor_id !== void 0 && query.actor_id !== "") {
444
+ params.set("actor_id", query.actor_id);
445
+ }
446
+ if (query.from !== void 0 && query.from !== "") {
447
+ params.set("from", query.from);
448
+ }
449
+ if (query.to !== void 0 && query.to !== "") {
450
+ params.set("to", query.to);
451
+ }
452
+ if (query.limit !== void 0) {
453
+ params.set("limit", String(query.limit));
454
+ }
455
+ if (query.cursor !== void 0 && query.cursor !== "") {
456
+ params.set("cursor", query.cursor);
457
+ }
458
+ return params;
459
+ }
460
+ function parseRetryAfter(raw) {
461
+ if (!raw) return void 0;
462
+ const seconds = Number(raw);
463
+ if (Number.isFinite(seconds)) return Math.max(0, seconds * 1e3);
464
+ const date = Date.parse(raw);
465
+ if (Number.isFinite(date)) {
466
+ const delta = date - Date.now();
467
+ return delta > 0 ? delta : 0;
468
+ }
469
+ return void 0;
470
+ }
471
+
472
+ // src/auditBundle.ts
473
+ var import_promises = require("fs/promises");
474
+ var import_node_crypto = require("crypto");
475
+ var GENESIS_HASH = "0".repeat(64);
476
+ var subtle = import_node_crypto.webcrypto.subtle;
477
+ function canonicalJSON(value) {
478
+ if (value === null || value === void 0) return "null";
479
+ if (typeof value === "number") return Number.isFinite(value) ? String(value) : "null";
480
+ if (typeof value === "boolean") return value ? "true" : "false";
481
+ if (typeof value === "string") return JSON.stringify(value);
482
+ if (Array.isArray(value)) return "[" + value.map(canonicalJSON).join(",") + "]";
483
+ if (typeof value === "object") {
484
+ const obj = value;
485
+ const keys = Object.keys(obj).sort();
486
+ return "{" + keys.map((k) => JSON.stringify(k) + ":" + canonicalJSON(obj[k])).join(",") + "}";
487
+ }
488
+ return "null";
489
+ }
490
+ async function sha256Hex(input) {
491
+ const buf = await subtle.digest("SHA-256", new TextEncoder().encode(input));
492
+ return Array.from(new Uint8Array(buf)).map((b) => b.toString(16).padStart(2, "0")).join("");
493
+ }
494
+ function signedBytesFor(bundle) {
495
+ const envelope = {
496
+ export_id: bundle.export_id,
497
+ org_id: bundle.org_id,
498
+ chain_head_hash: bundle.chain_head_hash,
499
+ event_count: bundle.event_count,
500
+ signed_at: bundle.signed_at,
501
+ events: bundle.events
502
+ };
503
+ return new TextEncoder().encode(JSON.stringify(envelope));
504
+ }
505
+ async function verifyChainEvents(events) {
506
+ const tamperedIds = [];
507
+ let adjacencyOk = true;
508
+ const first = events[0];
509
+ let prevHash = first && typeof first.previous_hash === "string" ? first.previous_hash : GENESIS_HASH;
510
+ for (let i = 0; i < events.length; i++) {
511
+ const e = events[i];
512
+ if (!e || typeof e.hash !== "string" || typeof e.previous_hash !== "string") {
513
+ tamperedIds.push(String(e?.id ?? `index_${i}`));
514
+ adjacencyOk = false;
515
+ continue;
516
+ }
517
+ if (e.previous_hash !== prevHash) adjacencyOk = false;
518
+ const canonical = canonicalJSON(e.payload ?? {});
519
+ const recomputed = await sha256Hex(prevHash + canonical);
520
+ if (recomputed !== e.hash) tamperedIds.push(String(e.id));
521
+ prevHash = e.hash;
522
+ }
523
+ return { adjacencyOk, tamperedIds };
524
+ }
525
+ function base64UrlDecode(s) {
526
+ const pad = s.length % 4 === 0 ? "" : "=".repeat(4 - s.length % 4);
527
+ const b64 = s.replace(/-/g, "+").replace(/_/g, "/") + pad;
528
+ const bin = Buffer.from(b64, "base64");
529
+ const out = new Uint8Array(bin.byteLength);
530
+ out.set(bin);
531
+ return out;
532
+ }
533
+ async function importSpkiPem(pem) {
534
+ const b64 = pem.replace(/-----BEGIN PUBLIC KEY-----/, "").replace(/-----END PUBLIC KEY-----/, "").replace(/\s+/g, "");
535
+ const bytes = Uint8Array.from(Buffer.from(b64, "base64"));
536
+ return subtle.importKey("spki", bytes, { name: "Ed25519" }, false, ["verify"]);
537
+ }
538
+ async function resolveKeys(options) {
539
+ const out = [];
540
+ if (options?.keys) out.push(...options.keys);
541
+ if (options?.publicKeysPem) {
542
+ for (let i = 0; i < options.publicKeysPem.length; i++) {
543
+ const pem = options.publicKeysPem[i];
544
+ if (!pem) continue;
545
+ try {
546
+ const pk = await importSpkiPem(pem);
547
+ out.push({ keyId: `pem_${i}`, publicKey: pk });
548
+ } catch {
549
+ }
550
+ }
551
+ }
552
+ return out;
553
+ }
554
+ async function verifyAuditBundle(bundle, keys) {
555
+ const events = Array.isArray(bundle.events) ? bundle.events : [];
556
+ const { adjacencyOk, tamperedIds } = await verifyChainEvents(events);
557
+ const last = events[events.length - 1];
558
+ const lastHash = last && typeof last.hash === "string" ? last.hash : GENESIS_HASH;
559
+ const headHashMatches = typeof bundle.chain_head_hash === "string" ? bundle.chain_head_hash === lastHash : false;
560
+ const chainIntegrityOk = adjacencyOk && tamperedIds.length === 0 && headHashMatches;
561
+ let signatureValid = false;
562
+ let matchedKeyId;
563
+ let reason;
564
+ if (keys.length === 0) {
565
+ reason = "no signing keys configured (signing_keys table empty and ATLASENT_EXPORT_SIGNING_KEY_PUBLIC unset)";
566
+ } else if (typeof bundle.signature !== "string" || bundle.signature.length === 0) {
567
+ reason = "bundle carries no signature";
568
+ } else {
569
+ try {
570
+ const sigBytes = base64UrlDecode(bundle.signature);
571
+ const envelopeBytes = signedBytesFor(bundle);
572
+ const hint = typeof bundle.signing_key_id === "string" ? bundle.signing_key_id : null;
573
+ const ordered = hint ? [
574
+ ...keys.filter((k) => k.keyId === hint),
575
+ ...keys.filter((k) => k.keyId !== hint)
576
+ ] : Array.from(keys);
577
+ for (const k of ordered) {
578
+ const ok = await subtle.verify("Ed25519", k.publicKey, sigBytes, envelopeBytes);
579
+ if (ok) {
580
+ signatureValid = true;
581
+ matchedKeyId = k.keyId;
582
+ break;
583
+ }
584
+ }
585
+ if (!signatureValid) {
586
+ reason = `signature did not verify under any of ${keys.length} configured public key(s)`;
587
+ }
588
+ } catch (err) {
589
+ reason = `signature check failed: ${err instanceof Error ? err.message : String(err)}`;
590
+ }
591
+ }
592
+ if (!chainIntegrityOk && reason === void 0) {
593
+ if (tamperedIds.length > 0) reason = `hash mismatch for ${tamperedIds.length} event(s)`;
594
+ else if (!adjacencyOk) reason = "chain adjacency broken";
595
+ else if (!headHashMatches) reason = "chain_head_hash does not match last event";
596
+ }
597
+ return {
598
+ chainIntegrityOk,
599
+ signatureValid,
600
+ headHashMatches,
601
+ tamperedEventIds: tamperedIds,
602
+ matchedKeyId,
603
+ reason,
604
+ verified: chainIntegrityOk && signatureValid
605
+ };
606
+ }
607
+ async function verifyBundle(pathOrBundle, options) {
608
+ let bundle;
609
+ if (typeof pathOrBundle === "string") {
610
+ const raw = await (0, import_promises.readFile)(pathOrBundle, "utf8");
611
+ const parsed = JSON.parse(raw);
612
+ bundle = parsed && typeof parsed === "object" && "bundle" in parsed && typeof parsed.bundle === "object" ? parsed.bundle : parsed;
613
+ } else {
614
+ bundle = pathOrBundle;
615
+ }
616
+ const keys = await resolveKeys(options);
617
+ return verifyAuditBundle(bundle, keys);
618
+ }
619
+
620
+ // src/protect.ts
621
+ var sharedClient = null;
622
+ var overrides = {};
623
+ function configure(options) {
624
+ overrides = { ...overrides, ...options };
625
+ sharedClient = null;
626
+ }
627
+ function getClient() {
628
+ if (sharedClient) return sharedClient;
629
+ const apiKey = overrides.apiKey ?? process.env.ATLASENT_API_KEY;
630
+ if (!apiKey) {
631
+ throw new AtlaSentError(
632
+ "AtlaSent is not configured. Set ATLASENT_API_KEY in the environment, or call atlasent.configure({ apiKey }).",
633
+ { code: "invalid_api_key" }
634
+ );
635
+ }
636
+ const options = { apiKey };
637
+ if (overrides.baseUrl !== void 0) options.baseUrl = overrides.baseUrl;
638
+ if (overrides.timeoutMs !== void 0) options.timeoutMs = overrides.timeoutMs;
639
+ if (overrides.fetch !== void 0) options.fetch = overrides.fetch;
640
+ sharedClient = new AtlaSentClient(options);
641
+ return sharedClient;
642
+ }
643
+ function wireDecisionToDenied(serverDecision) {
644
+ const lower = serverDecision.toLowerCase();
645
+ if (lower === "hold" || lower === "escalate") return lower;
646
+ return "deny";
647
+ }
648
+ async function protect(request) {
649
+ const client = getClient();
650
+ const evaluation = await client.evaluate(request);
651
+ if (evaluation.decision !== "ALLOW") {
652
+ throw new AtlaSentDeniedError({
653
+ decision: wireDecisionToDenied(evaluation.decision),
654
+ evaluationId: evaluation.permitId,
655
+ reason: evaluation.reason,
656
+ auditHash: evaluation.auditHash
657
+ });
658
+ }
659
+ const verifyRequest = {
660
+ permitId: evaluation.permitId,
661
+ agent: request.agent,
662
+ action: request.action
663
+ };
664
+ if (request.context !== void 0) verifyRequest.context = request.context;
665
+ const verification = await client.verifyPermit(verifyRequest);
666
+ if (!verification.verified) {
667
+ throw new AtlaSentDeniedError({
668
+ decision: "deny",
669
+ evaluationId: evaluation.permitId,
670
+ reason: `Permit failed verification (${verification.outcome})`,
671
+ auditHash: evaluation.auditHash
672
+ });
673
+ }
674
+ return {
675
+ permitId: evaluation.permitId,
676
+ permitHash: verification.permitHash,
677
+ auditHash: evaluation.auditHash,
678
+ reason: evaluation.reason,
679
+ timestamp: verification.timestamp
680
+ };
681
+ }
682
+
683
+ // src/retry.ts
684
+ var DEFAULT_RETRY_POLICY = {
685
+ maxAttempts: 3,
686
+ baseDelayMs: 250,
687
+ maxDelayMs: 7e3
688
+ };
689
+ var RETRYABLE_CODES = /* @__PURE__ */ new Set([
690
+ "network",
691
+ "timeout",
692
+ "rate_limited",
693
+ "server_error",
694
+ "bad_response"
695
+ ]);
696
+ function isRetryable(err) {
697
+ if (!(err instanceof AtlaSentError)) return false;
698
+ if (err.code === void 0) return false;
699
+ return RETRYABLE_CODES.has(err.code);
700
+ }
701
+ function computeBackoffMs(attempt, policy = {}, err, random = Math.random) {
702
+ const merged = mergePolicy(policy);
703
+ const safeAttempt = Math.max(0, Math.floor(attempt));
704
+ const exp = Math.min(safeAttempt, 30);
705
+ const ceiling = Math.min(merged.maxDelayMs, merged.baseDelayMs * 2 ** exp);
706
+ const jittered = Math.floor(ceiling * clampUnit(random()));
707
+ const retryAfterMs = err instanceof AtlaSentError && typeof err.retryAfterMs === "number" ? Math.max(0, err.retryAfterMs) : 0;
708
+ return Math.max(retryAfterMs, jittered);
709
+ }
710
+ function hasAttemptsLeft(attempt, policy = {}) {
711
+ const merged = mergePolicy(policy);
712
+ return attempt + 1 < merged.maxAttempts;
713
+ }
714
+ function mergePolicy(policy) {
715
+ const maxAttempts = Math.max(
716
+ 1,
717
+ Math.floor(policy.maxAttempts ?? DEFAULT_RETRY_POLICY.maxAttempts)
718
+ );
719
+ const baseDelayMs = Math.max(
720
+ 0,
721
+ policy.baseDelayMs ?? DEFAULT_RETRY_POLICY.baseDelayMs
722
+ );
723
+ const maxDelayMs = Math.max(
724
+ baseDelayMs,
725
+ policy.maxDelayMs ?? DEFAULT_RETRY_POLICY.maxDelayMs
726
+ );
727
+ return { maxAttempts, baseDelayMs, maxDelayMs };
728
+ }
729
+ function clampUnit(n) {
730
+ if (!Number.isFinite(n)) return 0;
731
+ if (n < 0) return 0;
732
+ if (n >= 1) return 0.999999999;
733
+ return n;
734
+ }
735
+
736
+ // src/index.ts
737
+ var atlasent = {
738
+ protect,
739
+ configure,
740
+ verifyBundle,
741
+ AtlaSentClient,
742
+ AtlaSentError,
743
+ AtlaSentDeniedError
744
+ };
745
+ var index_default = atlasent;
746
+ // Annotate the CommonJS export names for ESM import in node:
747
+ 0 && (module.exports = {
748
+ AtlaSentClient,
749
+ AtlaSentDeniedError,
750
+ AtlaSentError,
751
+ DEFAULT_RETRY_POLICY,
752
+ canonicalJSON,
753
+ computeBackoffMs,
754
+ configure,
755
+ hasAttemptsLeft,
756
+ isRetryable,
757
+ mergePolicy,
758
+ protect,
759
+ signedBytesFor,
760
+ verifyAuditBundle,
761
+ verifyBundle
762
+ });
763
+ //# sourceMappingURL=index.cjs.map