@atlasent/sdk 2.5.0 → 2.12.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 +35 -0
- package/dist/behavior.cjs +2 -1
- package/dist/behavior.cjs.map +1 -1
- package/dist/behavior.d.cts +32 -5
- package/dist/behavior.d.ts +32 -5
- package/dist/behavior.js +2 -1
- package/dist/behavior.js.map +1 -1
- package/dist/hono.cjs +1198 -29
- package/dist/hono.cjs.map +1 -1
- package/dist/hono.d.cts +2 -2
- package/dist/hono.d.ts +2 -2
- package/dist/hono.js +1197 -29
- package/dist/hono.js.map +1 -1
- package/dist/index.cjs +6033 -1189
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +4062 -766
- package/dist/index.d.ts +4062 -766
- package/dist/index.js +5939 -1188
- package/dist/index.js.map +1 -1
- package/dist/{protect-DiRVfVLq.d.cts → protect-B6w-WQMB.d.cts} +886 -206
- package/dist/{protect-DiRVfVLq.d.ts → protect-B6w-WQMB.d.ts} +886 -206
- package/package.json +6 -1
package/dist/hono.cjs
CHANGED
|
@@ -86,7 +86,8 @@ var KNOWN_PERMIT_OUTCOMES = /* @__PURE__ */ new Set([
|
|
|
86
86
|
"permit_consumed",
|
|
87
87
|
"permit_expired",
|
|
88
88
|
"permit_revoked",
|
|
89
|
-
"permit_not_found"
|
|
89
|
+
"permit_not_found",
|
|
90
|
+
"permit_signing_key_revoked"
|
|
90
91
|
]);
|
|
91
92
|
function normalizePermitOutcome(raw) {
|
|
92
93
|
if (raw !== void 0 && KNOWN_PERMIT_OUTCOMES.has(raw)) {
|
|
@@ -147,8 +148,199 @@ var AtlaSentDeniedError = class extends AtlaSentError {
|
|
|
147
148
|
get isNotFound() {
|
|
148
149
|
return this.outcome === "permit_not_found";
|
|
149
150
|
}
|
|
151
|
+
/**
|
|
152
|
+
* `true` when the permit's signing key KID appears in the
|
|
153
|
+
* trust-root revocation list (ADR-005 D3 R2/R3 key rotation).
|
|
154
|
+
*/
|
|
155
|
+
get isSigningKeyRevoked() {
|
|
156
|
+
return this.outcome === "permit_signing_key_revoked";
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
var BundleVerificationError = class extends AtlaSentError {
|
|
160
|
+
name = "BundleVerificationError";
|
|
161
|
+
reason;
|
|
162
|
+
snapshotValidUntil;
|
|
163
|
+
snapshotFetchedAt;
|
|
164
|
+
snapshotSource;
|
|
165
|
+
kid;
|
|
166
|
+
constructor(init) {
|
|
167
|
+
super(`AtlaSent audit bundle verification failed: ${init.reason}`);
|
|
168
|
+
this.reason = init.reason;
|
|
169
|
+
this.snapshotValidUntil = init.snapshotValidUntil;
|
|
170
|
+
this.snapshotFetchedAt = init.snapshotFetchedAt;
|
|
171
|
+
this.snapshotSource = init.snapshotSource;
|
|
172
|
+
this.kid = init.kid;
|
|
173
|
+
}
|
|
150
174
|
};
|
|
151
175
|
|
|
176
|
+
// src/trustRoot.ts
|
|
177
|
+
var import_node_fs = require("fs");
|
|
178
|
+
var import_node_url = require("url");
|
|
179
|
+
var import_node_path = require("path");
|
|
180
|
+
var import_meta = {};
|
|
181
|
+
var REFRESH_INTERVAL_MS_DEFAULT = 4 * 60 * 60 * 1e3;
|
|
182
|
+
var REFRESH_INTERVAL_MS_FLOOR = 5 * 60 * 1e3;
|
|
183
|
+
var KEYS_BASE_URL = "https://keys.atlasent.io/.well-known";
|
|
184
|
+
var _halfLifeWarningEmitted = false;
|
|
185
|
+
var _expiredWarningEmitted = false;
|
|
186
|
+
var TrustRootManager = class {
|
|
187
|
+
_snapshot;
|
|
188
|
+
_refreshTimer = null;
|
|
189
|
+
_opts;
|
|
190
|
+
constructor(initialSnapshot, opts = {}) {
|
|
191
|
+
this._snapshot = initialSnapshot;
|
|
192
|
+
const intervalMs = Math.max(
|
|
193
|
+
opts.refreshIntervalMs ?? REFRESH_INTERVAL_MS_DEFAULT,
|
|
194
|
+
REFRESH_INTERVAL_MS_FLOOR
|
|
195
|
+
);
|
|
196
|
+
this._opts = {
|
|
197
|
+
refreshBaseUrl: opts.refreshBaseUrl ?? KEYS_BASE_URL,
|
|
198
|
+
refreshIntervalMs: intervalMs,
|
|
199
|
+
disableRefresh: opts.disableRefresh ?? false,
|
|
200
|
+
fetch: opts.fetch ?? (typeof globalThis !== "undefined" && globalThis.fetch ? globalThis.fetch.bind(globalThis) : ((_url) => Promise.reject(new Error("fetch not available"))))
|
|
201
|
+
};
|
|
202
|
+
if (!this._opts.disableRefresh) {
|
|
203
|
+
this._scheduleRefresh();
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
getSnapshot() {
|
|
207
|
+
return this._snapshot;
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Check whether the snapshot is expired, emit one-time warnings at
|
|
211
|
+
* half-life and expiry. Returns "ok" | "half_life" | "expired".
|
|
212
|
+
*
|
|
213
|
+
* Emits console.warn once per process at half-life (ADR-005 D3).
|
|
214
|
+
* Emits console.warn once per process on expiry.
|
|
215
|
+
*/
|
|
216
|
+
checkExpiry() {
|
|
217
|
+
const snap = this._snapshot;
|
|
218
|
+
const now = Date.now();
|
|
219
|
+
const issuedAt = new Date(snap.issued_at).getTime();
|
|
220
|
+
const validUntil = new Date(snap.valid_until).getTime();
|
|
221
|
+
if (now > validUntil) {
|
|
222
|
+
if (!_expiredWarningEmitted) {
|
|
223
|
+
_expiredWarningEmitted = true;
|
|
224
|
+
const daysAgo = Math.floor((now - validUntil) / (24 * 60 * 60 * 1e3));
|
|
225
|
+
console.warn(
|
|
226
|
+
`[atlasent] Trust snapshot expired ${daysAgo} day(s) ago (valid_until: ${snap.valid_until}). Update to a newer SDK build or enable allowExpiredSnapshot.`
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
return "expired";
|
|
230
|
+
}
|
|
231
|
+
const window = validUntil - issuedAt;
|
|
232
|
+
const halfLife = issuedAt + window / 2;
|
|
233
|
+
if (now > halfLife) {
|
|
234
|
+
if (!_halfLifeWarningEmitted) {
|
|
235
|
+
_halfLifeWarningEmitted = true;
|
|
236
|
+
const daysLeft = Math.floor((validUntil - now) / (24 * 60 * 60 * 1e3));
|
|
237
|
+
console.warn(
|
|
238
|
+
`[atlasent] Trust snapshot at half-life: expires in ${daysLeft} day(s) (valid_until: ${snap.valid_until}). Plan an SDK update.`
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
return "half_life";
|
|
242
|
+
}
|
|
243
|
+
return "ok";
|
|
244
|
+
}
|
|
245
|
+
/** Look up a key entry by kid. Returns undefined if not found. */
|
|
246
|
+
lookupKey(kid) {
|
|
247
|
+
return this._snapshot.keys.find((k) => k.kid === kid);
|
|
248
|
+
}
|
|
249
|
+
/** Returns true if the kid appears in revoked_keys. */
|
|
250
|
+
isRevoked(kid) {
|
|
251
|
+
return this._snapshot.revoked_keys.some((r) => r.kid === kid);
|
|
252
|
+
}
|
|
253
|
+
/** Replace the snapshot (e.g. after a successful refresh). */
|
|
254
|
+
replaceSnapshot(next) {
|
|
255
|
+
this._snapshot = next;
|
|
256
|
+
}
|
|
257
|
+
stopRefresh() {
|
|
258
|
+
if (this._refreshTimer !== null) {
|
|
259
|
+
clearInterval(this._refreshTimer);
|
|
260
|
+
this._refreshTimer = null;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
_scheduleRefresh() {
|
|
264
|
+
this._refreshTimer = setInterval(() => {
|
|
265
|
+
void this._doRefresh();
|
|
266
|
+
}, this._opts.refreshIntervalMs);
|
|
267
|
+
if (this._refreshTimer && typeof this._refreshTimer === "object" && "unref" in this._refreshTimer) {
|
|
268
|
+
this._refreshTimer.unref();
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
async _doRefresh() {
|
|
272
|
+
try {
|
|
273
|
+
const base = this._opts.refreshBaseUrl.replace(/\/$/, "");
|
|
274
|
+
const [keysRes, revocRes] = await Promise.all([
|
|
275
|
+
this._opts.fetch(`${base}/atlasent-verifier-keys.json`),
|
|
276
|
+
this._opts.fetch(`${base}/atlasent-revocations.json`)
|
|
277
|
+
]);
|
|
278
|
+
const indexRes = await this._opts.fetch(`${base}/atlasent-trust-root.json`);
|
|
279
|
+
if (!keysRes.ok || !revocRes.ok || !indexRes.ok) return;
|
|
280
|
+
const [keys, revoc, index] = await Promise.all([
|
|
281
|
+
keysRes.json(),
|
|
282
|
+
revocRes.json(),
|
|
283
|
+
indexRes.json()
|
|
284
|
+
]);
|
|
285
|
+
if (!index.valid_until || !Array.isArray(keys.keys)) return;
|
|
286
|
+
this._snapshot = {
|
|
287
|
+
valid_until: index.valid_until,
|
|
288
|
+
issued_at: index.issued_at ?? this._snapshot.issued_at,
|
|
289
|
+
keys: keys.keys,
|
|
290
|
+
revoked_keys: revoc.revoked_keys ?? [],
|
|
291
|
+
revoked_identities: revoc.revoked_identities ?? []
|
|
292
|
+
};
|
|
293
|
+
} catch {
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
};
|
|
297
|
+
function _loadVendorSnapshot() {
|
|
298
|
+
try {
|
|
299
|
+
let packageRoot;
|
|
300
|
+
try {
|
|
301
|
+
const thisFile = (0, import_node_url.fileURLToPath)(import_meta.url);
|
|
302
|
+
packageRoot = (0, import_node_path.resolve)((0, import_node_path.dirname)(thisFile), "..", "..");
|
|
303
|
+
} catch {
|
|
304
|
+
packageRoot = (0, import_node_path.resolve)(__dirname, "..", "..");
|
|
305
|
+
}
|
|
306
|
+
const vendorDir = (0, import_node_path.resolve)(packageRoot, "vendor", "trust-root");
|
|
307
|
+
const index = JSON.parse(
|
|
308
|
+
(0, import_node_fs.readFileSync)((0, import_node_path.resolve)(vendorDir, "atlasent-trust-root.json"), "utf8")
|
|
309
|
+
);
|
|
310
|
+
const verifierKeys = JSON.parse(
|
|
311
|
+
(0, import_node_fs.readFileSync)((0, import_node_path.resolve)(vendorDir, "atlasent-verifier-keys.json"), "utf8")
|
|
312
|
+
);
|
|
313
|
+
const revocations = JSON.parse(
|
|
314
|
+
(0, import_node_fs.readFileSync)((0, import_node_path.resolve)(vendorDir, "atlasent-revocations.json"), "utf8")
|
|
315
|
+
);
|
|
316
|
+
return {
|
|
317
|
+
valid_until: index.valid_until,
|
|
318
|
+
issued_at: index.issued_at,
|
|
319
|
+
keys: verifierKeys.keys ?? [],
|
|
320
|
+
revoked_keys: revocations.revoked_keys ?? [],
|
|
321
|
+
revoked_identities: revocations.revoked_identities ?? []
|
|
322
|
+
};
|
|
323
|
+
} catch {
|
|
324
|
+
return {
|
|
325
|
+
valid_until: "2099-01-01T00:00:00Z",
|
|
326
|
+
issued_at: "2026-05-26T00:00:00Z",
|
|
327
|
+
keys: [],
|
|
328
|
+
revoked_keys: [],
|
|
329
|
+
revoked_identities: []
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
var _globalManager = null;
|
|
334
|
+
function getGlobalTrustRootManager(opts) {
|
|
335
|
+
if (!_globalManager) {
|
|
336
|
+
_globalManager = new TrustRootManager(
|
|
337
|
+
_loadVendorSnapshot(),
|
|
338
|
+
opts ?? { disableRefresh: false }
|
|
339
|
+
);
|
|
340
|
+
}
|
|
341
|
+
return _globalManager;
|
|
342
|
+
}
|
|
343
|
+
|
|
152
344
|
// src/types.ts
|
|
153
345
|
var PRODUCTION_DEPLOY_ACTION = "production.deploy";
|
|
154
346
|
var DEPLOY_GATE_CODES = Object.freeze({
|
|
@@ -173,9 +365,14 @@ function normalizeEvaluateRequest(input) {
|
|
|
173
365
|
action_type: legacy.action,
|
|
174
366
|
actor_id: legacy.agent
|
|
175
367
|
};
|
|
176
|
-
if (legacy.context !== void 0)
|
|
177
|
-
|
|
178
|
-
|
|
368
|
+
if (legacy.context !== void 0) normalized.context = legacy.context;
|
|
369
|
+
const l = legacy;
|
|
370
|
+
if (l.explain !== void 0) normalized.explain = l.explain;
|
|
371
|
+
if (l.environment !== void 0) normalized.environment = l.environment;
|
|
372
|
+
if (l.resource !== void 0) normalized.resource = l.resource;
|
|
373
|
+
if (l.current_state !== void 0) normalized.current_state = l.current_state;
|
|
374
|
+
if (l.proposed_state !== void 0) normalized.proposed_state = l.proposed_state;
|
|
375
|
+
if (l.execution_binding !== void 0) normalized.execution_binding = l.execution_binding;
|
|
179
376
|
return normalized;
|
|
180
377
|
}
|
|
181
378
|
return input;
|
|
@@ -183,9 +380,9 @@ function normalizeEvaluateRequest(input) {
|
|
|
183
380
|
|
|
184
381
|
// src/retry.ts
|
|
185
382
|
var DEFAULT_RETRY_POLICY = {
|
|
186
|
-
maxAttempts:
|
|
187
|
-
baseDelayMs:
|
|
188
|
-
maxDelayMs:
|
|
383
|
+
maxAttempts: 3,
|
|
384
|
+
baseDelayMs: 250,
|
|
385
|
+
maxDelayMs: 1e4
|
|
189
386
|
};
|
|
190
387
|
var RETRYABLE_CODES = /* @__PURE__ */ new Set([
|
|
191
388
|
"network",
|
|
@@ -234,10 +431,371 @@ function clampUnit(n) {
|
|
|
234
431
|
return n;
|
|
235
432
|
}
|
|
236
433
|
|
|
434
|
+
// src/scim.ts
|
|
435
|
+
var SCIM_USER_SCHEMA = "urn:ietf:params:scim:schemas:core:2.0:User";
|
|
436
|
+
var SCIM_GROUP_SCHEMA = "urn:ietf:params:scim:schemas:core:2.0:Group";
|
|
437
|
+
function scimUsersPath(orgId) {
|
|
438
|
+
return `/scim/v2/${encodeURIComponent(orgId)}/Users`;
|
|
439
|
+
}
|
|
440
|
+
function scimGroupsPath(orgId) {
|
|
441
|
+
return `/scim/v2/${encodeURIComponent(orgId)}/Groups`;
|
|
442
|
+
}
|
|
443
|
+
function buildScimQuery(filter, startIndex, count) {
|
|
444
|
+
const params = new URLSearchParams();
|
|
445
|
+
if (filter !== void 0) params.set("filter", filter);
|
|
446
|
+
if (startIndex !== void 0) params.set("startIndex", String(startIndex));
|
|
447
|
+
if (count !== void 0) params.set("count", String(count));
|
|
448
|
+
return params.size > 0 ? params : void 0;
|
|
449
|
+
}
|
|
450
|
+
function makeScimClient(postFn, getFn, putFn, deleteFn) {
|
|
451
|
+
const users = {
|
|
452
|
+
async list(params) {
|
|
453
|
+
const qs = buildScimQuery(
|
|
454
|
+
params.filter,
|
|
455
|
+
params.startIndex,
|
|
456
|
+
params.count
|
|
457
|
+
);
|
|
458
|
+
const { body } = await getFn(
|
|
459
|
+
scimUsersPath(params.orgId),
|
|
460
|
+
qs
|
|
461
|
+
);
|
|
462
|
+
return body;
|
|
463
|
+
},
|
|
464
|
+
async create(orgId, user) {
|
|
465
|
+
const payload = user.schemas ? user : { ...user, schemas: [SCIM_USER_SCHEMA] };
|
|
466
|
+
const { body } = await postFn(scimUsersPath(orgId), payload);
|
|
467
|
+
return body;
|
|
468
|
+
},
|
|
469
|
+
async update(orgId, id, user) {
|
|
470
|
+
const payload = user.schemas ? user : { ...user, schemas: [SCIM_USER_SCHEMA] };
|
|
471
|
+
const { body } = await putFn(
|
|
472
|
+
`${scimUsersPath(orgId)}/${encodeURIComponent(id)}`,
|
|
473
|
+
payload
|
|
474
|
+
);
|
|
475
|
+
return body;
|
|
476
|
+
},
|
|
477
|
+
async delete(orgId, id) {
|
|
478
|
+
return deleteFn(
|
|
479
|
+
`${scimUsersPath(orgId)}/${encodeURIComponent(id)}`
|
|
480
|
+
);
|
|
481
|
+
}
|
|
482
|
+
};
|
|
483
|
+
const groups = {
|
|
484
|
+
async list(params) {
|
|
485
|
+
const qs = buildScimQuery(
|
|
486
|
+
params.filter,
|
|
487
|
+
params.startIndex,
|
|
488
|
+
params.count
|
|
489
|
+
);
|
|
490
|
+
const { body } = await getFn(
|
|
491
|
+
scimGroupsPath(params.orgId),
|
|
492
|
+
qs
|
|
493
|
+
);
|
|
494
|
+
return body;
|
|
495
|
+
},
|
|
496
|
+
async create(orgId, group) {
|
|
497
|
+
const payload = group["schemas"] ? group : { ...group, schemas: [SCIM_GROUP_SCHEMA] };
|
|
498
|
+
const { body } = await postFn(
|
|
499
|
+
scimGroupsPath(orgId),
|
|
500
|
+
payload
|
|
501
|
+
);
|
|
502
|
+
return body;
|
|
503
|
+
},
|
|
504
|
+
async delete(orgId, id) {
|
|
505
|
+
return deleteFn(
|
|
506
|
+
`${scimGroupsPath(orgId)}/${encodeURIComponent(id)}`
|
|
507
|
+
);
|
|
508
|
+
}
|
|
509
|
+
};
|
|
510
|
+
return { users, groups };
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
// src/evidence-bundle.ts
|
|
514
|
+
function wireToBundle(w) {
|
|
515
|
+
return {
|
|
516
|
+
bundleId: w.bundle_id,
|
|
517
|
+
orgId: w.org_id,
|
|
518
|
+
incidentId: w.incident_id,
|
|
519
|
+
status: w.status,
|
|
520
|
+
includedPermits: w.included_permits ?? [],
|
|
521
|
+
includeOverrides: w.include_overrides ?? false,
|
|
522
|
+
format: w.format,
|
|
523
|
+
createdAt: w.created_at,
|
|
524
|
+
expiresAt: w.expires_at,
|
|
525
|
+
...w.download_url !== void 0 ? { downloadUrl: w.download_url } : {},
|
|
526
|
+
...w.metadata !== void 0 ? { metadata: w.metadata } : {}
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
function makeEvidenceBundleClient(postFn, getFn, getRawFn) {
|
|
530
|
+
return {
|
|
531
|
+
async list(params = {}) {
|
|
532
|
+
const qs = new URLSearchParams();
|
|
533
|
+
if (params.executionId !== void 0) qs.set("execution_id", params.executionId);
|
|
534
|
+
if (params.limit !== void 0) qs.set("limit", String(params.limit));
|
|
535
|
+
if (params.cursor !== void 0) qs.set("cursor", params.cursor);
|
|
536
|
+
const { body } = await getFn("/v1/evidence-bundles", qs.size > 0 ? qs : void 0);
|
|
537
|
+
return {
|
|
538
|
+
bundles: (body.bundles ?? []).map(wireToBundle),
|
|
539
|
+
nextCursor: body.next_cursor ?? null
|
|
540
|
+
};
|
|
541
|
+
},
|
|
542
|
+
async create(params) {
|
|
543
|
+
const payload = {
|
|
544
|
+
incident_id: params.incidentId
|
|
545
|
+
};
|
|
546
|
+
if (params.includedPermits !== void 0) {
|
|
547
|
+
payload["included_permits"] = params.includedPermits;
|
|
548
|
+
}
|
|
549
|
+
if (params.includeOverrides !== void 0) {
|
|
550
|
+
payload["include_overrides"] = params.includeOverrides;
|
|
551
|
+
}
|
|
552
|
+
const { body } = await postFn(
|
|
553
|
+
"/v1/evidence-bundles",
|
|
554
|
+
payload
|
|
555
|
+
);
|
|
556
|
+
return wireToBundle(body);
|
|
557
|
+
},
|
|
558
|
+
async get(bundleId) {
|
|
559
|
+
const { body } = await getFn(
|
|
560
|
+
`/v1/evidence-bundles/${encodeURIComponent(bundleId)}`
|
|
561
|
+
);
|
|
562
|
+
return wireToBundle(body);
|
|
563
|
+
},
|
|
564
|
+
async download(bundleId, format = "json") {
|
|
565
|
+
const qs = new URLSearchParams({ format });
|
|
566
|
+
const raw = await getRawFn(
|
|
567
|
+
`/v1/evidence-bundles/${encodeURIComponent(bundleId)}/download?${qs}`
|
|
568
|
+
);
|
|
569
|
+
return Buffer.from(raw);
|
|
570
|
+
}
|
|
571
|
+
};
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
// src/auth.ts
|
|
575
|
+
function wireToTokenResponse(w) {
|
|
576
|
+
return {
|
|
577
|
+
accessToken: w.access_token,
|
|
578
|
+
refreshToken: w.refresh_token,
|
|
579
|
+
tokenType: w.token_type,
|
|
580
|
+
expiresIn: w.expires_in,
|
|
581
|
+
...w.scope !== void 0 ? { scope: w.scope } : {},
|
|
582
|
+
...w.idp_id !== void 0 ? { idpId: w.idp_id } : {}
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
function wireToIdpConnection(w) {
|
|
586
|
+
return {
|
|
587
|
+
id: w.id,
|
|
588
|
+
name: w.name,
|
|
589
|
+
provider: w.provider,
|
|
590
|
+
enabled: w.enabled,
|
|
591
|
+
isDefault: w.default,
|
|
592
|
+
...w.domains !== void 0 ? { domains: w.domains } : {},
|
|
593
|
+
createdAt: w.created_at
|
|
594
|
+
};
|
|
595
|
+
}
|
|
596
|
+
function makeAuthClient(postFn, getFn) {
|
|
597
|
+
return {
|
|
598
|
+
async refresh(refreshToken) {
|
|
599
|
+
const { body } = await postFn(
|
|
600
|
+
"/v1/auth/token/refresh",
|
|
601
|
+
{ refresh_token: refreshToken, grant_type: "refresh_token" }
|
|
602
|
+
);
|
|
603
|
+
return wireToTokenResponse(body);
|
|
604
|
+
},
|
|
605
|
+
async refreshWithIdp(idpId, refreshToken) {
|
|
606
|
+
const path = `/v1/auth/idp/${encodeURIComponent(idpId)}/token/refresh`;
|
|
607
|
+
const { body } = await postFn(path, {
|
|
608
|
+
refresh_token: refreshToken,
|
|
609
|
+
grant_type: "refresh_token",
|
|
610
|
+
idp_id: idpId
|
|
611
|
+
});
|
|
612
|
+
return wireToTokenResponse(body);
|
|
613
|
+
},
|
|
614
|
+
async listIdpConnections() {
|
|
615
|
+
const { body } = await getFn(
|
|
616
|
+
"/v1/auth/idp-connections"
|
|
617
|
+
);
|
|
618
|
+
return (body.connections ?? []).map(wireToIdpConnection);
|
|
619
|
+
}
|
|
620
|
+
};
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
// src/sso.ts
|
|
624
|
+
function wireToSsoConnection(w) {
|
|
625
|
+
return {
|
|
626
|
+
id: w.id,
|
|
627
|
+
organizationId: w.organization_id,
|
|
628
|
+
name: w.name,
|
|
629
|
+
protocol: w.protocol,
|
|
630
|
+
idpEntityId: w.idp_entity_id,
|
|
631
|
+
metadataUrl: w.metadata_url,
|
|
632
|
+
metadataXml: w.metadata_xml,
|
|
633
|
+
emailDomain: w.email_domain,
|
|
634
|
+
enforceForDomain: w.enforce_for_domain,
|
|
635
|
+
isActive: w.is_active,
|
|
636
|
+
supabaseProviderId: w.supabase_provider_id,
|
|
637
|
+
createdBy: w.created_by,
|
|
638
|
+
createdAt: w.created_at,
|
|
639
|
+
updatedAt: w.updated_at
|
|
640
|
+
};
|
|
641
|
+
}
|
|
642
|
+
function wireToSsoJitRule(w) {
|
|
643
|
+
return {
|
|
644
|
+
id: w.id,
|
|
645
|
+
connectionId: w.connection_id,
|
|
646
|
+
organizationId: w.organization_id,
|
|
647
|
+
claimAttribute: w.claim_attribute,
|
|
648
|
+
claimValue: w.claim_value,
|
|
649
|
+
grantedRole: w.granted_role,
|
|
650
|
+
precedence: w.precedence,
|
|
651
|
+
isActive: w.is_active,
|
|
652
|
+
createdAt: w.created_at,
|
|
653
|
+
updatedAt: w.updated_at
|
|
654
|
+
};
|
|
655
|
+
}
|
|
656
|
+
function wireToSsoReadiness(w) {
|
|
657
|
+
return {
|
|
658
|
+
connectionConfigured: w.connection_configured,
|
|
659
|
+
connectionTested: w.connection_tested,
|
|
660
|
+
breakGlassSet: w.break_glass_set,
|
|
661
|
+
serviceApiKeysReviewed: w.service_api_keys_reviewed
|
|
662
|
+
};
|
|
663
|
+
}
|
|
664
|
+
function ssoConnectionInputToWire(input) {
|
|
665
|
+
const w = {};
|
|
666
|
+
if (input.name !== void 0) w["name"] = input.name;
|
|
667
|
+
if (input.protocol !== void 0) w["protocol"] = input.protocol;
|
|
668
|
+
if (input.idpEntityId !== void 0) w["idp_entity_id"] = input.idpEntityId;
|
|
669
|
+
if (input.metadataUrl !== void 0) w["metadata_url"] = input.metadataUrl;
|
|
670
|
+
if (input.metadataXml !== void 0) w["metadata_xml"] = input.metadataXml;
|
|
671
|
+
if (input.emailDomain !== void 0) w["email_domain"] = input.emailDomain;
|
|
672
|
+
if (input.enforceForDomain !== void 0) w["enforce_for_domain"] = input.enforceForDomain;
|
|
673
|
+
return w;
|
|
674
|
+
}
|
|
675
|
+
function makeSsoClient(getFn, postFn, patchFn, deleteFn) {
|
|
676
|
+
return {
|
|
677
|
+
async listConnections() {
|
|
678
|
+
const { body } = await getFn("/v1/sso/connections");
|
|
679
|
+
return { connections: (body.connections ?? []).map(wireToSsoConnection) };
|
|
680
|
+
},
|
|
681
|
+
async getConnection(id) {
|
|
682
|
+
const { body } = await getFn(`/v1/sso/connections/${encodeURIComponent(id)}`);
|
|
683
|
+
return wireToSsoConnection(body);
|
|
684
|
+
},
|
|
685
|
+
async createConnection(input) {
|
|
686
|
+
const { body } = await postFn("/v1/sso/connections", ssoConnectionInputToWire(input));
|
|
687
|
+
return wireToSsoConnection(body);
|
|
688
|
+
},
|
|
689
|
+
async updateConnection(id, input) {
|
|
690
|
+
const { body } = await patchFn(
|
|
691
|
+
`/v1/sso/connections/${encodeURIComponent(id)}`,
|
|
692
|
+
ssoConnectionInputToWire(input)
|
|
693
|
+
);
|
|
694
|
+
return wireToSsoConnection(body);
|
|
695
|
+
},
|
|
696
|
+
async deleteConnection(id) {
|
|
697
|
+
await deleteFn(`/v1/sso/connections/${encodeURIComponent(id)}`);
|
|
698
|
+
},
|
|
699
|
+
async activateConnection(id) {
|
|
700
|
+
const { body } = await postFn(
|
|
701
|
+
`/v1/sso/connections/${encodeURIComponent(id)}/activate`,
|
|
702
|
+
{}
|
|
703
|
+
);
|
|
704
|
+
return { ok: body.ok, supabaseProviderId: body.supabase_provider_id };
|
|
705
|
+
},
|
|
706
|
+
async enforce(action) {
|
|
707
|
+
const { body } = await postFn("/v1/sso/enforce", { action });
|
|
708
|
+
return {
|
|
709
|
+
ok: body.ok,
|
|
710
|
+
action: body.action,
|
|
711
|
+
enforceSso: body.enforce_sso,
|
|
712
|
+
enforceSsoAt: body.enforce_sso_at
|
|
713
|
+
};
|
|
714
|
+
},
|
|
715
|
+
async getStatus() {
|
|
716
|
+
const { body } = await getFn("/v1/sso/status");
|
|
717
|
+
return wireToSsoReadiness(body.readiness);
|
|
718
|
+
},
|
|
719
|
+
async listJitRules(connectionId) {
|
|
720
|
+
const qs = connectionId ? new URLSearchParams({ connection_id: connectionId }) : void 0;
|
|
721
|
+
const { body } = await getFn("/v1/sso/jit-rules", qs);
|
|
722
|
+
return { rules: (body.rules ?? []).map(wireToSsoJitRule) };
|
|
723
|
+
},
|
|
724
|
+
async createJitRule(input) {
|
|
725
|
+
const payload = {
|
|
726
|
+
connection_id: input.connectionId,
|
|
727
|
+
claim_attribute: input.claimAttribute,
|
|
728
|
+
claim_value: input.claimValue,
|
|
729
|
+
granted_role: input.grantedRole
|
|
730
|
+
};
|
|
731
|
+
if (input.precedence !== void 0) payload["precedence"] = input.precedence;
|
|
732
|
+
const { body } = await postFn("/v1/sso/jit-rules", payload);
|
|
733
|
+
return wireToSsoJitRule(body);
|
|
734
|
+
},
|
|
735
|
+
async patchJitRule(id, patch) {
|
|
736
|
+
const payload = {};
|
|
737
|
+
if (patch.claimAttribute !== void 0) payload["claim_attribute"] = patch.claimAttribute;
|
|
738
|
+
if (patch.claimValue !== void 0) payload["claim_value"] = patch.claimValue;
|
|
739
|
+
if (patch.grantedRole !== void 0) payload["granted_role"] = patch.grantedRole;
|
|
740
|
+
if (patch.precedence !== void 0) payload["precedence"] = patch.precedence;
|
|
741
|
+
if (patch.isActive !== void 0) payload["is_active"] = patch.isActive;
|
|
742
|
+
const { body } = await patchFn(
|
|
743
|
+
`/v1/sso/jit-rules/${encodeURIComponent(id)}`,
|
|
744
|
+
payload
|
|
745
|
+
);
|
|
746
|
+
return wireToSsoJitRule(body);
|
|
747
|
+
},
|
|
748
|
+
async deleteJitRule(id) {
|
|
749
|
+
await deleteFn(`/v1/sso/jit-rules/${encodeURIComponent(id)}`);
|
|
750
|
+
}
|
|
751
|
+
};
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
// src/access-governance-log.ts
|
|
755
|
+
function wireToEvent(w) {
|
|
756
|
+
return {
|
|
757
|
+
id: w.id,
|
|
758
|
+
eventType: w.event_type,
|
|
759
|
+
orgId: w.org_id,
|
|
760
|
+
actorId: w.actor_id,
|
|
761
|
+
actorEmail: w.actor_email,
|
|
762
|
+
ipAddress: w.ip_address,
|
|
763
|
+
metadata: w.metadata ?? {},
|
|
764
|
+
createdAt: w.created_at
|
|
765
|
+
};
|
|
766
|
+
}
|
|
767
|
+
function makeAccessGovernanceLogClient(getFn) {
|
|
768
|
+
return {
|
|
769
|
+
async list(query = {}) {
|
|
770
|
+
const qs = new URLSearchParams();
|
|
771
|
+
if (query.limit !== void 0) qs.set("limit", String(query.limit));
|
|
772
|
+
if (query.cursor) qs.set("cursor", query.cursor);
|
|
773
|
+
if (query.eventType) qs.set("event_type", query.eventType);
|
|
774
|
+
if (query.actorId) qs.set("actor_id", query.actorId);
|
|
775
|
+
if (query.from) qs.set("from", query.from);
|
|
776
|
+
if (query.to) qs.set("to", query.to);
|
|
777
|
+
const { body } = await getFn(
|
|
778
|
+
"/v1/access-governance-log",
|
|
779
|
+
qs.size > 0 ? qs : void 0
|
|
780
|
+
);
|
|
781
|
+
return {
|
|
782
|
+
events: (body.events ?? []).map(wireToEvent),
|
|
783
|
+
nextCursor: body.next_cursor,
|
|
784
|
+
totalCount: body.total_count ?? 0
|
|
785
|
+
};
|
|
786
|
+
}
|
|
787
|
+
};
|
|
788
|
+
}
|
|
789
|
+
|
|
237
790
|
// src/client.ts
|
|
238
791
|
var DEFAULT_BASE_URL = "https://api.atlasent.io";
|
|
239
792
|
var DEFAULT_TIMEOUT_MS = 1e4;
|
|
240
|
-
var SDK_VERSION = "2.
|
|
793
|
+
var SDK_VERSION = "2.10.0";
|
|
794
|
+
var warnedBrowser = false;
|
|
795
|
+
var V1_EVALUATE_BATCH_PATH = "/v1/evaluate/batch";
|
|
796
|
+
var V1_EVALUATE_BATCH_LEGACY_PATH = "/v1-evaluate-batch";
|
|
797
|
+
var V1_EVALUATE_STREAM_PATH = "/v1/evaluate/stream";
|
|
798
|
+
var V1_EVALUATE_STREAM_LEGACY_PATH = "/v1-evaluate-stream";
|
|
241
799
|
function _buildUserAgent() {
|
|
242
800
|
const isNode2 = typeof process !== "undefined" && typeof process?.versions?.node === "string";
|
|
243
801
|
return isNode2 ? `@atlasent/sdk/${SDK_VERSION} node/${process.version}` : `@atlasent/sdk/${SDK_VERSION} browser`;
|
|
@@ -301,6 +859,18 @@ var AtlaSentClient = class {
|
|
|
301
859
|
fetchImpl;
|
|
302
860
|
userAgent;
|
|
303
861
|
retryPolicy;
|
|
862
|
+
/** SCIM 2.0 provisioning sub-client. Access as `client.scim`. */
|
|
863
|
+
scim;
|
|
864
|
+
/** Evidence bundle sub-client. Access as `client.evidenceBundles`. */
|
|
865
|
+
evidenceBundles;
|
|
866
|
+
/** Auth / token management sub-client. Access as `client.auth`. */
|
|
867
|
+
auth;
|
|
868
|
+
/** SSO administration sub-client. Access as `client.sso`. */
|
|
869
|
+
sso;
|
|
870
|
+
/** Access governance log sub-client. Access as `client.accessGovernanceLog`. */
|
|
871
|
+
accessGovernanceLog;
|
|
872
|
+
/** Trust-root snapshot manager for this client instance. */
|
|
873
|
+
trustRoot;
|
|
304
874
|
constructor(options) {
|
|
305
875
|
if (!options.apiKey || typeof options.apiKey !== "string") {
|
|
306
876
|
throw new AtlaSentError("apiKey is required", {
|
|
@@ -313,6 +883,12 @@ var AtlaSentClient = class {
|
|
|
313
883
|
{ code: "network" }
|
|
314
884
|
);
|
|
315
885
|
}
|
|
886
|
+
if (!warnedBrowser && typeof globalThis["window"] !== "undefined" && typeof process === "undefined") {
|
|
887
|
+
warnedBrowser = true;
|
|
888
|
+
console.warn(
|
|
889
|
+
"[@atlasent/sdk] Running in a browser environment. API keys should not be exposed in client-side bundles. Use a server-side proxy instead."
|
|
890
|
+
);
|
|
891
|
+
}
|
|
316
892
|
this.apiKey = _validateApiKey(options.apiKey);
|
|
317
893
|
this.baseUrl = _enforceTls(options.baseUrl ?? DEFAULT_BASE_URL).replace(
|
|
318
894
|
/\/+$/,
|
|
@@ -322,6 +898,44 @@ var AtlaSentClient = class {
|
|
|
322
898
|
this.fetchImpl = options.fetch ?? globalThis.fetch.bind(globalThis);
|
|
323
899
|
this.userAgent = _buildUserAgent();
|
|
324
900
|
this.retryPolicy = mergePolicy(options.retryPolicy ?? {});
|
|
901
|
+
this.scim = makeScimClient(
|
|
902
|
+
(path, body, query) => this._post(path, body, query),
|
|
903
|
+
(path, query) => this._get(path, query),
|
|
904
|
+
(path, body) => this._put(path, body),
|
|
905
|
+
(path) => this._delete(path)
|
|
906
|
+
);
|
|
907
|
+
this.evidenceBundles = makeEvidenceBundleClient(
|
|
908
|
+
(path, body) => this._post(path, body),
|
|
909
|
+
(path, query) => this._get(path, query),
|
|
910
|
+
(path) => this._getRaw(path)
|
|
911
|
+
);
|
|
912
|
+
this.auth = makeAuthClient(
|
|
913
|
+
(path, body) => this._post(path, body),
|
|
914
|
+
(path) => this._get(path)
|
|
915
|
+
);
|
|
916
|
+
this.sso = makeSsoClient(
|
|
917
|
+
(path, query) => this._get(path, query),
|
|
918
|
+
(path, body) => this._post(path, body),
|
|
919
|
+
(path, body) => this._patch(path, body),
|
|
920
|
+
(path) => this._delete(path)
|
|
921
|
+
);
|
|
922
|
+
this.accessGovernanceLog = makeAccessGovernanceLogClient(
|
|
923
|
+
(path, query) => this._get(path, query)
|
|
924
|
+
);
|
|
925
|
+
if (options.trustRootUrl !== void 0 || options.trustSnapshotRefreshMs !== void 0) {
|
|
926
|
+
const globalSnap = getGlobalTrustRootManager({ disableRefresh: true }).getSnapshot();
|
|
927
|
+
this.trustRoot = new TrustRootManager(globalSnap, {
|
|
928
|
+
...options.trustRootUrl !== void 0 && { refreshBaseUrl: options.trustRootUrl },
|
|
929
|
+
...options.trustSnapshotRefreshMs !== void 0 && { refreshIntervalMs: options.trustSnapshotRefreshMs }
|
|
930
|
+
});
|
|
931
|
+
} else {
|
|
932
|
+
this.trustRoot = getGlobalTrustRootManager();
|
|
933
|
+
}
|
|
934
|
+
this.trustRoot.checkExpiry();
|
|
935
|
+
}
|
|
936
|
+
/** Return the current trust-root snapshot (pinned or last successful refresh). */
|
|
937
|
+
getTrustSnapshot() {
|
|
938
|
+
return this.trustRoot.getSnapshot();
|
|
325
939
|
}
|
|
326
940
|
/**
|
|
327
941
|
* Ask the policy engine whether an agent action is permitted.
|
|
@@ -347,6 +961,12 @@ var AtlaSentClient = class {
|
|
|
347
961
|
actor_id: normalized.actor_id,
|
|
348
962
|
context: normalized.context ?? {}
|
|
349
963
|
};
|
|
964
|
+
if (normalized.explain !== void 0) body.explain = normalized.explain;
|
|
965
|
+
if (normalized.environment !== void 0) body.environment = normalized.environment;
|
|
966
|
+
if (normalized.resource !== void 0) body.resource = normalized.resource;
|
|
967
|
+
if (normalized.current_state !== void 0) body.current_state = normalized.current_state;
|
|
968
|
+
if (normalized.proposed_state !== void 0) body.proposed_state = normalized.proposed_state;
|
|
969
|
+
if (normalized.execution_binding !== void 0) body.execution_binding = normalized.execution_binding;
|
|
350
970
|
const { body: wire, rateLimit } = await this.post(
|
|
351
971
|
"/v1-evaluate",
|
|
352
972
|
body
|
|
@@ -383,9 +1003,224 @@ var AtlaSentClient = class {
|
|
|
383
1003
|
reason,
|
|
384
1004
|
auditHash: wire.audit_hash ?? "",
|
|
385
1005
|
timestamp: wire.timestamp ?? "",
|
|
1006
|
+
rateLimit,
|
|
1007
|
+
...wire.risk_envelope && {
|
|
1008
|
+
riskEnvelope: {
|
|
1009
|
+
weightedScore: wire.risk_envelope.weighted_score,
|
|
1010
|
+
engineDecision: wire.risk_envelope.engine_decision,
|
|
1011
|
+
envelopeDecision: wire.risk_envelope.envelope_decision,
|
|
1012
|
+
promoted: wire.risk_envelope.promoted,
|
|
1013
|
+
hardBlocks: wire.risk_envelope.hard_blocks ?? [],
|
|
1014
|
+
...wire.risk_envelope.factors && { factors: wire.risk_envelope.factors }
|
|
1015
|
+
}
|
|
1016
|
+
},
|
|
1017
|
+
...wire.risk_class !== void 0 && { riskClass: wire.risk_class },
|
|
1018
|
+
...wire.authority_basis && {
|
|
1019
|
+
authorityBasis: {
|
|
1020
|
+
kind: wire.authority_basis.kind,
|
|
1021
|
+
...wire.authority_basis.reference !== void 0 && { reference: wire.authority_basis.reference },
|
|
1022
|
+
...wire.authority_basis.granted_by !== void 0 && { grantedBy: wire.authority_basis.granted_by },
|
|
1023
|
+
...wire.authority_basis.rationale !== void 0 && { rationale: wire.authority_basis.rationale },
|
|
1024
|
+
...wire.authority_basis.expires_at !== void 0 && { expiresAt: wire.authority_basis.expires_at }
|
|
1025
|
+
}
|
|
1026
|
+
},
|
|
1027
|
+
...wire.escalation_id !== void 0 && { escalationId: wire.escalation_id }
|
|
1028
|
+
};
|
|
1029
|
+
}
|
|
1030
|
+
/**
|
|
1031
|
+
* Batch evaluate — send up to 100 decisions in a single round-trip.
|
|
1032
|
+
*
|
|
1033
|
+
* Wraps `POST /v1/evaluate/batch` (with fallback to
|
|
1034
|
+
* `POST /v1-evaluate-batch` on older runtimes). The server evaluates each item
|
|
1035
|
+
* against the active policy bundle and returns results in the same
|
|
1036
|
+
* order as the input. One rate-limit token is consumed for the
|
|
1037
|
+
* whole batch, and one audit-chain entry lists every included
|
|
1038
|
+
* decision id.
|
|
1039
|
+
*
|
|
1040
|
+
* A per-item policy `deny` is **not** thrown — it appears as
|
|
1041
|
+
* `item.decision === "deny"` in the returned items. A whole-batch
|
|
1042
|
+
* network error, 4xx, or 5xx throws {@link AtlaSentError}.
|
|
1043
|
+
*
|
|
1044
|
+
* Requires the `v2_batch` tenant feature flag to be enabled on the
|
|
1045
|
+
* org (returns 404 when off). Requires scope `evaluate:write`.
|
|
1046
|
+
*
|
|
1047
|
+
* @param requests - 1–100 evaluate items.
|
|
1048
|
+
* @param batchId - Optional caller-supplied UUID for idempotency.
|
|
1049
|
+
* A retried call with the same `batchId` and identical items
|
|
1050
|
+
* returns the cached response within 24 h (`replayed: true`).
|
|
1051
|
+
*/
|
|
1052
|
+
async evaluateBatch(requests, batchId) {
|
|
1053
|
+
if (!Array.isArray(requests) || requests.length === 0) {
|
|
1054
|
+
throw new AtlaSentError(
|
|
1055
|
+
"evaluateBatch: requests must be a non-empty array",
|
|
1056
|
+
{ code: "bad_request" }
|
|
1057
|
+
);
|
|
1058
|
+
}
|
|
1059
|
+
if (requests.length > 100) {
|
|
1060
|
+
throw new AtlaSentError(
|
|
1061
|
+
`evaluateBatch: requests.length ${requests.length} exceeds the 100-item cap`,
|
|
1062
|
+
{ code: "bad_request" }
|
|
1063
|
+
);
|
|
1064
|
+
}
|
|
1065
|
+
const wireItems = requests.map((r) => ({
|
|
1066
|
+
action_type: r.action,
|
|
1067
|
+
actor_id: r.agent,
|
|
1068
|
+
context: r.context ?? {}
|
|
1069
|
+
}));
|
|
1070
|
+
const wireBody = { items: wireItems };
|
|
1071
|
+
if (batchId) wireBody.batch_id = batchId;
|
|
1072
|
+
const { body: wire, rateLimit } = await this.postWithPathFallback(
|
|
1073
|
+
V1_EVALUATE_BATCH_PATH,
|
|
1074
|
+
V1_EVALUATE_BATCH_LEGACY_PATH,
|
|
1075
|
+
wireBody
|
|
1076
|
+
);
|
|
1077
|
+
const items = (wire.items ?? []).map(
|
|
1078
|
+
(item) => {
|
|
1079
|
+
const rawDecision = typeof item.decision === "string" ? item.decision.toLowerCase() : void 0;
|
|
1080
|
+
const decision = rawDecision === "allow" || rawDecision === "deny" || rawDecision === "hold" || rawDecision === "escalate" ? rawDecision : void 0;
|
|
1081
|
+
return {
|
|
1082
|
+
index: item.index,
|
|
1083
|
+
...decision !== void 0 ? { decision } : {},
|
|
1084
|
+
...item.decision_id ? { decisionId: item.decision_id } : {},
|
|
1085
|
+
...item.permit_token != null ? { permitToken: item.permit_token } : {},
|
|
1086
|
+
...item.reason != null ? { reason: item.reason } : {},
|
|
1087
|
+
...item.audit_entry_hash ? { auditHash: item.audit_entry_hash } : {},
|
|
1088
|
+
...item.timestamp ? { timestamp: item.timestamp } : {},
|
|
1089
|
+
...item.error ? { error: item.error } : {},
|
|
1090
|
+
...item.message ? { message: item.message } : {}
|
|
1091
|
+
};
|
|
1092
|
+
}
|
|
1093
|
+
);
|
|
1094
|
+
return {
|
|
1095
|
+
batchId: wire.batch_id,
|
|
1096
|
+
items,
|
|
1097
|
+
partial: wire.partial ?? false,
|
|
1098
|
+
...wire.replayed ? { replayed: wire.replayed } : {},
|
|
386
1099
|
rateLimit
|
|
387
1100
|
};
|
|
388
1101
|
}
|
|
1102
|
+
/**
|
|
1103
|
+
* Subscribe to a live stream of decisions for this org.
|
|
1104
|
+
*
|
|
1105
|
+
* Wraps `GET /v1-decisions-stream`. The server emits one SSE frame
|
|
1106
|
+
* per audit event and sends a heartbeat every 15 s. The session
|
|
1107
|
+
* auto-closes after `maxSeconds` (default 30 min); reconnect with
|
|
1108
|
+
* the last received `event.id` to resume without replaying history.
|
|
1109
|
+
*
|
|
1110
|
+
* ```ts
|
|
1111
|
+
* const controller = new AbortController();
|
|
1112
|
+
* for await (const event of client.subscribeDecisions({ signal: controller.signal })) {
|
|
1113
|
+
* if (event.type === "heartbeat") continue;
|
|
1114
|
+
* console.log(event.type, event.decision, event.actorId);
|
|
1115
|
+
* if (event.type === "session_end") break; // reconnect
|
|
1116
|
+
* }
|
|
1117
|
+
* ```
|
|
1118
|
+
*
|
|
1119
|
+
* Requires scope `audit:read`. Requires the `v2_decisions_stream`
|
|
1120
|
+
* tenant feature flag (returns 404 when off).
|
|
1121
|
+
*/
|
|
1122
|
+
async *subscribeDecisions(opts = {}) {
|
|
1123
|
+
const url = new URL(`${this.baseUrl}/v1-decisions-stream`);
|
|
1124
|
+
if (opts.types?.length) url.searchParams.set("types", opts.types.join(","));
|
|
1125
|
+
if (opts.actorId) url.searchParams.set("actor_id", opts.actorId);
|
|
1126
|
+
if (opts.maxSeconds !== void 0) url.searchParams.set("max_seconds", String(opts.maxSeconds));
|
|
1127
|
+
const headers = {
|
|
1128
|
+
Accept: "text/event-stream",
|
|
1129
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
1130
|
+
"User-Agent": this.userAgent,
|
|
1131
|
+
// ADR-025: declare the wire-protocol version we were built
|
|
1132
|
+
// against. Runtime serves this version's response shape; older
|
|
1133
|
+
// versions outside the compatibility window get 426.
|
|
1134
|
+
"X-AtlaSent-Protocol-Version": "1"
|
|
1135
|
+
};
|
|
1136
|
+
if (opts.lastEventId) headers["Last-Event-ID"] = opts.lastEventId;
|
|
1137
|
+
let response;
|
|
1138
|
+
try {
|
|
1139
|
+
response = await this.fetchImpl(url.toString(), {
|
|
1140
|
+
method: "GET",
|
|
1141
|
+
headers,
|
|
1142
|
+
...opts.signal ? { signal: opts.signal } : {}
|
|
1143
|
+
});
|
|
1144
|
+
} catch (err) {
|
|
1145
|
+
if (err instanceof Error && err.name === "AbortError") return;
|
|
1146
|
+
throw new AtlaSentError(
|
|
1147
|
+
`Failed to connect to decisions stream: ${err instanceof Error ? err.message : String(err)}`,
|
|
1148
|
+
{ code: "network" }
|
|
1149
|
+
);
|
|
1150
|
+
}
|
|
1151
|
+
if (!response.ok) {
|
|
1152
|
+
const code = response.status === 401 ? "invalid_api_key" : "server_error";
|
|
1153
|
+
throw new AtlaSentError(
|
|
1154
|
+
`Decisions stream returned ${response.status}`,
|
|
1155
|
+
{ code, status: response.status }
|
|
1156
|
+
);
|
|
1157
|
+
}
|
|
1158
|
+
if (!response.body) {
|
|
1159
|
+
throw new AtlaSentError("Decisions stream response has no body", { code: "bad_response" });
|
|
1160
|
+
}
|
|
1161
|
+
const reader = response.body.getReader();
|
|
1162
|
+
const decoder = new TextDecoder("utf-8");
|
|
1163
|
+
let buf = "";
|
|
1164
|
+
try {
|
|
1165
|
+
while (true) {
|
|
1166
|
+
let chunk;
|
|
1167
|
+
try {
|
|
1168
|
+
chunk = await reader.read();
|
|
1169
|
+
} catch (err) {
|
|
1170
|
+
if (err instanceof Error && err.name === "AbortError") return;
|
|
1171
|
+
throw new AtlaSentError(
|
|
1172
|
+
`Decisions stream read error: ${err instanceof Error ? err.message : String(err)}`,
|
|
1173
|
+
{ code: "network" }
|
|
1174
|
+
);
|
|
1175
|
+
}
|
|
1176
|
+
if (chunk.done) break;
|
|
1177
|
+
buf += decoder.decode(chunk.value, { stream: true });
|
|
1178
|
+
const rawBlocks = buf.split("\n\n");
|
|
1179
|
+
buf = rawBlocks.pop() ?? "";
|
|
1180
|
+
for (const block of rawBlocks) {
|
|
1181
|
+
if (!block.trim()) continue;
|
|
1182
|
+
if (block.trimStart().startsWith(":")) {
|
|
1183
|
+
yield { type: "heartbeat" };
|
|
1184
|
+
continue;
|
|
1185
|
+
}
|
|
1186
|
+
let id;
|
|
1187
|
+
let eventType = "audit_event";
|
|
1188
|
+
let dataLine = "";
|
|
1189
|
+
for (const line of block.split("\n")) {
|
|
1190
|
+
if (line.startsWith("id:")) id = line.slice(3).trim();
|
|
1191
|
+
else if (line.startsWith("event:")) eventType = line.slice(6).trim();
|
|
1192
|
+
else if (line.startsWith("data:")) dataLine = line.slice(5).trim();
|
|
1193
|
+
}
|
|
1194
|
+
if (!dataLine) continue;
|
|
1195
|
+
let parsed;
|
|
1196
|
+
try {
|
|
1197
|
+
parsed = JSON.parse(dataLine);
|
|
1198
|
+
} catch {
|
|
1199
|
+
continue;
|
|
1200
|
+
}
|
|
1201
|
+
if (eventType === "session_end") {
|
|
1202
|
+
yield { ...id !== void 0 ? { id } : {}, type: "session_end", payload: parsed };
|
|
1203
|
+
return;
|
|
1204
|
+
}
|
|
1205
|
+
const decision = typeof parsed.decision === "string" ? parsed.decision.toLowerCase() : void 0;
|
|
1206
|
+
yield {
|
|
1207
|
+
...id !== void 0 ? { id } : {},
|
|
1208
|
+
type: eventType,
|
|
1209
|
+
...decision ? { decision } : {},
|
|
1210
|
+
...typeof parsed.actor_id === "string" ? { actorId: parsed.actor_id } : {},
|
|
1211
|
+
...typeof parsed.resource_type === "string" ? { resourceType: parsed.resource_type } : {},
|
|
1212
|
+
...typeof parsed.resource_id === "string" ? { resourceId: parsed.resource_id } : {},
|
|
1213
|
+
...parsed.payload && typeof parsed.payload === "object" ? { payload: parsed.payload } : {},
|
|
1214
|
+
...typeof parsed.hash === "string" ? { hash: parsed.hash } : {},
|
|
1215
|
+
...typeof parsed.previous_hash === "string" ? { previousHash: parsed.previous_hash } : {},
|
|
1216
|
+
...typeof parsed.occurred_at === "string" ? { occurredAt: parsed.occurred_at } : {}
|
|
1217
|
+
};
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
} finally {
|
|
1221
|
+
reader.releaseLock();
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
389
1224
|
/**
|
|
390
1225
|
* Pre-flight evaluation that always returns the constraint trace.
|
|
391
1226
|
*
|
|
@@ -452,7 +1287,17 @@ var AtlaSentClient = class {
|
|
|
452
1287
|
reason,
|
|
453
1288
|
auditHash: wire.audit_hash ?? "",
|
|
454
1289
|
timestamp: wire.timestamp ?? "",
|
|
455
|
-
rateLimit
|
|
1290
|
+
rateLimit,
|
|
1291
|
+
...wire.risk_envelope && {
|
|
1292
|
+
riskEnvelope: {
|
|
1293
|
+
weightedScore: wire.risk_envelope.weighted_score,
|
|
1294
|
+
engineDecision: wire.risk_envelope.engine_decision,
|
|
1295
|
+
envelopeDecision: wire.risk_envelope.envelope_decision,
|
|
1296
|
+
promoted: wire.risk_envelope.promoted,
|
|
1297
|
+
hardBlocks: wire.risk_envelope.hard_blocks ?? [],
|
|
1298
|
+
...wire.risk_envelope.factors && { factors: wire.risk_envelope.factors }
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
456
1301
|
};
|
|
457
1302
|
let constraintTrace = null;
|
|
458
1303
|
if (wire.constraint_trace !== void 0 && wire.constraint_trace !== null && typeof wire.constraint_trace === "object") {
|
|
@@ -501,6 +1346,7 @@ var AtlaSentClient = class {
|
|
|
501
1346
|
outcome: wire.outcome ?? "",
|
|
502
1347
|
permitHash: wire.permit_hash ?? "",
|
|
503
1348
|
timestamp: wire.timestamp ?? "",
|
|
1349
|
+
expiresAt: wire.expires_at ?? null,
|
|
504
1350
|
rateLimit
|
|
505
1351
|
};
|
|
506
1352
|
}
|
|
@@ -518,6 +1364,7 @@ var AtlaSentClient = class {
|
|
|
518
1364
|
const agent = input.agent ?? "ci-deploy-bot";
|
|
519
1365
|
const action = input.action ?? PRODUCTION_DEPLOY_ACTION;
|
|
520
1366
|
const context = input.context ?? {};
|
|
1367
|
+
const environment = typeof context.environment === "string" ? context.environment : typeof context.environment_name === "string" ? context.environment_name : void 0;
|
|
521
1368
|
const evaluation = await this.evaluate({ agent, action, context });
|
|
522
1369
|
if (evaluation.decision !== "allow") {
|
|
523
1370
|
return {
|
|
@@ -534,7 +1381,8 @@ var AtlaSentClient = class {
|
|
|
534
1381
|
permitId: evaluation.permitId,
|
|
535
1382
|
agent,
|
|
536
1383
|
action,
|
|
537
|
-
context
|
|
1384
|
+
context,
|
|
1385
|
+
...environment !== void 0 ? { environment } : {}
|
|
538
1386
|
});
|
|
539
1387
|
if (!verification.verified) {
|
|
540
1388
|
return {
|
|
@@ -748,15 +1596,15 @@ var AtlaSentClient = class {
|
|
|
748
1596
|
*/
|
|
749
1597
|
async keySelf() {
|
|
750
1598
|
const { body: wire, rateLimit } = await this.get("/v1-api-key-self");
|
|
751
|
-
if (typeof wire.key_id !== "string" || typeof wire.
|
|
1599
|
+
if (typeof wire.key_id !== "string" || typeof wire.org_id !== "string") {
|
|
752
1600
|
throw new AtlaSentError(
|
|
753
|
-
"Malformed response from /v1-api-key-self: missing `key_id` or `
|
|
1601
|
+
"Malformed response from /v1-api-key-self: missing `key_id` or `org_id`",
|
|
754
1602
|
{ code: "bad_response" }
|
|
755
1603
|
);
|
|
756
1604
|
}
|
|
757
1605
|
return {
|
|
758
1606
|
keyId: wire.key_id,
|
|
759
|
-
|
|
1607
|
+
orgId: wire.org_id,
|
|
760
1608
|
environment: wire.environment,
|
|
761
1609
|
scopes: wire.scopes ?? [],
|
|
762
1610
|
allowedCidrs: wire.allowed_cidrs ?? null,
|
|
@@ -823,7 +1671,153 @@ var AtlaSentClient = class {
|
|
|
823
1671
|
return { ...wire, rateLimit };
|
|
824
1672
|
}
|
|
825
1673
|
/**
|
|
826
|
-
*
|
|
1674
|
+
* Re-evaluate a recorded decision against its originally-pinned policy
|
|
1675
|
+
* bundle and engine version, and report whether the result agrees with
|
|
1676
|
+
* what was recorded.
|
|
1677
|
+
*
|
|
1678
|
+
* Wraps `POST /v1-decisions-replay/:id/replay`. **Side-effect-free** — no
|
|
1679
|
+
* audit chain row is written and no permit is issued (per ADR-016).
|
|
1680
|
+
* Useful for compliance review, regression testing of bundle changes,
|
|
1681
|
+
* and post-incident investigation.
|
|
1682
|
+
*
|
|
1683
|
+
* Outcomes encoded in the response:
|
|
1684
|
+
* - `variance: "NONE"` — replay agrees with the original decision.
|
|
1685
|
+
* - `variance: "DECISION_CHANGED"` — same envelope, same bundle, different
|
|
1686
|
+
* decision. Almost always indicates non-determinism in a rule
|
|
1687
|
+
* (e.g. wall-clock comparison) and warrants investigation.
|
|
1688
|
+
* - `variance: "ENVELOPE_DRIFT"` — the recorded request envelope no longer
|
|
1689
|
+
* hashes to the recorded value. The replay short-circuits without
|
|
1690
|
+
* running the engine; `replay_decision` is absent. Treat as evidence
|
|
1691
|
+
* of substrate tamper or a recorder bug.
|
|
1692
|
+
*
|
|
1693
|
+
* Server-side 409 responses (replay refused because the engine version
|
|
1694
|
+
* does not accept replay, or because no bundle was pinned) surface as
|
|
1695
|
+
* `AtlaSentError` with `code: "replay_not_eligible"` — callers should
|
|
1696
|
+
* treat them as expected for old / un-pinned decisions, not as bugs.
|
|
1697
|
+
*
|
|
1698
|
+
* Requires the `evaluate:write` API key scope.
|
|
1699
|
+
*
|
|
1700
|
+
* @param decisionId The UUID of the recorded decision to replay.
|
|
1701
|
+
* Matches `execution_evaluations.request_id`.
|
|
1702
|
+
*
|
|
1703
|
+
* @example
|
|
1704
|
+
* ```ts
|
|
1705
|
+
* const result = await client.replayDecision("dec_abc123");
|
|
1706
|
+
* if (result.variance === "DECISION_CHANGED") {
|
|
1707
|
+
* console.warn(
|
|
1708
|
+
* `Decision ${result.decision_id} changed on replay: ` +
|
|
1709
|
+
* `${result.original_decision} → ${result.replay_decision}`,
|
|
1710
|
+
* );
|
|
1711
|
+
* }
|
|
1712
|
+
* ```
|
|
1713
|
+
*/
|
|
1714
|
+
async replayDecision(decisionId) {
|
|
1715
|
+
if (typeof decisionId !== "string" || decisionId.length === 0) {
|
|
1716
|
+
throw new AtlaSentError("decisionId is required", {
|
|
1717
|
+
code: "bad_request"
|
|
1718
|
+
});
|
|
1719
|
+
}
|
|
1720
|
+
const path = `/v1-decisions-replay/${encodeURIComponent(decisionId)}/replay`;
|
|
1721
|
+
const { body: wire, rateLimit } = await this.post(
|
|
1722
|
+
path,
|
|
1723
|
+
{}
|
|
1724
|
+
);
|
|
1725
|
+
if (typeof wire.decision_id !== "string" || typeof wire.original_decision !== "string" || typeof wire.engine_version_kind !== "string" || typeof wire.accepts_replay !== "boolean" || typeof wire.variance !== "string" || typeof wire.envelope_verification !== "string" || typeof wire.replayed_at !== "string") {
|
|
1726
|
+
throw new AtlaSentError(
|
|
1727
|
+
"Malformed response from /v1-decisions-replay/:id/replay: missing required fields",
|
|
1728
|
+
{ code: "bad_response" }
|
|
1729
|
+
);
|
|
1730
|
+
}
|
|
1731
|
+
return { ...wire, rateLimit };
|
|
1732
|
+
}
|
|
1733
|
+
/**
|
|
1734
|
+
* ADR-015 Phase C — SDK-canonical replay runtime.
|
|
1735
|
+
*
|
|
1736
|
+
* Re-evaluates a recorded decision against its originally-pinned policy
|
|
1737
|
+
* bundle and engine version via `POST /v1/decisions/:id/replay`.
|
|
1738
|
+
* Side-effect-free server-side: no audit chain row is written and no
|
|
1739
|
+
* permit is issued (ADR-016 `mode: "replay"` sentinel).
|
|
1740
|
+
*
|
|
1741
|
+
* Differences from {@link replayDecision} (the 2.7.0 raw-wire surface):
|
|
1742
|
+
*
|
|
1743
|
+
* | | `replayDecision()` | `replay()` |
|
|
1744
|
+
* | --- | --- | --- |
|
|
1745
|
+
* | Path | `/v1-decisions-replay/:id/replay` | `/v1/decisions/:id/replay` |
|
|
1746
|
+
* | Variance | raw wire (`DECISION_CHANGED`) | SDK-canonical (`POLICY_DRIFT`) |
|
|
1747
|
+
* | 409 handling | throws `AtlaSentError` | returns `ENGINE_DRIFT` / `BUNDLE_MISSING` |
|
|
1748
|
+
* | Input shape | `decisionId: string` | `{ evaluationId }` |
|
|
1749
|
+
*
|
|
1750
|
+
* **Never throws on `409 replay_not_eligible`** — instead returns a
|
|
1751
|
+
* `ReplayResponse` with `varianceKind: "ENGINE_DRIFT"` (engine retired
|
|
1752
|
+
* beyond archival window) or `"BUNDLE_MISSING"` (no bundle pinned on
|
|
1753
|
+
* the original evaluation). Callers can always `switch` on
|
|
1754
|
+
* `result.varianceKind` without a try/catch.
|
|
1755
|
+
*
|
|
1756
|
+
* Fix-forward note: this method was originally landed in PR #275 but
|
|
1757
|
+
* dropped from the squash merge. The TS types (`ReplayResponse`,
|
|
1758
|
+
* `ReplayRequest`) and CHANGELOG made it through; the method itself
|
|
1759
|
+
* did not. Restored here to match the Python {@link
|
|
1760
|
+
* AtlaSentClient}.replay() that landed in atlasent-sdk@2.6.0 (Python).
|
|
1761
|
+
*/
|
|
1762
|
+
async replay(input) {
|
|
1763
|
+
if (!input || typeof input.evaluationId !== "string" || input.evaluationId.length === 0) {
|
|
1764
|
+
throw new AtlaSentError("evaluationId is required", {
|
|
1765
|
+
code: "bad_request"
|
|
1766
|
+
});
|
|
1767
|
+
}
|
|
1768
|
+
const path = `/v1/decisions/${encodeURIComponent(input.evaluationId)}/replay`;
|
|
1769
|
+
let wire;
|
|
1770
|
+
let rateLimit;
|
|
1771
|
+
try {
|
|
1772
|
+
const result = await this.post(path, {});
|
|
1773
|
+
wire = result.body;
|
|
1774
|
+
rateLimit = result.rateLimit;
|
|
1775
|
+
} catch (err) {
|
|
1776
|
+
if (err instanceof AtlaSentError && err.status === 409) {
|
|
1777
|
+
const msg = (err.message ?? "").toLowerCase();
|
|
1778
|
+
const varianceKind2 = msg.includes("bundle") ? "BUNDLE_MISSING" : "ENGINE_DRIFT";
|
|
1779
|
+
return {
|
|
1780
|
+
decisionId: input.evaluationId,
|
|
1781
|
+
varianceKind: varianceKind2,
|
|
1782
|
+
originalDecision: "deny",
|
|
1783
|
+
acceptsReplay: false,
|
|
1784
|
+
replayedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1785
|
+
rateLimit: null
|
|
1786
|
+
};
|
|
1787
|
+
}
|
|
1788
|
+
throw err;
|
|
1789
|
+
}
|
|
1790
|
+
const VARIANCE_MAP = {
|
|
1791
|
+
NONE: "NONE",
|
|
1792
|
+
DECISION_CHANGED: "POLICY_DRIFT",
|
|
1793
|
+
ENVELOPE_DRIFT: "ENVELOPE_DRIFT",
|
|
1794
|
+
CHAIN_TAMPER: "CHAIN_TAMPER",
|
|
1795
|
+
BUNDLE_MISSING: "BUNDLE_MISSING",
|
|
1796
|
+
ENGINE_DRIFT: "ENGINE_DRIFT"
|
|
1797
|
+
};
|
|
1798
|
+
const rawVariance = typeof wire.variance === "string" ? wire.variance : "";
|
|
1799
|
+
const varianceKind = VARIANCE_MAP[rawVariance] ?? "NONE";
|
|
1800
|
+
const replayDec = typeof wire.replay_decision === "string" ? wire.replay_decision.toLowerCase() : void 0;
|
|
1801
|
+
const originalDec = typeof wire.original_decision === "string" ? wire.original_decision.toLowerCase() : "deny";
|
|
1802
|
+
const response = {
|
|
1803
|
+
decisionId: typeof wire.decision_id === "string" ? wire.decision_id : input.evaluationId,
|
|
1804
|
+
varianceKind,
|
|
1805
|
+
originalDecision: originalDec,
|
|
1806
|
+
acceptsReplay: typeof wire.accepts_replay === "boolean" ? wire.accepts_replay : true,
|
|
1807
|
+
replayedAt: typeof wire.replayed_at === "string" ? wire.replayed_at : (/* @__PURE__ */ new Date()).toISOString(),
|
|
1808
|
+
rateLimit
|
|
1809
|
+
};
|
|
1810
|
+
if (typeof wire.original_deny_code === "string") response.originalDenyCode = wire.original_deny_code;
|
|
1811
|
+
if (replayDec !== void 0) response.replayedDecision = replayDec;
|
|
1812
|
+
if (typeof wire.replay_deny_code === "string") response.replayedDenyCode = wire.replay_deny_code;
|
|
1813
|
+
if (typeof wire.engine_version === "string") response.engineVersion = wire.engine_version;
|
|
1814
|
+
if (typeof wire.engine_version_kind === "string") response.engineVersionKind = wire.engine_version_kind;
|
|
1815
|
+
if (typeof wire.envelope_verification === "string") response.envelopeVerification = wire.envelope_verification;
|
|
1816
|
+
return response;
|
|
1817
|
+
}
|
|
1818
|
+
/**
|
|
1819
|
+
* Open a streaming evaluation session against `POST /v1/evaluate/stream`
|
|
1820
|
+
* (with fallback to `POST /v1-evaluate-stream` on older runtimes).
|
|
827
1821
|
*
|
|
828
1822
|
* Yields {@link StreamDecisionEvent} and {@link StreamProgressEvent} objects
|
|
829
1823
|
* as the server emits them. The iterator ends cleanly when the server sends
|
|
@@ -861,7 +1855,7 @@ var AtlaSentClient = class {
|
|
|
861
1855
|
api_key: this.apiKey
|
|
862
1856
|
};
|
|
863
1857
|
const requestId = globalThis.crypto.randomUUID();
|
|
864
|
-
|
|
1858
|
+
let streamPath = V1_EVALUATE_STREAM_PATH;
|
|
865
1859
|
let lastEventId;
|
|
866
1860
|
let retryCount = 0;
|
|
867
1861
|
while (true) {
|
|
@@ -870,6 +1864,8 @@ var AtlaSentClient = class {
|
|
|
870
1864
|
"Content-Type": "application/json",
|
|
871
1865
|
Authorization: `Bearer ${this.apiKey}`,
|
|
872
1866
|
"User-Agent": this.userAgent,
|
|
1867
|
+
// ADR-025: wire-protocol version declared on every request.
|
|
1868
|
+
"X-AtlaSent-Protocol-Version": "1",
|
|
873
1869
|
"X-Request-ID": requestId
|
|
874
1870
|
};
|
|
875
1871
|
if (lastEventId !== void 0) {
|
|
@@ -879,7 +1875,7 @@ var AtlaSentClient = class {
|
|
|
879
1875
|
const signal = opts.signal ? AbortSignal.any([connectionTimeoutSignal, opts.signal]) : connectionTimeoutSignal;
|
|
880
1876
|
let response;
|
|
881
1877
|
try {
|
|
882
|
-
response = await this.fetchImpl(
|
|
1878
|
+
response = await this.fetchImpl(`${this.baseUrl}${streamPath}`, {
|
|
883
1879
|
method: "POST",
|
|
884
1880
|
headers,
|
|
885
1881
|
body: JSON.stringify(body),
|
|
@@ -895,6 +1891,10 @@ var AtlaSentClient = class {
|
|
|
895
1891
|
throw mapped;
|
|
896
1892
|
}
|
|
897
1893
|
if (!response.ok) {
|
|
1894
|
+
if (streamPath === V1_EVALUATE_STREAM_PATH && (response.status === 404 || response.status === 405)) {
|
|
1895
|
+
streamPath = V1_EVALUATE_STREAM_LEGACY_PATH;
|
|
1896
|
+
continue;
|
|
1897
|
+
}
|
|
898
1898
|
throw await buildHttpError(response, requestId);
|
|
899
1899
|
}
|
|
900
1900
|
if (!response.body) {
|
|
@@ -946,6 +1946,16 @@ var AtlaSentClient = class {
|
|
|
946
1946
|
async post(path, body, query) {
|
|
947
1947
|
return this.request(path, "POST", body, query);
|
|
948
1948
|
}
|
|
1949
|
+
async postWithPathFallback(primaryPath, fallbackPath, body, query) {
|
|
1950
|
+
try {
|
|
1951
|
+
return await this.post(primaryPath, body, query);
|
|
1952
|
+
} catch (err) {
|
|
1953
|
+
if (err instanceof AtlaSentError && (err.status === 404 || err.status === 405)) {
|
|
1954
|
+
return this.post(fallbackPath, body, query);
|
|
1955
|
+
}
|
|
1956
|
+
throw err;
|
|
1957
|
+
}
|
|
1958
|
+
}
|
|
949
1959
|
async get(path, query) {
|
|
950
1960
|
return this.request(path, "GET", void 0, query);
|
|
951
1961
|
}
|
|
@@ -957,7 +1967,9 @@ var AtlaSentClient = class {
|
|
|
957
1967
|
Accept: "application/json",
|
|
958
1968
|
Authorization: `Bearer ${this.apiKey}`,
|
|
959
1969
|
"User-Agent": this.userAgent,
|
|
960
|
-
"X-Request-ID": requestId
|
|
1970
|
+
"X-Request-ID": requestId,
|
|
1971
|
+
// ADR-025: wire-protocol version declared on every request.
|
|
1972
|
+
"X-AtlaSent-Protocol-Version": "1"
|
|
961
1973
|
};
|
|
962
1974
|
if (method === "POST") headers["Content-Type"] = "application/json";
|
|
963
1975
|
const bodyStr = method === "POST" ? JSON.stringify(body) : void 0;
|
|
@@ -1573,6 +2585,145 @@ var AtlaSentClient = class {
|
|
|
1573
2585
|
);
|
|
1574
2586
|
return body;
|
|
1575
2587
|
}
|
|
2588
|
+
// ── Constrained governance agents (read surface) ──────────────────────────
|
|
2589
|
+
//
|
|
2590
|
+
// Three GETs onto the v1-governance-agents edge function. Doctrine:
|
|
2591
|
+
// findings produced by these endpoints are advisory signal, never
|
|
2592
|
+
// authority. There is no `runGovernanceAgent` method on this client —
|
|
2593
|
+
// invocation belongs in CI (atlasent-action `governance-agents` mode),
|
|
2594
|
+
// not in application code.
|
|
2595
|
+
/**
|
|
2596
|
+
* List the advisory governance-agent registry for the calling org.
|
|
2597
|
+
*
|
|
2598
|
+
* Calls `GET /v1/governance/agents`. The registry is reference data
|
|
2599
|
+
* seeded at runtime-DB migration time; every row has
|
|
2600
|
+
* `authority_class = "advisory"` and `can_authorize = false` —
|
|
2601
|
+
* structural invariants enforced by the schema, not policy.
|
|
2602
|
+
*/
|
|
2603
|
+
async listGovernanceAgents() {
|
|
2604
|
+
const { body } = await this.get(
|
|
2605
|
+
"/v1/governance/agents"
|
|
2606
|
+
);
|
|
2607
|
+
return [...body.agents ?? []];
|
|
2608
|
+
}
|
|
2609
|
+
/**
|
|
2610
|
+
* List advisory findings emitted against one governed change.
|
|
2611
|
+
*
|
|
2612
|
+
* Calls `GET /v1/governance/findings?change_id=…[&agent_slug=…]`.
|
|
2613
|
+
* Returns the typed-finding rows in `created_at DESC` order, including
|
|
2614
|
+
* `routed_gate_id` when the finding→gate trigger linked them. Findings
|
|
2615
|
+
* with `can_authorize === false` (always) are advisory; rendering them
|
|
2616
|
+
* never satisfies a gate.
|
|
2617
|
+
*/
|
|
2618
|
+
async listGovernanceFindings(query) {
|
|
2619
|
+
if (!query?.change_id) {
|
|
2620
|
+
throw new AtlaSentError("change_id is required", { code: "bad_request" });
|
|
2621
|
+
}
|
|
2622
|
+
const params = new URLSearchParams({ change_id: query.change_id });
|
|
2623
|
+
if (query.agent_slug) params.set("agent_slug", query.agent_slug);
|
|
2624
|
+
const { body } = await this.get(
|
|
2625
|
+
"/v1/governance/findings",
|
|
2626
|
+
params
|
|
2627
|
+
);
|
|
2628
|
+
return [...body.findings ?? []];
|
|
2629
|
+
}
|
|
2630
|
+
/**
|
|
2631
|
+
* List agent run records against one governed change.
|
|
2632
|
+
*
|
|
2633
|
+
* Calls `GET /v1/governance/evaluations?change_id=…[&agent_slug=…]`.
|
|
2634
|
+
* Returns every persisted evaluation, including `failed` / `timeout`
|
|
2635
|
+
* runs and `completed` runs with zero findings — the latter is the
|
|
2636
|
+
* positive signal "the agent ran and found nothing", which the UI
|
|
2637
|
+
* surfaces as `clear`.
|
|
2638
|
+
*/
|
|
2639
|
+
async listGovernanceEvaluations(query) {
|
|
2640
|
+
if (!query?.change_id) {
|
|
2641
|
+
throw new AtlaSentError("change_id is required", { code: "bad_request" });
|
|
2642
|
+
}
|
|
2643
|
+
const params = new URLSearchParams({ change_id: query.change_id });
|
|
2644
|
+
if (query.agent_slug) params.set("agent_slug", query.agent_slug);
|
|
2645
|
+
const { body } = await this.get(
|
|
2646
|
+
"/v1/governance/evaluations",
|
|
2647
|
+
params
|
|
2648
|
+
);
|
|
2649
|
+
return [...body.evaluations ?? []];
|
|
2650
|
+
}
|
|
2651
|
+
// ── Private adapters for sub-client factories ──────────────────────────────
|
|
2652
|
+
// Thin wrappers that expose the private request infrastructure to sub-client
|
|
2653
|
+
// factories (scim, evidenceBundles, auth) without widening the public API.
|
|
2654
|
+
async _post(path, body, query) {
|
|
2655
|
+
const { body: b } = await this.post(path, body, query);
|
|
2656
|
+
return { body: b };
|
|
2657
|
+
}
|
|
2658
|
+
async _get(path, query) {
|
|
2659
|
+
const { body: b } = await this.get(path, query);
|
|
2660
|
+
return { body: b };
|
|
2661
|
+
}
|
|
2662
|
+
async _put(path, body) {
|
|
2663
|
+
return this._requestRaw(path, "PUT", body, void 0);
|
|
2664
|
+
}
|
|
2665
|
+
async _patch(path, body) {
|
|
2666
|
+
return this._requestRaw(path, "PATCH", body, void 0);
|
|
2667
|
+
}
|
|
2668
|
+
async _delete(path) {
|
|
2669
|
+
await this._requestRaw(path, "DELETE", void 0, void 0);
|
|
2670
|
+
}
|
|
2671
|
+
async _getRaw(path) {
|
|
2672
|
+
const url = `${this.baseUrl}${path}`;
|
|
2673
|
+
const requestId = globalThis.crypto.randomUUID();
|
|
2674
|
+
const headers = {
|
|
2675
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
2676
|
+
"User-Agent": this.userAgent,
|
|
2677
|
+
"X-Request-ID": requestId,
|
|
2678
|
+
"X-AtlaSent-Protocol-Version": "1"
|
|
2679
|
+
};
|
|
2680
|
+
const response = await this.fetchImpl(url, {
|
|
2681
|
+
method: "GET",
|
|
2682
|
+
headers,
|
|
2683
|
+
signal: AbortSignal.timeout(this.timeoutMs)
|
|
2684
|
+
});
|
|
2685
|
+
if (!response.ok) {
|
|
2686
|
+
const text = await response.text().catch(() => "");
|
|
2687
|
+
throw new AtlaSentError(`GET ${path} returned ${response.status}`, {
|
|
2688
|
+
code: response.status >= 500 ? "server_error" : "bad_request",
|
|
2689
|
+
status: response.status,
|
|
2690
|
+
requestId
|
|
2691
|
+
});
|
|
2692
|
+
}
|
|
2693
|
+
return response.arrayBuffer();
|
|
2694
|
+
}
|
|
2695
|
+
async _requestRaw(path, method, body, query) {
|
|
2696
|
+
const qs = query && Array.from(query).length > 0 ? `?${query.toString()}` : "";
|
|
2697
|
+
const url = `${this.baseUrl}${path}${qs}`;
|
|
2698
|
+
const requestId = globalThis.crypto.randomUUID();
|
|
2699
|
+
const headers = {
|
|
2700
|
+
Accept: "application/json",
|
|
2701
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
2702
|
+
"User-Agent": this.userAgent,
|
|
2703
|
+
"X-Request-ID": requestId,
|
|
2704
|
+
"X-AtlaSent-Protocol-Version": "1"
|
|
2705
|
+
};
|
|
2706
|
+
if (method === "PUT" && body !== void 0) {
|
|
2707
|
+
headers["Content-Type"] = "application/json";
|
|
2708
|
+
}
|
|
2709
|
+
const init = { method, headers, signal: AbortSignal.timeout(this.timeoutMs) };
|
|
2710
|
+
if (method === "PUT" && body !== void 0) {
|
|
2711
|
+
init.body = JSON.stringify(body);
|
|
2712
|
+
}
|
|
2713
|
+
const response = await this.fetchImpl(url, init);
|
|
2714
|
+
if (!response.ok) {
|
|
2715
|
+
const text = await response.text().catch(() => "");
|
|
2716
|
+
throw new AtlaSentError(`${method} ${path} returned ${response.status}`, {
|
|
2717
|
+
code: response.status >= 500 ? "server_error" : "bad_request",
|
|
2718
|
+
status: response.status,
|
|
2719
|
+
requestId
|
|
2720
|
+
});
|
|
2721
|
+
}
|
|
2722
|
+
if (method === "DELETE") {
|
|
2723
|
+
return { body: {} };
|
|
2724
|
+
}
|
|
2725
|
+
return { body: await response.json() };
|
|
2726
|
+
}
|
|
1576
2727
|
};
|
|
1577
2728
|
function parseRateLimitHeaders(headers) {
|
|
1578
2729
|
const rawLimit = headers.get("x-ratelimit-limit");
|
|
@@ -1718,7 +2869,7 @@ function buildAuditEventsQuery(query) {
|
|
|
1718
2869
|
return params;
|
|
1719
2870
|
}
|
|
1720
2871
|
function sleep(ms) {
|
|
1721
|
-
return new Promise((
|
|
2872
|
+
return new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
1722
2873
|
}
|
|
1723
2874
|
function parseRetryAfter(raw) {
|
|
1724
2875
|
if (!raw) return void 0;
|
|
@@ -1739,14 +2890,14 @@ async function* parseSseStream(body, requestId, timeoutMs, onEventId) {
|
|
|
1739
2890
|
if (timeoutMs <= 0) {
|
|
1740
2891
|
return reader.read();
|
|
1741
2892
|
}
|
|
1742
|
-
return new Promise((
|
|
2893
|
+
return new Promise((resolve3, reject) => {
|
|
1743
2894
|
const timer = setTimeout(() => {
|
|
1744
2895
|
reject(new StreamTimeoutError(timeoutMs));
|
|
1745
2896
|
}, timeoutMs);
|
|
1746
2897
|
reader.read().then(
|
|
1747
2898
|
(result) => {
|
|
1748
2899
|
clearTimeout(timer);
|
|
1749
|
-
|
|
2900
|
+
resolve3(result);
|
|
1750
2901
|
},
|
|
1751
2902
|
(err) => {
|
|
1752
2903
|
clearTimeout(timer);
|
|
@@ -1874,6 +3025,7 @@ function getClient() {
|
|
|
1874
3025
|
sharedClient = new AtlaSentClient(options);
|
|
1875
3026
|
return sharedClient;
|
|
1876
3027
|
}
|
|
3028
|
+
var ACTION_TYPE_RE = /^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)+$/;
|
|
1877
3029
|
function wireDecisionToDenied(serverDecision) {
|
|
1878
3030
|
const lower = serverDecision.toLowerCase();
|
|
1879
3031
|
if (lower === "hold" || lower === "escalate") return lower;
|
|
@@ -1912,6 +3064,21 @@ async function computeExecutionHash(payload) {
|
|
|
1912
3064
|
}
|
|
1913
3065
|
}
|
|
1914
3066
|
async function protect(request) {
|
|
3067
|
+
if (!ACTION_TYPE_RE.test(request.action)) {
|
|
3068
|
+
throw new AtlaSentError(
|
|
3069
|
+
`action must be in dot-notation format (e.g. "production.deploy"). Got: ${JSON.stringify(request.action)}`,
|
|
3070
|
+
{ code: "bad_request" }
|
|
3071
|
+
);
|
|
3072
|
+
}
|
|
3073
|
+
const trustMgr = getGlobalTrustRootManager({ disableRefresh: false });
|
|
3074
|
+
if (trustMgr.checkExpiry() === "expired") {
|
|
3075
|
+
const snap = trustMgr.getSnapshot();
|
|
3076
|
+
throw new BundleVerificationError({
|
|
3077
|
+
reason: "trust_snapshot_expired",
|
|
3078
|
+
snapshotValidUntil: snap.valid_until,
|
|
3079
|
+
snapshotFetchedAt: snap.issued_at
|
|
3080
|
+
});
|
|
3081
|
+
}
|
|
1915
3082
|
const client = getClient();
|
|
1916
3083
|
const evaluation = await client.evaluate(request);
|
|
1917
3084
|
if (evaluation.decision !== "allow") {
|
|
@@ -1922,12 +3089,13 @@ async function protect(request) {
|
|
|
1922
3089
|
auditHash: evaluation.auditHash
|
|
1923
3090
|
});
|
|
1924
3091
|
}
|
|
1925
|
-
const environment = request.context?.environment
|
|
1926
|
-
|
|
1927
|
-
|
|
3092
|
+
const environment = request.context?.environment;
|
|
3093
|
+
if (!environment) {
|
|
3094
|
+
throw new AtlaSentError(
|
|
3095
|
+
'context.environment is required. Pass the environment where this action executes (e.g. "production", "staging").',
|
|
3096
|
+
{ code: "bad_request" }
|
|
1928
3097
|
);
|
|
1929
|
-
|
|
1930
|
-
})();
|
|
3098
|
+
}
|
|
1931
3099
|
const evaluatePayload = {
|
|
1932
3100
|
action_type: request.action,
|
|
1933
3101
|
actor_id: request.agent,
|
|
@@ -1958,21 +3126,22 @@ async function protect(request) {
|
|
|
1958
3126
|
permitHash: verification.permitHash,
|
|
1959
3127
|
auditHash: evaluation.auditHash,
|
|
1960
3128
|
reason: evaluation.reason,
|
|
1961
|
-
timestamp: verification.timestamp
|
|
3129
|
+
timestamp: verification.timestamp,
|
|
3130
|
+
permitExpiresAt: verification.expiresAt ?? null
|
|
1962
3131
|
};
|
|
1963
3132
|
}
|
|
1964
3133
|
|
|
1965
3134
|
// src/hono.ts
|
|
1966
3135
|
var DEFAULT_CONTEXT_KEY = "atlasent";
|
|
1967
|
-
async function
|
|
3136
|
+
async function resolve2(value, c) {
|
|
1968
3137
|
return typeof value === "function" ? await value(c) : value;
|
|
1969
3138
|
}
|
|
1970
3139
|
function atlaSentGuard(options) {
|
|
1971
3140
|
const contextKey = options.key ?? DEFAULT_CONTEXT_KEY;
|
|
1972
3141
|
return async (c, next) => {
|
|
1973
3142
|
const [agent, action, ctx] = await Promise.all([
|
|
1974
|
-
|
|
1975
|
-
|
|
3143
|
+
resolve2(options.agent, c),
|
|
3144
|
+
resolve2(options.action, c),
|
|
1976
3145
|
options.context ? options.context(c) : Promise.resolve(void 0)
|
|
1977
3146
|
]);
|
|
1978
3147
|
const request = { agent, action };
|