@atlasent/sdk 2.5.0 → 2.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +35 -0
- package/dist/hono.cjs +445 -11
- package/dist/hono.cjs.map +1 -1
- package/dist/hono.d.cts +2 -2
- package/dist/hono.d.ts +2 -2
- package/dist/hono.js +445 -11
- package/dist/hono.js.map +1 -1
- package/dist/index.cjs +2185 -13
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1699 -180
- package/dist/index.d.ts +1699 -180
- package/dist/index.js +2159 -13
- package/dist/index.js.map +1 -1
- package/dist/{protect-DiRVfVLq.d.cts → protect-C0t0fP1y.d.cts} +449 -2
- package/dist/{protect-DiRVfVLq.d.ts → protect-C0t0fP1y.d.ts} +449 -2
- package/package.json +6 -1
package/dist/index.js
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
2
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
3
|
+
}) : x)(function(x) {
|
|
4
|
+
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
5
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
6
|
+
});
|
|
7
|
+
|
|
1
8
|
// src/errors.ts
|
|
2
9
|
var StreamTimeoutError = class extends Error {
|
|
3
10
|
name = "StreamTimeoutError";
|
|
@@ -163,9 +170,8 @@ function normalizeEvaluateRequest(input) {
|
|
|
163
170
|
action_type: legacy.action,
|
|
164
171
|
actor_id: legacy.agent
|
|
165
172
|
};
|
|
166
|
-
if (legacy.context !== void 0)
|
|
167
|
-
|
|
168
|
-
}
|
|
173
|
+
if (legacy.context !== void 0) normalized.context = legacy.context;
|
|
174
|
+
if (legacy.explain !== void 0) normalized.explain = legacy.explain;
|
|
169
175
|
return normalized;
|
|
170
176
|
}
|
|
171
177
|
return input;
|
|
@@ -353,6 +359,7 @@ var AtlaSentClient = class {
|
|
|
353
359
|
actor_id: normalized.actor_id,
|
|
354
360
|
context: normalized.context ?? {}
|
|
355
361
|
};
|
|
362
|
+
if (normalized.explain !== void 0) body.explain = normalized.explain;
|
|
356
363
|
const { body: wire, rateLimit } = await this.post(
|
|
357
364
|
"/v1-evaluate",
|
|
358
365
|
body
|
|
@@ -389,9 +396,211 @@ var AtlaSentClient = class {
|
|
|
389
396
|
reason,
|
|
390
397
|
auditHash: wire.audit_hash ?? "",
|
|
391
398
|
timestamp: wire.timestamp ?? "",
|
|
399
|
+
rateLimit,
|
|
400
|
+
...wire.risk_envelope && {
|
|
401
|
+
riskEnvelope: {
|
|
402
|
+
weightedScore: wire.risk_envelope.weighted_score,
|
|
403
|
+
engineDecision: wire.risk_envelope.engine_decision,
|
|
404
|
+
envelopeDecision: wire.risk_envelope.envelope_decision,
|
|
405
|
+
promoted: wire.risk_envelope.promoted,
|
|
406
|
+
hardBlocks: wire.risk_envelope.hard_blocks ?? [],
|
|
407
|
+
...wire.risk_envelope.factors && { factors: wire.risk_envelope.factors }
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
/**
|
|
413
|
+
* Batch evaluate — send up to 100 decisions in a single round-trip.
|
|
414
|
+
*
|
|
415
|
+
* Wraps `POST /v1-evaluate-batch`. The server evaluates each item
|
|
416
|
+
* against the active policy bundle and returns results in the same
|
|
417
|
+
* order as the input. One rate-limit token is consumed for the
|
|
418
|
+
* whole batch, and one audit-chain entry lists every included
|
|
419
|
+
* decision id.
|
|
420
|
+
*
|
|
421
|
+
* A per-item policy `deny` is **not** thrown — it appears as
|
|
422
|
+
* `item.decision === "deny"` in the returned items. A whole-batch
|
|
423
|
+
* network error, 4xx, or 5xx throws {@link AtlaSentError}.
|
|
424
|
+
*
|
|
425
|
+
* Requires the `v2_batch` tenant feature flag to be enabled on the
|
|
426
|
+
* org (returns 404 when off). Requires scope `evaluate:write`.
|
|
427
|
+
*
|
|
428
|
+
* @param requests - 1–100 evaluate items.
|
|
429
|
+
* @param batchId - Optional caller-supplied UUID for idempotency.
|
|
430
|
+
* A retried call with the same `batchId` and identical items
|
|
431
|
+
* returns the cached response within 24 h (`replayed: true`).
|
|
432
|
+
*/
|
|
433
|
+
async evaluateBatch(requests, batchId) {
|
|
434
|
+
if (!Array.isArray(requests) || requests.length === 0) {
|
|
435
|
+
throw new AtlaSentError(
|
|
436
|
+
"evaluateBatch: requests must be a non-empty array",
|
|
437
|
+
{ code: "bad_request" }
|
|
438
|
+
);
|
|
439
|
+
}
|
|
440
|
+
if (requests.length > 100) {
|
|
441
|
+
throw new AtlaSentError(
|
|
442
|
+
`evaluateBatch: requests.length ${requests.length} exceeds the 100-item cap`,
|
|
443
|
+
{ code: "bad_request" }
|
|
444
|
+
);
|
|
445
|
+
}
|
|
446
|
+
const wireItems = requests.map((r) => ({
|
|
447
|
+
action_type: r.action,
|
|
448
|
+
actor_id: r.agent,
|
|
449
|
+
context: r.context ?? {}
|
|
450
|
+
}));
|
|
451
|
+
const wireBody = { items: wireItems };
|
|
452
|
+
if (batchId) wireBody.batch_id = batchId;
|
|
453
|
+
const { body: wire, rateLimit } = await this.post(
|
|
454
|
+
"/v1-evaluate-batch",
|
|
455
|
+
wireBody
|
|
456
|
+
);
|
|
457
|
+
const items = (wire.items ?? []).map(
|
|
458
|
+
(item) => {
|
|
459
|
+
const rawDecision = typeof item.decision === "string" ? item.decision.toLowerCase() : void 0;
|
|
460
|
+
const decision = rawDecision === "allow" || rawDecision === "deny" || rawDecision === "hold" || rawDecision === "escalate" ? rawDecision : void 0;
|
|
461
|
+
return {
|
|
462
|
+
index: item.index,
|
|
463
|
+
...decision !== void 0 ? { decision } : {},
|
|
464
|
+
...item.decision_id ? { decisionId: item.decision_id } : {},
|
|
465
|
+
...item.permit_token != null ? { permitToken: item.permit_token } : {},
|
|
466
|
+
...item.reason != null ? { reason: item.reason } : {},
|
|
467
|
+
...item.audit_entry_hash ? { auditHash: item.audit_entry_hash } : {},
|
|
468
|
+
...item.timestamp ? { timestamp: item.timestamp } : {},
|
|
469
|
+
...item.error ? { error: item.error } : {},
|
|
470
|
+
...item.message ? { message: item.message } : {}
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
);
|
|
474
|
+
return {
|
|
475
|
+
batchId: wire.batch_id,
|
|
476
|
+
items,
|
|
477
|
+
partial: wire.partial ?? false,
|
|
478
|
+
...wire.replayed ? { replayed: wire.replayed } : {},
|
|
392
479
|
rateLimit
|
|
393
480
|
};
|
|
394
481
|
}
|
|
482
|
+
/**
|
|
483
|
+
* Subscribe to a live stream of decisions for this org.
|
|
484
|
+
*
|
|
485
|
+
* Wraps `GET /v1-decisions-stream`. The server emits one SSE frame
|
|
486
|
+
* per audit event and sends a heartbeat every 15 s. The session
|
|
487
|
+
* auto-closes after `maxSeconds` (default 30 min); reconnect with
|
|
488
|
+
* the last received `event.id` to resume without replaying history.
|
|
489
|
+
*
|
|
490
|
+
* ```ts
|
|
491
|
+
* const controller = new AbortController();
|
|
492
|
+
* for await (const event of client.subscribeDecisions({ signal: controller.signal })) {
|
|
493
|
+
* if (event.type === "heartbeat") continue;
|
|
494
|
+
* console.log(event.type, event.decision, event.actorId);
|
|
495
|
+
* if (event.type === "session_end") break; // reconnect
|
|
496
|
+
* }
|
|
497
|
+
* ```
|
|
498
|
+
*
|
|
499
|
+
* Requires scope `audit:read`. Requires the `v2_decisions_stream`
|
|
500
|
+
* tenant feature flag (returns 404 when off).
|
|
501
|
+
*/
|
|
502
|
+
async *subscribeDecisions(opts = {}) {
|
|
503
|
+
const url = new URL(`${this.baseUrl}/v1-decisions-stream`);
|
|
504
|
+
if (opts.types?.length) url.searchParams.set("types", opts.types.join(","));
|
|
505
|
+
if (opts.actorId) url.searchParams.set("actor_id", opts.actorId);
|
|
506
|
+
if (opts.maxSeconds !== void 0) url.searchParams.set("max_seconds", String(opts.maxSeconds));
|
|
507
|
+
const headers = {
|
|
508
|
+
Accept: "text/event-stream",
|
|
509
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
510
|
+
"User-Agent": this.userAgent,
|
|
511
|
+
// ADR-025: declare the wire-protocol version we were built
|
|
512
|
+
// against. Runtime serves this version's response shape; older
|
|
513
|
+
// versions outside the compatibility window get 426.
|
|
514
|
+
"X-AtlaSent-Protocol-Version": "1"
|
|
515
|
+
};
|
|
516
|
+
if (opts.lastEventId) headers["Last-Event-ID"] = opts.lastEventId;
|
|
517
|
+
let response;
|
|
518
|
+
try {
|
|
519
|
+
response = await this.fetchImpl(url.toString(), {
|
|
520
|
+
method: "GET",
|
|
521
|
+
headers,
|
|
522
|
+
...opts.signal ? { signal: opts.signal } : {}
|
|
523
|
+
});
|
|
524
|
+
} catch (err) {
|
|
525
|
+
if (err instanceof Error && err.name === "AbortError") return;
|
|
526
|
+
throw new AtlaSentError(
|
|
527
|
+
`Failed to connect to decisions stream: ${err instanceof Error ? err.message : String(err)}`,
|
|
528
|
+
{ code: "network" }
|
|
529
|
+
);
|
|
530
|
+
}
|
|
531
|
+
if (!response.ok) {
|
|
532
|
+
const code = response.status === 401 ? "invalid_api_key" : "server_error";
|
|
533
|
+
throw new AtlaSentError(
|
|
534
|
+
`Decisions stream returned ${response.status}`,
|
|
535
|
+
{ code, status: response.status }
|
|
536
|
+
);
|
|
537
|
+
}
|
|
538
|
+
if (!response.body) {
|
|
539
|
+
throw new AtlaSentError("Decisions stream response has no body", { code: "bad_response" });
|
|
540
|
+
}
|
|
541
|
+
const reader = response.body.getReader();
|
|
542
|
+
const decoder = new TextDecoder("utf-8");
|
|
543
|
+
let buf = "";
|
|
544
|
+
try {
|
|
545
|
+
while (true) {
|
|
546
|
+
let chunk;
|
|
547
|
+
try {
|
|
548
|
+
chunk = await reader.read();
|
|
549
|
+
} catch (err) {
|
|
550
|
+
if (err instanceof Error && err.name === "AbortError") return;
|
|
551
|
+
throw new AtlaSentError(
|
|
552
|
+
`Decisions stream read error: ${err instanceof Error ? err.message : String(err)}`,
|
|
553
|
+
{ code: "network" }
|
|
554
|
+
);
|
|
555
|
+
}
|
|
556
|
+
if (chunk.done) break;
|
|
557
|
+
buf += decoder.decode(chunk.value, { stream: true });
|
|
558
|
+
const rawBlocks = buf.split("\n\n");
|
|
559
|
+
buf = rawBlocks.pop() ?? "";
|
|
560
|
+
for (const block of rawBlocks) {
|
|
561
|
+
if (!block.trim()) continue;
|
|
562
|
+
if (block.trimStart().startsWith(":")) {
|
|
563
|
+
yield { type: "heartbeat" };
|
|
564
|
+
continue;
|
|
565
|
+
}
|
|
566
|
+
let id;
|
|
567
|
+
let eventType = "audit_event";
|
|
568
|
+
let dataLine = "";
|
|
569
|
+
for (const line of block.split("\n")) {
|
|
570
|
+
if (line.startsWith("id:")) id = line.slice(3).trim();
|
|
571
|
+
else if (line.startsWith("event:")) eventType = line.slice(6).trim();
|
|
572
|
+
else if (line.startsWith("data:")) dataLine = line.slice(5).trim();
|
|
573
|
+
}
|
|
574
|
+
if (!dataLine) continue;
|
|
575
|
+
let parsed;
|
|
576
|
+
try {
|
|
577
|
+
parsed = JSON.parse(dataLine);
|
|
578
|
+
} catch {
|
|
579
|
+
continue;
|
|
580
|
+
}
|
|
581
|
+
if (eventType === "session_end") {
|
|
582
|
+
yield { ...id !== void 0 ? { id } : {}, type: "session_end", payload: parsed };
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
585
|
+
const decision = typeof parsed.decision === "string" ? parsed.decision.toLowerCase() : void 0;
|
|
586
|
+
yield {
|
|
587
|
+
...id !== void 0 ? { id } : {},
|
|
588
|
+
type: eventType,
|
|
589
|
+
...decision ? { decision } : {},
|
|
590
|
+
...typeof parsed.actor_id === "string" ? { actorId: parsed.actor_id } : {},
|
|
591
|
+
...typeof parsed.resource_type === "string" ? { resourceType: parsed.resource_type } : {},
|
|
592
|
+
...typeof parsed.resource_id === "string" ? { resourceId: parsed.resource_id } : {},
|
|
593
|
+
...parsed.payload && typeof parsed.payload === "object" ? { payload: parsed.payload } : {},
|
|
594
|
+
...typeof parsed.hash === "string" ? { hash: parsed.hash } : {},
|
|
595
|
+
...typeof parsed.previous_hash === "string" ? { previousHash: parsed.previous_hash } : {},
|
|
596
|
+
...typeof parsed.occurred_at === "string" ? { occurredAt: parsed.occurred_at } : {}
|
|
597
|
+
};
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
} finally {
|
|
601
|
+
reader.releaseLock();
|
|
602
|
+
}
|
|
603
|
+
}
|
|
395
604
|
/**
|
|
396
605
|
* Pre-flight evaluation that always returns the constraint trace.
|
|
397
606
|
*
|
|
@@ -458,7 +667,17 @@ var AtlaSentClient = class {
|
|
|
458
667
|
reason,
|
|
459
668
|
auditHash: wire.audit_hash ?? "",
|
|
460
669
|
timestamp: wire.timestamp ?? "",
|
|
461
|
-
rateLimit
|
|
670
|
+
rateLimit,
|
|
671
|
+
...wire.risk_envelope && {
|
|
672
|
+
riskEnvelope: {
|
|
673
|
+
weightedScore: wire.risk_envelope.weighted_score,
|
|
674
|
+
engineDecision: wire.risk_envelope.engine_decision,
|
|
675
|
+
envelopeDecision: wire.risk_envelope.envelope_decision,
|
|
676
|
+
promoted: wire.risk_envelope.promoted,
|
|
677
|
+
hardBlocks: wire.risk_envelope.hard_blocks ?? [],
|
|
678
|
+
...wire.risk_envelope.factors && { factors: wire.risk_envelope.factors }
|
|
679
|
+
}
|
|
680
|
+
}
|
|
462
681
|
};
|
|
463
682
|
let constraintTrace = null;
|
|
464
683
|
if (wire.constraint_trace !== void 0 && wire.constraint_trace !== null && typeof wire.constraint_trace === "object") {
|
|
@@ -507,6 +726,7 @@ var AtlaSentClient = class {
|
|
|
507
726
|
outcome: wire.outcome ?? "",
|
|
508
727
|
permitHash: wire.permit_hash ?? "",
|
|
509
728
|
timestamp: wire.timestamp ?? "",
|
|
729
|
+
expiresAt: wire.expires_at ?? null,
|
|
510
730
|
rateLimit
|
|
511
731
|
};
|
|
512
732
|
}
|
|
@@ -828,6 +1048,151 @@ var AtlaSentClient = class {
|
|
|
828
1048
|
}
|
|
829
1049
|
return { ...wire, rateLimit };
|
|
830
1050
|
}
|
|
1051
|
+
/**
|
|
1052
|
+
* Re-evaluate a recorded decision against its originally-pinned policy
|
|
1053
|
+
* bundle and engine version, and report whether the result agrees with
|
|
1054
|
+
* what was recorded.
|
|
1055
|
+
*
|
|
1056
|
+
* Wraps `POST /v1-decisions-replay/:id/replay`. **Side-effect-free** — no
|
|
1057
|
+
* audit chain row is written and no permit is issued (per ADR-016).
|
|
1058
|
+
* Useful for compliance review, regression testing of bundle changes,
|
|
1059
|
+
* and post-incident investigation.
|
|
1060
|
+
*
|
|
1061
|
+
* Outcomes encoded in the response:
|
|
1062
|
+
* - `variance: "NONE"` — replay agrees with the original decision.
|
|
1063
|
+
* - `variance: "DECISION_CHANGED"` — same envelope, same bundle, different
|
|
1064
|
+
* decision. Almost always indicates non-determinism in a rule
|
|
1065
|
+
* (e.g. wall-clock comparison) and warrants investigation.
|
|
1066
|
+
* - `variance: "ENVELOPE_DRIFT"` — the recorded request envelope no longer
|
|
1067
|
+
* hashes to the recorded value. The replay short-circuits without
|
|
1068
|
+
* running the engine; `replay_decision` is absent. Treat as evidence
|
|
1069
|
+
* of substrate tamper or a recorder bug.
|
|
1070
|
+
*
|
|
1071
|
+
* Server-side 409 responses (replay refused because the engine version
|
|
1072
|
+
* does not accept replay, or because no bundle was pinned) surface as
|
|
1073
|
+
* `AtlaSentError` with `code: "replay_not_eligible"` — callers should
|
|
1074
|
+
* treat them as expected for old / un-pinned decisions, not as bugs.
|
|
1075
|
+
*
|
|
1076
|
+
* Requires the `evaluate:write` API key scope.
|
|
1077
|
+
*
|
|
1078
|
+
* @param decisionId The UUID of the recorded decision to replay.
|
|
1079
|
+
* Matches `execution_evaluations.request_id`.
|
|
1080
|
+
*
|
|
1081
|
+
* @example
|
|
1082
|
+
* ```ts
|
|
1083
|
+
* const result = await client.replayDecision("dec_abc123");
|
|
1084
|
+
* if (result.variance === "DECISION_CHANGED") {
|
|
1085
|
+
* console.warn(
|
|
1086
|
+
* `Decision ${result.decision_id} changed on replay: ` +
|
|
1087
|
+
* `${result.original_decision} → ${result.replay_decision}`,
|
|
1088
|
+
* );
|
|
1089
|
+
* }
|
|
1090
|
+
* ```
|
|
1091
|
+
*/
|
|
1092
|
+
async replayDecision(decisionId) {
|
|
1093
|
+
if (typeof decisionId !== "string" || decisionId.length === 0) {
|
|
1094
|
+
throw new AtlaSentError("decisionId is required", {
|
|
1095
|
+
code: "bad_request"
|
|
1096
|
+
});
|
|
1097
|
+
}
|
|
1098
|
+
const path = `/v1-decisions-replay/${encodeURIComponent(decisionId)}/replay`;
|
|
1099
|
+
const { body: wire, rateLimit } = await this.post(
|
|
1100
|
+
path,
|
|
1101
|
+
{}
|
|
1102
|
+
);
|
|
1103
|
+
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") {
|
|
1104
|
+
throw new AtlaSentError(
|
|
1105
|
+
"Malformed response from /v1-decisions-replay/:id/replay: missing required fields",
|
|
1106
|
+
{ code: "bad_response" }
|
|
1107
|
+
);
|
|
1108
|
+
}
|
|
1109
|
+
return { ...wire, rateLimit };
|
|
1110
|
+
}
|
|
1111
|
+
/**
|
|
1112
|
+
* ADR-015 Phase C — SDK-canonical replay runtime.
|
|
1113
|
+
*
|
|
1114
|
+
* Re-evaluates a recorded decision against its originally-pinned policy
|
|
1115
|
+
* bundle and engine version via `POST /v1/decisions/:id/replay`.
|
|
1116
|
+
* Side-effect-free server-side: no audit chain row is written and no
|
|
1117
|
+
* permit is issued (ADR-016 `mode: "replay"` sentinel).
|
|
1118
|
+
*
|
|
1119
|
+
* Differences from {@link replayDecision} (the 2.7.0 raw-wire surface):
|
|
1120
|
+
*
|
|
1121
|
+
* | | `replayDecision()` | `replay()` |
|
|
1122
|
+
* | --- | --- | --- |
|
|
1123
|
+
* | Path | `/v1-decisions-replay/:id/replay` | `/v1/decisions/:id/replay` |
|
|
1124
|
+
* | Variance | raw wire (`DECISION_CHANGED`) | SDK-canonical (`POLICY_DRIFT`) |
|
|
1125
|
+
* | 409 handling | throws `AtlaSentError` | returns `ENGINE_DRIFT` / `BUNDLE_MISSING` |
|
|
1126
|
+
* | Input shape | `decisionId: string` | `{ evaluationId }` |
|
|
1127
|
+
*
|
|
1128
|
+
* **Never throws on `409 replay_not_eligible`** — instead returns a
|
|
1129
|
+
* `ReplayResponse` with `varianceKind: "ENGINE_DRIFT"` (engine retired
|
|
1130
|
+
* beyond archival window) or `"BUNDLE_MISSING"` (no bundle pinned on
|
|
1131
|
+
* the original evaluation). Callers can always `switch` on
|
|
1132
|
+
* `result.varianceKind` without a try/catch.
|
|
1133
|
+
*
|
|
1134
|
+
* Fix-forward note: this method was originally landed in PR #275 but
|
|
1135
|
+
* dropped from the squash merge. The TS types (`ReplayResponse`,
|
|
1136
|
+
* `ReplayRequest`) and CHANGELOG made it through; the method itself
|
|
1137
|
+
* did not. Restored here to match the Python {@link
|
|
1138
|
+
* AtlaSentClient}.replay() that landed in atlasent-sdk@2.6.0 (Python).
|
|
1139
|
+
*/
|
|
1140
|
+
async replay(input) {
|
|
1141
|
+
if (!input || typeof input.evaluationId !== "string" || input.evaluationId.length === 0) {
|
|
1142
|
+
throw new AtlaSentError("evaluationId is required", {
|
|
1143
|
+
code: "bad_request"
|
|
1144
|
+
});
|
|
1145
|
+
}
|
|
1146
|
+
const path = `/v1/decisions/${encodeURIComponent(input.evaluationId)}/replay`;
|
|
1147
|
+
let wire;
|
|
1148
|
+
let rateLimit;
|
|
1149
|
+
try {
|
|
1150
|
+
const result = await this.post(path, {});
|
|
1151
|
+
wire = result.body;
|
|
1152
|
+
rateLimit = result.rateLimit;
|
|
1153
|
+
} catch (err) {
|
|
1154
|
+
if (err instanceof AtlaSentError && err.status === 409) {
|
|
1155
|
+
const msg = (err.message ?? "").toLowerCase();
|
|
1156
|
+
const varianceKind2 = msg.includes("bundle") ? "BUNDLE_MISSING" : "ENGINE_DRIFT";
|
|
1157
|
+
return {
|
|
1158
|
+
decisionId: input.evaluationId,
|
|
1159
|
+
varianceKind: varianceKind2,
|
|
1160
|
+
originalDecision: "deny",
|
|
1161
|
+
acceptsReplay: false,
|
|
1162
|
+
replayedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1163
|
+
rateLimit: null
|
|
1164
|
+
};
|
|
1165
|
+
}
|
|
1166
|
+
throw err;
|
|
1167
|
+
}
|
|
1168
|
+
const VARIANCE_MAP = {
|
|
1169
|
+
NONE: "NONE",
|
|
1170
|
+
DECISION_CHANGED: "POLICY_DRIFT",
|
|
1171
|
+
ENVELOPE_DRIFT: "ENVELOPE_DRIFT",
|
|
1172
|
+
CHAIN_TAMPER: "CHAIN_TAMPER",
|
|
1173
|
+
BUNDLE_MISSING: "BUNDLE_MISSING",
|
|
1174
|
+
ENGINE_DRIFT: "ENGINE_DRIFT"
|
|
1175
|
+
};
|
|
1176
|
+
const rawVariance = typeof wire.variance === "string" ? wire.variance : "";
|
|
1177
|
+
const varianceKind = VARIANCE_MAP[rawVariance] ?? "NONE";
|
|
1178
|
+
const replayDec = typeof wire.replay_decision === "string" ? wire.replay_decision.toLowerCase() : void 0;
|
|
1179
|
+
const originalDec = typeof wire.original_decision === "string" ? wire.original_decision.toLowerCase() : "deny";
|
|
1180
|
+
const response = {
|
|
1181
|
+
decisionId: typeof wire.decision_id === "string" ? wire.decision_id : input.evaluationId,
|
|
1182
|
+
varianceKind,
|
|
1183
|
+
originalDecision: originalDec,
|
|
1184
|
+
acceptsReplay: typeof wire.accepts_replay === "boolean" ? wire.accepts_replay : true,
|
|
1185
|
+
replayedAt: typeof wire.replayed_at === "string" ? wire.replayed_at : (/* @__PURE__ */ new Date()).toISOString(),
|
|
1186
|
+
rateLimit
|
|
1187
|
+
};
|
|
1188
|
+
if (typeof wire.original_deny_code === "string") response.originalDenyCode = wire.original_deny_code;
|
|
1189
|
+
if (replayDec !== void 0) response.replayedDecision = replayDec;
|
|
1190
|
+
if (typeof wire.replay_deny_code === "string") response.replayedDenyCode = wire.replay_deny_code;
|
|
1191
|
+
if (typeof wire.engine_version === "string") response.engineVersion = wire.engine_version;
|
|
1192
|
+
if (typeof wire.engine_version_kind === "string") response.engineVersionKind = wire.engine_version_kind;
|
|
1193
|
+
if (typeof wire.envelope_verification === "string") response.envelopeVerification = wire.envelope_verification;
|
|
1194
|
+
return response;
|
|
1195
|
+
}
|
|
831
1196
|
/**
|
|
832
1197
|
* Open a streaming evaluation session against `POST /v1-evaluate-stream`.
|
|
833
1198
|
*
|
|
@@ -876,6 +1241,8 @@ var AtlaSentClient = class {
|
|
|
876
1241
|
"Content-Type": "application/json",
|
|
877
1242
|
Authorization: `Bearer ${this.apiKey}`,
|
|
878
1243
|
"User-Agent": this.userAgent,
|
|
1244
|
+
// ADR-025: wire-protocol version declared on every request.
|
|
1245
|
+
"X-AtlaSent-Protocol-Version": "1",
|
|
879
1246
|
"X-Request-ID": requestId
|
|
880
1247
|
};
|
|
881
1248
|
if (lastEventId !== void 0) {
|
|
@@ -963,7 +1330,9 @@ var AtlaSentClient = class {
|
|
|
963
1330
|
Accept: "application/json",
|
|
964
1331
|
Authorization: `Bearer ${this.apiKey}`,
|
|
965
1332
|
"User-Agent": this.userAgent,
|
|
966
|
-
"X-Request-ID": requestId
|
|
1333
|
+
"X-Request-ID": requestId,
|
|
1334
|
+
// ADR-025: wire-protocol version declared on every request.
|
|
1335
|
+
"X-AtlaSent-Protocol-Version": "1"
|
|
967
1336
|
};
|
|
968
1337
|
if (method === "POST") headers["Content-Type"] = "application/json";
|
|
969
1338
|
const bodyStr = method === "POST" ? JSON.stringify(body) : void 0;
|
|
@@ -1579,6 +1948,69 @@ var AtlaSentClient = class {
|
|
|
1579
1948
|
);
|
|
1580
1949
|
return body;
|
|
1581
1950
|
}
|
|
1951
|
+
// ── Constrained governance agents (read surface) ──────────────────────────
|
|
1952
|
+
//
|
|
1953
|
+
// Three GETs onto the v1-governance-agents edge function. Doctrine:
|
|
1954
|
+
// findings produced by these endpoints are advisory signal, never
|
|
1955
|
+
// authority. There is no `runGovernanceAgent` method on this client —
|
|
1956
|
+
// invocation belongs in CI (atlasent-action `governance-agents` mode),
|
|
1957
|
+
// not in application code.
|
|
1958
|
+
/**
|
|
1959
|
+
* List the advisory governance-agent registry for the calling org.
|
|
1960
|
+
*
|
|
1961
|
+
* Calls `GET /v1/governance/agents`. The registry is reference data
|
|
1962
|
+
* seeded at runtime-DB migration time; every row has
|
|
1963
|
+
* `authority_class = "advisory"` and `can_authorize = false` —
|
|
1964
|
+
* structural invariants enforced by the schema, not policy.
|
|
1965
|
+
*/
|
|
1966
|
+
async listGovernanceAgents() {
|
|
1967
|
+
const { body } = await this.get(
|
|
1968
|
+
"/v1/governance/agents"
|
|
1969
|
+
);
|
|
1970
|
+
return [...body.agents ?? []];
|
|
1971
|
+
}
|
|
1972
|
+
/**
|
|
1973
|
+
* List advisory findings emitted against one governed change.
|
|
1974
|
+
*
|
|
1975
|
+
* Calls `GET /v1/governance/findings?change_id=…[&agent_slug=…]`.
|
|
1976
|
+
* Returns the typed-finding rows in `created_at DESC` order, including
|
|
1977
|
+
* `routed_gate_id` when the finding→gate trigger linked them. Findings
|
|
1978
|
+
* with `can_authorize === false` (always) are advisory; rendering them
|
|
1979
|
+
* never satisfies a gate.
|
|
1980
|
+
*/
|
|
1981
|
+
async listGovernanceFindings(query) {
|
|
1982
|
+
if (!query?.change_id) {
|
|
1983
|
+
throw new AtlaSentError("change_id is required", { code: "bad_request" });
|
|
1984
|
+
}
|
|
1985
|
+
const params = new URLSearchParams({ change_id: query.change_id });
|
|
1986
|
+
if (query.agent_slug) params.set("agent_slug", query.agent_slug);
|
|
1987
|
+
const { body } = await this.get(
|
|
1988
|
+
"/v1/governance/findings",
|
|
1989
|
+
params
|
|
1990
|
+
);
|
|
1991
|
+
return [...body.findings ?? []];
|
|
1992
|
+
}
|
|
1993
|
+
/**
|
|
1994
|
+
* List agent run records against one governed change.
|
|
1995
|
+
*
|
|
1996
|
+
* Calls `GET /v1/governance/evaluations?change_id=…[&agent_slug=…]`.
|
|
1997
|
+
* Returns every persisted evaluation, including `failed` / `timeout`
|
|
1998
|
+
* runs and `completed` runs with zero findings — the latter is the
|
|
1999
|
+
* positive signal "the agent ran and found nothing", which the UI
|
|
2000
|
+
* surfaces as `clear`.
|
|
2001
|
+
*/
|
|
2002
|
+
async listGovernanceEvaluations(query) {
|
|
2003
|
+
if (!query?.change_id) {
|
|
2004
|
+
throw new AtlaSentError("change_id is required", { code: "bad_request" });
|
|
2005
|
+
}
|
|
2006
|
+
const params = new URLSearchParams({ change_id: query.change_id });
|
|
2007
|
+
if (query.agent_slug) params.set("agent_slug", query.agent_slug);
|
|
2008
|
+
const { body } = await this.get(
|
|
2009
|
+
"/v1/governance/evaluations",
|
|
2010
|
+
params
|
|
2011
|
+
);
|
|
2012
|
+
return [...body.evaluations ?? []];
|
|
2013
|
+
}
|
|
1582
2014
|
};
|
|
1583
2015
|
function parseRateLimitHeaders(headers) {
|
|
1584
2016
|
const rawLimit = headers.get("x-ratelimit-limit");
|
|
@@ -2005,6 +2437,174 @@ async function verifyBundle(pathOrBundle, options) {
|
|
|
2005
2437
|
return verifyAuditBundle(bundle, keys);
|
|
2006
2438
|
}
|
|
2007
2439
|
|
|
2440
|
+
// src/evidenceEngine.ts
|
|
2441
|
+
function buildWhyTrace(decision, reasons, trace) {
|
|
2442
|
+
if (!trace) {
|
|
2443
|
+
return {
|
|
2444
|
+
decision,
|
|
2445
|
+
summary: formatSummary(decision, reasons, void 0, void 0),
|
|
2446
|
+
policy_evaluations: [],
|
|
2447
|
+
total_stages_evaluated: 0
|
|
2448
|
+
};
|
|
2449
|
+
}
|
|
2450
|
+
const matchedPolicyId = typeof trace.matching_policy_id === "string" ? trace.matching_policy_id : void 0;
|
|
2451
|
+
let terminalStage;
|
|
2452
|
+
let totalStages = 0;
|
|
2453
|
+
const policyEvaluations = (trace.rules_evaluated ?? []).map((policy) => {
|
|
2454
|
+
const wasDecisive = matchedPolicyId === policy.policy_id;
|
|
2455
|
+
let foundTerminal = false;
|
|
2456
|
+
const stages = (policy.stages ?? []).map(
|
|
2457
|
+
(s, idx) => {
|
|
2458
|
+
totalStages++;
|
|
2459
|
+
const isLast = idx === (policy.stages?.length ?? 1) - 1;
|
|
2460
|
+
const candidateForTerminal = wasDecisive && !foundTerminal && (s.matched || isLast);
|
|
2461
|
+
let impact = "passing";
|
|
2462
|
+
if (candidateForTerminal) {
|
|
2463
|
+
impact = "terminal";
|
|
2464
|
+
foundTerminal = true;
|
|
2465
|
+
terminalStage = {
|
|
2466
|
+
stage: s.stage,
|
|
2467
|
+
...s.rule !== void 0 ? { rule: s.rule } : {},
|
|
2468
|
+
matched: s.matched,
|
|
2469
|
+
...s.detail !== void 0 ? { detail: s.detail } : {},
|
|
2470
|
+
impact: "terminal"
|
|
2471
|
+
};
|
|
2472
|
+
} else if (s.matched) {
|
|
2473
|
+
impact = "contributing";
|
|
2474
|
+
}
|
|
2475
|
+
return {
|
|
2476
|
+
stage: s.stage,
|
|
2477
|
+
...s.rule !== void 0 ? { rule: s.rule } : {},
|
|
2478
|
+
matched: s.matched,
|
|
2479
|
+
...s.detail !== void 0 ? { detail: s.detail } : {},
|
|
2480
|
+
impact
|
|
2481
|
+
};
|
|
2482
|
+
}
|
|
2483
|
+
);
|
|
2484
|
+
return {
|
|
2485
|
+
policy_id: policy.policy_id,
|
|
2486
|
+
decision: policy.decision,
|
|
2487
|
+
fingerprint: policy.fingerprint,
|
|
2488
|
+
...policy.risk_score !== void 0 ? { risk_score: policy.risk_score } : {},
|
|
2489
|
+
stages,
|
|
2490
|
+
was_decisive: wasDecisive
|
|
2491
|
+
};
|
|
2492
|
+
});
|
|
2493
|
+
return {
|
|
2494
|
+
decision,
|
|
2495
|
+
summary: formatSummary(decision, reasons, matchedPolicyId, terminalStage),
|
|
2496
|
+
...matchedPolicyId !== void 0 ? { matched_policy_id: matchedPolicyId } : {},
|
|
2497
|
+
policy_evaluations: policyEvaluations,
|
|
2498
|
+
...terminalStage !== void 0 ? { terminal_stage: terminalStage } : {},
|
|
2499
|
+
total_stages_evaluated: totalStages
|
|
2500
|
+
};
|
|
2501
|
+
}
|
|
2502
|
+
function formatSummary(decision, reasons, matchedPolicyId, terminalStage) {
|
|
2503
|
+
const reason0 = reasons.length > 0 ? reasons[0] : void 0;
|
|
2504
|
+
switch (decision) {
|
|
2505
|
+
case "allow":
|
|
2506
|
+
return reason0 ? `Allowed: ${reason0}` : "Allowed: all policy checks passed.";
|
|
2507
|
+
case "deny":
|
|
2508
|
+
if (reason0) return `Denied: ${reason0}`;
|
|
2509
|
+
if (terminalStage?.detail)
|
|
2510
|
+
return `Denied at stage "${terminalStage.stage}": ${terminalStage.detail}`;
|
|
2511
|
+
if (terminalStage)
|
|
2512
|
+
return `Denied at stage "${terminalStage.stage}".`;
|
|
2513
|
+
if (matchedPolicyId)
|
|
2514
|
+
return `Denied by policy ${matchedPolicyId}.`;
|
|
2515
|
+
return "Denied: policy check failed.";
|
|
2516
|
+
case "hold":
|
|
2517
|
+
return reason0 ? `Held for review: ${reason0}` : "Held pending human review.";
|
|
2518
|
+
case "escalate":
|
|
2519
|
+
return reason0 ? `Escalated: ${reason0}` : "Escalated to a human reviewer.";
|
|
2520
|
+
}
|
|
2521
|
+
}
|
|
2522
|
+
function sortedJSON(val) {
|
|
2523
|
+
if (val === null || val === void 0) return "null";
|
|
2524
|
+
if (typeof val === "number")
|
|
2525
|
+
return Number.isFinite(val) ? String(val) : "null";
|
|
2526
|
+
if (typeof val === "boolean") return val ? "true" : "false";
|
|
2527
|
+
if (typeof val === "string") return JSON.stringify(val);
|
|
2528
|
+
if (Array.isArray(val)) return "[" + val.map(sortedJSON).join(",") + "]";
|
|
2529
|
+
if (typeof val === "object") {
|
|
2530
|
+
const obj = val;
|
|
2531
|
+
return "{" + Object.keys(obj).sort().map((k) => JSON.stringify(k) + ":" + sortedJSON(obj[k])).join(",") + "}";
|
|
2532
|
+
}
|
|
2533
|
+
return "null";
|
|
2534
|
+
}
|
|
2535
|
+
function hexEncode(bytes) {
|
|
2536
|
+
return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
2537
|
+
}
|
|
2538
|
+
async function sha256Hex2(input) {
|
|
2539
|
+
const bytes = new TextEncoder().encode(input);
|
|
2540
|
+
if (typeof globalThis !== "undefined" && globalThis.crypto?.subtle?.digest) {
|
|
2541
|
+
const buf = await globalThis.crypto.subtle.digest("SHA-256", bytes);
|
|
2542
|
+
return hexEncode(new Uint8Array(buf));
|
|
2543
|
+
}
|
|
2544
|
+
try {
|
|
2545
|
+
const { createHash } = await import(
|
|
2546
|
+
/* @vite-ignore */
|
|
2547
|
+
/* webpackIgnore: true */
|
|
2548
|
+
"crypto"
|
|
2549
|
+
);
|
|
2550
|
+
return createHash("sha256").update(input, "utf8").digest("hex");
|
|
2551
|
+
} catch {
|
|
2552
|
+
return "";
|
|
2553
|
+
}
|
|
2554
|
+
}
|
|
2555
|
+
async function computeContextHash(context) {
|
|
2556
|
+
return sha256Hex2(sortedJSON(context));
|
|
2557
|
+
}
|
|
2558
|
+
function buildDecisionReceiptPayload(args) {
|
|
2559
|
+
return {
|
|
2560
|
+
receipt_id: args.receipt_id,
|
|
2561
|
+
evaluation_id: args.evaluation_id,
|
|
2562
|
+
org_id: args.org_id,
|
|
2563
|
+
decision: args.decision,
|
|
2564
|
+
action: args.action,
|
|
2565
|
+
actor: args.actor,
|
|
2566
|
+
resource_type: args.resource_type ?? null,
|
|
2567
|
+
resource_id: args.resource_id ?? null,
|
|
2568
|
+
reasons: Array.from(args.reasons),
|
|
2569
|
+
why_summary: args.why_summary,
|
|
2570
|
+
permit_id: args.permit_id ?? null,
|
|
2571
|
+
permit_hash: args.permit_hash ?? null,
|
|
2572
|
+
audit_hash: args.audit_hash,
|
|
2573
|
+
context_hash: args.context_hash,
|
|
2574
|
+
issued_at: args.issued_at,
|
|
2575
|
+
expires_at: args.expires_at ?? null
|
|
2576
|
+
};
|
|
2577
|
+
}
|
|
2578
|
+
function receiptSigningInput(payload) {
|
|
2579
|
+
return payload.receipt_id + "\n" + payload.issued_at + "\n" + JSON.stringify(payload);
|
|
2580
|
+
}
|
|
2581
|
+
async function signDecisionReceiptHmac(payload, secret) {
|
|
2582
|
+
const input = receiptSigningInput(payload);
|
|
2583
|
+
const keyBytes = new TextEncoder().encode(secret);
|
|
2584
|
+
const msgBytes = new TextEncoder().encode(input);
|
|
2585
|
+
if (typeof globalThis !== "undefined" && globalThis.crypto?.subtle) {
|
|
2586
|
+
const key = await globalThis.crypto.subtle.importKey(
|
|
2587
|
+
"raw",
|
|
2588
|
+
keyBytes,
|
|
2589
|
+
{ name: "HMAC", hash: "SHA-256" },
|
|
2590
|
+
false,
|
|
2591
|
+
["sign"]
|
|
2592
|
+
);
|
|
2593
|
+
const sig = await globalThis.crypto.subtle.sign("HMAC", key, msgBytes);
|
|
2594
|
+
return hexEncode(new Uint8Array(sig));
|
|
2595
|
+
}
|
|
2596
|
+
try {
|
|
2597
|
+
const { createHmac: createHmac2 } = await import(
|
|
2598
|
+
/* @vite-ignore */
|
|
2599
|
+
/* webpackIgnore: true */
|
|
2600
|
+
"crypto"
|
|
2601
|
+
);
|
|
2602
|
+
return createHmac2("sha256", secret).update(input).digest("hex");
|
|
2603
|
+
} catch {
|
|
2604
|
+
return "";
|
|
2605
|
+
}
|
|
2606
|
+
}
|
|
2607
|
+
|
|
2008
2608
|
// src/protect.ts
|
|
2009
2609
|
var sharedClient = null;
|
|
2010
2610
|
var overrides = {};
|
|
@@ -2035,6 +2635,7 @@ function getClient() {
|
|
|
2035
2635
|
sharedClient = new AtlaSentClient(options);
|
|
2036
2636
|
return sharedClient;
|
|
2037
2637
|
}
|
|
2638
|
+
var ACTION_TYPE_RE = /^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)+$/;
|
|
2038
2639
|
function wireDecisionToDenied(serverDecision) {
|
|
2039
2640
|
const lower = serverDecision.toLowerCase();
|
|
2040
2641
|
if (lower === "hold" || lower === "escalate") return lower;
|
|
@@ -2072,7 +2673,19 @@ async function computeExecutionHash(payload) {
|
|
|
2072
2673
|
return "";
|
|
2073
2674
|
}
|
|
2074
2675
|
}
|
|
2676
|
+
function generateReceiptId() {
|
|
2677
|
+
if (typeof globalThis !== "undefined" && typeof globalThis.crypto?.randomUUID === "function") {
|
|
2678
|
+
return globalThis.crypto.randomUUID();
|
|
2679
|
+
}
|
|
2680
|
+
return `rcpt_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;
|
|
2681
|
+
}
|
|
2075
2682
|
async function protect(request) {
|
|
2683
|
+
if (!ACTION_TYPE_RE.test(request.action)) {
|
|
2684
|
+
throw new AtlaSentError(
|
|
2685
|
+
`action must be in dot-notation format (e.g. "production.deploy"). Got: ${JSON.stringify(request.action)}`,
|
|
2686
|
+
{ code: "bad_request" }
|
|
2687
|
+
);
|
|
2688
|
+
}
|
|
2076
2689
|
const client = getClient();
|
|
2077
2690
|
const evaluation = await client.evaluate(request);
|
|
2078
2691
|
if (evaluation.decision !== "allow") {
|
|
@@ -2083,12 +2696,71 @@ async function protect(request) {
|
|
|
2083
2696
|
auditHash: evaluation.auditHash
|
|
2084
2697
|
});
|
|
2085
2698
|
}
|
|
2086
|
-
const environment = request.context?.environment
|
|
2087
|
-
|
|
2088
|
-
|
|
2699
|
+
const environment = request.context?.environment;
|
|
2700
|
+
if (!environment) {
|
|
2701
|
+
throw new AtlaSentError(
|
|
2702
|
+
'context.environment is required. Pass the environment where this action executes (e.g. "production", "staging").',
|
|
2703
|
+
{ code: "bad_request" }
|
|
2704
|
+
);
|
|
2705
|
+
}
|
|
2706
|
+
const evaluatePayload = {
|
|
2707
|
+
action_type: request.action,
|
|
2708
|
+
actor_id: request.agent,
|
|
2709
|
+
context: request.context ?? {}
|
|
2710
|
+
};
|
|
2711
|
+
const execution_hash = await computeExecutionHash(evaluatePayload);
|
|
2712
|
+
const verifyRequest = {
|
|
2713
|
+
permitId: evaluation.permitId,
|
|
2714
|
+
agent: request.agent,
|
|
2715
|
+
action: request.action,
|
|
2716
|
+
environment,
|
|
2717
|
+
...execution_hash ? { execution_hash } : {}
|
|
2718
|
+
};
|
|
2719
|
+
if (request.context !== void 0) verifyRequest.context = request.context;
|
|
2720
|
+
const verification = await client.verifyPermit(verifyRequest);
|
|
2721
|
+
if (!verification.verified) {
|
|
2722
|
+
const outcome = normalizePermitOutcome(verification.outcome);
|
|
2723
|
+
throw new AtlaSentDeniedError({
|
|
2724
|
+
decision: "deny",
|
|
2725
|
+
evaluationId: evaluation.permitId,
|
|
2726
|
+
reason: `Permit failed verification (${verification.outcome})`,
|
|
2727
|
+
auditHash: evaluation.auditHash,
|
|
2728
|
+
...outcome !== void 0 && { outcome }
|
|
2729
|
+
});
|
|
2730
|
+
}
|
|
2731
|
+
return {
|
|
2732
|
+
permitId: evaluation.permitId,
|
|
2733
|
+
permitHash: verification.permitHash,
|
|
2734
|
+
auditHash: evaluation.auditHash,
|
|
2735
|
+
reason: evaluation.reason,
|
|
2736
|
+
timestamp: verification.timestamp,
|
|
2737
|
+
permitExpiresAt: verification.expiresAt ?? null
|
|
2738
|
+
};
|
|
2739
|
+
}
|
|
2740
|
+
async function protectWithEvidence(request, opts = {}) {
|
|
2741
|
+
if (!ACTION_TYPE_RE.test(request.action)) {
|
|
2742
|
+
throw new AtlaSentError(
|
|
2743
|
+
`action must be in dot-notation format (e.g. "production.deploy"). Got: ${JSON.stringify(request.action)}`,
|
|
2744
|
+
{ code: "bad_request" }
|
|
2745
|
+
);
|
|
2746
|
+
}
|
|
2747
|
+
const client = getClient();
|
|
2748
|
+
const evaluation = await client.evaluate(request);
|
|
2749
|
+
if (evaluation.decision !== "allow") {
|
|
2750
|
+
throw new AtlaSentDeniedError({
|
|
2751
|
+
decision: wireDecisionToDenied(evaluation.decision),
|
|
2752
|
+
evaluationId: evaluation.permitId,
|
|
2753
|
+
reason: evaluation.reason,
|
|
2754
|
+
auditHash: evaluation.auditHash
|
|
2755
|
+
});
|
|
2756
|
+
}
|
|
2757
|
+
const environment = request.context?.environment;
|
|
2758
|
+
if (!environment) {
|
|
2759
|
+
throw new AtlaSentError(
|
|
2760
|
+
'context.environment is required. Pass the environment where this action executes (e.g. "production", "staging").',
|
|
2761
|
+
{ code: "bad_request" }
|
|
2089
2762
|
);
|
|
2090
|
-
|
|
2091
|
-
})();
|
|
2763
|
+
}
|
|
2092
2764
|
const evaluatePayload = {
|
|
2093
2765
|
action_type: request.action,
|
|
2094
2766
|
actor_id: request.agent,
|
|
@@ -2114,12 +2786,68 @@ async function protect(request) {
|
|
|
2114
2786
|
...outcome !== void 0 && { outcome }
|
|
2115
2787
|
});
|
|
2116
2788
|
}
|
|
2789
|
+
const contextHash = await computeContextHash(request.context ?? {});
|
|
2790
|
+
const whyTrace = buildWhyTrace(
|
|
2791
|
+
"allow",
|
|
2792
|
+
evaluation.reasons,
|
|
2793
|
+
opts.constraintTrace ?? null
|
|
2794
|
+
);
|
|
2795
|
+
const issuedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2796
|
+
const receiptId = generateReceiptId();
|
|
2797
|
+
const orgId = evaluation.permit?.orgId ?? "";
|
|
2798
|
+
const payload = buildDecisionReceiptPayload({
|
|
2799
|
+
receipt_id: receiptId,
|
|
2800
|
+
evaluation_id: evaluation.evaluationId,
|
|
2801
|
+
org_id: orgId,
|
|
2802
|
+
decision: "allow",
|
|
2803
|
+
action: request.action,
|
|
2804
|
+
actor: request.agent,
|
|
2805
|
+
resource_type: request.context?.resource_type ?? null,
|
|
2806
|
+
resource_id: request.context?.resource_id ?? null,
|
|
2807
|
+
reasons: evaluation.reasons,
|
|
2808
|
+
why_summary: whyTrace.summary,
|
|
2809
|
+
permit_id: evaluation.permitId,
|
|
2810
|
+
permit_hash: verification.permitHash,
|
|
2811
|
+
audit_hash: evaluation.auditHash,
|
|
2812
|
+
context_hash: contextHash,
|
|
2813
|
+
issued_at: issuedAt
|
|
2814
|
+
});
|
|
2815
|
+
let signature = null;
|
|
2816
|
+
let algorithm = "none";
|
|
2817
|
+
if (opts.signingSecret) {
|
|
2818
|
+
signature = await signDecisionReceiptHmac(payload, opts.signingSecret);
|
|
2819
|
+
algorithm = "hmac-sha256";
|
|
2820
|
+
}
|
|
2821
|
+
const receipt = {
|
|
2822
|
+
receipt_id: receiptId,
|
|
2823
|
+
evaluation_id: evaluation.evaluationId,
|
|
2824
|
+
org_id: orgId,
|
|
2825
|
+
decision: "allow",
|
|
2826
|
+
action: request.action,
|
|
2827
|
+
actor: request.agent,
|
|
2828
|
+
resource_type: request.context?.resource_type ?? null,
|
|
2829
|
+
resource_id: request.context?.resource_id ?? null,
|
|
2830
|
+
reasons: evaluation.reasons,
|
|
2831
|
+
why_trace: opts.constraintTrace !== void 0 ? whyTrace : null,
|
|
2832
|
+
permit_id: evaluation.permitId,
|
|
2833
|
+
permit_hash: verification.permitHash,
|
|
2834
|
+
audit_hash: evaluation.auditHash,
|
|
2835
|
+
context_hash: contextHash,
|
|
2836
|
+
issued_at: issuedAt,
|
|
2837
|
+
expires_at: null,
|
|
2838
|
+
algorithm,
|
|
2839
|
+
signature,
|
|
2840
|
+
signing_key_id: opts.signingKeyId ?? null,
|
|
2841
|
+
payload
|
|
2842
|
+
};
|
|
2117
2843
|
return {
|
|
2118
2844
|
permitId: evaluation.permitId,
|
|
2119
2845
|
permitHash: verification.permitHash,
|
|
2120
2846
|
auditHash: evaluation.auditHash,
|
|
2121
2847
|
reason: evaluation.reason,
|
|
2122
|
-
timestamp: verification.timestamp
|
|
2848
|
+
timestamp: verification.timestamp,
|
|
2849
|
+
permitExpiresAt: verification.expiresAt ?? null,
|
|
2850
|
+
receipt
|
|
2123
2851
|
};
|
|
2124
2852
|
}
|
|
2125
2853
|
|
|
@@ -3025,8 +3753,8 @@ async function _hmacSha256Hex(payload, secret) {
|
|
|
3025
3753
|
const sig = await crypto.subtle.sign("HMAC", key, enc.encode(payload));
|
|
3026
3754
|
return Array.from(new Uint8Array(sig)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
3027
3755
|
}
|
|
3028
|
-
const { createHmac } = await import("crypto");
|
|
3029
|
-
return
|
|
3756
|
+
const { createHmac: createHmac2 } = await import("crypto");
|
|
3757
|
+
return createHmac2("sha256", secret).update(payload).digest("hex");
|
|
3030
3758
|
}
|
|
3031
3759
|
async function verifyWebhook(payload, signature, secret) {
|
|
3032
3760
|
try {
|
|
@@ -3424,6 +4152,1391 @@ async function safeJson(response, path, requestId) {
|
|
|
3424
4152
|
}
|
|
3425
4153
|
}
|
|
3426
4154
|
|
|
4155
|
+
// src/approvalRuntime.ts
|
|
4156
|
+
var _runtimeConfig = {};
|
|
4157
|
+
function configureApprovalRuntime(config) {
|
|
4158
|
+
_runtimeConfig = { ..._runtimeConfig, ...config };
|
|
4159
|
+
}
|
|
4160
|
+
function resolveConfig(overrides2) {
|
|
4161
|
+
const apiKey = overrides2?.apiKey ?? _runtimeConfig.apiKey ?? (typeof process !== "undefined" && process.env ? process.env["ATLASENT_API_KEY"] : void 0);
|
|
4162
|
+
if (!apiKey) {
|
|
4163
|
+
throw new AtlaSentError(
|
|
4164
|
+
"ApprovalRuntime: no API key configured. Set ATLASENT_API_KEY or call configureApprovalRuntime({ apiKey }).",
|
|
4165
|
+
{ code: "invalid_api_key" }
|
|
4166
|
+
);
|
|
4167
|
+
}
|
|
4168
|
+
return {
|
|
4169
|
+
apiKey,
|
|
4170
|
+
baseUrl: overrides2?.baseUrl ?? _runtimeConfig.baseUrl ?? "https://api.atlasent.io",
|
|
4171
|
+
requestTimeoutMs: _runtimeConfig.timeoutMs ?? 3e4
|
|
4172
|
+
};
|
|
4173
|
+
}
|
|
4174
|
+
async function apiPost(path, body, cfg) {
|
|
4175
|
+
const url = `${cfg.baseUrl}${path}`;
|
|
4176
|
+
let resp;
|
|
4177
|
+
try {
|
|
4178
|
+
resp = await fetch(url, {
|
|
4179
|
+
method: "POST",
|
|
4180
|
+
headers: {
|
|
4181
|
+
Accept: "application/json",
|
|
4182
|
+
Authorization: `Bearer ${cfg.apiKey}`,
|
|
4183
|
+
"Content-Type": "application/json"
|
|
4184
|
+
},
|
|
4185
|
+
body: JSON.stringify(body),
|
|
4186
|
+
signal: AbortSignal.timeout(cfg.requestTimeoutMs)
|
|
4187
|
+
});
|
|
4188
|
+
} catch (err) {
|
|
4189
|
+
throw new AtlaSentError(
|
|
4190
|
+
`ApprovalRuntime: network error calling ${path}`,
|
|
4191
|
+
{ code: "network", cause: err }
|
|
4192
|
+
);
|
|
4193
|
+
}
|
|
4194
|
+
if (!resp.ok) {
|
|
4195
|
+
const text = await resp.text().catch(() => "");
|
|
4196
|
+
const code = resp.status === 401 ? "invalid_api_key" : resp.status === 403 ? "forbidden" : resp.status === 429 ? "rate_limited" : "server_error";
|
|
4197
|
+
throw new AtlaSentError(
|
|
4198
|
+
`ApprovalRuntime: API error ${resp.status} at ${path}: ${text.slice(0, 200)}`,
|
|
4199
|
+
{ code, status: resp.status }
|
|
4200
|
+
);
|
|
4201
|
+
}
|
|
4202
|
+
return resp.json();
|
|
4203
|
+
}
|
|
4204
|
+
async function apiGet(path, cfg) {
|
|
4205
|
+
const url = `${cfg.baseUrl}${path}`;
|
|
4206
|
+
let resp;
|
|
4207
|
+
try {
|
|
4208
|
+
resp = await fetch(url, {
|
|
4209
|
+
method: "GET",
|
|
4210
|
+
headers: {
|
|
4211
|
+
Accept: "application/json",
|
|
4212
|
+
Authorization: `Bearer ${cfg.apiKey}`
|
|
4213
|
+
},
|
|
4214
|
+
signal: AbortSignal.timeout(cfg.requestTimeoutMs)
|
|
4215
|
+
});
|
|
4216
|
+
} catch (err) {
|
|
4217
|
+
throw new AtlaSentError(
|
|
4218
|
+
`ApprovalRuntime: network error calling ${path}`,
|
|
4219
|
+
{ code: "network", cause: err }
|
|
4220
|
+
);
|
|
4221
|
+
}
|
|
4222
|
+
if (!resp.ok) {
|
|
4223
|
+
const text = await resp.text().catch(() => "");
|
|
4224
|
+
const code = resp.status === 401 ? "invalid_api_key" : resp.status === 403 ? "forbidden" : resp.status === 429 ? "rate_limited" : "server_error";
|
|
4225
|
+
throw new AtlaSentError(
|
|
4226
|
+
`ApprovalRuntime: API error ${resp.status} at ${path}: ${text.slice(0, 200)}`,
|
|
4227
|
+
{ code, status: resp.status }
|
|
4228
|
+
);
|
|
4229
|
+
}
|
|
4230
|
+
return resp.json();
|
|
4231
|
+
}
|
|
4232
|
+
function sleep2(ms) {
|
|
4233
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
4234
|
+
}
|
|
4235
|
+
var EscalationDeniedError = class extends Error {
|
|
4236
|
+
name = "EscalationDeniedError";
|
|
4237
|
+
escalationId;
|
|
4238
|
+
outcome;
|
|
4239
|
+
constructor(outcome) {
|
|
4240
|
+
super(
|
|
4241
|
+
`Escalation ${outcome.escalation.id} was rejected` + (outcome.resolutionNote ? `: ${outcome.resolutionNote}` : "")
|
|
4242
|
+
);
|
|
4243
|
+
this.escalationId = outcome.escalation.id;
|
|
4244
|
+
this.outcome = outcome;
|
|
4245
|
+
}
|
|
4246
|
+
};
|
|
4247
|
+
var EscalationTimeoutError = class extends Error {
|
|
4248
|
+
name = "EscalationTimeoutError";
|
|
4249
|
+
escalationId;
|
|
4250
|
+
outcome;
|
|
4251
|
+
constructor(outcome) {
|
|
4252
|
+
super(
|
|
4253
|
+
`Escalation ${outcome.escalation.id} timed out waiting for approval`
|
|
4254
|
+
);
|
|
4255
|
+
this.escalationId = outcome.escalation.id;
|
|
4256
|
+
this.outcome = outcome;
|
|
4257
|
+
}
|
|
4258
|
+
};
|
|
4259
|
+
async function createEscalation(opts) {
|
|
4260
|
+
const { apiKey, baseUrl, ...hitlBody } = opts;
|
|
4261
|
+
const cfg = resolveConfig({
|
|
4262
|
+
...apiKey !== void 0 ? { apiKey } : {},
|
|
4263
|
+
...baseUrl !== void 0 ? { baseUrl } : {}
|
|
4264
|
+
});
|
|
4265
|
+
const body = {
|
|
4266
|
+
agent_id: hitlBody.agent_id ?? "unknown",
|
|
4267
|
+
escalation_reason: hitlBody.escalation_reason ?? "Policy hold \u2014 awaiting human approval",
|
|
4268
|
+
...hitlBody
|
|
4269
|
+
};
|
|
4270
|
+
const escalation = await apiPost("/v1/hitl", body, cfg);
|
|
4271
|
+
return {
|
|
4272
|
+
escalationId: escalation.id,
|
|
4273
|
+
createdAt: escalation.created_at,
|
|
4274
|
+
timeoutAt: escalation.timeout_at ?? null,
|
|
4275
|
+
assignedToRole: escalation.assigned_to_role ?? null
|
|
4276
|
+
};
|
|
4277
|
+
}
|
|
4278
|
+
async function waitForEscalationApproval(opts) {
|
|
4279
|
+
const cfg = resolveConfig({
|
|
4280
|
+
...opts.apiKey !== void 0 ? { apiKey: opts.apiKey } : {},
|
|
4281
|
+
...opts.baseUrl !== void 0 ? { baseUrl: opts.baseUrl } : {}
|
|
4282
|
+
});
|
|
4283
|
+
const waitMs = opts.waitMs ?? 6e5;
|
|
4284
|
+
const pollIntervalMs = Math.max(opts.pollIntervalMs ?? 5e3, 1e3);
|
|
4285
|
+
const deadline = Date.now() + waitMs;
|
|
4286
|
+
const toOutcome = (escalation2) => {
|
|
4287
|
+
const terminal = escalation2.status === "approved" || escalation2.status === "rejected" || escalation2.status === "auto_approved" || escalation2.status === "timed_out";
|
|
4288
|
+
if (!terminal) return null;
|
|
4289
|
+
const status = escalation2.status === "approved" || escalation2.status === "auto_approved" ? "approved" : escalation2.status === "timed_out" ? "timed_out" : "rejected";
|
|
4290
|
+
return {
|
|
4291
|
+
status,
|
|
4292
|
+
escalation: escalation2,
|
|
4293
|
+
resolvedBy: escalation2.resolved_by ?? null,
|
|
4294
|
+
resolutionNote: escalation2.resolution_note ?? null,
|
|
4295
|
+
resolvedAt: escalation2.resolved_at ?? null
|
|
4296
|
+
};
|
|
4297
|
+
};
|
|
4298
|
+
while (Date.now() < deadline) {
|
|
4299
|
+
const escalation2 = await apiGet(
|
|
4300
|
+
`/v1/escalations/${opts.escalationId}`,
|
|
4301
|
+
cfg
|
|
4302
|
+
);
|
|
4303
|
+
const outcome2 = toOutcome(escalation2);
|
|
4304
|
+
if (outcome2) return outcome2;
|
|
4305
|
+
const remaining = deadline - Date.now();
|
|
4306
|
+
if (remaining <= 0) break;
|
|
4307
|
+
await sleep2(Math.min(pollIntervalMs, remaining));
|
|
4308
|
+
}
|
|
4309
|
+
const escalation = await apiGet(
|
|
4310
|
+
`/v1/escalations/${opts.escalationId}`,
|
|
4311
|
+
cfg
|
|
4312
|
+
);
|
|
4313
|
+
const outcome = toOutcome(escalation);
|
|
4314
|
+
if (outcome) return outcome;
|
|
4315
|
+
return {
|
|
4316
|
+
status: "timed_out",
|
|
4317
|
+
escalation,
|
|
4318
|
+
resolvedBy: null,
|
|
4319
|
+
resolutionNote: "Client-side wait timeout elapsed",
|
|
4320
|
+
resolvedAt: null
|
|
4321
|
+
};
|
|
4322
|
+
}
|
|
4323
|
+
async function protectOrEscalate(request, opts = {}) {
|
|
4324
|
+
try {
|
|
4325
|
+
const permit = await protect(request);
|
|
4326
|
+
return {
|
|
4327
|
+
...permit,
|
|
4328
|
+
escalationId: "",
|
|
4329
|
+
resolvedBy: null,
|
|
4330
|
+
resolutionNote: null,
|
|
4331
|
+
resolvedAt: permit.timestamp,
|
|
4332
|
+
approvalBasis: "direct_policy"
|
|
4333
|
+
};
|
|
4334
|
+
} catch (err) {
|
|
4335
|
+
if (!(err instanceof AtlaSentDeniedError) || err.decision !== "hold" && err.decision !== "escalate") {
|
|
4336
|
+
throw err;
|
|
4337
|
+
}
|
|
4338
|
+
}
|
|
4339
|
+
const proposedAction = opts.proposedAction ?? request.context;
|
|
4340
|
+
const handle = await createEscalation({
|
|
4341
|
+
agent_id: opts.agentId ?? request.agent,
|
|
4342
|
+
escalation_reason: opts.escalationReason ?? `Policy hold for "${request.action}" by "${request.agent}"`,
|
|
4343
|
+
...proposedAction !== void 0 ? { proposed_action: proposedAction } : {},
|
|
4344
|
+
...opts.riskScore !== void 0 ? { risk_score: opts.riskScore } : {},
|
|
4345
|
+
...opts.assignedToRole !== void 0 ? { assigned_to_role: opts.assignedToRole } : {},
|
|
4346
|
+
...opts.quorumRequired !== void 0 ? { quorum_required: opts.quorumRequired } : {},
|
|
4347
|
+
...opts.fallbackDecision !== void 0 ? { fallback_decision: opts.fallbackDecision } : {},
|
|
4348
|
+
...opts.timeoutAt !== void 0 ? { timeout_at: opts.timeoutAt } : {},
|
|
4349
|
+
...opts.metadata !== void 0 ? { metadata: opts.metadata } : {},
|
|
4350
|
+
...opts.apiKey !== void 0 ? { apiKey: opts.apiKey } : {},
|
|
4351
|
+
...opts.baseUrl !== void 0 ? { baseUrl: opts.baseUrl } : {}
|
|
4352
|
+
});
|
|
4353
|
+
opts.onEscalationCreated?.(handle);
|
|
4354
|
+
const outcome = await waitForEscalationApproval({
|
|
4355
|
+
escalationId: handle.escalationId,
|
|
4356
|
+
...opts.waitMs !== void 0 ? { waitMs: opts.waitMs } : {},
|
|
4357
|
+
...opts.pollIntervalMs !== void 0 ? { pollIntervalMs: opts.pollIntervalMs } : {},
|
|
4358
|
+
...opts.apiKey !== void 0 ? { apiKey: opts.apiKey } : {},
|
|
4359
|
+
...opts.baseUrl !== void 0 ? { baseUrl: opts.baseUrl } : {}
|
|
4360
|
+
});
|
|
4361
|
+
if (outcome.status === "rejected") throw new EscalationDeniedError(outcome);
|
|
4362
|
+
if (outcome.status === "timed_out") throw new EscalationTimeoutError(outcome);
|
|
4363
|
+
return {
|
|
4364
|
+
permitId: `escl_${handle.escalationId}`,
|
|
4365
|
+
permitHash: "",
|
|
4366
|
+
auditHash: outcome.escalation.id,
|
|
4367
|
+
reason: outcome.resolutionNote ?? "Approved by human reviewer",
|
|
4368
|
+
timestamp: outcome.resolvedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
4369
|
+
permitExpiresAt: null,
|
|
4370
|
+
escalationId: handle.escalationId,
|
|
4371
|
+
resolvedBy: outcome.resolvedBy,
|
|
4372
|
+
resolutionNote: outcome.resolutionNote,
|
|
4373
|
+
resolvedAt: outcome.resolvedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
4374
|
+
approvalBasis: "human_approval"
|
|
4375
|
+
};
|
|
4376
|
+
}
|
|
4377
|
+
async function requestOverride(opts) {
|
|
4378
|
+
const cfg = resolveConfig({
|
|
4379
|
+
...opts.apiKey !== void 0 ? { apiKey: opts.apiKey } : {},
|
|
4380
|
+
...opts.baseUrl !== void 0 ? { baseUrl: opts.baseUrl } : {}
|
|
4381
|
+
});
|
|
4382
|
+
const body = {
|
|
4383
|
+
reason: opts.reason,
|
|
4384
|
+
evaluationId: opts.evaluationId,
|
|
4385
|
+
...opts.ttlSeconds !== void 0 && { ttlSeconds: opts.ttlSeconds },
|
|
4386
|
+
...opts.metadata !== void 0 && { metadata: opts.metadata }
|
|
4387
|
+
};
|
|
4388
|
+
return apiPost("/v1/overrides", body, cfg);
|
|
4389
|
+
}
|
|
4390
|
+
|
|
4391
|
+
// src/actionContext.ts
|
|
4392
|
+
function buildActionContext(input) {
|
|
4393
|
+
const env = typeof input.environment === "string" ? { name: input.environment } : input.environment;
|
|
4394
|
+
const ctx = {
|
|
4395
|
+
actor: input.actor,
|
|
4396
|
+
...input.resource !== void 0 && { resource: input.resource },
|
|
4397
|
+
...env !== void 0 && { environment: env },
|
|
4398
|
+
...input.action_meta !== void 0 && { action_meta: input.action_meta },
|
|
4399
|
+
...input.history !== void 0 && { history: input.history },
|
|
4400
|
+
...input.extra ?? {}
|
|
4401
|
+
};
|
|
4402
|
+
if (env?.name !== void 0) ctx.environment_name = env.name;
|
|
4403
|
+
if (input.resource?.type !== void 0)
|
|
4404
|
+
ctx.resource_type = input.resource.type;
|
|
4405
|
+
if (input.resource?.id !== void 0) ctx.resource_id = input.resource.id;
|
|
4406
|
+
return ctx;
|
|
4407
|
+
}
|
|
4408
|
+
function getNestedValue(obj, path) {
|
|
4409
|
+
return path.split(".").reduce((cur, key) => {
|
|
4410
|
+
if (cur !== null && typeof cur === "object") {
|
|
4411
|
+
return cur[key];
|
|
4412
|
+
}
|
|
4413
|
+
return void 0;
|
|
4414
|
+
}, obj);
|
|
4415
|
+
}
|
|
4416
|
+
function validateActionContext(ctx, opts = {}) {
|
|
4417
|
+
const errors = [];
|
|
4418
|
+
const warnings = [];
|
|
4419
|
+
const ctxObj = ctx;
|
|
4420
|
+
if (ctx.actor !== void 0) {
|
|
4421
|
+
if (!ctx.actor.id || typeof ctx.actor.id !== "string") {
|
|
4422
|
+
errors.push({
|
|
4423
|
+
field: "actor.id",
|
|
4424
|
+
code: "required",
|
|
4425
|
+
message: "actor.id is required when actor is provided"
|
|
4426
|
+
});
|
|
4427
|
+
}
|
|
4428
|
+
}
|
|
4429
|
+
const hasEnvName = ctx.environment?.name !== void 0 || ctx.environment_name !== void 0;
|
|
4430
|
+
if (!hasEnvName) {
|
|
4431
|
+
warnings.push({
|
|
4432
|
+
field: "environment.name",
|
|
4433
|
+
code: "recommended",
|
|
4434
|
+
message: "environment.name is not set; protect() will default to 'production' with a console warning"
|
|
4435
|
+
});
|
|
4436
|
+
}
|
|
4437
|
+
if (!opts.skipCrossFieldChecks) {
|
|
4438
|
+
const amount = ctx.action_meta?.estimated_amount;
|
|
4439
|
+
if (typeof amount === "number" && amount > 0) {
|
|
4440
|
+
if (!ctx.action_meta?.currency) {
|
|
4441
|
+
errors.push({
|
|
4442
|
+
field: "action_meta.currency",
|
|
4443
|
+
code: "cross_field",
|
|
4444
|
+
message: "action_meta.currency is required when action_meta.estimated_amount > 0"
|
|
4445
|
+
});
|
|
4446
|
+
} else if (!/^[A-Z]{3}$/.test(ctx.action_meta.currency)) {
|
|
4447
|
+
errors.push({
|
|
4448
|
+
field: "action_meta.currency",
|
|
4449
|
+
code: "invalid_value",
|
|
4450
|
+
message: `action_meta.currency '${ctx.action_meta.currency}' is not a valid ISO 4217 code (expected 3 uppercase letters)`
|
|
4451
|
+
});
|
|
4452
|
+
}
|
|
4453
|
+
}
|
|
4454
|
+
}
|
|
4455
|
+
if (ctx.history?.last_action_at !== void 0) {
|
|
4456
|
+
const ts = new Date(ctx.history.last_action_at);
|
|
4457
|
+
if (isNaN(ts.getTime())) {
|
|
4458
|
+
errors.push({
|
|
4459
|
+
field: "history.last_action_at",
|
|
4460
|
+
code: "invalid_type",
|
|
4461
|
+
message: `history.last_action_at '${ctx.history.last_action_at}' is not a valid ISO-8601 timestamp`
|
|
4462
|
+
});
|
|
4463
|
+
}
|
|
4464
|
+
}
|
|
4465
|
+
const knownSensitivities = /* @__PURE__ */ new Set([
|
|
4466
|
+
"public",
|
|
4467
|
+
"internal",
|
|
4468
|
+
"confidential",
|
|
4469
|
+
"restricted"
|
|
4470
|
+
]);
|
|
4471
|
+
if (ctx.resource?.sensitivity !== void 0 && !knownSensitivities.has(ctx.resource.sensitivity)) {
|
|
4472
|
+
errors.push({
|
|
4473
|
+
field: "resource.sensitivity",
|
|
4474
|
+
code: "invalid_value",
|
|
4475
|
+
message: `resource.sensitivity '${ctx.resource.sensitivity}' is not one of: public, internal, confidential, restricted`
|
|
4476
|
+
});
|
|
4477
|
+
}
|
|
4478
|
+
for (const fieldPath of opts.requiredFields ?? []) {
|
|
4479
|
+
const value = getNestedValue(ctxObj, fieldPath);
|
|
4480
|
+
if (value === void 0 || value === null || value === "") {
|
|
4481
|
+
errors.push({
|
|
4482
|
+
field: fieldPath,
|
|
4483
|
+
code: "required",
|
|
4484
|
+
message: `${fieldPath} is required by the caller's validation rules`
|
|
4485
|
+
});
|
|
4486
|
+
}
|
|
4487
|
+
}
|
|
4488
|
+
return { valid: errors.length === 0, errors, warnings };
|
|
4489
|
+
}
|
|
4490
|
+
var DEFAULT_REDACTION_RULES = [
|
|
4491
|
+
{
|
|
4492
|
+
field: /password|passwd|passphrase/i,
|
|
4493
|
+
mode: "remove"
|
|
4494
|
+
},
|
|
4495
|
+
{
|
|
4496
|
+
field: /secret|private_key|client_secret|signing_secret/i,
|
|
4497
|
+
mode: "remove"
|
|
4498
|
+
},
|
|
4499
|
+
{
|
|
4500
|
+
field: /api_key|apikey|access_key|access_token/i,
|
|
4501
|
+
mode: "remove"
|
|
4502
|
+
},
|
|
4503
|
+
{
|
|
4504
|
+
field: /\btoken\b|auth_token|bearer/i,
|
|
4505
|
+
mode: "mask"
|
|
4506
|
+
},
|
|
4507
|
+
{
|
|
4508
|
+
field: /\bssn\b|social_security|tax_id|\bsin\b/i,
|
|
4509
|
+
mode: "remove"
|
|
4510
|
+
},
|
|
4511
|
+
{
|
|
4512
|
+
field: /credit_card|card_number|pan\b|cvv|cvc|expiry/i,
|
|
4513
|
+
mode: "remove"
|
|
4514
|
+
},
|
|
4515
|
+
{
|
|
4516
|
+
field: /\bemail\b/i,
|
|
4517
|
+
mode: "mask"
|
|
4518
|
+
},
|
|
4519
|
+
{
|
|
4520
|
+
field: /phone|mobile|cell\b/i,
|
|
4521
|
+
mode: "mask"
|
|
4522
|
+
},
|
|
4523
|
+
{
|
|
4524
|
+
field: /\bip\b|ip_address|remote_addr/i,
|
|
4525
|
+
mode: "mask"
|
|
4526
|
+
},
|
|
4527
|
+
{
|
|
4528
|
+
field: /dob|date_of_birth|birth_date|birthdate/i,
|
|
4529
|
+
mode: "remove"
|
|
4530
|
+
}
|
|
4531
|
+
];
|
|
4532
|
+
var MASK_PLACEHOLDER = "[REDACTED]";
|
|
4533
|
+
function matchesRule(key, rule) {
|
|
4534
|
+
if (typeof rule.field === "string") {
|
|
4535
|
+
return key.toLowerCase() === rule.field.toLowerCase();
|
|
4536
|
+
}
|
|
4537
|
+
return rule.field.test(key);
|
|
4538
|
+
}
|
|
4539
|
+
function redactValue(value, mode) {
|
|
4540
|
+
if (mode === "remove") return void 0;
|
|
4541
|
+
if (mode === "mask") return MASK_PLACEHOLDER;
|
|
4542
|
+
return "[HASHED]";
|
|
4543
|
+
}
|
|
4544
|
+
function redactObject(obj, rules, currentPath) {
|
|
4545
|
+
const result = {};
|
|
4546
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
4547
|
+
const fieldPath = currentPath ? `${currentPath}.${key}` : key;
|
|
4548
|
+
const matchingRule = rules.find(
|
|
4549
|
+
(r) => matchesRule(key, r) && (r.path === void 0 || r.path === fieldPath)
|
|
4550
|
+
);
|
|
4551
|
+
if (matchingRule) {
|
|
4552
|
+
const redacted = redactValue(value, matchingRule.mode);
|
|
4553
|
+
if (redacted !== void 0) result[key] = redacted;
|
|
4554
|
+
} else if (Array.isArray(value)) {
|
|
4555
|
+
result[key] = value.map(
|
|
4556
|
+
(item) => item !== null && typeof item === "object" && !Array.isArray(item) ? redactObject(item, rules, fieldPath) : item
|
|
4557
|
+
);
|
|
4558
|
+
} else if (value !== null && typeof value === "object") {
|
|
4559
|
+
result[key] = redactObject(
|
|
4560
|
+
value,
|
|
4561
|
+
rules,
|
|
4562
|
+
fieldPath
|
|
4563
|
+
);
|
|
4564
|
+
} else {
|
|
4565
|
+
result[key] = value;
|
|
4566
|
+
}
|
|
4567
|
+
}
|
|
4568
|
+
return result;
|
|
4569
|
+
}
|
|
4570
|
+
function redactContext(ctx, rules = DEFAULT_REDACTION_RULES) {
|
|
4571
|
+
return redactObject(
|
|
4572
|
+
ctx,
|
|
4573
|
+
rules,
|
|
4574
|
+
""
|
|
4575
|
+
);
|
|
4576
|
+
}
|
|
4577
|
+
function flattenActionContext(ctx) {
|
|
4578
|
+
const flat = {};
|
|
4579
|
+
for (const [key, value] of Object.entries(ctx)) {
|
|
4580
|
+
flat[key] = value;
|
|
4581
|
+
}
|
|
4582
|
+
const envName = ctx.environment?.name ?? ctx.environment_name;
|
|
4583
|
+
if (envName !== void 0) {
|
|
4584
|
+
flat["environment_name"] = envName;
|
|
4585
|
+
}
|
|
4586
|
+
return flat;
|
|
4587
|
+
}
|
|
4588
|
+
|
|
4589
|
+
// src/shadow.ts
|
|
4590
|
+
var _defaultConfig = {};
|
|
4591
|
+
function configureShadow(config) {
|
|
4592
|
+
_defaultConfig = { ..._defaultConfig, ...config };
|
|
4593
|
+
}
|
|
4594
|
+
async function protectShadow(request, opts) {
|
|
4595
|
+
const merged = { ..._defaultConfig, ...opts };
|
|
4596
|
+
const mode = merged.mode ?? "observe";
|
|
4597
|
+
if (mode === "enforce") {
|
|
4598
|
+
const start2 = Date.now();
|
|
4599
|
+
const permit = await protect(request);
|
|
4600
|
+
const outcome = {
|
|
4601
|
+
decision: "permit",
|
|
4602
|
+
permit,
|
|
4603
|
+
error: null,
|
|
4604
|
+
would_have_blocked: false,
|
|
4605
|
+
latencyMs: Date.now() - start2,
|
|
4606
|
+
evaluationId: permit.permitId,
|
|
4607
|
+
request,
|
|
4608
|
+
mode
|
|
4609
|
+
};
|
|
4610
|
+
await _notify(outcome, merged);
|
|
4611
|
+
return outcome;
|
|
4612
|
+
}
|
|
4613
|
+
const start = Date.now();
|
|
4614
|
+
try {
|
|
4615
|
+
const permit = await protect(request);
|
|
4616
|
+
const outcome = {
|
|
4617
|
+
decision: "permit",
|
|
4618
|
+
permit,
|
|
4619
|
+
error: null,
|
|
4620
|
+
would_have_blocked: false,
|
|
4621
|
+
latencyMs: Date.now() - start,
|
|
4622
|
+
evaluationId: permit.permitId,
|
|
4623
|
+
request,
|
|
4624
|
+
mode
|
|
4625
|
+
};
|
|
4626
|
+
await _notify(outcome, merged);
|
|
4627
|
+
if (merged.reportToApi) {
|
|
4628
|
+
void reportShadowEvent(outcome, merged).catch(() => void 0);
|
|
4629
|
+
}
|
|
4630
|
+
return outcome;
|
|
4631
|
+
} catch (err) {
|
|
4632
|
+
if (err instanceof AtlaSentDeniedError) {
|
|
4633
|
+
const outcome = {
|
|
4634
|
+
decision: err.decision,
|
|
4635
|
+
permit: null,
|
|
4636
|
+
error: err,
|
|
4637
|
+
would_have_blocked: true,
|
|
4638
|
+
latencyMs: Date.now() - start,
|
|
4639
|
+
evaluationId: err.evaluationId ?? null,
|
|
4640
|
+
request,
|
|
4641
|
+
mode
|
|
4642
|
+
};
|
|
4643
|
+
if (mode === "warn") {
|
|
4644
|
+
console.warn(
|
|
4645
|
+
`[AtlaSent shadow:warn] Action '${request.action}' would have been blocked (decision=${err.decision}, evaluationId=${err.evaluationId ?? "unknown"})`
|
|
4646
|
+
);
|
|
4647
|
+
}
|
|
4648
|
+
await _notify(outcome, merged);
|
|
4649
|
+
if (merged.reportToApi) {
|
|
4650
|
+
void reportShadowEvent(outcome, merged).catch(() => void 0);
|
|
4651
|
+
}
|
|
4652
|
+
return outcome;
|
|
4653
|
+
}
|
|
4654
|
+
throw err;
|
|
4655
|
+
}
|
|
4656
|
+
}
|
|
4657
|
+
async function _notify(outcome, config) {
|
|
4658
|
+
if (config.onOutcome) {
|
|
4659
|
+
try {
|
|
4660
|
+
await config.onOutcome(outcome);
|
|
4661
|
+
} catch {
|
|
4662
|
+
}
|
|
4663
|
+
}
|
|
4664
|
+
}
|
|
4665
|
+
async function reportShadowEvent(outcome, opts) {
|
|
4666
|
+
const apiKey = opts?.apiKey ?? _defaultConfig.apiKey ?? process.env["ATLASENT_API_KEY"] ?? "";
|
|
4667
|
+
const baseUrl = opts?.baseUrl ?? _defaultConfig.baseUrl ?? process.env["ATLASENT_BASE_URL"] ?? "https://api.atlasent.ai";
|
|
4668
|
+
const payload = {
|
|
4669
|
+
action: outcome.request.action,
|
|
4670
|
+
agentId: outcome.request.agent ?? null,
|
|
4671
|
+
decision: outcome.decision,
|
|
4672
|
+
would_have_blocked: outcome.would_have_blocked,
|
|
4673
|
+
latencyMs: outcome.latencyMs,
|
|
4674
|
+
evaluationId: outcome.evaluationId,
|
|
4675
|
+
mode: outcome.mode,
|
|
4676
|
+
...outcome.error ? { deniedReason: outcome.error.message } : {},
|
|
4677
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
4678
|
+
};
|
|
4679
|
+
const response = await fetch(`${baseUrl}/v1/shadow-events`, {
|
|
4680
|
+
method: "POST",
|
|
4681
|
+
headers: {
|
|
4682
|
+
"Content-Type": "application/json",
|
|
4683
|
+
Authorization: `Bearer ${apiKey}`
|
|
4684
|
+
},
|
|
4685
|
+
body: JSON.stringify(payload)
|
|
4686
|
+
});
|
|
4687
|
+
if (!response.ok && response.status >= 500) {
|
|
4688
|
+
throw new Error(`Shadow event reporting failed: ${response.status}`);
|
|
4689
|
+
}
|
|
4690
|
+
}
|
|
4691
|
+
|
|
4692
|
+
// src/controlSurface.ts
|
|
4693
|
+
var _config = {};
|
|
4694
|
+
function configureControlSurface(config) {
|
|
4695
|
+
_config = { ..._config, ...config };
|
|
4696
|
+
}
|
|
4697
|
+
function resolveConfig2(opts) {
|
|
4698
|
+
return {
|
|
4699
|
+
apiKey: opts?.apiKey ?? _config.apiKey ?? process.env["ATLASENT_API_KEY"] ?? "",
|
|
4700
|
+
baseUrl: opts?.baseUrl ?? _config.baseUrl ?? process.env["ATLASENT_BASE_URL"] ?? "https://api.atlasent.ai",
|
|
4701
|
+
timeoutMs: opts?.timeoutMs ?? _config.timeoutMs ?? 1e4
|
|
4702
|
+
};
|
|
4703
|
+
}
|
|
4704
|
+
async function apiGet2(path, config) {
|
|
4705
|
+
const controller = new AbortController();
|
|
4706
|
+
const timer = setTimeout(() => controller.abort(), config.timeoutMs);
|
|
4707
|
+
try {
|
|
4708
|
+
const res = await fetch(`${config.baseUrl}${path}`, {
|
|
4709
|
+
method: "GET",
|
|
4710
|
+
headers: {
|
|
4711
|
+
Authorization: `Bearer ${config.apiKey}`,
|
|
4712
|
+
Accept: "application/json"
|
|
4713
|
+
},
|
|
4714
|
+
signal: controller.signal
|
|
4715
|
+
});
|
|
4716
|
+
if (!res.ok) {
|
|
4717
|
+
throw new Error(`HTTP ${res.status}`);
|
|
4718
|
+
}
|
|
4719
|
+
return res.json();
|
|
4720
|
+
} finally {
|
|
4721
|
+
clearTimeout(timer);
|
|
4722
|
+
}
|
|
4723
|
+
}
|
|
4724
|
+
async function apiPost2(path, body, config) {
|
|
4725
|
+
const controller = new AbortController();
|
|
4726
|
+
const timer = setTimeout(() => controller.abort(), config.timeoutMs);
|
|
4727
|
+
try {
|
|
4728
|
+
const res = await fetch(`${config.baseUrl}${path}`, {
|
|
4729
|
+
method: "POST",
|
|
4730
|
+
headers: {
|
|
4731
|
+
"Content-Type": "application/json",
|
|
4732
|
+
Authorization: `Bearer ${config.apiKey}`
|
|
4733
|
+
},
|
|
4734
|
+
body: JSON.stringify(body),
|
|
4735
|
+
signal: controller.signal
|
|
4736
|
+
});
|
|
4737
|
+
if (!res.ok) {
|
|
4738
|
+
throw new Error(`HTTP ${res.status}`);
|
|
4739
|
+
}
|
|
4740
|
+
return res.json();
|
|
4741
|
+
} finally {
|
|
4742
|
+
clearTimeout(timer);
|
|
4743
|
+
}
|
|
4744
|
+
}
|
|
4745
|
+
async function checkIntegrationHealth(opts) {
|
|
4746
|
+
const config = resolveConfig2(opts);
|
|
4747
|
+
const errors = [];
|
|
4748
|
+
let apiReachable = false;
|
|
4749
|
+
let authenticated = false;
|
|
4750
|
+
let latencyMs = null;
|
|
4751
|
+
let apiVersion = null;
|
|
4752
|
+
if (!config.apiKey) {
|
|
4753
|
+
errors.push("ATLASENT_API_KEY is not configured");
|
|
4754
|
+
}
|
|
4755
|
+
const start = Date.now();
|
|
4756
|
+
try {
|
|
4757
|
+
const data = await apiGet2("/v1/health", config);
|
|
4758
|
+
latencyMs = Date.now() - start;
|
|
4759
|
+
apiReachable = true;
|
|
4760
|
+
apiVersion = data.version ?? null;
|
|
4761
|
+
if (data.status === "ok" || data.status === "healthy") {
|
|
4762
|
+
authenticated = true;
|
|
4763
|
+
} else {
|
|
4764
|
+
errors.push(`API health status: ${data.status ?? "unknown"}`);
|
|
4765
|
+
}
|
|
4766
|
+
} catch (err) {
|
|
4767
|
+
latencyMs = Date.now() - start;
|
|
4768
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
4769
|
+
if (message.includes("401") || message.includes("403")) {
|
|
4770
|
+
apiReachable = true;
|
|
4771
|
+
errors.push("API key is invalid or lacks required permissions");
|
|
4772
|
+
} else {
|
|
4773
|
+
errors.push(`API unreachable: ${message}`);
|
|
4774
|
+
}
|
|
4775
|
+
}
|
|
4776
|
+
return {
|
|
4777
|
+
healthy: apiReachable && authenticated && errors.length === 0,
|
|
4778
|
+
apiReachable,
|
|
4779
|
+
authenticated,
|
|
4780
|
+
latencyMs,
|
|
4781
|
+
apiVersion,
|
|
4782
|
+
checkedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4783
|
+
errors
|
|
4784
|
+
};
|
|
4785
|
+
}
|
|
4786
|
+
async function reportProtectedAction(opts) {
|
|
4787
|
+
const config = resolveConfig2(opts);
|
|
4788
|
+
return apiPost2(
|
|
4789
|
+
"/v1/control-surface/actions",
|
|
4790
|
+
{
|
|
4791
|
+
action_class: opts.actionClass,
|
|
4792
|
+
enforcement_mode: opts.enforcementMode ?? "observe",
|
|
4793
|
+
schema_id: opts.schemaId ?? null,
|
|
4794
|
+
tags: opts.tags ?? []
|
|
4795
|
+
},
|
|
4796
|
+
config
|
|
4797
|
+
);
|
|
4798
|
+
}
|
|
4799
|
+
async function getEnforcementStatus(opts) {
|
|
4800
|
+
const config = resolveConfig2(opts);
|
|
4801
|
+
return apiGet2(
|
|
4802
|
+
`/v1/control-surface/actions/${encodeURIComponent(opts.actionClass)}/status`,
|
|
4803
|
+
config
|
|
4804
|
+
);
|
|
4805
|
+
}
|
|
4806
|
+
async function getOrgSummary(opts) {
|
|
4807
|
+
const config = resolveConfig2(opts);
|
|
4808
|
+
return apiGet2("/v1/control-surface/summary", config);
|
|
4809
|
+
}
|
|
4810
|
+
|
|
4811
|
+
// src/verticals/deployGate.ts
|
|
4812
|
+
function resolveEnvActor() {
|
|
4813
|
+
return process.env["GITHUB_ACTOR"] ?? process.env["GITLAB_USER_LOGIN"] ?? process.env["CIRCLE_USERNAME"] ?? process.env["BITBUCKET_STEP_TRIGGERER_UUID"] ?? void 0;
|
|
4814
|
+
}
|
|
4815
|
+
function resolveEnvSha() {
|
|
4816
|
+
return process.env["GITHUB_SHA"] ?? process.env["CI_COMMIT_SHA"] ?? process.env["CIRCLE_SHA1"] ?? process.env["BITBUCKET_COMMIT"] ?? void 0;
|
|
4817
|
+
}
|
|
4818
|
+
function resolveEnvWorkflow() {
|
|
4819
|
+
return process.env["GITHUB_WORKFLOW"] ?? process.env["CI_PIPELINE_NAME"] ?? process.env["CIRCLE_WORKFLOW_NAME"] ?? void 0;
|
|
4820
|
+
}
|
|
4821
|
+
async function protectDeploy(opts) {
|
|
4822
|
+
const actorId = opts.actorId ?? resolveEnvActor() ?? "ci-system";
|
|
4823
|
+
const sha = opts.sha ?? resolveEnvSha();
|
|
4824
|
+
const workflow = opts.workflow ?? resolveEnvWorkflow();
|
|
4825
|
+
const environment = opts.environment ?? "production";
|
|
4826
|
+
const isProduction = environment === "production";
|
|
4827
|
+
const ctx = buildActionContext({
|
|
4828
|
+
actor: {
|
|
4829
|
+
id: actorId,
|
|
4830
|
+
type: "service_account",
|
|
4831
|
+
...opts.actorLabel !== void 0 ? { label: opts.actorLabel } : {}
|
|
4832
|
+
},
|
|
4833
|
+
resource: {
|
|
4834
|
+
id: opts.service,
|
|
4835
|
+
type: opts.resourceType ?? "service"
|
|
4836
|
+
},
|
|
4837
|
+
environment,
|
|
4838
|
+
action_meta: {
|
|
4839
|
+
risk_level: isProduction ? "critical" : "medium",
|
|
4840
|
+
reversibility: "partial",
|
|
4841
|
+
...opts.description !== void 0 ? { description: opts.description } : sha !== void 0 ? { description: `Deploy ${sha.slice(0, 8)} to ${environment}` } : {}
|
|
4842
|
+
},
|
|
4843
|
+
extra: {
|
|
4844
|
+
sha,
|
|
4845
|
+
workflow
|
|
4846
|
+
}
|
|
4847
|
+
});
|
|
4848
|
+
const request = {
|
|
4849
|
+
action: "production.deploy",
|
|
4850
|
+
agent: actorId,
|
|
4851
|
+
context: flattenActionContext(ctx)
|
|
4852
|
+
};
|
|
4853
|
+
if (opts.requireApproval || isProduction) {
|
|
4854
|
+
return protectOrEscalate(request, {
|
|
4855
|
+
escalationReason: `Production deployment of ${opts.service} requires human approval`,
|
|
4856
|
+
assignedToRole: opts.assignedToRole ?? "release-manager",
|
|
4857
|
+
waitMs: opts.waitMs ?? 30 * 60 * 1e3,
|
|
4858
|
+
...opts.onEscalationCreated !== void 0 ? { onEscalationCreated: opts.onEscalationCreated } : {},
|
|
4859
|
+
...opts.apiKey !== void 0 ? { apiKey: opts.apiKey } : {},
|
|
4860
|
+
...opts.baseUrl !== void 0 ? { baseUrl: opts.baseUrl } : {}
|
|
4861
|
+
});
|
|
4862
|
+
}
|
|
4863
|
+
return protect(request);
|
|
4864
|
+
}
|
|
4865
|
+
|
|
4866
|
+
// src/verticals/closeGovernance.ts
|
|
4867
|
+
var ACTION_RISK = {
|
|
4868
|
+
"period.close": "critical",
|
|
4869
|
+
"period.reopen": "critical",
|
|
4870
|
+
"reconciliation.lock": "high",
|
|
4871
|
+
"data.export": "high"
|
|
4872
|
+
};
|
|
4873
|
+
var ACTION_REVERSIBILITY = {
|
|
4874
|
+
"period.close": "partial",
|
|
4875
|
+
"period.reopen": "partial",
|
|
4876
|
+
"reconciliation.lock": "reversible",
|
|
4877
|
+
"data.export": "irreversible"
|
|
4878
|
+
};
|
|
4879
|
+
async function protectCloseAction(opts) {
|
|
4880
|
+
const ctx = buildActionContext({
|
|
4881
|
+
actor: {
|
|
4882
|
+
id: opts.closedBy,
|
|
4883
|
+
type: "human",
|
|
4884
|
+
trust_level: "medium"
|
|
4885
|
+
},
|
|
4886
|
+
resource: {
|
|
4887
|
+
id: opts.entityId,
|
|
4888
|
+
type: "accounting_entity",
|
|
4889
|
+
sensitivity: opts.dataClassification ?? "confidential",
|
|
4890
|
+
...opts.entityName !== void 0 ? { name: opts.entityName } : {}
|
|
4891
|
+
},
|
|
4892
|
+
environment: "production",
|
|
4893
|
+
action_meta: {
|
|
4894
|
+
risk_level: ACTION_RISK[opts.action],
|
|
4895
|
+
reversibility: ACTION_REVERSIBILITY[opts.action],
|
|
4896
|
+
description: opts.description ?? `${opts.action} for period ${opts.periodLabel} on entity ${opts.entityId}`
|
|
4897
|
+
},
|
|
4898
|
+
extra: {
|
|
4899
|
+
period_label: opts.periodLabel,
|
|
4900
|
+
close_action: opts.action
|
|
4901
|
+
}
|
|
4902
|
+
});
|
|
4903
|
+
return protectOrEscalate(
|
|
4904
|
+
{
|
|
4905
|
+
action: opts.action,
|
|
4906
|
+
agent: opts.closedBy,
|
|
4907
|
+
context: flattenActionContext(ctx)
|
|
4908
|
+
},
|
|
4909
|
+
{
|
|
4910
|
+
escalationReason: `Accounting ${opts.action} for period '${opts.periodLabel}' requires approval`,
|
|
4911
|
+
assignedToRole: opts.assignedToRole ?? "controller",
|
|
4912
|
+
quorumRequired: opts.requireDualApproval ?? opts.action === "period.close" ? "simple_majority" : "single_approver",
|
|
4913
|
+
waitMs: opts.waitMs ?? 24 * 60 * 60 * 1e3,
|
|
4914
|
+
...opts.onEscalationCreated !== void 0 ? { onEscalationCreated: opts.onEscalationCreated } : {},
|
|
4915
|
+
...opts.apiKey !== void 0 ? { apiKey: opts.apiKey } : {},
|
|
4916
|
+
...opts.baseUrl !== void 0 ? { baseUrl: opts.baseUrl } : {}
|
|
4917
|
+
}
|
|
4918
|
+
);
|
|
4919
|
+
}
|
|
4920
|
+
|
|
4921
|
+
// src/verticals/paymentRelease.ts
|
|
4922
|
+
var ISO_4217 = /^[A-Z]{3}$/;
|
|
4923
|
+
async function protectPaymentRelease(opts) {
|
|
4924
|
+
if (!ISO_4217.test(opts.currency)) {
|
|
4925
|
+
throw new TypeError(
|
|
4926
|
+
`Invalid currency code '${opts.currency}': must be a 3-letter ISO 4217 code (e.g. USD, EUR, GBP)`
|
|
4927
|
+
);
|
|
4928
|
+
}
|
|
4929
|
+
if (opts.amount <= 0) {
|
|
4930
|
+
throw new RangeError(`Payment amount must be greater than 0, got ${opts.amount}`);
|
|
4931
|
+
}
|
|
4932
|
+
const escalateThreshold = opts.autoEscalateAbove ?? 1e4;
|
|
4933
|
+
const dualThreshold = opts.requireDualApprovalAbove ?? 1e5;
|
|
4934
|
+
const needsEscalation = opts.amount > escalateThreshold;
|
|
4935
|
+
const needsDual = opts.amount > dualThreshold;
|
|
4936
|
+
const ctx = buildActionContext({
|
|
4937
|
+
actor: {
|
|
4938
|
+
id: opts.authorizedBy,
|
|
4939
|
+
type: "human"
|
|
4940
|
+
},
|
|
4941
|
+
resource: {
|
|
4942
|
+
id: opts.vendorId,
|
|
4943
|
+
type: "vendor",
|
|
4944
|
+
...opts.vendorName !== void 0 ? { name: opts.vendorName } : {}
|
|
4945
|
+
},
|
|
4946
|
+
environment: "production",
|
|
4947
|
+
action_meta: {
|
|
4948
|
+
risk_level: opts.amount > dualThreshold ? "critical" : opts.amount > escalateThreshold ? "high" : "medium",
|
|
4949
|
+
reversibility: "irreversible",
|
|
4950
|
+
estimated_amount: opts.amount,
|
|
4951
|
+
currency: opts.currency,
|
|
4952
|
+
description: opts.description ?? `Release ${opts.currency} ${opts.amount.toLocaleString()} to ${opts.vendorName ?? opts.vendorId}`
|
|
4953
|
+
},
|
|
4954
|
+
extra: {
|
|
4955
|
+
reference: opts.reference
|
|
4956
|
+
}
|
|
4957
|
+
});
|
|
4958
|
+
const request = {
|
|
4959
|
+
action: "payment.release",
|
|
4960
|
+
agent: opts.authorizedBy,
|
|
4961
|
+
context: flattenActionContext(ctx)
|
|
4962
|
+
};
|
|
4963
|
+
if (needsEscalation) {
|
|
4964
|
+
return protectOrEscalate(request, {
|
|
4965
|
+
escalationReason: `Payment of ${opts.currency} ${opts.amount.toLocaleString()} to ${opts.vendorName ?? opts.vendorId} exceeds auto-approval threshold of ${opts.currency} ${escalateThreshold.toLocaleString()}`,
|
|
4966
|
+
assignedToRole: opts.assignedToRole ?? "finance-approver",
|
|
4967
|
+
quorumRequired: needsDual ? "simple_majority" : "single_approver",
|
|
4968
|
+
waitMs: opts.waitMs ?? 4 * 60 * 60 * 1e3,
|
|
4969
|
+
...opts.onEscalationCreated !== void 0 ? { onEscalationCreated: opts.onEscalationCreated } : {},
|
|
4970
|
+
...opts.apiKey !== void 0 ? { apiKey: opts.apiKey } : {},
|
|
4971
|
+
...opts.baseUrl !== void 0 ? { baseUrl: opts.baseUrl } : {}
|
|
4972
|
+
});
|
|
4973
|
+
}
|
|
4974
|
+
return protect(request);
|
|
4975
|
+
}
|
|
4976
|
+
|
|
4977
|
+
// src/verticals/agentTools.ts
|
|
4978
|
+
var HIGH_RISK_TOOLS = /* @__PURE__ */ new Set([
|
|
4979
|
+
"bash",
|
|
4980
|
+
"shell",
|
|
4981
|
+
"exec",
|
|
4982
|
+
"execute_code",
|
|
4983
|
+
"run_command",
|
|
4984
|
+
"write_file",
|
|
4985
|
+
"delete_file",
|
|
4986
|
+
"overwrite_file",
|
|
4987
|
+
"sql_execute",
|
|
4988
|
+
"db_write",
|
|
4989
|
+
"db_delete",
|
|
4990
|
+
"send_email",
|
|
4991
|
+
"send_message",
|
|
4992
|
+
"post_to_slack",
|
|
4993
|
+
"create_pr",
|
|
4994
|
+
"merge_pr",
|
|
4995
|
+
"push_code",
|
|
4996
|
+
"deploy",
|
|
4997
|
+
"release",
|
|
4998
|
+
"make_payment",
|
|
4999
|
+
"transfer_funds",
|
|
5000
|
+
"create_user",
|
|
5001
|
+
"delete_user",
|
|
5002
|
+
"modify_permissions",
|
|
5003
|
+
"aws_cli",
|
|
5004
|
+
"gcloud",
|
|
5005
|
+
"kubectl"
|
|
5006
|
+
]);
|
|
5007
|
+
var CRITICAL_TOOLS = /* @__PURE__ */ new Set([
|
|
5008
|
+
"bash",
|
|
5009
|
+
"shell",
|
|
5010
|
+
"exec",
|
|
5011
|
+
"execute_code",
|
|
5012
|
+
"run_command",
|
|
5013
|
+
"delete_file",
|
|
5014
|
+
"db_delete",
|
|
5015
|
+
"make_payment",
|
|
5016
|
+
"transfer_funds",
|
|
5017
|
+
"delete_user",
|
|
5018
|
+
"modify_permissions",
|
|
5019
|
+
"deploy",
|
|
5020
|
+
"release"
|
|
5021
|
+
]);
|
|
5022
|
+
function classifyToolRisk(toolName) {
|
|
5023
|
+
const normalized = toolName.toLowerCase().replace(/[^a-z0-9_]/g, "_");
|
|
5024
|
+
if (CRITICAL_TOOLS.has(normalized)) return "critical";
|
|
5025
|
+
if (HIGH_RISK_TOOLS.has(normalized)) return "high";
|
|
5026
|
+
if (normalized.includes("write") || normalized.includes("create") || normalized.includes("update")) return "medium";
|
|
5027
|
+
return "low";
|
|
5028
|
+
}
|
|
5029
|
+
async function protectToolCall(opts) {
|
|
5030
|
+
const inferredRisk = opts.riskLevel ?? classifyToolRisk(opts.toolName);
|
|
5031
|
+
const mode = opts.mode ?? (inferredRisk === "low" ? "enforce" : "escalate");
|
|
5032
|
+
const ctx = buildActionContext({
|
|
5033
|
+
actor: {
|
|
5034
|
+
id: opts.agentId,
|
|
5035
|
+
type: "agent",
|
|
5036
|
+
...opts.sessionId !== void 0 ? { session_id: opts.sessionId } : {}
|
|
5037
|
+
},
|
|
5038
|
+
resource: {
|
|
5039
|
+
type: "agent_tool",
|
|
5040
|
+
id: opts.toolName
|
|
5041
|
+
},
|
|
5042
|
+
environment: "production",
|
|
5043
|
+
action_meta: {
|
|
5044
|
+
risk_level: inferredRisk,
|
|
5045
|
+
reversibility: inferredRisk === "critical" || inferredRisk === "high" ? "irreversible" : "reversible",
|
|
5046
|
+
description: opts.description ?? `Agent ${opts.agentId} calling tool '${opts.toolName}'`
|
|
5047
|
+
},
|
|
5048
|
+
extra: {
|
|
5049
|
+
tool_args_keys: Object.keys(opts.toolArgs),
|
|
5050
|
+
session_id: opts.sessionId
|
|
5051
|
+
}
|
|
5052
|
+
});
|
|
5053
|
+
const request = {
|
|
5054
|
+
action: `agent_tool.${opts.toolName}`,
|
|
5055
|
+
agent: opts.agentId,
|
|
5056
|
+
context: flattenActionContext(ctx)
|
|
5057
|
+
};
|
|
5058
|
+
if (mode === "observe") {
|
|
5059
|
+
return protectShadow(request, { mode: "observe" });
|
|
5060
|
+
}
|
|
5061
|
+
if (mode === "escalate" || inferredRisk === "critical") {
|
|
5062
|
+
return protectOrEscalate(request, {
|
|
5063
|
+
escalationReason: `Agent '${opts.agentId}' is calling ${inferredRisk}-risk tool '${opts.toolName}'`,
|
|
5064
|
+
assignedToRole: opts.assignedToRole ?? "agent-supervisor",
|
|
5065
|
+
riskScore: inferredRisk === "critical" ? 1 : inferredRisk === "high" ? 0.75 : 0.5,
|
|
5066
|
+
waitMs: opts.waitMs ?? 15 * 60 * 1e3,
|
|
5067
|
+
...opts.onEscalationCreated !== void 0 ? { onEscalationCreated: opts.onEscalationCreated } : {},
|
|
5068
|
+
...opts.apiKey !== void 0 ? { apiKey: opts.apiKey } : {},
|
|
5069
|
+
...opts.baseUrl !== void 0 ? { baseUrl: opts.baseUrl } : {}
|
|
5070
|
+
});
|
|
5071
|
+
}
|
|
5072
|
+
return protect(request);
|
|
5073
|
+
}
|
|
5074
|
+
|
|
5075
|
+
// src/claimLineage.ts
|
|
5076
|
+
import { createHmac, randomUUID } from "crypto";
|
|
5077
|
+
var NOT_APPLICABLE = { notApplicable: true };
|
|
5078
|
+
function isNotApplicable(v) {
|
|
5079
|
+
return typeof v === "object" && v !== null && v.notApplicable === true;
|
|
5080
|
+
}
|
|
5081
|
+
var SDK_VERSION2 = "@atlasent/sdk@1.4.2";
|
|
5082
|
+
function canonicalize(value) {
|
|
5083
|
+
if (value === null || value === void 0) return "null";
|
|
5084
|
+
if (typeof value === "number") return Number.isFinite(value) ? String(value) : "null";
|
|
5085
|
+
if (typeof value === "boolean") return value ? "true" : "false";
|
|
5086
|
+
if (typeof value === "string") return JSON.stringify(value);
|
|
5087
|
+
if (Array.isArray(value)) return "[" + value.map(canonicalize).join(",") + "]";
|
|
5088
|
+
if (typeof value === "object") {
|
|
5089
|
+
const obj = value;
|
|
5090
|
+
const keys = Object.keys(obj).sort();
|
|
5091
|
+
return "{" + keys.map((k) => JSON.stringify(k) + ":" + canonicalize(obj[k])).join(",") + "}";
|
|
5092
|
+
}
|
|
5093
|
+
return "null";
|
|
5094
|
+
}
|
|
5095
|
+
function sha256Hex3(input) {
|
|
5096
|
+
const { createHash } = __require("crypto");
|
|
5097
|
+
return createHash("sha256").update(input).digest("hex");
|
|
5098
|
+
}
|
|
5099
|
+
function hmacSha256Base64url(payload, secret) {
|
|
5100
|
+
return createHmac("sha256", secret).update(payload).digest("base64url");
|
|
5101
|
+
}
|
|
5102
|
+
function computeLinkHash(link) {
|
|
5103
|
+
return sha256Hex3(canonicalize(link));
|
|
5104
|
+
}
|
|
5105
|
+
function slotStatus(input, slot) {
|
|
5106
|
+
if (isNotApplicable(input)) return "not_applicable";
|
|
5107
|
+
if (slot !== null) return "present";
|
|
5108
|
+
return "missing";
|
|
5109
|
+
}
|
|
5110
|
+
function toDeploySlot(input) {
|
|
5111
|
+
if (input === void 0 || isNotApplicable(input)) return null;
|
|
5112
|
+
return {
|
|
5113
|
+
deploy_id: input.deploy_id,
|
|
5114
|
+
environment: input.environment,
|
|
5115
|
+
sha: input.sha,
|
|
5116
|
+
actor_id: input.actor_id,
|
|
5117
|
+
deployed_at: input.deployed_at,
|
|
5118
|
+
gate_permit_token: input.gate_permit_token
|
|
5119
|
+
};
|
|
5120
|
+
}
|
|
5121
|
+
function toIntegrationSlot(input) {
|
|
5122
|
+
if (input === void 0 || isNotApplicable(input)) return null;
|
|
5123
|
+
const run = input;
|
|
5124
|
+
return {
|
|
5125
|
+
run_id: run.id,
|
|
5126
|
+
framework: run.framework,
|
|
5127
|
+
period_start: run.period_start,
|
|
5128
|
+
period_end: run.period_end,
|
|
5129
|
+
status: run.status,
|
|
5130
|
+
passing_control_count: (run.controls ?? []).filter((c) => c.status === "pass").length,
|
|
5131
|
+
failing_control_count: (run.controls ?? []).filter((c) => c.status !== "pass").length,
|
|
5132
|
+
run_completed_at: run.created_at
|
|
5133
|
+
};
|
|
5134
|
+
}
|
|
5135
|
+
function toApprovalSlot(input) {
|
|
5136
|
+
if (input === void 0 || isNotApplicable(input)) return null;
|
|
5137
|
+
if ("escalation" in input) {
|
|
5138
|
+
const chain = input;
|
|
5139
|
+
const approvals = chain.approvals;
|
|
5140
|
+
const lastApproved = approvals.filter((a) => a.decision === "approve").map((a) => a.created_at).sort().at(-1) ?? chain.escalation.created_at;
|
|
5141
|
+
return {
|
|
5142
|
+
approval_id: chain.escalation.id,
|
|
5143
|
+
approval_kind: "hitl_chain",
|
|
5144
|
+
quorum_type: hitlQuorumToSlotQuorum(chain.escalation.quorum_required),
|
|
5145
|
+
approver_count: approvals.filter((a) => a.decision === "approve").length,
|
|
5146
|
+
approver_ids: approvals.filter((a) => a.decision === "approve").map((a) => a.user_id ?? a.actor_label ?? "unknown"),
|
|
5147
|
+
approved_at: lastApproved,
|
|
5148
|
+
artifact_hash: chain.artifact_hash
|
|
5149
|
+
};
|
|
5150
|
+
}
|
|
5151
|
+
const artifact = input;
|
|
5152
|
+
return {
|
|
5153
|
+
approval_id: artifact.approval_id,
|
|
5154
|
+
approval_kind: "approval_artifact",
|
|
5155
|
+
quorum_type: artifact.quorum_type,
|
|
5156
|
+
approver_count: artifact.approver_ids.length,
|
|
5157
|
+
approver_ids: artifact.approver_ids,
|
|
5158
|
+
approved_at: artifact.approved_at,
|
|
5159
|
+
artifact_hash: artifact.artifact_hash
|
|
5160
|
+
};
|
|
5161
|
+
}
|
|
5162
|
+
function hitlQuorumToSlotQuorum(tier) {
|
|
5163
|
+
switch (tier) {
|
|
5164
|
+
case "single_approver":
|
|
5165
|
+
return "single_approver";
|
|
5166
|
+
case "two_thirds":
|
|
5167
|
+
return "two_thirds";
|
|
5168
|
+
case "unanimous":
|
|
5169
|
+
return "unanimous";
|
|
5170
|
+
default:
|
|
5171
|
+
return "simple_majority";
|
|
5172
|
+
}
|
|
5173
|
+
}
|
|
5174
|
+
function toRuntimeSlot(receipt, verifiedAtCreation) {
|
|
5175
|
+
return {
|
|
5176
|
+
permit_token: receipt.permit_id ?? receipt.receipt_id,
|
|
5177
|
+
audit_hash: receipt.audit_hash,
|
|
5178
|
+
decision: receipt.decision === "allow" ? "allow" : receipt.decision === "escalate" ? "escalate" : "deny",
|
|
5179
|
+
decision_id: receipt.evaluation_id,
|
|
5180
|
+
evaluated_at: receipt.issued_at,
|
|
5181
|
+
algorithm: receipt.algorithm,
|
|
5182
|
+
signature: receipt.signature,
|
|
5183
|
+
permit_revoked_at: null,
|
|
5184
|
+
verified_at_claim_time: receipt.decision === "allow",
|
|
5185
|
+
verified_at_link_creation: verifiedAtCreation
|
|
5186
|
+
};
|
|
5187
|
+
}
|
|
5188
|
+
function buildChecklist(runtime, deployStatus, integrationStatus, approvalStatus, delta, lastVerifiedAt, now) {
|
|
5189
|
+
const deltaComputed = delta.status === "computed";
|
|
5190
|
+
const policyDriftClean = deltaComputed ? !delta.policy_drift_detected : null;
|
|
5191
|
+
const schemaDriftClean = !delta.schema_drift_detected;
|
|
5192
|
+
const allPass = runtime.verified_at_claim_time && runtime.verified_at_link_creation && deltaComputed && policyDriftClean === true && schemaDriftClean && deployStatus !== "missing" && integrationStatus !== "missing" && approvalStatus !== "missing";
|
|
5193
|
+
return {
|
|
5194
|
+
runtime_evidence_present: true,
|
|
5195
|
+
verified_at_claim_time: runtime.verified_at_claim_time,
|
|
5196
|
+
verified_at_link_creation: runtime.verified_at_link_creation,
|
|
5197
|
+
deploy_evidence_status: deployStatus,
|
|
5198
|
+
integration_evidence_status: integrationStatus,
|
|
5199
|
+
approval_artifact_status: approvalStatus,
|
|
5200
|
+
delta_computed: deltaComputed,
|
|
5201
|
+
policy_drift_clean: policyDriftClean,
|
|
5202
|
+
schema_drift_clean: schemaDriftClean,
|
|
5203
|
+
all_pass: allPass,
|
|
5204
|
+
last_verified_at: lastVerifiedAt,
|
|
5205
|
+
computed_at: now
|
|
5206
|
+
};
|
|
5207
|
+
}
|
|
5208
|
+
function buildClaimEvidenceLink(opts) {
|
|
5209
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
5210
|
+
const linkId = `cel_${randomUUID().replace(/-/g, "")}`;
|
|
5211
|
+
const orgId = opts.orgId ?? opts.runtimeEvidence.org_id;
|
|
5212
|
+
const schemaVersion = opts.schemaVersion ?? SDK_VERSION2;
|
|
5213
|
+
const deploySlot = toDeploySlot(opts.deployEvidence);
|
|
5214
|
+
const integrationSlot = toIntegrationSlot(opts.integrationEvidence);
|
|
5215
|
+
const approvalSlot = toApprovalSlot(opts.approvalArtifact);
|
|
5216
|
+
const deployStatus = slotStatus(opts.deployEvidence, deploySlot);
|
|
5217
|
+
const integrationStatus = slotStatus(opts.integrationEvidence, integrationSlot);
|
|
5218
|
+
const approvalStatus = slotStatus(opts.approvalArtifact, approvalSlot);
|
|
5219
|
+
const verifiedAtCreation = opts.runtimeEvidence.decision === "allow";
|
|
5220
|
+
const runtime = toRuntimeSlot(opts.runtimeEvidence, verifiedAtCreation);
|
|
5221
|
+
const delta = {
|
|
5222
|
+
status: "pending",
|
|
5223
|
+
computed_at: null,
|
|
5224
|
+
policy_version_at_claim: null,
|
|
5225
|
+
policy_version_current: null,
|
|
5226
|
+
policy_drift_detected: null,
|
|
5227
|
+
schema_version_at_claim: schemaVersion,
|
|
5228
|
+
schema_version_current: schemaVersion,
|
|
5229
|
+
schema_drift_detected: false,
|
|
5230
|
+
drift_details: []
|
|
5231
|
+
};
|
|
5232
|
+
const lastVerifiedAt = verifiedAtCreation ? now : null;
|
|
5233
|
+
const checklist = buildChecklist(
|
|
5234
|
+
runtime,
|
|
5235
|
+
deployStatus,
|
|
5236
|
+
integrationStatus,
|
|
5237
|
+
approvalStatus,
|
|
5238
|
+
delta,
|
|
5239
|
+
lastVerifiedAt,
|
|
5240
|
+
now
|
|
5241
|
+
);
|
|
5242
|
+
const linkAlgorithm = opts.signingSecret ? "hmac-sha256" : "none";
|
|
5243
|
+
const body = {
|
|
5244
|
+
version: "claim_evidence_link.v1",
|
|
5245
|
+
link_id: linkId,
|
|
5246
|
+
claim_id: opts.claimId,
|
|
5247
|
+
org_id: orgId,
|
|
5248
|
+
linked_at: now,
|
|
5249
|
+
updated_at: now,
|
|
5250
|
+
revision: 1,
|
|
5251
|
+
link_algorithm: linkAlgorithm,
|
|
5252
|
+
runtime_evidence: runtime,
|
|
5253
|
+
deploy_evidence: deploySlot,
|
|
5254
|
+
integration_evidence: integrationSlot,
|
|
5255
|
+
approval_artifact: approvalSlot,
|
|
5256
|
+
delta,
|
|
5257
|
+
verification_checklist: checklist
|
|
5258
|
+
};
|
|
5259
|
+
const linkHash = computeLinkHash(body);
|
|
5260
|
+
const linkSignature = opts.signingSecret ? hmacSha256Base64url(linkHash, opts.signingSecret) : null;
|
|
5261
|
+
return { ...body, link_hash: linkHash, link_signature: linkSignature };
|
|
5262
|
+
}
|
|
5263
|
+
function verifyClaimEvidenceLink(link, opts = {}) {
|
|
5264
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
5265
|
+
const { link_hash: _lh, link_signature: _ls, ...body } = link;
|
|
5266
|
+
const expectedHash = computeLinkHash(
|
|
5267
|
+
body
|
|
5268
|
+
);
|
|
5269
|
+
const hashValid = expectedHash === link.link_hash;
|
|
5270
|
+
let sigValid = true;
|
|
5271
|
+
if (link.link_algorithm === "hmac-sha256") {
|
|
5272
|
+
if (!opts.signingSecret) {
|
|
5273
|
+
sigValid = false;
|
|
5274
|
+
} else {
|
|
5275
|
+
const expected = hmacSha256Base64url(link.link_hash, opts.signingSecret);
|
|
5276
|
+
sigValid = expected === link.link_signature;
|
|
5277
|
+
}
|
|
5278
|
+
}
|
|
5279
|
+
const runtime = {
|
|
5280
|
+
...link.runtime_evidence,
|
|
5281
|
+
// If the link hash or signature is invalid, mark creation-time verification as failed
|
|
5282
|
+
verified_at_link_creation: hashValid && sigValid ? link.runtime_evidence.verified_at_link_creation : false
|
|
5283
|
+
};
|
|
5284
|
+
const checklist = buildChecklist(
|
|
5285
|
+
runtime,
|
|
5286
|
+
link.verification_checklist.deploy_evidence_status,
|
|
5287
|
+
link.verification_checklist.integration_evidence_status,
|
|
5288
|
+
link.verification_checklist.approval_artifact_status,
|
|
5289
|
+
link.delta,
|
|
5290
|
+
runtime.verified_at_link_creation ? link.verification_checklist.last_verified_at ?? now : null,
|
|
5291
|
+
now
|
|
5292
|
+
);
|
|
5293
|
+
const updatedBody = {
|
|
5294
|
+
version: link.version,
|
|
5295
|
+
link_id: link.link_id,
|
|
5296
|
+
claim_id: link.claim_id,
|
|
5297
|
+
org_id: link.org_id,
|
|
5298
|
+
linked_at: link.linked_at,
|
|
5299
|
+
updated_at: now,
|
|
5300
|
+
revision: link.revision + 1,
|
|
5301
|
+
link_algorithm: link.link_algorithm,
|
|
5302
|
+
runtime_evidence: runtime,
|
|
5303
|
+
deploy_evidence: link.deploy_evidence,
|
|
5304
|
+
integration_evidence: link.integration_evidence,
|
|
5305
|
+
approval_artifact: link.approval_artifact,
|
|
5306
|
+
delta: link.delta,
|
|
5307
|
+
verification_checklist: checklist
|
|
5308
|
+
};
|
|
5309
|
+
const newHash = computeLinkHash(updatedBody);
|
|
5310
|
+
const newSignature = opts.signingSecret ? hmacSha256Base64url(newHash, opts.signingSecret) : link.link_algorithm === "none" ? null : link.link_signature;
|
|
5311
|
+
const updatedLink = {
|
|
5312
|
+
...updatedBody,
|
|
5313
|
+
link_hash: newHash,
|
|
5314
|
+
link_signature: newSignature
|
|
5315
|
+
};
|
|
5316
|
+
const failedSlots = [];
|
|
5317
|
+
if (!hashValid) failedSlots.push("link_hash");
|
|
5318
|
+
if (!sigValid) failedSlots.push("link_signature");
|
|
5319
|
+
if (!checklist.verified_at_claim_time) failedSlots.push("verified_at_claim_time");
|
|
5320
|
+
if (!checklist.verified_at_link_creation) failedSlots.push("verified_at_link_creation");
|
|
5321
|
+
if (!checklist.delta_computed) failedSlots.push("delta_computed");
|
|
5322
|
+
if (checklist.policy_drift_clean === false) failedSlots.push("policy_drift_clean");
|
|
5323
|
+
if (!checklist.schema_drift_clean) failedSlots.push("schema_drift_clean");
|
|
5324
|
+
if (checklist.deploy_evidence_status === "missing") failedSlots.push("deploy_evidence_status");
|
|
5325
|
+
if (checklist.integration_evidence_status === "missing") failedSlots.push("integration_evidence_status");
|
|
5326
|
+
if (checklist.approval_artifact_status === "missing") failedSlots.push("approval_artifact_status");
|
|
5327
|
+
const valid = failedSlots.length === 0;
|
|
5328
|
+
if (!valid) {
|
|
5329
|
+
throw new AtlaSentError(
|
|
5330
|
+
`ClaimEvidenceLink verification failed: ${failedSlots.join(", ")}`,
|
|
5331
|
+
{ code: "claim_evidence_incomplete" }
|
|
5332
|
+
);
|
|
5333
|
+
}
|
|
5334
|
+
return { link: updatedLink, valid, failedSlots };
|
|
5335
|
+
}
|
|
5336
|
+
function buildClaimEvidenceLinkFromActionBundle(bundle, opts) {
|
|
5337
|
+
const runtimeEvidence = {
|
|
5338
|
+
receipt_id: bundle.receipt.receipt_id,
|
|
5339
|
+
evaluation_id: bundle.receipt.evaluation_id,
|
|
5340
|
+
org_id: opts.orgId ?? "",
|
|
5341
|
+
decision: bundle.receipt.decision,
|
|
5342
|
+
action: bundle.action,
|
|
5343
|
+
actor: bundle.actor,
|
|
5344
|
+
resource_type: null,
|
|
5345
|
+
resource_id: null,
|
|
5346
|
+
reasons: [],
|
|
5347
|
+
why_trace: null,
|
|
5348
|
+
permit_id: bundle.receipt.permit_id,
|
|
5349
|
+
permit_hash: null,
|
|
5350
|
+
audit_hash: bundle.receipt.audit_hash ?? "",
|
|
5351
|
+
context_hash: "",
|
|
5352
|
+
issued_at: bundle.receipt.issued_at,
|
|
5353
|
+
expires_at: null,
|
|
5354
|
+
algorithm: bundle.receipt.algorithm,
|
|
5355
|
+
signature: bundle.receipt.signature,
|
|
5356
|
+
signing_key_id: null,
|
|
5357
|
+
payload: {
|
|
5358
|
+
receipt_id: bundle.receipt.receipt_id,
|
|
5359
|
+
evaluation_id: bundle.receipt.evaluation_id,
|
|
5360
|
+
org_id: opts.orgId ?? "",
|
|
5361
|
+
decision: bundle.receipt.decision,
|
|
5362
|
+
action: bundle.action,
|
|
5363
|
+
actor: bundle.actor,
|
|
5364
|
+
resource_type: null,
|
|
5365
|
+
resource_id: null,
|
|
5366
|
+
reasons: [],
|
|
5367
|
+
why_summary: "",
|
|
5368
|
+
permit_id: bundle.receipt.permit_id,
|
|
5369
|
+
permit_hash: null,
|
|
5370
|
+
audit_hash: bundle.receipt.audit_hash ?? "",
|
|
5371
|
+
context_hash: "",
|
|
5372
|
+
issued_at: bundle.receipt.issued_at,
|
|
5373
|
+
expires_at: null
|
|
5374
|
+
}
|
|
5375
|
+
};
|
|
5376
|
+
const deployEvidence = opts.deployNotApplicable ? NOT_APPLICABLE : {
|
|
5377
|
+
deploy_id: bundle.bundle_id,
|
|
5378
|
+
environment: bundle.environment,
|
|
5379
|
+
sha: bundle.sha,
|
|
5380
|
+
actor_id: bundle.actor,
|
|
5381
|
+
deployed_at: bundle.generated_at,
|
|
5382
|
+
gate_permit_token: bundle.receipt.permit_id ?? bundle.receipt.receipt_id
|
|
5383
|
+
};
|
|
5384
|
+
return buildClaimEvidenceLink({
|
|
5385
|
+
claimId: opts.claimId,
|
|
5386
|
+
...opts.orgId !== void 0 ? { orgId: opts.orgId } : {},
|
|
5387
|
+
runtimeEvidence,
|
|
5388
|
+
deployEvidence,
|
|
5389
|
+
...opts.signingSecret !== void 0 ? { signingSecret: opts.signingSecret } : {},
|
|
5390
|
+
...opts.schemaVersion !== void 0 ? { schemaVersion: opts.schemaVersion } : {}
|
|
5391
|
+
});
|
|
5392
|
+
}
|
|
5393
|
+
|
|
5394
|
+
// src/bccae.ts
|
|
5395
|
+
var DEFAULT_BASE_URL2 = "https://api.atlasent.io";
|
|
5396
|
+
var DEFAULT_TIMEOUT_MS2 = 1e4;
|
|
5397
|
+
function enforceTls(raw) {
|
|
5398
|
+
let parsed;
|
|
5399
|
+
try {
|
|
5400
|
+
parsed = new URL(raw);
|
|
5401
|
+
} catch {
|
|
5402
|
+
throw new AtlaSentError(
|
|
5403
|
+
"BCCAEClient baseUrl is not a valid URL",
|
|
5404
|
+
{ code: "network" }
|
|
5405
|
+
);
|
|
5406
|
+
}
|
|
5407
|
+
if (parsed.protocol === "http:") {
|
|
5408
|
+
const h = parsed.hostname;
|
|
5409
|
+
if (h !== "localhost" && h !== "127.0.0.1" && h !== "[::1]") {
|
|
5410
|
+
throw new AtlaSentError(
|
|
5411
|
+
"BCCAEClient baseUrl must use https:// for non-local endpoints",
|
|
5412
|
+
{ code: "network" }
|
|
5413
|
+
);
|
|
5414
|
+
}
|
|
5415
|
+
}
|
|
5416
|
+
return parsed.origin;
|
|
5417
|
+
}
|
|
5418
|
+
function generateBccaeNonce() {
|
|
5419
|
+
const bytes = new Uint8Array(32);
|
|
5420
|
+
globalThis.crypto.getRandomValues(bytes);
|
|
5421
|
+
return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
5422
|
+
}
|
|
5423
|
+
var BCCAEClient = class {
|
|
5424
|
+
apiKey;
|
|
5425
|
+
baseUrl;
|
|
5426
|
+
timeoutMs;
|
|
5427
|
+
fetchImpl;
|
|
5428
|
+
constructor(options) {
|
|
5429
|
+
if (!options.apiKey || typeof options.apiKey !== "string") {
|
|
5430
|
+
throw new AtlaSentError("BCCAEClient: apiKey is required", {
|
|
5431
|
+
code: "invalid_api_key"
|
|
5432
|
+
});
|
|
5433
|
+
}
|
|
5434
|
+
this.apiKey = options.apiKey;
|
|
5435
|
+
this.baseUrl = enforceTls(options.baseUrl ?? DEFAULT_BASE_URL2);
|
|
5436
|
+
this.timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS2;
|
|
5437
|
+
this.fetchImpl = options.fetch ?? globalThis.fetch.bind(globalThis);
|
|
5438
|
+
}
|
|
5439
|
+
async evaluate(input) {
|
|
5440
|
+
const { body } = await this.post(
|
|
5441
|
+
"/v1/bccae/evaluations",
|
|
5442
|
+
input
|
|
5443
|
+
);
|
|
5444
|
+
return body;
|
|
5445
|
+
}
|
|
5446
|
+
async execute(input) {
|
|
5447
|
+
const { body } = await this.post(
|
|
5448
|
+
"/v1/bccae/execute",
|
|
5449
|
+
input
|
|
5450
|
+
);
|
|
5451
|
+
return body;
|
|
5452
|
+
}
|
|
5453
|
+
async revoke(input) {
|
|
5454
|
+
const { body } = await this.post(
|
|
5455
|
+
"/v1/bccae/revocations",
|
|
5456
|
+
input
|
|
5457
|
+
);
|
|
5458
|
+
return body;
|
|
5459
|
+
}
|
|
5460
|
+
async getEvidence(evidenceId) {
|
|
5461
|
+
if (!evidenceId || typeof evidenceId !== "string") {
|
|
5462
|
+
throw new AtlaSentError("BCCAEClient: evidenceId is required", {
|
|
5463
|
+
code: "bad_request"
|
|
5464
|
+
});
|
|
5465
|
+
}
|
|
5466
|
+
const { body } = await this.get(
|
|
5467
|
+
`/v1/bccae/evidence/${encodeURIComponent(evidenceId)}`
|
|
5468
|
+
);
|
|
5469
|
+
return body;
|
|
5470
|
+
}
|
|
5471
|
+
// ── HTTP primitives ─────────────────────────────────────────────────────────
|
|
5472
|
+
async post(path, body) {
|
|
5473
|
+
return this.request(path, "POST", body);
|
|
5474
|
+
}
|
|
5475
|
+
async get(path) {
|
|
5476
|
+
return this.request(path, "GET", void 0);
|
|
5477
|
+
}
|
|
5478
|
+
async request(path, method, body) {
|
|
5479
|
+
const url = `${this.baseUrl}${path}`;
|
|
5480
|
+
const headers = {
|
|
5481
|
+
Accept: "application/json",
|
|
5482
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
5483
|
+
"User-Agent": "atlasent-bccae-client/1.0"
|
|
5484
|
+
};
|
|
5485
|
+
if (method === "POST") headers["Content-Type"] = "application/json";
|
|
5486
|
+
let response;
|
|
5487
|
+
try {
|
|
5488
|
+
response = await this.fetchImpl(url, {
|
|
5489
|
+
method,
|
|
5490
|
+
headers,
|
|
5491
|
+
signal: AbortSignal.timeout(this.timeoutMs),
|
|
5492
|
+
...method === "POST" ? { body: JSON.stringify(body) } : {}
|
|
5493
|
+
});
|
|
5494
|
+
} catch (err) {
|
|
5495
|
+
throw new AtlaSentError(
|
|
5496
|
+
`BCCAEClient: network error on ${method} ${path}: ${err instanceof Error ? err.message : String(err)}`,
|
|
5497
|
+
{ code: "network" }
|
|
5498
|
+
);
|
|
5499
|
+
}
|
|
5500
|
+
let responseBody;
|
|
5501
|
+
try {
|
|
5502
|
+
responseBody = await response.json();
|
|
5503
|
+
} catch {
|
|
5504
|
+
throw new AtlaSentError(
|
|
5505
|
+
`BCCAEClient: non-JSON response (status ${response.status}) from ${method} ${path}`,
|
|
5506
|
+
{ code: "network" }
|
|
5507
|
+
);
|
|
5508
|
+
}
|
|
5509
|
+
if (!response.ok) {
|
|
5510
|
+
const err = responseBody;
|
|
5511
|
+
const message = typeof err?.message === "string" ? err.message : `BCCAE request failed with status ${response.status}`;
|
|
5512
|
+
const code = response.status === 401 ? "invalid_api_key" : response.status === 403 ? "forbidden" : response.status === 429 ? "rate_limited" : response.status >= 500 ? "server_error" : "network";
|
|
5513
|
+
throw new AtlaSentError(message, { code });
|
|
5514
|
+
}
|
|
5515
|
+
return { body: responseBody };
|
|
5516
|
+
}
|
|
5517
|
+
};
|
|
5518
|
+
|
|
5519
|
+
// src/governanceAgents.ts
|
|
5520
|
+
var SEVERITY_RANK = {
|
|
5521
|
+
info: 1,
|
|
5522
|
+
low: 2,
|
|
5523
|
+
medium: 3,
|
|
5524
|
+
high: 4,
|
|
5525
|
+
blocker: 5
|
|
5526
|
+
};
|
|
5527
|
+
function highestAgentFindingSeverity(findings) {
|
|
5528
|
+
let best = null;
|
|
5529
|
+
let rank = 0;
|
|
5530
|
+
for (const f of findings) {
|
|
5531
|
+
const r = SEVERITY_RANK[f.severity];
|
|
5532
|
+
if (r > rank) {
|
|
5533
|
+
rank = r;
|
|
5534
|
+
best = f.severity;
|
|
5535
|
+
}
|
|
5536
|
+
}
|
|
5537
|
+
return best;
|
|
5538
|
+
}
|
|
5539
|
+
|
|
3427
5540
|
// src/index.ts
|
|
3428
5541
|
var atlasent = {
|
|
3429
5542
|
protect,
|
|
@@ -3445,13 +5558,18 @@ export {
|
|
|
3445
5558
|
AtlaSentDeniedError,
|
|
3446
5559
|
AtlaSentError,
|
|
3447
5560
|
AtlaSentEscalateError,
|
|
5561
|
+
BCCAEClient,
|
|
3448
5562
|
DEFAULT_INCENTIVE_CONFIG,
|
|
5563
|
+
DEFAULT_REDACTION_RULES,
|
|
3449
5564
|
DEFAULT_RETRY_POLICY,
|
|
3450
5565
|
DEFAULT_RISK_TIER_THRESHOLDS,
|
|
3451
5566
|
DEPLOYMENT_PRODUCTION_ACTION,
|
|
3452
5567
|
DEPLOY_GATE_CODES,
|
|
5568
|
+
EscalationDeniedError,
|
|
5569
|
+
EscalationTimeoutError,
|
|
3453
5570
|
FeatureNotEnabledError,
|
|
3454
5571
|
GovernanceEnforcementError,
|
|
5572
|
+
NOT_APPLICABLE,
|
|
3455
5573
|
PRODUCTION_DEPLOY_ACTION,
|
|
3456
5574
|
PermitRevoked,
|
|
3457
5575
|
StreamParseError,
|
|
@@ -3466,6 +5584,9 @@ export {
|
|
|
3466
5584
|
assertWebhook,
|
|
3467
5585
|
authorizeStream,
|
|
3468
5586
|
budgetUtilizationSeverity,
|
|
5587
|
+
buildActionContext,
|
|
5588
|
+
buildClaimEvidenceLink,
|
|
5589
|
+
buildClaimEvidenceLinkFromActionBundle,
|
|
3469
5590
|
buildLiabilityChain,
|
|
3470
5591
|
buildLiabilityVisualization,
|
|
3471
5592
|
buildRiskTimeline,
|
|
@@ -3474,9 +5595,11 @@ export {
|
|
|
3474
5595
|
canonicalizeForEvidence,
|
|
3475
5596
|
checkAutonomousBounds,
|
|
3476
5597
|
checkBudgetConstraints,
|
|
5598
|
+
checkIntegrationHealth,
|
|
3477
5599
|
clampTokenDuration,
|
|
3478
5600
|
classifyCommand,
|
|
3479
5601
|
classifyRiskTier,
|
|
5602
|
+
classifyToolRisk,
|
|
3480
5603
|
computeApprovalRiskScore,
|
|
3481
5604
|
computeBackoffMs,
|
|
3482
5605
|
computeEscalatedApprovalCount,
|
|
@@ -3489,6 +5612,10 @@ export {
|
|
|
3489
5612
|
computeRemediationUrgency,
|
|
3490
5613
|
computeSignalEngagementRate,
|
|
3491
5614
|
configure,
|
|
5615
|
+
configureApprovalRuntime,
|
|
5616
|
+
configureControlSurface,
|
|
5617
|
+
configureShadow,
|
|
5618
|
+
createEscalation,
|
|
3492
5619
|
index_default as default,
|
|
3493
5620
|
delegationPropagationHadEffect,
|
|
3494
5621
|
deployGate,
|
|
@@ -3503,10 +5630,15 @@ export {
|
|
|
3503
5630
|
evaluateMany,
|
|
3504
5631
|
evidenceRunPasses,
|
|
3505
5632
|
findPrimaryLiabilityParties,
|
|
5633
|
+
flattenActionContext,
|
|
3506
5634
|
formatPolicySyncDiff,
|
|
5635
|
+
generateBccaeNonce,
|
|
5636
|
+
getEnforcementStatus,
|
|
5637
|
+
getOrgSummary,
|
|
3507
5638
|
graphql,
|
|
3508
5639
|
hasAttemptsLeft,
|
|
3509
5640
|
hhiToConcentrationScore,
|
|
5641
|
+
highestAgentFindingSeverity,
|
|
3510
5642
|
highestSeverityAction,
|
|
3511
5643
|
hitlRequiredApproverCount,
|
|
3512
5644
|
isBudgetExceptionActive,
|
|
@@ -3526,6 +5658,17 @@ export {
|
|
|
3526
5658
|
normalizeEvaluateResponse,
|
|
3527
5659
|
normalizePermitOutcome,
|
|
3528
5660
|
protect,
|
|
5661
|
+
protectCloseAction,
|
|
5662
|
+
protectDeploy,
|
|
5663
|
+
protectOrEscalate,
|
|
5664
|
+
protectPaymentRelease,
|
|
5665
|
+
protectShadow,
|
|
5666
|
+
protectToolCall,
|
|
5667
|
+
protectWithEvidence,
|
|
5668
|
+
redactContext,
|
|
5669
|
+
reportProtectedAction,
|
|
5670
|
+
reportShadowEvent,
|
|
5671
|
+
requestOverride,
|
|
3529
5672
|
requirePermit,
|
|
3530
5673
|
scoreToRiskTier,
|
|
3531
5674
|
serializeSignableContent,
|
|
@@ -3533,12 +5676,15 @@ export {
|
|
|
3533
5676
|
summarizeCrossOrgPermission,
|
|
3534
5677
|
transitionDispute,
|
|
3535
5678
|
transitionReversal,
|
|
5679
|
+
validateActionContext,
|
|
3536
5680
|
validateLiabilityChain,
|
|
3537
5681
|
verifyAuditBundle,
|
|
3538
5682
|
verifyBundle,
|
|
5683
|
+
verifyClaimEvidenceLink,
|
|
3539
5684
|
verifyEvidenceBundleStructure,
|
|
3540
5685
|
verifyWebhook,
|
|
3541
5686
|
verifyWebhookSignature,
|
|
5687
|
+
waitForEscalationApproval,
|
|
3542
5688
|
withPermit,
|
|
3543
5689
|
withinAutonomousCeiling
|
|
3544
5690
|
};
|