@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.js
CHANGED
|
@@ -47,7 +47,8 @@ var KNOWN_PERMIT_OUTCOMES = /* @__PURE__ */ new Set([
|
|
|
47
47
|
"permit_consumed",
|
|
48
48
|
"permit_expired",
|
|
49
49
|
"permit_revoked",
|
|
50
|
-
"permit_not_found"
|
|
50
|
+
"permit_not_found",
|
|
51
|
+
"permit_signing_key_revoked"
|
|
51
52
|
]);
|
|
52
53
|
function normalizePermitOutcome(raw) {
|
|
53
54
|
if (raw !== void 0 && KNOWN_PERMIT_OUTCOMES.has(raw)) {
|
|
@@ -108,8 +109,198 @@ var AtlaSentDeniedError = class extends AtlaSentError {
|
|
|
108
109
|
get isNotFound() {
|
|
109
110
|
return this.outcome === "permit_not_found";
|
|
110
111
|
}
|
|
112
|
+
/**
|
|
113
|
+
* `true` when the permit's signing key KID appears in the
|
|
114
|
+
* trust-root revocation list (ADR-005 D3 R2/R3 key rotation).
|
|
115
|
+
*/
|
|
116
|
+
get isSigningKeyRevoked() {
|
|
117
|
+
return this.outcome === "permit_signing_key_revoked";
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
var BundleVerificationError = class extends AtlaSentError {
|
|
121
|
+
name = "BundleVerificationError";
|
|
122
|
+
reason;
|
|
123
|
+
snapshotValidUntil;
|
|
124
|
+
snapshotFetchedAt;
|
|
125
|
+
snapshotSource;
|
|
126
|
+
kid;
|
|
127
|
+
constructor(init) {
|
|
128
|
+
super(`AtlaSent audit bundle verification failed: ${init.reason}`);
|
|
129
|
+
this.reason = init.reason;
|
|
130
|
+
this.snapshotValidUntil = init.snapshotValidUntil;
|
|
131
|
+
this.snapshotFetchedAt = init.snapshotFetchedAt;
|
|
132
|
+
this.snapshotSource = init.snapshotSource;
|
|
133
|
+
this.kid = init.kid;
|
|
134
|
+
}
|
|
111
135
|
};
|
|
112
136
|
|
|
137
|
+
// src/trustRoot.ts
|
|
138
|
+
import { readFileSync } from "fs";
|
|
139
|
+
import { fileURLToPath } from "url";
|
|
140
|
+
import { resolve, dirname } from "path";
|
|
141
|
+
var REFRESH_INTERVAL_MS_DEFAULT = 4 * 60 * 60 * 1e3;
|
|
142
|
+
var REFRESH_INTERVAL_MS_FLOOR = 5 * 60 * 1e3;
|
|
143
|
+
var KEYS_BASE_URL = "https://keys.atlasent.io/.well-known";
|
|
144
|
+
var _halfLifeWarningEmitted = false;
|
|
145
|
+
var _expiredWarningEmitted = false;
|
|
146
|
+
var TrustRootManager = class {
|
|
147
|
+
_snapshot;
|
|
148
|
+
_refreshTimer = null;
|
|
149
|
+
_opts;
|
|
150
|
+
constructor(initialSnapshot, opts = {}) {
|
|
151
|
+
this._snapshot = initialSnapshot;
|
|
152
|
+
const intervalMs = Math.max(
|
|
153
|
+
opts.refreshIntervalMs ?? REFRESH_INTERVAL_MS_DEFAULT,
|
|
154
|
+
REFRESH_INTERVAL_MS_FLOOR
|
|
155
|
+
);
|
|
156
|
+
this._opts = {
|
|
157
|
+
refreshBaseUrl: opts.refreshBaseUrl ?? KEYS_BASE_URL,
|
|
158
|
+
refreshIntervalMs: intervalMs,
|
|
159
|
+
disableRefresh: opts.disableRefresh ?? false,
|
|
160
|
+
fetch: opts.fetch ?? (typeof globalThis !== "undefined" && globalThis.fetch ? globalThis.fetch.bind(globalThis) : ((_url) => Promise.reject(new Error("fetch not available"))))
|
|
161
|
+
};
|
|
162
|
+
if (!this._opts.disableRefresh) {
|
|
163
|
+
this._scheduleRefresh();
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
getSnapshot() {
|
|
167
|
+
return this._snapshot;
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Check whether the snapshot is expired, emit one-time warnings at
|
|
171
|
+
* half-life and expiry. Returns "ok" | "half_life" | "expired".
|
|
172
|
+
*
|
|
173
|
+
* Emits console.warn once per process at half-life (ADR-005 D3).
|
|
174
|
+
* Emits console.warn once per process on expiry.
|
|
175
|
+
*/
|
|
176
|
+
checkExpiry() {
|
|
177
|
+
const snap = this._snapshot;
|
|
178
|
+
const now = Date.now();
|
|
179
|
+
const issuedAt = new Date(snap.issued_at).getTime();
|
|
180
|
+
const validUntil = new Date(snap.valid_until).getTime();
|
|
181
|
+
if (now > validUntil) {
|
|
182
|
+
if (!_expiredWarningEmitted) {
|
|
183
|
+
_expiredWarningEmitted = true;
|
|
184
|
+
const daysAgo = Math.floor((now - validUntil) / (24 * 60 * 60 * 1e3));
|
|
185
|
+
console.warn(
|
|
186
|
+
`[atlasent] Trust snapshot expired ${daysAgo} day(s) ago (valid_until: ${snap.valid_until}). Update to a newer SDK build or enable allowExpiredSnapshot.`
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
return "expired";
|
|
190
|
+
}
|
|
191
|
+
const window = validUntil - issuedAt;
|
|
192
|
+
const halfLife = issuedAt + window / 2;
|
|
193
|
+
if (now > halfLife) {
|
|
194
|
+
if (!_halfLifeWarningEmitted) {
|
|
195
|
+
_halfLifeWarningEmitted = true;
|
|
196
|
+
const daysLeft = Math.floor((validUntil - now) / (24 * 60 * 60 * 1e3));
|
|
197
|
+
console.warn(
|
|
198
|
+
`[atlasent] Trust snapshot at half-life: expires in ${daysLeft} day(s) (valid_until: ${snap.valid_until}). Plan an SDK update.`
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
return "half_life";
|
|
202
|
+
}
|
|
203
|
+
return "ok";
|
|
204
|
+
}
|
|
205
|
+
/** Look up a key entry by kid. Returns undefined if not found. */
|
|
206
|
+
lookupKey(kid) {
|
|
207
|
+
return this._snapshot.keys.find((k) => k.kid === kid);
|
|
208
|
+
}
|
|
209
|
+
/** Returns true if the kid appears in revoked_keys. */
|
|
210
|
+
isRevoked(kid) {
|
|
211
|
+
return this._snapshot.revoked_keys.some((r) => r.kid === kid);
|
|
212
|
+
}
|
|
213
|
+
/** Replace the snapshot (e.g. after a successful refresh). */
|
|
214
|
+
replaceSnapshot(next) {
|
|
215
|
+
this._snapshot = next;
|
|
216
|
+
}
|
|
217
|
+
stopRefresh() {
|
|
218
|
+
if (this._refreshTimer !== null) {
|
|
219
|
+
clearInterval(this._refreshTimer);
|
|
220
|
+
this._refreshTimer = null;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
_scheduleRefresh() {
|
|
224
|
+
this._refreshTimer = setInterval(() => {
|
|
225
|
+
void this._doRefresh();
|
|
226
|
+
}, this._opts.refreshIntervalMs);
|
|
227
|
+
if (this._refreshTimer && typeof this._refreshTimer === "object" && "unref" in this._refreshTimer) {
|
|
228
|
+
this._refreshTimer.unref();
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
async _doRefresh() {
|
|
232
|
+
try {
|
|
233
|
+
const base = this._opts.refreshBaseUrl.replace(/\/$/, "");
|
|
234
|
+
const [keysRes, revocRes] = await Promise.all([
|
|
235
|
+
this._opts.fetch(`${base}/atlasent-verifier-keys.json`),
|
|
236
|
+
this._opts.fetch(`${base}/atlasent-revocations.json`)
|
|
237
|
+
]);
|
|
238
|
+
const indexRes = await this._opts.fetch(`${base}/atlasent-trust-root.json`);
|
|
239
|
+
if (!keysRes.ok || !revocRes.ok || !indexRes.ok) return;
|
|
240
|
+
const [keys, revoc, index] = await Promise.all([
|
|
241
|
+
keysRes.json(),
|
|
242
|
+
revocRes.json(),
|
|
243
|
+
indexRes.json()
|
|
244
|
+
]);
|
|
245
|
+
if (!index.valid_until || !Array.isArray(keys.keys)) return;
|
|
246
|
+
this._snapshot = {
|
|
247
|
+
valid_until: index.valid_until,
|
|
248
|
+
issued_at: index.issued_at ?? this._snapshot.issued_at,
|
|
249
|
+
keys: keys.keys,
|
|
250
|
+
revoked_keys: revoc.revoked_keys ?? [],
|
|
251
|
+
revoked_identities: revoc.revoked_identities ?? []
|
|
252
|
+
};
|
|
253
|
+
} catch {
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
};
|
|
257
|
+
function _loadVendorSnapshot() {
|
|
258
|
+
try {
|
|
259
|
+
let packageRoot;
|
|
260
|
+
try {
|
|
261
|
+
const thisFile = fileURLToPath(import.meta.url);
|
|
262
|
+
packageRoot = resolve(dirname(thisFile), "..", "..");
|
|
263
|
+
} catch {
|
|
264
|
+
packageRoot = resolve(__dirname, "..", "..");
|
|
265
|
+
}
|
|
266
|
+
const vendorDir = resolve(packageRoot, "vendor", "trust-root");
|
|
267
|
+
const index = JSON.parse(
|
|
268
|
+
readFileSync(resolve(vendorDir, "atlasent-trust-root.json"), "utf8")
|
|
269
|
+
);
|
|
270
|
+
const verifierKeys = JSON.parse(
|
|
271
|
+
readFileSync(resolve(vendorDir, "atlasent-verifier-keys.json"), "utf8")
|
|
272
|
+
);
|
|
273
|
+
const revocations = JSON.parse(
|
|
274
|
+
readFileSync(resolve(vendorDir, "atlasent-revocations.json"), "utf8")
|
|
275
|
+
);
|
|
276
|
+
return {
|
|
277
|
+
valid_until: index.valid_until,
|
|
278
|
+
issued_at: index.issued_at,
|
|
279
|
+
keys: verifierKeys.keys ?? [],
|
|
280
|
+
revoked_keys: revocations.revoked_keys ?? [],
|
|
281
|
+
revoked_identities: revocations.revoked_identities ?? []
|
|
282
|
+
};
|
|
283
|
+
} catch {
|
|
284
|
+
return {
|
|
285
|
+
valid_until: "2099-01-01T00:00:00Z",
|
|
286
|
+
issued_at: "2026-05-26T00:00:00Z",
|
|
287
|
+
keys: [],
|
|
288
|
+
revoked_keys: [],
|
|
289
|
+
revoked_identities: []
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
var _globalManager = null;
|
|
294
|
+
function getGlobalTrustRootManager(opts) {
|
|
295
|
+
if (!_globalManager) {
|
|
296
|
+
_globalManager = new TrustRootManager(
|
|
297
|
+
_loadVendorSnapshot(),
|
|
298
|
+
opts ?? { disableRefresh: false }
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
return _globalManager;
|
|
302
|
+
}
|
|
303
|
+
|
|
113
304
|
// src/types.ts
|
|
114
305
|
var PRODUCTION_DEPLOY_ACTION = "production.deploy";
|
|
115
306
|
var DEPLOY_GATE_CODES = Object.freeze({
|
|
@@ -134,9 +325,14 @@ function normalizeEvaluateRequest(input) {
|
|
|
134
325
|
action_type: legacy.action,
|
|
135
326
|
actor_id: legacy.agent
|
|
136
327
|
};
|
|
137
|
-
if (legacy.context !== void 0)
|
|
138
|
-
|
|
139
|
-
|
|
328
|
+
if (legacy.context !== void 0) normalized.context = legacy.context;
|
|
329
|
+
const l = legacy;
|
|
330
|
+
if (l.explain !== void 0) normalized.explain = l.explain;
|
|
331
|
+
if (l.environment !== void 0) normalized.environment = l.environment;
|
|
332
|
+
if (l.resource !== void 0) normalized.resource = l.resource;
|
|
333
|
+
if (l.current_state !== void 0) normalized.current_state = l.current_state;
|
|
334
|
+
if (l.proposed_state !== void 0) normalized.proposed_state = l.proposed_state;
|
|
335
|
+
if (l.execution_binding !== void 0) normalized.execution_binding = l.execution_binding;
|
|
140
336
|
return normalized;
|
|
141
337
|
}
|
|
142
338
|
return input;
|
|
@@ -144,9 +340,9 @@ function normalizeEvaluateRequest(input) {
|
|
|
144
340
|
|
|
145
341
|
// src/retry.ts
|
|
146
342
|
var DEFAULT_RETRY_POLICY = {
|
|
147
|
-
maxAttempts:
|
|
148
|
-
baseDelayMs:
|
|
149
|
-
maxDelayMs:
|
|
343
|
+
maxAttempts: 3,
|
|
344
|
+
baseDelayMs: 250,
|
|
345
|
+
maxDelayMs: 1e4
|
|
150
346
|
};
|
|
151
347
|
var RETRYABLE_CODES = /* @__PURE__ */ new Set([
|
|
152
348
|
"network",
|
|
@@ -195,10 +391,371 @@ function clampUnit(n) {
|
|
|
195
391
|
return n;
|
|
196
392
|
}
|
|
197
393
|
|
|
394
|
+
// src/scim.ts
|
|
395
|
+
var SCIM_USER_SCHEMA = "urn:ietf:params:scim:schemas:core:2.0:User";
|
|
396
|
+
var SCIM_GROUP_SCHEMA = "urn:ietf:params:scim:schemas:core:2.0:Group";
|
|
397
|
+
function scimUsersPath(orgId) {
|
|
398
|
+
return `/scim/v2/${encodeURIComponent(orgId)}/Users`;
|
|
399
|
+
}
|
|
400
|
+
function scimGroupsPath(orgId) {
|
|
401
|
+
return `/scim/v2/${encodeURIComponent(orgId)}/Groups`;
|
|
402
|
+
}
|
|
403
|
+
function buildScimQuery(filter, startIndex, count) {
|
|
404
|
+
const params = new URLSearchParams();
|
|
405
|
+
if (filter !== void 0) params.set("filter", filter);
|
|
406
|
+
if (startIndex !== void 0) params.set("startIndex", String(startIndex));
|
|
407
|
+
if (count !== void 0) params.set("count", String(count));
|
|
408
|
+
return params.size > 0 ? params : void 0;
|
|
409
|
+
}
|
|
410
|
+
function makeScimClient(postFn, getFn, putFn, deleteFn) {
|
|
411
|
+
const users = {
|
|
412
|
+
async list(params) {
|
|
413
|
+
const qs = buildScimQuery(
|
|
414
|
+
params.filter,
|
|
415
|
+
params.startIndex,
|
|
416
|
+
params.count
|
|
417
|
+
);
|
|
418
|
+
const { body } = await getFn(
|
|
419
|
+
scimUsersPath(params.orgId),
|
|
420
|
+
qs
|
|
421
|
+
);
|
|
422
|
+
return body;
|
|
423
|
+
},
|
|
424
|
+
async create(orgId, user) {
|
|
425
|
+
const payload = user.schemas ? user : { ...user, schemas: [SCIM_USER_SCHEMA] };
|
|
426
|
+
const { body } = await postFn(scimUsersPath(orgId), payload);
|
|
427
|
+
return body;
|
|
428
|
+
},
|
|
429
|
+
async update(orgId, id, user) {
|
|
430
|
+
const payload = user.schemas ? user : { ...user, schemas: [SCIM_USER_SCHEMA] };
|
|
431
|
+
const { body } = await putFn(
|
|
432
|
+
`${scimUsersPath(orgId)}/${encodeURIComponent(id)}`,
|
|
433
|
+
payload
|
|
434
|
+
);
|
|
435
|
+
return body;
|
|
436
|
+
},
|
|
437
|
+
async delete(orgId, id) {
|
|
438
|
+
return deleteFn(
|
|
439
|
+
`${scimUsersPath(orgId)}/${encodeURIComponent(id)}`
|
|
440
|
+
);
|
|
441
|
+
}
|
|
442
|
+
};
|
|
443
|
+
const groups = {
|
|
444
|
+
async list(params) {
|
|
445
|
+
const qs = buildScimQuery(
|
|
446
|
+
params.filter,
|
|
447
|
+
params.startIndex,
|
|
448
|
+
params.count
|
|
449
|
+
);
|
|
450
|
+
const { body } = await getFn(
|
|
451
|
+
scimGroupsPath(params.orgId),
|
|
452
|
+
qs
|
|
453
|
+
);
|
|
454
|
+
return body;
|
|
455
|
+
},
|
|
456
|
+
async create(orgId, group) {
|
|
457
|
+
const payload = group["schemas"] ? group : { ...group, schemas: [SCIM_GROUP_SCHEMA] };
|
|
458
|
+
const { body } = await postFn(
|
|
459
|
+
scimGroupsPath(orgId),
|
|
460
|
+
payload
|
|
461
|
+
);
|
|
462
|
+
return body;
|
|
463
|
+
},
|
|
464
|
+
async delete(orgId, id) {
|
|
465
|
+
return deleteFn(
|
|
466
|
+
`${scimGroupsPath(orgId)}/${encodeURIComponent(id)}`
|
|
467
|
+
);
|
|
468
|
+
}
|
|
469
|
+
};
|
|
470
|
+
return { users, groups };
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// src/evidence-bundle.ts
|
|
474
|
+
function wireToBundle(w) {
|
|
475
|
+
return {
|
|
476
|
+
bundleId: w.bundle_id,
|
|
477
|
+
orgId: w.org_id,
|
|
478
|
+
incidentId: w.incident_id,
|
|
479
|
+
status: w.status,
|
|
480
|
+
includedPermits: w.included_permits ?? [],
|
|
481
|
+
includeOverrides: w.include_overrides ?? false,
|
|
482
|
+
format: w.format,
|
|
483
|
+
createdAt: w.created_at,
|
|
484
|
+
expiresAt: w.expires_at,
|
|
485
|
+
...w.download_url !== void 0 ? { downloadUrl: w.download_url } : {},
|
|
486
|
+
...w.metadata !== void 0 ? { metadata: w.metadata } : {}
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
function makeEvidenceBundleClient(postFn, getFn, getRawFn) {
|
|
490
|
+
return {
|
|
491
|
+
async list(params = {}) {
|
|
492
|
+
const qs = new URLSearchParams();
|
|
493
|
+
if (params.executionId !== void 0) qs.set("execution_id", params.executionId);
|
|
494
|
+
if (params.limit !== void 0) qs.set("limit", String(params.limit));
|
|
495
|
+
if (params.cursor !== void 0) qs.set("cursor", params.cursor);
|
|
496
|
+
const { body } = await getFn("/v1/evidence-bundles", qs.size > 0 ? qs : void 0);
|
|
497
|
+
return {
|
|
498
|
+
bundles: (body.bundles ?? []).map(wireToBundle),
|
|
499
|
+
nextCursor: body.next_cursor ?? null
|
|
500
|
+
};
|
|
501
|
+
},
|
|
502
|
+
async create(params) {
|
|
503
|
+
const payload = {
|
|
504
|
+
incident_id: params.incidentId
|
|
505
|
+
};
|
|
506
|
+
if (params.includedPermits !== void 0) {
|
|
507
|
+
payload["included_permits"] = params.includedPermits;
|
|
508
|
+
}
|
|
509
|
+
if (params.includeOverrides !== void 0) {
|
|
510
|
+
payload["include_overrides"] = params.includeOverrides;
|
|
511
|
+
}
|
|
512
|
+
const { body } = await postFn(
|
|
513
|
+
"/v1/evidence-bundles",
|
|
514
|
+
payload
|
|
515
|
+
);
|
|
516
|
+
return wireToBundle(body);
|
|
517
|
+
},
|
|
518
|
+
async get(bundleId) {
|
|
519
|
+
const { body } = await getFn(
|
|
520
|
+
`/v1/evidence-bundles/${encodeURIComponent(bundleId)}`
|
|
521
|
+
);
|
|
522
|
+
return wireToBundle(body);
|
|
523
|
+
},
|
|
524
|
+
async download(bundleId, format = "json") {
|
|
525
|
+
const qs = new URLSearchParams({ format });
|
|
526
|
+
const raw = await getRawFn(
|
|
527
|
+
`/v1/evidence-bundles/${encodeURIComponent(bundleId)}/download?${qs}`
|
|
528
|
+
);
|
|
529
|
+
return Buffer.from(raw);
|
|
530
|
+
}
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// src/auth.ts
|
|
535
|
+
function wireToTokenResponse(w) {
|
|
536
|
+
return {
|
|
537
|
+
accessToken: w.access_token,
|
|
538
|
+
refreshToken: w.refresh_token,
|
|
539
|
+
tokenType: w.token_type,
|
|
540
|
+
expiresIn: w.expires_in,
|
|
541
|
+
...w.scope !== void 0 ? { scope: w.scope } : {},
|
|
542
|
+
...w.idp_id !== void 0 ? { idpId: w.idp_id } : {}
|
|
543
|
+
};
|
|
544
|
+
}
|
|
545
|
+
function wireToIdpConnection(w) {
|
|
546
|
+
return {
|
|
547
|
+
id: w.id,
|
|
548
|
+
name: w.name,
|
|
549
|
+
provider: w.provider,
|
|
550
|
+
enabled: w.enabled,
|
|
551
|
+
isDefault: w.default,
|
|
552
|
+
...w.domains !== void 0 ? { domains: w.domains } : {},
|
|
553
|
+
createdAt: w.created_at
|
|
554
|
+
};
|
|
555
|
+
}
|
|
556
|
+
function makeAuthClient(postFn, getFn) {
|
|
557
|
+
return {
|
|
558
|
+
async refresh(refreshToken) {
|
|
559
|
+
const { body } = await postFn(
|
|
560
|
+
"/v1/auth/token/refresh",
|
|
561
|
+
{ refresh_token: refreshToken, grant_type: "refresh_token" }
|
|
562
|
+
);
|
|
563
|
+
return wireToTokenResponse(body);
|
|
564
|
+
},
|
|
565
|
+
async refreshWithIdp(idpId, refreshToken) {
|
|
566
|
+
const path = `/v1/auth/idp/${encodeURIComponent(idpId)}/token/refresh`;
|
|
567
|
+
const { body } = await postFn(path, {
|
|
568
|
+
refresh_token: refreshToken,
|
|
569
|
+
grant_type: "refresh_token",
|
|
570
|
+
idp_id: idpId
|
|
571
|
+
});
|
|
572
|
+
return wireToTokenResponse(body);
|
|
573
|
+
},
|
|
574
|
+
async listIdpConnections() {
|
|
575
|
+
const { body } = await getFn(
|
|
576
|
+
"/v1/auth/idp-connections"
|
|
577
|
+
);
|
|
578
|
+
return (body.connections ?? []).map(wireToIdpConnection);
|
|
579
|
+
}
|
|
580
|
+
};
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
// src/sso.ts
|
|
584
|
+
function wireToSsoConnection(w) {
|
|
585
|
+
return {
|
|
586
|
+
id: w.id,
|
|
587
|
+
organizationId: w.organization_id,
|
|
588
|
+
name: w.name,
|
|
589
|
+
protocol: w.protocol,
|
|
590
|
+
idpEntityId: w.idp_entity_id,
|
|
591
|
+
metadataUrl: w.metadata_url,
|
|
592
|
+
metadataXml: w.metadata_xml,
|
|
593
|
+
emailDomain: w.email_domain,
|
|
594
|
+
enforceForDomain: w.enforce_for_domain,
|
|
595
|
+
isActive: w.is_active,
|
|
596
|
+
supabaseProviderId: w.supabase_provider_id,
|
|
597
|
+
createdBy: w.created_by,
|
|
598
|
+
createdAt: w.created_at,
|
|
599
|
+
updatedAt: w.updated_at
|
|
600
|
+
};
|
|
601
|
+
}
|
|
602
|
+
function wireToSsoJitRule(w) {
|
|
603
|
+
return {
|
|
604
|
+
id: w.id,
|
|
605
|
+
connectionId: w.connection_id,
|
|
606
|
+
organizationId: w.organization_id,
|
|
607
|
+
claimAttribute: w.claim_attribute,
|
|
608
|
+
claimValue: w.claim_value,
|
|
609
|
+
grantedRole: w.granted_role,
|
|
610
|
+
precedence: w.precedence,
|
|
611
|
+
isActive: w.is_active,
|
|
612
|
+
createdAt: w.created_at,
|
|
613
|
+
updatedAt: w.updated_at
|
|
614
|
+
};
|
|
615
|
+
}
|
|
616
|
+
function wireToSsoReadiness(w) {
|
|
617
|
+
return {
|
|
618
|
+
connectionConfigured: w.connection_configured,
|
|
619
|
+
connectionTested: w.connection_tested,
|
|
620
|
+
breakGlassSet: w.break_glass_set,
|
|
621
|
+
serviceApiKeysReviewed: w.service_api_keys_reviewed
|
|
622
|
+
};
|
|
623
|
+
}
|
|
624
|
+
function ssoConnectionInputToWire(input) {
|
|
625
|
+
const w = {};
|
|
626
|
+
if (input.name !== void 0) w["name"] = input.name;
|
|
627
|
+
if (input.protocol !== void 0) w["protocol"] = input.protocol;
|
|
628
|
+
if (input.idpEntityId !== void 0) w["idp_entity_id"] = input.idpEntityId;
|
|
629
|
+
if (input.metadataUrl !== void 0) w["metadata_url"] = input.metadataUrl;
|
|
630
|
+
if (input.metadataXml !== void 0) w["metadata_xml"] = input.metadataXml;
|
|
631
|
+
if (input.emailDomain !== void 0) w["email_domain"] = input.emailDomain;
|
|
632
|
+
if (input.enforceForDomain !== void 0) w["enforce_for_domain"] = input.enforceForDomain;
|
|
633
|
+
return w;
|
|
634
|
+
}
|
|
635
|
+
function makeSsoClient(getFn, postFn, patchFn, deleteFn) {
|
|
636
|
+
return {
|
|
637
|
+
async listConnections() {
|
|
638
|
+
const { body } = await getFn("/v1/sso/connections");
|
|
639
|
+
return { connections: (body.connections ?? []).map(wireToSsoConnection) };
|
|
640
|
+
},
|
|
641
|
+
async getConnection(id) {
|
|
642
|
+
const { body } = await getFn(`/v1/sso/connections/${encodeURIComponent(id)}`);
|
|
643
|
+
return wireToSsoConnection(body);
|
|
644
|
+
},
|
|
645
|
+
async createConnection(input) {
|
|
646
|
+
const { body } = await postFn("/v1/sso/connections", ssoConnectionInputToWire(input));
|
|
647
|
+
return wireToSsoConnection(body);
|
|
648
|
+
},
|
|
649
|
+
async updateConnection(id, input) {
|
|
650
|
+
const { body } = await patchFn(
|
|
651
|
+
`/v1/sso/connections/${encodeURIComponent(id)}`,
|
|
652
|
+
ssoConnectionInputToWire(input)
|
|
653
|
+
);
|
|
654
|
+
return wireToSsoConnection(body);
|
|
655
|
+
},
|
|
656
|
+
async deleteConnection(id) {
|
|
657
|
+
await deleteFn(`/v1/sso/connections/${encodeURIComponent(id)}`);
|
|
658
|
+
},
|
|
659
|
+
async activateConnection(id) {
|
|
660
|
+
const { body } = await postFn(
|
|
661
|
+
`/v1/sso/connections/${encodeURIComponent(id)}/activate`,
|
|
662
|
+
{}
|
|
663
|
+
);
|
|
664
|
+
return { ok: body.ok, supabaseProviderId: body.supabase_provider_id };
|
|
665
|
+
},
|
|
666
|
+
async enforce(action) {
|
|
667
|
+
const { body } = await postFn("/v1/sso/enforce", { action });
|
|
668
|
+
return {
|
|
669
|
+
ok: body.ok,
|
|
670
|
+
action: body.action,
|
|
671
|
+
enforceSso: body.enforce_sso,
|
|
672
|
+
enforceSsoAt: body.enforce_sso_at
|
|
673
|
+
};
|
|
674
|
+
},
|
|
675
|
+
async getStatus() {
|
|
676
|
+
const { body } = await getFn("/v1/sso/status");
|
|
677
|
+
return wireToSsoReadiness(body.readiness);
|
|
678
|
+
},
|
|
679
|
+
async listJitRules(connectionId) {
|
|
680
|
+
const qs = connectionId ? new URLSearchParams({ connection_id: connectionId }) : void 0;
|
|
681
|
+
const { body } = await getFn("/v1/sso/jit-rules", qs);
|
|
682
|
+
return { rules: (body.rules ?? []).map(wireToSsoJitRule) };
|
|
683
|
+
},
|
|
684
|
+
async createJitRule(input) {
|
|
685
|
+
const payload = {
|
|
686
|
+
connection_id: input.connectionId,
|
|
687
|
+
claim_attribute: input.claimAttribute,
|
|
688
|
+
claim_value: input.claimValue,
|
|
689
|
+
granted_role: input.grantedRole
|
|
690
|
+
};
|
|
691
|
+
if (input.precedence !== void 0) payload["precedence"] = input.precedence;
|
|
692
|
+
const { body } = await postFn("/v1/sso/jit-rules", payload);
|
|
693
|
+
return wireToSsoJitRule(body);
|
|
694
|
+
},
|
|
695
|
+
async patchJitRule(id, patch) {
|
|
696
|
+
const payload = {};
|
|
697
|
+
if (patch.claimAttribute !== void 0) payload["claim_attribute"] = patch.claimAttribute;
|
|
698
|
+
if (patch.claimValue !== void 0) payload["claim_value"] = patch.claimValue;
|
|
699
|
+
if (patch.grantedRole !== void 0) payload["granted_role"] = patch.grantedRole;
|
|
700
|
+
if (patch.precedence !== void 0) payload["precedence"] = patch.precedence;
|
|
701
|
+
if (patch.isActive !== void 0) payload["is_active"] = patch.isActive;
|
|
702
|
+
const { body } = await patchFn(
|
|
703
|
+
`/v1/sso/jit-rules/${encodeURIComponent(id)}`,
|
|
704
|
+
payload
|
|
705
|
+
);
|
|
706
|
+
return wireToSsoJitRule(body);
|
|
707
|
+
},
|
|
708
|
+
async deleteJitRule(id) {
|
|
709
|
+
await deleteFn(`/v1/sso/jit-rules/${encodeURIComponent(id)}`);
|
|
710
|
+
}
|
|
711
|
+
};
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
// src/access-governance-log.ts
|
|
715
|
+
function wireToEvent(w) {
|
|
716
|
+
return {
|
|
717
|
+
id: w.id,
|
|
718
|
+
eventType: w.event_type,
|
|
719
|
+
orgId: w.org_id,
|
|
720
|
+
actorId: w.actor_id,
|
|
721
|
+
actorEmail: w.actor_email,
|
|
722
|
+
ipAddress: w.ip_address,
|
|
723
|
+
metadata: w.metadata ?? {},
|
|
724
|
+
createdAt: w.created_at
|
|
725
|
+
};
|
|
726
|
+
}
|
|
727
|
+
function makeAccessGovernanceLogClient(getFn) {
|
|
728
|
+
return {
|
|
729
|
+
async list(query = {}) {
|
|
730
|
+
const qs = new URLSearchParams();
|
|
731
|
+
if (query.limit !== void 0) qs.set("limit", String(query.limit));
|
|
732
|
+
if (query.cursor) qs.set("cursor", query.cursor);
|
|
733
|
+
if (query.eventType) qs.set("event_type", query.eventType);
|
|
734
|
+
if (query.actorId) qs.set("actor_id", query.actorId);
|
|
735
|
+
if (query.from) qs.set("from", query.from);
|
|
736
|
+
if (query.to) qs.set("to", query.to);
|
|
737
|
+
const { body } = await getFn(
|
|
738
|
+
"/v1/access-governance-log",
|
|
739
|
+
qs.size > 0 ? qs : void 0
|
|
740
|
+
);
|
|
741
|
+
return {
|
|
742
|
+
events: (body.events ?? []).map(wireToEvent),
|
|
743
|
+
nextCursor: body.next_cursor,
|
|
744
|
+
totalCount: body.total_count ?? 0
|
|
745
|
+
};
|
|
746
|
+
}
|
|
747
|
+
};
|
|
748
|
+
}
|
|
749
|
+
|
|
198
750
|
// src/client.ts
|
|
199
751
|
var DEFAULT_BASE_URL = "https://api.atlasent.io";
|
|
200
752
|
var DEFAULT_TIMEOUT_MS = 1e4;
|
|
201
|
-
var SDK_VERSION = "2.
|
|
753
|
+
var SDK_VERSION = "2.10.0";
|
|
754
|
+
var warnedBrowser = false;
|
|
755
|
+
var V1_EVALUATE_BATCH_PATH = "/v1/evaluate/batch";
|
|
756
|
+
var V1_EVALUATE_BATCH_LEGACY_PATH = "/v1-evaluate-batch";
|
|
757
|
+
var V1_EVALUATE_STREAM_PATH = "/v1/evaluate/stream";
|
|
758
|
+
var V1_EVALUATE_STREAM_LEGACY_PATH = "/v1-evaluate-stream";
|
|
202
759
|
function _buildUserAgent() {
|
|
203
760
|
const isNode2 = typeof process !== "undefined" && typeof process?.versions?.node === "string";
|
|
204
761
|
return isNode2 ? `@atlasent/sdk/${SDK_VERSION} node/${process.version}` : `@atlasent/sdk/${SDK_VERSION} browser`;
|
|
@@ -262,6 +819,18 @@ var AtlaSentClient = class {
|
|
|
262
819
|
fetchImpl;
|
|
263
820
|
userAgent;
|
|
264
821
|
retryPolicy;
|
|
822
|
+
/** SCIM 2.0 provisioning sub-client. Access as `client.scim`. */
|
|
823
|
+
scim;
|
|
824
|
+
/** Evidence bundle sub-client. Access as `client.evidenceBundles`. */
|
|
825
|
+
evidenceBundles;
|
|
826
|
+
/** Auth / token management sub-client. Access as `client.auth`. */
|
|
827
|
+
auth;
|
|
828
|
+
/** SSO administration sub-client. Access as `client.sso`. */
|
|
829
|
+
sso;
|
|
830
|
+
/** Access governance log sub-client. Access as `client.accessGovernanceLog`. */
|
|
831
|
+
accessGovernanceLog;
|
|
832
|
+
/** Trust-root snapshot manager for this client instance. */
|
|
833
|
+
trustRoot;
|
|
265
834
|
constructor(options) {
|
|
266
835
|
if (!options.apiKey || typeof options.apiKey !== "string") {
|
|
267
836
|
throw new AtlaSentError("apiKey is required", {
|
|
@@ -274,6 +843,12 @@ var AtlaSentClient = class {
|
|
|
274
843
|
{ code: "network" }
|
|
275
844
|
);
|
|
276
845
|
}
|
|
846
|
+
if (!warnedBrowser && typeof globalThis["window"] !== "undefined" && typeof process === "undefined") {
|
|
847
|
+
warnedBrowser = true;
|
|
848
|
+
console.warn(
|
|
849
|
+
"[@atlasent/sdk] Running in a browser environment. API keys should not be exposed in client-side bundles. Use a server-side proxy instead."
|
|
850
|
+
);
|
|
851
|
+
}
|
|
277
852
|
this.apiKey = _validateApiKey(options.apiKey);
|
|
278
853
|
this.baseUrl = _enforceTls(options.baseUrl ?? DEFAULT_BASE_URL).replace(
|
|
279
854
|
/\/+$/,
|
|
@@ -283,6 +858,44 @@ var AtlaSentClient = class {
|
|
|
283
858
|
this.fetchImpl = options.fetch ?? globalThis.fetch.bind(globalThis);
|
|
284
859
|
this.userAgent = _buildUserAgent();
|
|
285
860
|
this.retryPolicy = mergePolicy(options.retryPolicy ?? {});
|
|
861
|
+
this.scim = makeScimClient(
|
|
862
|
+
(path, body, query) => this._post(path, body, query),
|
|
863
|
+
(path, query) => this._get(path, query),
|
|
864
|
+
(path, body) => this._put(path, body),
|
|
865
|
+
(path) => this._delete(path)
|
|
866
|
+
);
|
|
867
|
+
this.evidenceBundles = makeEvidenceBundleClient(
|
|
868
|
+
(path, body) => this._post(path, body),
|
|
869
|
+
(path, query) => this._get(path, query),
|
|
870
|
+
(path) => this._getRaw(path)
|
|
871
|
+
);
|
|
872
|
+
this.auth = makeAuthClient(
|
|
873
|
+
(path, body) => this._post(path, body),
|
|
874
|
+
(path) => this._get(path)
|
|
875
|
+
);
|
|
876
|
+
this.sso = makeSsoClient(
|
|
877
|
+
(path, query) => this._get(path, query),
|
|
878
|
+
(path, body) => this._post(path, body),
|
|
879
|
+
(path, body) => this._patch(path, body),
|
|
880
|
+
(path) => this._delete(path)
|
|
881
|
+
);
|
|
882
|
+
this.accessGovernanceLog = makeAccessGovernanceLogClient(
|
|
883
|
+
(path, query) => this._get(path, query)
|
|
884
|
+
);
|
|
885
|
+
if (options.trustRootUrl !== void 0 || options.trustSnapshotRefreshMs !== void 0) {
|
|
886
|
+
const globalSnap = getGlobalTrustRootManager({ disableRefresh: true }).getSnapshot();
|
|
887
|
+
this.trustRoot = new TrustRootManager(globalSnap, {
|
|
888
|
+
...options.trustRootUrl !== void 0 && { refreshBaseUrl: options.trustRootUrl },
|
|
889
|
+
...options.trustSnapshotRefreshMs !== void 0 && { refreshIntervalMs: options.trustSnapshotRefreshMs }
|
|
890
|
+
});
|
|
891
|
+
} else {
|
|
892
|
+
this.trustRoot = getGlobalTrustRootManager();
|
|
893
|
+
}
|
|
894
|
+
this.trustRoot.checkExpiry();
|
|
895
|
+
}
|
|
896
|
+
/** Return the current trust-root snapshot (pinned or last successful refresh). */
|
|
897
|
+
getTrustSnapshot() {
|
|
898
|
+
return this.trustRoot.getSnapshot();
|
|
286
899
|
}
|
|
287
900
|
/**
|
|
288
901
|
* Ask the policy engine whether an agent action is permitted.
|
|
@@ -308,6 +921,12 @@ var AtlaSentClient = class {
|
|
|
308
921
|
actor_id: normalized.actor_id,
|
|
309
922
|
context: normalized.context ?? {}
|
|
310
923
|
};
|
|
924
|
+
if (normalized.explain !== void 0) body.explain = normalized.explain;
|
|
925
|
+
if (normalized.environment !== void 0) body.environment = normalized.environment;
|
|
926
|
+
if (normalized.resource !== void 0) body.resource = normalized.resource;
|
|
927
|
+
if (normalized.current_state !== void 0) body.current_state = normalized.current_state;
|
|
928
|
+
if (normalized.proposed_state !== void 0) body.proposed_state = normalized.proposed_state;
|
|
929
|
+
if (normalized.execution_binding !== void 0) body.execution_binding = normalized.execution_binding;
|
|
311
930
|
const { body: wire, rateLimit } = await this.post(
|
|
312
931
|
"/v1-evaluate",
|
|
313
932
|
body
|
|
@@ -344,9 +963,224 @@ var AtlaSentClient = class {
|
|
|
344
963
|
reason,
|
|
345
964
|
auditHash: wire.audit_hash ?? "",
|
|
346
965
|
timestamp: wire.timestamp ?? "",
|
|
966
|
+
rateLimit,
|
|
967
|
+
...wire.risk_envelope && {
|
|
968
|
+
riskEnvelope: {
|
|
969
|
+
weightedScore: wire.risk_envelope.weighted_score,
|
|
970
|
+
engineDecision: wire.risk_envelope.engine_decision,
|
|
971
|
+
envelopeDecision: wire.risk_envelope.envelope_decision,
|
|
972
|
+
promoted: wire.risk_envelope.promoted,
|
|
973
|
+
hardBlocks: wire.risk_envelope.hard_blocks ?? [],
|
|
974
|
+
...wire.risk_envelope.factors && { factors: wire.risk_envelope.factors }
|
|
975
|
+
}
|
|
976
|
+
},
|
|
977
|
+
...wire.risk_class !== void 0 && { riskClass: wire.risk_class },
|
|
978
|
+
...wire.authority_basis && {
|
|
979
|
+
authorityBasis: {
|
|
980
|
+
kind: wire.authority_basis.kind,
|
|
981
|
+
...wire.authority_basis.reference !== void 0 && { reference: wire.authority_basis.reference },
|
|
982
|
+
...wire.authority_basis.granted_by !== void 0 && { grantedBy: wire.authority_basis.granted_by },
|
|
983
|
+
...wire.authority_basis.rationale !== void 0 && { rationale: wire.authority_basis.rationale },
|
|
984
|
+
...wire.authority_basis.expires_at !== void 0 && { expiresAt: wire.authority_basis.expires_at }
|
|
985
|
+
}
|
|
986
|
+
},
|
|
987
|
+
...wire.escalation_id !== void 0 && { escalationId: wire.escalation_id }
|
|
988
|
+
};
|
|
989
|
+
}
|
|
990
|
+
/**
|
|
991
|
+
* Batch evaluate — send up to 100 decisions in a single round-trip.
|
|
992
|
+
*
|
|
993
|
+
* Wraps `POST /v1/evaluate/batch` (with fallback to
|
|
994
|
+
* `POST /v1-evaluate-batch` on older runtimes). The server evaluates each item
|
|
995
|
+
* against the active policy bundle and returns results in the same
|
|
996
|
+
* order as the input. One rate-limit token is consumed for the
|
|
997
|
+
* whole batch, and one audit-chain entry lists every included
|
|
998
|
+
* decision id.
|
|
999
|
+
*
|
|
1000
|
+
* A per-item policy `deny` is **not** thrown — it appears as
|
|
1001
|
+
* `item.decision === "deny"` in the returned items. A whole-batch
|
|
1002
|
+
* network error, 4xx, or 5xx throws {@link AtlaSentError}.
|
|
1003
|
+
*
|
|
1004
|
+
* Requires the `v2_batch` tenant feature flag to be enabled on the
|
|
1005
|
+
* org (returns 404 when off). Requires scope `evaluate:write`.
|
|
1006
|
+
*
|
|
1007
|
+
* @param requests - 1–100 evaluate items.
|
|
1008
|
+
* @param batchId - Optional caller-supplied UUID for idempotency.
|
|
1009
|
+
* A retried call with the same `batchId` and identical items
|
|
1010
|
+
* returns the cached response within 24 h (`replayed: true`).
|
|
1011
|
+
*/
|
|
1012
|
+
async evaluateBatch(requests, batchId) {
|
|
1013
|
+
if (!Array.isArray(requests) || requests.length === 0) {
|
|
1014
|
+
throw new AtlaSentError(
|
|
1015
|
+
"evaluateBatch: requests must be a non-empty array",
|
|
1016
|
+
{ code: "bad_request" }
|
|
1017
|
+
);
|
|
1018
|
+
}
|
|
1019
|
+
if (requests.length > 100) {
|
|
1020
|
+
throw new AtlaSentError(
|
|
1021
|
+
`evaluateBatch: requests.length ${requests.length} exceeds the 100-item cap`,
|
|
1022
|
+
{ code: "bad_request" }
|
|
1023
|
+
);
|
|
1024
|
+
}
|
|
1025
|
+
const wireItems = requests.map((r) => ({
|
|
1026
|
+
action_type: r.action,
|
|
1027
|
+
actor_id: r.agent,
|
|
1028
|
+
context: r.context ?? {}
|
|
1029
|
+
}));
|
|
1030
|
+
const wireBody = { items: wireItems };
|
|
1031
|
+
if (batchId) wireBody.batch_id = batchId;
|
|
1032
|
+
const { body: wire, rateLimit } = await this.postWithPathFallback(
|
|
1033
|
+
V1_EVALUATE_BATCH_PATH,
|
|
1034
|
+
V1_EVALUATE_BATCH_LEGACY_PATH,
|
|
1035
|
+
wireBody
|
|
1036
|
+
);
|
|
1037
|
+
const items = (wire.items ?? []).map(
|
|
1038
|
+
(item) => {
|
|
1039
|
+
const rawDecision = typeof item.decision === "string" ? item.decision.toLowerCase() : void 0;
|
|
1040
|
+
const decision = rawDecision === "allow" || rawDecision === "deny" || rawDecision === "hold" || rawDecision === "escalate" ? rawDecision : void 0;
|
|
1041
|
+
return {
|
|
1042
|
+
index: item.index,
|
|
1043
|
+
...decision !== void 0 ? { decision } : {},
|
|
1044
|
+
...item.decision_id ? { decisionId: item.decision_id } : {},
|
|
1045
|
+
...item.permit_token != null ? { permitToken: item.permit_token } : {},
|
|
1046
|
+
...item.reason != null ? { reason: item.reason } : {},
|
|
1047
|
+
...item.audit_entry_hash ? { auditHash: item.audit_entry_hash } : {},
|
|
1048
|
+
...item.timestamp ? { timestamp: item.timestamp } : {},
|
|
1049
|
+
...item.error ? { error: item.error } : {},
|
|
1050
|
+
...item.message ? { message: item.message } : {}
|
|
1051
|
+
};
|
|
1052
|
+
}
|
|
1053
|
+
);
|
|
1054
|
+
return {
|
|
1055
|
+
batchId: wire.batch_id,
|
|
1056
|
+
items,
|
|
1057
|
+
partial: wire.partial ?? false,
|
|
1058
|
+
...wire.replayed ? { replayed: wire.replayed } : {},
|
|
347
1059
|
rateLimit
|
|
348
1060
|
};
|
|
349
1061
|
}
|
|
1062
|
+
/**
|
|
1063
|
+
* Subscribe to a live stream of decisions for this org.
|
|
1064
|
+
*
|
|
1065
|
+
* Wraps `GET /v1-decisions-stream`. The server emits one SSE frame
|
|
1066
|
+
* per audit event and sends a heartbeat every 15 s. The session
|
|
1067
|
+
* auto-closes after `maxSeconds` (default 30 min); reconnect with
|
|
1068
|
+
* the last received `event.id` to resume without replaying history.
|
|
1069
|
+
*
|
|
1070
|
+
* ```ts
|
|
1071
|
+
* const controller = new AbortController();
|
|
1072
|
+
* for await (const event of client.subscribeDecisions({ signal: controller.signal })) {
|
|
1073
|
+
* if (event.type === "heartbeat") continue;
|
|
1074
|
+
* console.log(event.type, event.decision, event.actorId);
|
|
1075
|
+
* if (event.type === "session_end") break; // reconnect
|
|
1076
|
+
* }
|
|
1077
|
+
* ```
|
|
1078
|
+
*
|
|
1079
|
+
* Requires scope `audit:read`. Requires the `v2_decisions_stream`
|
|
1080
|
+
* tenant feature flag (returns 404 when off).
|
|
1081
|
+
*/
|
|
1082
|
+
async *subscribeDecisions(opts = {}) {
|
|
1083
|
+
const url = new URL(`${this.baseUrl}/v1-decisions-stream`);
|
|
1084
|
+
if (opts.types?.length) url.searchParams.set("types", opts.types.join(","));
|
|
1085
|
+
if (opts.actorId) url.searchParams.set("actor_id", opts.actorId);
|
|
1086
|
+
if (opts.maxSeconds !== void 0) url.searchParams.set("max_seconds", String(opts.maxSeconds));
|
|
1087
|
+
const headers = {
|
|
1088
|
+
Accept: "text/event-stream",
|
|
1089
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
1090
|
+
"User-Agent": this.userAgent,
|
|
1091
|
+
// ADR-025: declare the wire-protocol version we were built
|
|
1092
|
+
// against. Runtime serves this version's response shape; older
|
|
1093
|
+
// versions outside the compatibility window get 426.
|
|
1094
|
+
"X-AtlaSent-Protocol-Version": "1"
|
|
1095
|
+
};
|
|
1096
|
+
if (opts.lastEventId) headers["Last-Event-ID"] = opts.lastEventId;
|
|
1097
|
+
let response;
|
|
1098
|
+
try {
|
|
1099
|
+
response = await this.fetchImpl(url.toString(), {
|
|
1100
|
+
method: "GET",
|
|
1101
|
+
headers,
|
|
1102
|
+
...opts.signal ? { signal: opts.signal } : {}
|
|
1103
|
+
});
|
|
1104
|
+
} catch (err) {
|
|
1105
|
+
if (err instanceof Error && err.name === "AbortError") return;
|
|
1106
|
+
throw new AtlaSentError(
|
|
1107
|
+
`Failed to connect to decisions stream: ${err instanceof Error ? err.message : String(err)}`,
|
|
1108
|
+
{ code: "network" }
|
|
1109
|
+
);
|
|
1110
|
+
}
|
|
1111
|
+
if (!response.ok) {
|
|
1112
|
+
const code = response.status === 401 ? "invalid_api_key" : "server_error";
|
|
1113
|
+
throw new AtlaSentError(
|
|
1114
|
+
`Decisions stream returned ${response.status}`,
|
|
1115
|
+
{ code, status: response.status }
|
|
1116
|
+
);
|
|
1117
|
+
}
|
|
1118
|
+
if (!response.body) {
|
|
1119
|
+
throw new AtlaSentError("Decisions stream response has no body", { code: "bad_response" });
|
|
1120
|
+
}
|
|
1121
|
+
const reader = response.body.getReader();
|
|
1122
|
+
const decoder = new TextDecoder("utf-8");
|
|
1123
|
+
let buf = "";
|
|
1124
|
+
try {
|
|
1125
|
+
while (true) {
|
|
1126
|
+
let chunk;
|
|
1127
|
+
try {
|
|
1128
|
+
chunk = await reader.read();
|
|
1129
|
+
} catch (err) {
|
|
1130
|
+
if (err instanceof Error && err.name === "AbortError") return;
|
|
1131
|
+
throw new AtlaSentError(
|
|
1132
|
+
`Decisions stream read error: ${err instanceof Error ? err.message : String(err)}`,
|
|
1133
|
+
{ code: "network" }
|
|
1134
|
+
);
|
|
1135
|
+
}
|
|
1136
|
+
if (chunk.done) break;
|
|
1137
|
+
buf += decoder.decode(chunk.value, { stream: true });
|
|
1138
|
+
const rawBlocks = buf.split("\n\n");
|
|
1139
|
+
buf = rawBlocks.pop() ?? "";
|
|
1140
|
+
for (const block of rawBlocks) {
|
|
1141
|
+
if (!block.trim()) continue;
|
|
1142
|
+
if (block.trimStart().startsWith(":")) {
|
|
1143
|
+
yield { type: "heartbeat" };
|
|
1144
|
+
continue;
|
|
1145
|
+
}
|
|
1146
|
+
let id;
|
|
1147
|
+
let eventType = "audit_event";
|
|
1148
|
+
let dataLine = "";
|
|
1149
|
+
for (const line of block.split("\n")) {
|
|
1150
|
+
if (line.startsWith("id:")) id = line.slice(3).trim();
|
|
1151
|
+
else if (line.startsWith("event:")) eventType = line.slice(6).trim();
|
|
1152
|
+
else if (line.startsWith("data:")) dataLine = line.slice(5).trim();
|
|
1153
|
+
}
|
|
1154
|
+
if (!dataLine) continue;
|
|
1155
|
+
let parsed;
|
|
1156
|
+
try {
|
|
1157
|
+
parsed = JSON.parse(dataLine);
|
|
1158
|
+
} catch {
|
|
1159
|
+
continue;
|
|
1160
|
+
}
|
|
1161
|
+
if (eventType === "session_end") {
|
|
1162
|
+
yield { ...id !== void 0 ? { id } : {}, type: "session_end", payload: parsed };
|
|
1163
|
+
return;
|
|
1164
|
+
}
|
|
1165
|
+
const decision = typeof parsed.decision === "string" ? parsed.decision.toLowerCase() : void 0;
|
|
1166
|
+
yield {
|
|
1167
|
+
...id !== void 0 ? { id } : {},
|
|
1168
|
+
type: eventType,
|
|
1169
|
+
...decision ? { decision } : {},
|
|
1170
|
+
...typeof parsed.actor_id === "string" ? { actorId: parsed.actor_id } : {},
|
|
1171
|
+
...typeof parsed.resource_type === "string" ? { resourceType: parsed.resource_type } : {},
|
|
1172
|
+
...typeof parsed.resource_id === "string" ? { resourceId: parsed.resource_id } : {},
|
|
1173
|
+
...parsed.payload && typeof parsed.payload === "object" ? { payload: parsed.payload } : {},
|
|
1174
|
+
...typeof parsed.hash === "string" ? { hash: parsed.hash } : {},
|
|
1175
|
+
...typeof parsed.previous_hash === "string" ? { previousHash: parsed.previous_hash } : {},
|
|
1176
|
+
...typeof parsed.occurred_at === "string" ? { occurredAt: parsed.occurred_at } : {}
|
|
1177
|
+
};
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
} finally {
|
|
1181
|
+
reader.releaseLock();
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
350
1184
|
/**
|
|
351
1185
|
* Pre-flight evaluation that always returns the constraint trace.
|
|
352
1186
|
*
|
|
@@ -413,7 +1247,17 @@ var AtlaSentClient = class {
|
|
|
413
1247
|
reason,
|
|
414
1248
|
auditHash: wire.audit_hash ?? "",
|
|
415
1249
|
timestamp: wire.timestamp ?? "",
|
|
416
|
-
rateLimit
|
|
1250
|
+
rateLimit,
|
|
1251
|
+
...wire.risk_envelope && {
|
|
1252
|
+
riskEnvelope: {
|
|
1253
|
+
weightedScore: wire.risk_envelope.weighted_score,
|
|
1254
|
+
engineDecision: wire.risk_envelope.engine_decision,
|
|
1255
|
+
envelopeDecision: wire.risk_envelope.envelope_decision,
|
|
1256
|
+
promoted: wire.risk_envelope.promoted,
|
|
1257
|
+
hardBlocks: wire.risk_envelope.hard_blocks ?? [],
|
|
1258
|
+
...wire.risk_envelope.factors && { factors: wire.risk_envelope.factors }
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
417
1261
|
};
|
|
418
1262
|
let constraintTrace = null;
|
|
419
1263
|
if (wire.constraint_trace !== void 0 && wire.constraint_trace !== null && typeof wire.constraint_trace === "object") {
|
|
@@ -462,6 +1306,7 @@ var AtlaSentClient = class {
|
|
|
462
1306
|
outcome: wire.outcome ?? "",
|
|
463
1307
|
permitHash: wire.permit_hash ?? "",
|
|
464
1308
|
timestamp: wire.timestamp ?? "",
|
|
1309
|
+
expiresAt: wire.expires_at ?? null,
|
|
465
1310
|
rateLimit
|
|
466
1311
|
};
|
|
467
1312
|
}
|
|
@@ -479,6 +1324,7 @@ var AtlaSentClient = class {
|
|
|
479
1324
|
const agent = input.agent ?? "ci-deploy-bot";
|
|
480
1325
|
const action = input.action ?? PRODUCTION_DEPLOY_ACTION;
|
|
481
1326
|
const context = input.context ?? {};
|
|
1327
|
+
const environment = typeof context.environment === "string" ? context.environment : typeof context.environment_name === "string" ? context.environment_name : void 0;
|
|
482
1328
|
const evaluation = await this.evaluate({ agent, action, context });
|
|
483
1329
|
if (evaluation.decision !== "allow") {
|
|
484
1330
|
return {
|
|
@@ -495,7 +1341,8 @@ var AtlaSentClient = class {
|
|
|
495
1341
|
permitId: evaluation.permitId,
|
|
496
1342
|
agent,
|
|
497
1343
|
action,
|
|
498
|
-
context
|
|
1344
|
+
context,
|
|
1345
|
+
...environment !== void 0 ? { environment } : {}
|
|
499
1346
|
});
|
|
500
1347
|
if (!verification.verified) {
|
|
501
1348
|
return {
|
|
@@ -709,15 +1556,15 @@ var AtlaSentClient = class {
|
|
|
709
1556
|
*/
|
|
710
1557
|
async keySelf() {
|
|
711
1558
|
const { body: wire, rateLimit } = await this.get("/v1-api-key-self");
|
|
712
|
-
if (typeof wire.key_id !== "string" || typeof wire.
|
|
1559
|
+
if (typeof wire.key_id !== "string" || typeof wire.org_id !== "string") {
|
|
713
1560
|
throw new AtlaSentError(
|
|
714
|
-
"Malformed response from /v1-api-key-self: missing `key_id` or `
|
|
1561
|
+
"Malformed response from /v1-api-key-self: missing `key_id` or `org_id`",
|
|
715
1562
|
{ code: "bad_response" }
|
|
716
1563
|
);
|
|
717
1564
|
}
|
|
718
1565
|
return {
|
|
719
1566
|
keyId: wire.key_id,
|
|
720
|
-
|
|
1567
|
+
orgId: wire.org_id,
|
|
721
1568
|
environment: wire.environment,
|
|
722
1569
|
scopes: wire.scopes ?? [],
|
|
723
1570
|
allowedCidrs: wire.allowed_cidrs ?? null,
|
|
@@ -784,7 +1631,153 @@ var AtlaSentClient = class {
|
|
|
784
1631
|
return { ...wire, rateLimit };
|
|
785
1632
|
}
|
|
786
1633
|
/**
|
|
787
|
-
*
|
|
1634
|
+
* Re-evaluate a recorded decision against its originally-pinned policy
|
|
1635
|
+
* bundle and engine version, and report whether the result agrees with
|
|
1636
|
+
* what was recorded.
|
|
1637
|
+
*
|
|
1638
|
+
* Wraps `POST /v1-decisions-replay/:id/replay`. **Side-effect-free** — no
|
|
1639
|
+
* audit chain row is written and no permit is issued (per ADR-016).
|
|
1640
|
+
* Useful for compliance review, regression testing of bundle changes,
|
|
1641
|
+
* and post-incident investigation.
|
|
1642
|
+
*
|
|
1643
|
+
* Outcomes encoded in the response:
|
|
1644
|
+
* - `variance: "NONE"` — replay agrees with the original decision.
|
|
1645
|
+
* - `variance: "DECISION_CHANGED"` — same envelope, same bundle, different
|
|
1646
|
+
* decision. Almost always indicates non-determinism in a rule
|
|
1647
|
+
* (e.g. wall-clock comparison) and warrants investigation.
|
|
1648
|
+
* - `variance: "ENVELOPE_DRIFT"` — the recorded request envelope no longer
|
|
1649
|
+
* hashes to the recorded value. The replay short-circuits without
|
|
1650
|
+
* running the engine; `replay_decision` is absent. Treat as evidence
|
|
1651
|
+
* of substrate tamper or a recorder bug.
|
|
1652
|
+
*
|
|
1653
|
+
* Server-side 409 responses (replay refused because the engine version
|
|
1654
|
+
* does not accept replay, or because no bundle was pinned) surface as
|
|
1655
|
+
* `AtlaSentError` with `code: "replay_not_eligible"` — callers should
|
|
1656
|
+
* treat them as expected for old / un-pinned decisions, not as bugs.
|
|
1657
|
+
*
|
|
1658
|
+
* Requires the `evaluate:write` API key scope.
|
|
1659
|
+
*
|
|
1660
|
+
* @param decisionId The UUID of the recorded decision to replay.
|
|
1661
|
+
* Matches `execution_evaluations.request_id`.
|
|
1662
|
+
*
|
|
1663
|
+
* @example
|
|
1664
|
+
* ```ts
|
|
1665
|
+
* const result = await client.replayDecision("dec_abc123");
|
|
1666
|
+
* if (result.variance === "DECISION_CHANGED") {
|
|
1667
|
+
* console.warn(
|
|
1668
|
+
* `Decision ${result.decision_id} changed on replay: ` +
|
|
1669
|
+
* `${result.original_decision} → ${result.replay_decision}`,
|
|
1670
|
+
* );
|
|
1671
|
+
* }
|
|
1672
|
+
* ```
|
|
1673
|
+
*/
|
|
1674
|
+
async replayDecision(decisionId) {
|
|
1675
|
+
if (typeof decisionId !== "string" || decisionId.length === 0) {
|
|
1676
|
+
throw new AtlaSentError("decisionId is required", {
|
|
1677
|
+
code: "bad_request"
|
|
1678
|
+
});
|
|
1679
|
+
}
|
|
1680
|
+
const path = `/v1-decisions-replay/${encodeURIComponent(decisionId)}/replay`;
|
|
1681
|
+
const { body: wire, rateLimit } = await this.post(
|
|
1682
|
+
path,
|
|
1683
|
+
{}
|
|
1684
|
+
);
|
|
1685
|
+
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") {
|
|
1686
|
+
throw new AtlaSentError(
|
|
1687
|
+
"Malformed response from /v1-decisions-replay/:id/replay: missing required fields",
|
|
1688
|
+
{ code: "bad_response" }
|
|
1689
|
+
);
|
|
1690
|
+
}
|
|
1691
|
+
return { ...wire, rateLimit };
|
|
1692
|
+
}
|
|
1693
|
+
/**
|
|
1694
|
+
* ADR-015 Phase C — SDK-canonical replay runtime.
|
|
1695
|
+
*
|
|
1696
|
+
* Re-evaluates a recorded decision against its originally-pinned policy
|
|
1697
|
+
* bundle and engine version via `POST /v1/decisions/:id/replay`.
|
|
1698
|
+
* Side-effect-free server-side: no audit chain row is written and no
|
|
1699
|
+
* permit is issued (ADR-016 `mode: "replay"` sentinel).
|
|
1700
|
+
*
|
|
1701
|
+
* Differences from {@link replayDecision} (the 2.7.0 raw-wire surface):
|
|
1702
|
+
*
|
|
1703
|
+
* | | `replayDecision()` | `replay()` |
|
|
1704
|
+
* | --- | --- | --- |
|
|
1705
|
+
* | Path | `/v1-decisions-replay/:id/replay` | `/v1/decisions/:id/replay` |
|
|
1706
|
+
* | Variance | raw wire (`DECISION_CHANGED`) | SDK-canonical (`POLICY_DRIFT`) |
|
|
1707
|
+
* | 409 handling | throws `AtlaSentError` | returns `ENGINE_DRIFT` / `BUNDLE_MISSING` |
|
|
1708
|
+
* | Input shape | `decisionId: string` | `{ evaluationId }` |
|
|
1709
|
+
*
|
|
1710
|
+
* **Never throws on `409 replay_not_eligible`** — instead returns a
|
|
1711
|
+
* `ReplayResponse` with `varianceKind: "ENGINE_DRIFT"` (engine retired
|
|
1712
|
+
* beyond archival window) or `"BUNDLE_MISSING"` (no bundle pinned on
|
|
1713
|
+
* the original evaluation). Callers can always `switch` on
|
|
1714
|
+
* `result.varianceKind` without a try/catch.
|
|
1715
|
+
*
|
|
1716
|
+
* Fix-forward note: this method was originally landed in PR #275 but
|
|
1717
|
+
* dropped from the squash merge. The TS types (`ReplayResponse`,
|
|
1718
|
+
* `ReplayRequest`) and CHANGELOG made it through; the method itself
|
|
1719
|
+
* did not. Restored here to match the Python {@link
|
|
1720
|
+
* AtlaSentClient}.replay() that landed in atlasent-sdk@2.6.0 (Python).
|
|
1721
|
+
*/
|
|
1722
|
+
async replay(input) {
|
|
1723
|
+
if (!input || typeof input.evaluationId !== "string" || input.evaluationId.length === 0) {
|
|
1724
|
+
throw new AtlaSentError("evaluationId is required", {
|
|
1725
|
+
code: "bad_request"
|
|
1726
|
+
});
|
|
1727
|
+
}
|
|
1728
|
+
const path = `/v1/decisions/${encodeURIComponent(input.evaluationId)}/replay`;
|
|
1729
|
+
let wire;
|
|
1730
|
+
let rateLimit;
|
|
1731
|
+
try {
|
|
1732
|
+
const result = await this.post(path, {});
|
|
1733
|
+
wire = result.body;
|
|
1734
|
+
rateLimit = result.rateLimit;
|
|
1735
|
+
} catch (err) {
|
|
1736
|
+
if (err instanceof AtlaSentError && err.status === 409) {
|
|
1737
|
+
const msg = (err.message ?? "").toLowerCase();
|
|
1738
|
+
const varianceKind2 = msg.includes("bundle") ? "BUNDLE_MISSING" : "ENGINE_DRIFT";
|
|
1739
|
+
return {
|
|
1740
|
+
decisionId: input.evaluationId,
|
|
1741
|
+
varianceKind: varianceKind2,
|
|
1742
|
+
originalDecision: "deny",
|
|
1743
|
+
acceptsReplay: false,
|
|
1744
|
+
replayedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1745
|
+
rateLimit: null
|
|
1746
|
+
};
|
|
1747
|
+
}
|
|
1748
|
+
throw err;
|
|
1749
|
+
}
|
|
1750
|
+
const VARIANCE_MAP = {
|
|
1751
|
+
NONE: "NONE",
|
|
1752
|
+
DECISION_CHANGED: "POLICY_DRIFT",
|
|
1753
|
+
ENVELOPE_DRIFT: "ENVELOPE_DRIFT",
|
|
1754
|
+
CHAIN_TAMPER: "CHAIN_TAMPER",
|
|
1755
|
+
BUNDLE_MISSING: "BUNDLE_MISSING",
|
|
1756
|
+
ENGINE_DRIFT: "ENGINE_DRIFT"
|
|
1757
|
+
};
|
|
1758
|
+
const rawVariance = typeof wire.variance === "string" ? wire.variance : "";
|
|
1759
|
+
const varianceKind = VARIANCE_MAP[rawVariance] ?? "NONE";
|
|
1760
|
+
const replayDec = typeof wire.replay_decision === "string" ? wire.replay_decision.toLowerCase() : void 0;
|
|
1761
|
+
const originalDec = typeof wire.original_decision === "string" ? wire.original_decision.toLowerCase() : "deny";
|
|
1762
|
+
const response = {
|
|
1763
|
+
decisionId: typeof wire.decision_id === "string" ? wire.decision_id : input.evaluationId,
|
|
1764
|
+
varianceKind,
|
|
1765
|
+
originalDecision: originalDec,
|
|
1766
|
+
acceptsReplay: typeof wire.accepts_replay === "boolean" ? wire.accepts_replay : true,
|
|
1767
|
+
replayedAt: typeof wire.replayed_at === "string" ? wire.replayed_at : (/* @__PURE__ */ new Date()).toISOString(),
|
|
1768
|
+
rateLimit
|
|
1769
|
+
};
|
|
1770
|
+
if (typeof wire.original_deny_code === "string") response.originalDenyCode = wire.original_deny_code;
|
|
1771
|
+
if (replayDec !== void 0) response.replayedDecision = replayDec;
|
|
1772
|
+
if (typeof wire.replay_deny_code === "string") response.replayedDenyCode = wire.replay_deny_code;
|
|
1773
|
+
if (typeof wire.engine_version === "string") response.engineVersion = wire.engine_version;
|
|
1774
|
+
if (typeof wire.engine_version_kind === "string") response.engineVersionKind = wire.engine_version_kind;
|
|
1775
|
+
if (typeof wire.envelope_verification === "string") response.envelopeVerification = wire.envelope_verification;
|
|
1776
|
+
return response;
|
|
1777
|
+
}
|
|
1778
|
+
/**
|
|
1779
|
+
* Open a streaming evaluation session against `POST /v1/evaluate/stream`
|
|
1780
|
+
* (with fallback to `POST /v1-evaluate-stream` on older runtimes).
|
|
788
1781
|
*
|
|
789
1782
|
* Yields {@link StreamDecisionEvent} and {@link StreamProgressEvent} objects
|
|
790
1783
|
* as the server emits them. The iterator ends cleanly when the server sends
|
|
@@ -822,7 +1815,7 @@ var AtlaSentClient = class {
|
|
|
822
1815
|
api_key: this.apiKey
|
|
823
1816
|
};
|
|
824
1817
|
const requestId = globalThis.crypto.randomUUID();
|
|
825
|
-
|
|
1818
|
+
let streamPath = V1_EVALUATE_STREAM_PATH;
|
|
826
1819
|
let lastEventId;
|
|
827
1820
|
let retryCount = 0;
|
|
828
1821
|
while (true) {
|
|
@@ -831,6 +1824,8 @@ var AtlaSentClient = class {
|
|
|
831
1824
|
"Content-Type": "application/json",
|
|
832
1825
|
Authorization: `Bearer ${this.apiKey}`,
|
|
833
1826
|
"User-Agent": this.userAgent,
|
|
1827
|
+
// ADR-025: wire-protocol version declared on every request.
|
|
1828
|
+
"X-AtlaSent-Protocol-Version": "1",
|
|
834
1829
|
"X-Request-ID": requestId
|
|
835
1830
|
};
|
|
836
1831
|
if (lastEventId !== void 0) {
|
|
@@ -840,7 +1835,7 @@ var AtlaSentClient = class {
|
|
|
840
1835
|
const signal = opts.signal ? AbortSignal.any([connectionTimeoutSignal, opts.signal]) : connectionTimeoutSignal;
|
|
841
1836
|
let response;
|
|
842
1837
|
try {
|
|
843
|
-
response = await this.fetchImpl(
|
|
1838
|
+
response = await this.fetchImpl(`${this.baseUrl}${streamPath}`, {
|
|
844
1839
|
method: "POST",
|
|
845
1840
|
headers,
|
|
846
1841
|
body: JSON.stringify(body),
|
|
@@ -856,6 +1851,10 @@ var AtlaSentClient = class {
|
|
|
856
1851
|
throw mapped;
|
|
857
1852
|
}
|
|
858
1853
|
if (!response.ok) {
|
|
1854
|
+
if (streamPath === V1_EVALUATE_STREAM_PATH && (response.status === 404 || response.status === 405)) {
|
|
1855
|
+
streamPath = V1_EVALUATE_STREAM_LEGACY_PATH;
|
|
1856
|
+
continue;
|
|
1857
|
+
}
|
|
859
1858
|
throw await buildHttpError(response, requestId);
|
|
860
1859
|
}
|
|
861
1860
|
if (!response.body) {
|
|
@@ -907,6 +1906,16 @@ var AtlaSentClient = class {
|
|
|
907
1906
|
async post(path, body, query) {
|
|
908
1907
|
return this.request(path, "POST", body, query);
|
|
909
1908
|
}
|
|
1909
|
+
async postWithPathFallback(primaryPath, fallbackPath, body, query) {
|
|
1910
|
+
try {
|
|
1911
|
+
return await this.post(primaryPath, body, query);
|
|
1912
|
+
} catch (err) {
|
|
1913
|
+
if (err instanceof AtlaSentError && (err.status === 404 || err.status === 405)) {
|
|
1914
|
+
return this.post(fallbackPath, body, query);
|
|
1915
|
+
}
|
|
1916
|
+
throw err;
|
|
1917
|
+
}
|
|
1918
|
+
}
|
|
910
1919
|
async get(path, query) {
|
|
911
1920
|
return this.request(path, "GET", void 0, query);
|
|
912
1921
|
}
|
|
@@ -918,7 +1927,9 @@ var AtlaSentClient = class {
|
|
|
918
1927
|
Accept: "application/json",
|
|
919
1928
|
Authorization: `Bearer ${this.apiKey}`,
|
|
920
1929
|
"User-Agent": this.userAgent,
|
|
921
|
-
"X-Request-ID": requestId
|
|
1930
|
+
"X-Request-ID": requestId,
|
|
1931
|
+
// ADR-025: wire-protocol version declared on every request.
|
|
1932
|
+
"X-AtlaSent-Protocol-Version": "1"
|
|
922
1933
|
};
|
|
923
1934
|
if (method === "POST") headers["Content-Type"] = "application/json";
|
|
924
1935
|
const bodyStr = method === "POST" ? JSON.stringify(body) : void 0;
|
|
@@ -1534,6 +2545,145 @@ var AtlaSentClient = class {
|
|
|
1534
2545
|
);
|
|
1535
2546
|
return body;
|
|
1536
2547
|
}
|
|
2548
|
+
// ── Constrained governance agents (read surface) ──────────────────────────
|
|
2549
|
+
//
|
|
2550
|
+
// Three GETs onto the v1-governance-agents edge function. Doctrine:
|
|
2551
|
+
// findings produced by these endpoints are advisory signal, never
|
|
2552
|
+
// authority. There is no `runGovernanceAgent` method on this client —
|
|
2553
|
+
// invocation belongs in CI (atlasent-action `governance-agents` mode),
|
|
2554
|
+
// not in application code.
|
|
2555
|
+
/**
|
|
2556
|
+
* List the advisory governance-agent registry for the calling org.
|
|
2557
|
+
*
|
|
2558
|
+
* Calls `GET /v1/governance/agents`. The registry is reference data
|
|
2559
|
+
* seeded at runtime-DB migration time; every row has
|
|
2560
|
+
* `authority_class = "advisory"` and `can_authorize = false` —
|
|
2561
|
+
* structural invariants enforced by the schema, not policy.
|
|
2562
|
+
*/
|
|
2563
|
+
async listGovernanceAgents() {
|
|
2564
|
+
const { body } = await this.get(
|
|
2565
|
+
"/v1/governance/agents"
|
|
2566
|
+
);
|
|
2567
|
+
return [...body.agents ?? []];
|
|
2568
|
+
}
|
|
2569
|
+
/**
|
|
2570
|
+
* List advisory findings emitted against one governed change.
|
|
2571
|
+
*
|
|
2572
|
+
* Calls `GET /v1/governance/findings?change_id=…[&agent_slug=…]`.
|
|
2573
|
+
* Returns the typed-finding rows in `created_at DESC` order, including
|
|
2574
|
+
* `routed_gate_id` when the finding→gate trigger linked them. Findings
|
|
2575
|
+
* with `can_authorize === false` (always) are advisory; rendering them
|
|
2576
|
+
* never satisfies a gate.
|
|
2577
|
+
*/
|
|
2578
|
+
async listGovernanceFindings(query) {
|
|
2579
|
+
if (!query?.change_id) {
|
|
2580
|
+
throw new AtlaSentError("change_id is required", { code: "bad_request" });
|
|
2581
|
+
}
|
|
2582
|
+
const params = new URLSearchParams({ change_id: query.change_id });
|
|
2583
|
+
if (query.agent_slug) params.set("agent_slug", query.agent_slug);
|
|
2584
|
+
const { body } = await this.get(
|
|
2585
|
+
"/v1/governance/findings",
|
|
2586
|
+
params
|
|
2587
|
+
);
|
|
2588
|
+
return [...body.findings ?? []];
|
|
2589
|
+
}
|
|
2590
|
+
/**
|
|
2591
|
+
* List agent run records against one governed change.
|
|
2592
|
+
*
|
|
2593
|
+
* Calls `GET /v1/governance/evaluations?change_id=…[&agent_slug=…]`.
|
|
2594
|
+
* Returns every persisted evaluation, including `failed` / `timeout`
|
|
2595
|
+
* runs and `completed` runs with zero findings — the latter is the
|
|
2596
|
+
* positive signal "the agent ran and found nothing", which the UI
|
|
2597
|
+
* surfaces as `clear`.
|
|
2598
|
+
*/
|
|
2599
|
+
async listGovernanceEvaluations(query) {
|
|
2600
|
+
if (!query?.change_id) {
|
|
2601
|
+
throw new AtlaSentError("change_id is required", { code: "bad_request" });
|
|
2602
|
+
}
|
|
2603
|
+
const params = new URLSearchParams({ change_id: query.change_id });
|
|
2604
|
+
if (query.agent_slug) params.set("agent_slug", query.agent_slug);
|
|
2605
|
+
const { body } = await this.get(
|
|
2606
|
+
"/v1/governance/evaluations",
|
|
2607
|
+
params
|
|
2608
|
+
);
|
|
2609
|
+
return [...body.evaluations ?? []];
|
|
2610
|
+
}
|
|
2611
|
+
// ── Private adapters for sub-client factories ──────────────────────────────
|
|
2612
|
+
// Thin wrappers that expose the private request infrastructure to sub-client
|
|
2613
|
+
// factories (scim, evidenceBundles, auth) without widening the public API.
|
|
2614
|
+
async _post(path, body, query) {
|
|
2615
|
+
const { body: b } = await this.post(path, body, query);
|
|
2616
|
+
return { body: b };
|
|
2617
|
+
}
|
|
2618
|
+
async _get(path, query) {
|
|
2619
|
+
const { body: b } = await this.get(path, query);
|
|
2620
|
+
return { body: b };
|
|
2621
|
+
}
|
|
2622
|
+
async _put(path, body) {
|
|
2623
|
+
return this._requestRaw(path, "PUT", body, void 0);
|
|
2624
|
+
}
|
|
2625
|
+
async _patch(path, body) {
|
|
2626
|
+
return this._requestRaw(path, "PATCH", body, void 0);
|
|
2627
|
+
}
|
|
2628
|
+
async _delete(path) {
|
|
2629
|
+
await this._requestRaw(path, "DELETE", void 0, void 0);
|
|
2630
|
+
}
|
|
2631
|
+
async _getRaw(path) {
|
|
2632
|
+
const url = `${this.baseUrl}${path}`;
|
|
2633
|
+
const requestId = globalThis.crypto.randomUUID();
|
|
2634
|
+
const headers = {
|
|
2635
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
2636
|
+
"User-Agent": this.userAgent,
|
|
2637
|
+
"X-Request-ID": requestId,
|
|
2638
|
+
"X-AtlaSent-Protocol-Version": "1"
|
|
2639
|
+
};
|
|
2640
|
+
const response = await this.fetchImpl(url, {
|
|
2641
|
+
method: "GET",
|
|
2642
|
+
headers,
|
|
2643
|
+
signal: AbortSignal.timeout(this.timeoutMs)
|
|
2644
|
+
});
|
|
2645
|
+
if (!response.ok) {
|
|
2646
|
+
const text = await response.text().catch(() => "");
|
|
2647
|
+
throw new AtlaSentError(`GET ${path} returned ${response.status}`, {
|
|
2648
|
+
code: response.status >= 500 ? "server_error" : "bad_request",
|
|
2649
|
+
status: response.status,
|
|
2650
|
+
requestId
|
|
2651
|
+
});
|
|
2652
|
+
}
|
|
2653
|
+
return response.arrayBuffer();
|
|
2654
|
+
}
|
|
2655
|
+
async _requestRaw(path, method, body, query) {
|
|
2656
|
+
const qs = query && Array.from(query).length > 0 ? `?${query.toString()}` : "";
|
|
2657
|
+
const url = `${this.baseUrl}${path}${qs}`;
|
|
2658
|
+
const requestId = globalThis.crypto.randomUUID();
|
|
2659
|
+
const headers = {
|
|
2660
|
+
Accept: "application/json",
|
|
2661
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
2662
|
+
"User-Agent": this.userAgent,
|
|
2663
|
+
"X-Request-ID": requestId,
|
|
2664
|
+
"X-AtlaSent-Protocol-Version": "1"
|
|
2665
|
+
};
|
|
2666
|
+
if (method === "PUT" && body !== void 0) {
|
|
2667
|
+
headers["Content-Type"] = "application/json";
|
|
2668
|
+
}
|
|
2669
|
+
const init = { method, headers, signal: AbortSignal.timeout(this.timeoutMs) };
|
|
2670
|
+
if (method === "PUT" && body !== void 0) {
|
|
2671
|
+
init.body = JSON.stringify(body);
|
|
2672
|
+
}
|
|
2673
|
+
const response = await this.fetchImpl(url, init);
|
|
2674
|
+
if (!response.ok) {
|
|
2675
|
+
const text = await response.text().catch(() => "");
|
|
2676
|
+
throw new AtlaSentError(`${method} ${path} returned ${response.status}`, {
|
|
2677
|
+
code: response.status >= 500 ? "server_error" : "bad_request",
|
|
2678
|
+
status: response.status,
|
|
2679
|
+
requestId
|
|
2680
|
+
});
|
|
2681
|
+
}
|
|
2682
|
+
if (method === "DELETE") {
|
|
2683
|
+
return { body: {} };
|
|
2684
|
+
}
|
|
2685
|
+
return { body: await response.json() };
|
|
2686
|
+
}
|
|
1537
2687
|
};
|
|
1538
2688
|
function parseRateLimitHeaders(headers) {
|
|
1539
2689
|
const rawLimit = headers.get("x-ratelimit-limit");
|
|
@@ -1679,7 +2829,7 @@ function buildAuditEventsQuery(query) {
|
|
|
1679
2829
|
return params;
|
|
1680
2830
|
}
|
|
1681
2831
|
function sleep(ms) {
|
|
1682
|
-
return new Promise((
|
|
2832
|
+
return new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
1683
2833
|
}
|
|
1684
2834
|
function parseRetryAfter(raw) {
|
|
1685
2835
|
if (!raw) return void 0;
|
|
@@ -1700,14 +2850,14 @@ async function* parseSseStream(body, requestId, timeoutMs, onEventId) {
|
|
|
1700
2850
|
if (timeoutMs <= 0) {
|
|
1701
2851
|
return reader.read();
|
|
1702
2852
|
}
|
|
1703
|
-
return new Promise((
|
|
2853
|
+
return new Promise((resolve3, reject) => {
|
|
1704
2854
|
const timer = setTimeout(() => {
|
|
1705
2855
|
reject(new StreamTimeoutError(timeoutMs));
|
|
1706
2856
|
}, timeoutMs);
|
|
1707
2857
|
reader.read().then(
|
|
1708
2858
|
(result) => {
|
|
1709
2859
|
clearTimeout(timer);
|
|
1710
|
-
|
|
2860
|
+
resolve3(result);
|
|
1711
2861
|
},
|
|
1712
2862
|
(err) => {
|
|
1713
2863
|
clearTimeout(timer);
|
|
@@ -1835,6 +2985,7 @@ function getClient() {
|
|
|
1835
2985
|
sharedClient = new AtlaSentClient(options);
|
|
1836
2986
|
return sharedClient;
|
|
1837
2987
|
}
|
|
2988
|
+
var ACTION_TYPE_RE = /^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)+$/;
|
|
1838
2989
|
function wireDecisionToDenied(serverDecision) {
|
|
1839
2990
|
const lower = serverDecision.toLowerCase();
|
|
1840
2991
|
if (lower === "hold" || lower === "escalate") return lower;
|
|
@@ -1873,6 +3024,21 @@ async function computeExecutionHash(payload) {
|
|
|
1873
3024
|
}
|
|
1874
3025
|
}
|
|
1875
3026
|
async function protect(request) {
|
|
3027
|
+
if (!ACTION_TYPE_RE.test(request.action)) {
|
|
3028
|
+
throw new AtlaSentError(
|
|
3029
|
+
`action must be in dot-notation format (e.g. "production.deploy"). Got: ${JSON.stringify(request.action)}`,
|
|
3030
|
+
{ code: "bad_request" }
|
|
3031
|
+
);
|
|
3032
|
+
}
|
|
3033
|
+
const trustMgr = getGlobalTrustRootManager({ disableRefresh: false });
|
|
3034
|
+
if (trustMgr.checkExpiry() === "expired") {
|
|
3035
|
+
const snap = trustMgr.getSnapshot();
|
|
3036
|
+
throw new BundleVerificationError({
|
|
3037
|
+
reason: "trust_snapshot_expired",
|
|
3038
|
+
snapshotValidUntil: snap.valid_until,
|
|
3039
|
+
snapshotFetchedAt: snap.issued_at
|
|
3040
|
+
});
|
|
3041
|
+
}
|
|
1876
3042
|
const client = getClient();
|
|
1877
3043
|
const evaluation = await client.evaluate(request);
|
|
1878
3044
|
if (evaluation.decision !== "allow") {
|
|
@@ -1883,12 +3049,13 @@ async function protect(request) {
|
|
|
1883
3049
|
auditHash: evaluation.auditHash
|
|
1884
3050
|
});
|
|
1885
3051
|
}
|
|
1886
|
-
const environment = request.context?.environment
|
|
1887
|
-
|
|
1888
|
-
|
|
3052
|
+
const environment = request.context?.environment;
|
|
3053
|
+
if (!environment) {
|
|
3054
|
+
throw new AtlaSentError(
|
|
3055
|
+
'context.environment is required. Pass the environment where this action executes (e.g. "production", "staging").',
|
|
3056
|
+
{ code: "bad_request" }
|
|
1889
3057
|
);
|
|
1890
|
-
|
|
1891
|
-
})();
|
|
3058
|
+
}
|
|
1892
3059
|
const evaluatePayload = {
|
|
1893
3060
|
action_type: request.action,
|
|
1894
3061
|
actor_id: request.agent,
|
|
@@ -1919,21 +3086,22 @@ async function protect(request) {
|
|
|
1919
3086
|
permitHash: verification.permitHash,
|
|
1920
3087
|
auditHash: evaluation.auditHash,
|
|
1921
3088
|
reason: evaluation.reason,
|
|
1922
|
-
timestamp: verification.timestamp
|
|
3089
|
+
timestamp: verification.timestamp,
|
|
3090
|
+
permitExpiresAt: verification.expiresAt ?? null
|
|
1923
3091
|
};
|
|
1924
3092
|
}
|
|
1925
3093
|
|
|
1926
3094
|
// src/hono.ts
|
|
1927
3095
|
var DEFAULT_CONTEXT_KEY = "atlasent";
|
|
1928
|
-
async function
|
|
3096
|
+
async function resolve2(value, c) {
|
|
1929
3097
|
return typeof value === "function" ? await value(c) : value;
|
|
1930
3098
|
}
|
|
1931
3099
|
function atlaSentGuard(options) {
|
|
1932
3100
|
const contextKey = options.key ?? DEFAULT_CONTEXT_KEY;
|
|
1933
3101
|
return async (c, next) => {
|
|
1934
3102
|
const [agent, action, ctx] = await Promise.all([
|
|
1935
|
-
|
|
1936
|
-
|
|
3103
|
+
resolve2(options.agent, c),
|
|
3104
|
+
resolve2(options.action, c),
|
|
1937
3105
|
options.context ? options.context(c) : Promise.resolve(void 0)
|
|
1938
3106
|
]);
|
|
1939
3107
|
const request = { agent, action };
|