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