@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/LICENSE +190 -0
- package/README.md +133 -0
- package/dist/behavior.cjs +175 -0
- package/dist/behavior.cjs.map +1 -0
- package/dist/behavior.d.cts +241 -0
- package/dist/behavior.d.ts +241 -0
- package/dist/behavior.js +143 -0
- package/dist/behavior.js.map +1 -0
- package/dist/hono.cjs +580 -0
- package/dist/hono.cjs.map +1 -0
- package/dist/hono.d.cts +109 -0
- package/dist/hono.d.ts +109 -0
- package/dist/hono.js +550 -0
- package/dist/hono.js.map +1 -0
- package/dist/index.cjs +763 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +620 -0
- package/dist/index.d.ts +620 -0
- package/dist/index.js +723 -0
- package/dist/index.js.map +1 -0
- package/dist/protect-BKxcoR_2.d.cts +159 -0
- package/dist/protect-BKxcoR_2.d.ts +159 -0
- package/package.json +101 -0
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
|