@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/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
- normalized.context = legacy.context;
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: 4,
148
- baseDelayMs: 2e3,
149
- maxDelayMs: 16e3
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.2.0";
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.organization_id !== "string") {
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 `organization_id`",
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
- organizationId: wire.organization_id,
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
- * Open a streaming evaluation session against `POST /v1-evaluate-stream`.
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
- const url = `${this.baseUrl}/v1-evaluate-stream`;
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(url, {
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((resolve2) => setTimeout(resolve2, ms));
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((resolve2, reject) => {
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
- resolve2(result);
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
- console.warn(
1888
- "[atlasent] environment not set on evaluate request \u2014 defaulting to 'production'. Set context.environment explicitly to suppress."
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
- return "production";
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 resolve(value, c) {
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
- resolve(options.agent, c),
1936
- resolve(options.action, c),
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 };