sentinelayer-cli 0.41.0 → 0.43.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 +55 -0
- package/package.json +3 -2
- package/src/auth/admission-scope.js +34 -0
- package/src/auth/gate.js +6 -0
- package/src/auth/http.js +4 -0
- package/src/auth/service.js +6 -0
- package/src/board/done-gate.js +155 -0
- package/src/board/evidence-resolvers.js +138 -0
- package/src/cli.js +24 -1
- package/src/commands/board.js +116 -0
- package/src/commands/session.js +779 -47
- package/src/engram/build-state.js +183 -0
- package/src/engram/canonical.js +75 -0
- package/src/engram/document.js +190 -0
- package/src/engram/index.js +11 -2
- package/src/engram/ingest-document.js +107 -0
- package/src/engram/store.js +6 -1
- package/src/engram/tools.js +6 -0
- package/src/events/schema.js +18 -0
- package/src/legacy-cli.js +2 -2
- package/src/net/test-egress-guard.js +158 -0
- package/src/review/investor-dd-orchestrator.js +14 -2
- package/src/session/admission-access.js +256 -0
- package/src/session/admission-auth.js +319 -0
- package/src/session/admission-preimage.js +90 -0
- package/src/session/admission.js +641 -0
- package/src/session/checkpoints.js +25 -1
- package/src/session/codebase-context.js +54 -0
- package/src/session/coordination-guidance.js +1 -1
- package/src/session/daemon-spawn.js +20 -2
- package/src/session/daemon.js +7 -1
- package/src/session/event-identity.js +9 -2
- package/src/session/first-message.js +11 -3
- package/src/session/listener.js +3 -0
- package/src/session/live-source.js +47 -16
- package/src/session/message-edits.js +89 -0
- package/src/session/recap.js +26 -4
- package/src/session/remote-hydrate.js +7 -19
- package/src/session/store.js +13 -14
- package/src/session/stream.js +23 -3
- package/src/session/sync.js +128 -11
- package/src/session/tickets.js +316 -0
- package/src/session/title-sync.js +2 -1
- package/src/session/usage-report.js +41 -2
- package/src/session/wake/resolve-target.js +10 -5
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
// Fail-closed network egress guard for test processes.
|
|
2
|
+
//
|
|
3
|
+
// Why: on 2026-09-29 raw unit-test runs on a logged-in developer machine synced
|
|
4
|
+
// tens of thousands of junk sessions to the production API, and a later audit
|
|
5
|
+
// found read paths (fetchSessionFromApi, listSessionsFromApi, pollSessionEvents,
|
|
6
|
+
// ...) that ignore SENTINELAYER_SKIP_REMOTE_SYNC entirely. An env flag that each
|
|
7
|
+
// call site must remember to check is not a safety boundary.
|
|
8
|
+
//
|
|
9
|
+
// SCOPE (stated exactly): this wraps `globalThis.fetch` only. It does not cover
|
|
10
|
+
// node:http/https/net, a separately imported undici, or child tools (git, curl).
|
|
11
|
+
// At the time of writing, src/ makes its network calls through globalThis.fetch
|
|
12
|
+
// (and injected fetchImpl defaults to it).
|
|
13
|
+
//
|
|
14
|
+
// One normalization, validated AND dispatched: every call is converted ONCE with
|
|
15
|
+
// native Request semantics (`new Request(input, init)`, the same conversion fetch
|
|
16
|
+
// itself performs), then that exact Request's URL is checked and that exact
|
|
17
|
+
// Request is what reaches the network. The guard never validates one URL
|
|
18
|
+
// representation and dispatches another (e.g. an object whose .url and
|
|
19
|
+
// toString() disagree, or a getter that answers differently on a second read).
|
|
20
|
+
//
|
|
21
|
+
// Policy, inside a test process:
|
|
22
|
+
// - natively, only numeric loopback: 127.0.0.1 or [::1]. Host names (localhost,
|
|
23
|
+
// *.test, *.invalid, *.localhost) are refused: a name is not proof of loopback
|
|
24
|
+
// routing, and tests that use fixture names replace globalThis.fetch with a mock.
|
|
25
|
+
// - redirects in the default "follow" mode are walked manually and EVERY hop is
|
|
26
|
+
// checked against the same policy. Only 301/302/303/307/308 are followed. A
|
|
27
|
+
// 303, or a 301/302 after POST, becomes GET (body headers dropped); a 307/308
|
|
28
|
+
// that would re-send a body is refused (use redirect: "manual" in such a test).
|
|
29
|
+
// On an origin change Authorization/Proxy-Authorization/Cookie are dropped, as
|
|
30
|
+
// native fetch does. Explicit "manual"/"error" modes pass through unchanged.
|
|
31
|
+
// - data: and blob: never leave the process and are allowed.
|
|
32
|
+
//
|
|
33
|
+
// Activation: NODE_TEST_CONTEXT (set by `node --test` for test files) or
|
|
34
|
+
// SENTINELAYER_TEST_EGRESS_GUARD=1 (set by tests/setup-env.mjs and inherited by
|
|
35
|
+
// spawned children). Production processes set neither, so there it is a no-op.
|
|
36
|
+
// A test that installs its own `globalThis.fetch` mock replaces the wrapper, and
|
|
37
|
+
// restoring the saved original puts the guard back.
|
|
38
|
+
|
|
39
|
+
const GUARD_MARKER = Symbol.for("sentinelayer.testEgressGuard");
|
|
40
|
+
const NUMERIC_LOOPBACK_HOSTS = new Set(["127.0.0.1", "[::1]"]);
|
|
41
|
+
const MAX_REDIRECTS = 20;
|
|
42
|
+
// Only these are redirects (as in native fetch); a 300/304/305/306 with a Location header is returned as-is.
|
|
43
|
+
const REDIRECT_STATUSES = new Set([301, 302, 303, 307, 308]);
|
|
44
|
+
// Dropped when a redirect changes origin (as native undici does), so credentials never cross origins.
|
|
45
|
+
const CROSS_ORIGIN_STRIPPED_HEADERS = ["authorization", "proxy-authorization", "cookie"];
|
|
46
|
+
// Dropped when a redirect turns the request into a body-less GET (fetch spec request-body-header names).
|
|
47
|
+
const BODY_HEADERS = ["content-type", "content-length", "content-encoding", "content-language", "content-location"];
|
|
48
|
+
|
|
49
|
+
export class TestEgressBlockedError extends Error {
|
|
50
|
+
constructor(target, detail = "") {
|
|
51
|
+
super(
|
|
52
|
+
`test egress blocked: ${target}${detail ? ` (${detail})` : ""} ` +
|
|
53
|
+
"(tests may only reach 127.0.0.1 or [::1] natively; mock fetch for anything else)",
|
|
54
|
+
);
|
|
55
|
+
this.name = "TestEgressBlockedError";
|
|
56
|
+
this.code = "TEST_EGRESS_BLOCKED";
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function isTestEgressGuardRequested(env = process.env) {
|
|
61
|
+
return Boolean(String(env.NODE_TEST_CONTEXT || "").trim()) ||
|
|
62
|
+
String(env.SENTINELAYER_TEST_EGRESS_GUARD || "").trim() === "1";
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Policy on an already-canonical URL string (a Request's .url, or a redirect target resolved with new URL()).
|
|
66
|
+
export function isAllowedTestDestination(url) {
|
|
67
|
+
let parsed;
|
|
68
|
+
try {
|
|
69
|
+
parsed = new URL(String(url));
|
|
70
|
+
} catch {
|
|
71
|
+
return false; // unparseable: refuse rather than guess
|
|
72
|
+
}
|
|
73
|
+
if (parsed.protocol === "data:" || parsed.protocol === "blob:") return true; // never leaves the process
|
|
74
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return false;
|
|
75
|
+
// WHATWG URL canonicalizes IPv4 forms (127.1, 0x7f.0.0.1) to dotted decimal.
|
|
76
|
+
return NUMERIC_LOOPBACK_HOSTS.has(parsed.hostname.toLowerCase());
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const blockedForTests = [];
|
|
80
|
+
|
|
81
|
+
export function getBlockedTestEgress() {
|
|
82
|
+
return blockedForTests.slice();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function originOf(url) {
|
|
86
|
+
try {
|
|
87
|
+
return new URL(String(url)).origin;
|
|
88
|
+
} catch {
|
|
89
|
+
return "<unparseable>";
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function block(url, detail) {
|
|
94
|
+
const origin = originOf(url);
|
|
95
|
+
blockedForTests.push(origin);
|
|
96
|
+
return new TestEgressBlockedError(origin, detail);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function installTestEgressGuard({ env = process.env, force = false } = {}) {
|
|
100
|
+
if (!force && !isTestEgressGuardRequested(env)) return false;
|
|
101
|
+
const nativeFetch = globalThis.fetch;
|
|
102
|
+
const NativeRequest = globalThis.Request;
|
|
103
|
+
if (typeof nativeFetch !== "function" || typeof NativeRequest !== "function") return false;
|
|
104
|
+
if (nativeFetch[GUARD_MARKER]) return true; // idempotent
|
|
105
|
+
|
|
106
|
+
async function followChecked(thisArg, request, hops) {
|
|
107
|
+
const response = await nativeFetch.call(thisArg, new NativeRequest(request, { redirect: "manual" }));
|
|
108
|
+
const location = response.headers.get("location");
|
|
109
|
+
if (!REDIRECT_STATUSES.has(response.status) || location === null) return response;
|
|
110
|
+
let next;
|
|
111
|
+
try {
|
|
112
|
+
next = new URL(location, request.url).href;
|
|
113
|
+
} catch {
|
|
114
|
+
throw block(location, "unparseable redirect location");
|
|
115
|
+
}
|
|
116
|
+
if (!isAllowedTestDestination(next)) throw block(next, `redirect hop ${hops + 1}`);
|
|
117
|
+
if (hops + 1 > MAX_REDIRECTS) throw new TypeError("test fetch: too many redirects");
|
|
118
|
+
const method = request.method.toUpperCase();
|
|
119
|
+
const toGet = response.status === 303 ? method !== "HEAD"
|
|
120
|
+
: (response.status === 301 || response.status === 302) && method === "POST";
|
|
121
|
+
if (!toGet && request.body !== null && method !== "GET" && method !== "HEAD") {
|
|
122
|
+
throw block(next, `redirect ${response.status} would re-send a request body; use redirect: "manual"`);
|
|
123
|
+
}
|
|
124
|
+
const headers = new Headers(request.headers);
|
|
125
|
+
if (new URL(next).origin !== new URL(request.url).origin) {
|
|
126
|
+
for (const name of CROSS_ORIGIN_STRIPPED_HEADERS) headers.delete(name);
|
|
127
|
+
}
|
|
128
|
+
if (toGet) {
|
|
129
|
+
for (const name of BODY_HEADERS) headers.delete(name);
|
|
130
|
+
}
|
|
131
|
+
const nextRequest = new NativeRequest(next, {
|
|
132
|
+
method: toGet ? "GET" : method,
|
|
133
|
+
headers,
|
|
134
|
+
signal: request.signal,
|
|
135
|
+
redirect: "manual",
|
|
136
|
+
});
|
|
137
|
+
return followChecked(thisArg, nextRequest, hops + 1);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const guarded = function guardedTestFetch(input, init) {
|
|
141
|
+
let request;
|
|
142
|
+
try {
|
|
143
|
+
request = new NativeRequest(input, init); // the ONE conversion; exactly what fetch would do
|
|
144
|
+
} catch (error) {
|
|
145
|
+
return Promise.reject(error); // the same TypeError native fetch would raise
|
|
146
|
+
}
|
|
147
|
+
if (!isAllowedTestDestination(request.url)) return Promise.reject(block(request.url));
|
|
148
|
+
if (request.redirect !== "follow") return nativeFetch.call(this, request);
|
|
149
|
+
return followChecked(this, request, 0);
|
|
150
|
+
};
|
|
151
|
+
Object.defineProperty(guarded, GUARD_MARKER, { value: true });
|
|
152
|
+
globalThis.fetch = guarded;
|
|
153
|
+
return true;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export function isTestEgressGuardInstalled() {
|
|
157
|
+
return Boolean(typeof globalThis.fetch === "function" && globalThis.fetch[GUARD_MARKER]);
|
|
158
|
+
}
|
|
@@ -352,10 +352,18 @@ export async function runInvestorDd({
|
|
|
352
352
|
|
|
353
353
|
const streamPath = path.join(artifactBase, "stream.ndjson");
|
|
354
354
|
const streamHandle = await fsp.open(streamPath, "w");
|
|
355
|
+
let streamClosePromise = null;
|
|
356
|
+
|
|
357
|
+
const closeStream = () => {
|
|
358
|
+
streamClosePromise ??= streamHandle.close();
|
|
359
|
+
return streamClosePromise;
|
|
360
|
+
};
|
|
355
361
|
|
|
356
362
|
const emit = (event) => {
|
|
357
363
|
const enriched = { ...event, at: new Date().toISOString(), runId };
|
|
358
|
-
|
|
364
|
+
if (!streamClosePromise) {
|
|
365
|
+
streamHandle.write(`${JSON.stringify(enriched)}\n`).catch(() => {});
|
|
366
|
+
}
|
|
359
367
|
try {
|
|
360
368
|
onEvent(enriched);
|
|
361
369
|
} catch {
|
|
@@ -363,6 +371,7 @@ export async function runInvestorDd({
|
|
|
363
371
|
}
|
|
364
372
|
};
|
|
365
373
|
|
|
374
|
+
try {
|
|
366
375
|
const startTime = Date.now();
|
|
367
376
|
emit({ type: "investor_dd_start", rootPath, personas });
|
|
368
377
|
|
|
@@ -593,7 +602,7 @@ export async function runInvestorDd({
|
|
|
593
602
|
await writeJson(path.join(artifactBase, "progress.json"), ddProgress);
|
|
594
603
|
await writeJson(path.join(artifactBase, "summary.json"), summary);
|
|
595
604
|
|
|
596
|
-
await
|
|
605
|
+
await closeStream();
|
|
597
606
|
|
|
598
607
|
const artifactFiles = await fsp.readdir(artifactBase);
|
|
599
608
|
const manifest = {};
|
|
@@ -624,4 +633,7 @@ export async function runInvestorDd({
|
|
|
624
633
|
}
|
|
625
634
|
|
|
626
635
|
return runResult;
|
|
636
|
+
} finally {
|
|
637
|
+
await closeStream();
|
|
638
|
+
}
|
|
627
639
|
}
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
// Owner/delegator controls for the Agent Admission Contract.
|
|
2
|
+
//
|
|
3
|
+
// Agent-side key generation, approval polling, proof-of-possession claim, and
|
|
4
|
+
// credential storage remain in admission.js. This module is deliberately the
|
|
5
|
+
// human control plane only: inspect requests, approve/deny them, or revoke a
|
|
6
|
+
// live grant. Every mutation uses the same session CSRF + idempotency contract
|
|
7
|
+
// as the existing request/claim path.
|
|
8
|
+
|
|
9
|
+
import process from "node:process";
|
|
10
|
+
|
|
11
|
+
import { requestJson, requestJsonMutation } from "../auth/http.js";
|
|
12
|
+
import { resolveActiveAuthSession } from "../auth/service.js";
|
|
13
|
+
import {
|
|
14
|
+
SESSION_MUTATION_ORIGIN,
|
|
15
|
+
createSessionMutationHeaders,
|
|
16
|
+
createSessionMutationIdempotencyKey,
|
|
17
|
+
} from "./invitations.js";
|
|
18
|
+
|
|
19
|
+
export const ADMISSION_STATUSES = Object.freeze([
|
|
20
|
+
"pending",
|
|
21
|
+
"approved",
|
|
22
|
+
"active",
|
|
23
|
+
"denied",
|
|
24
|
+
"cancelled",
|
|
25
|
+
"expired",
|
|
26
|
+
"revoked",
|
|
27
|
+
"stopped",
|
|
28
|
+
]);
|
|
29
|
+
|
|
30
|
+
const ROUTES = Object.freeze({
|
|
31
|
+
decision: "POST /api/v1/sessions/{session_id}/admissions/{admission_id}/decision",
|
|
32
|
+
revoke: "POST /api/v1/sessions/{session_id}/admissions/{admission_id}/revoke",
|
|
33
|
+
mode: "POST /api/v1/sessions/{session_id}/admission-mode",
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
function normalizeString(value) {
|
|
37
|
+
return String(value ?? "").trim();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function required(value, label) {
|
|
41
|
+
const normalized = normalizeString(value);
|
|
42
|
+
if (!normalized) throw new Error(`${label} is required.`);
|
|
43
|
+
return normalized;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function authContext(targetPath, resolveAuthSession) {
|
|
47
|
+
const auth = await resolveAuthSession({
|
|
48
|
+
cwd: targetPath,
|
|
49
|
+
env: process.env,
|
|
50
|
+
autoRotate: false,
|
|
51
|
+
});
|
|
52
|
+
if (!auth?.token || !auth?.apiUrl) {
|
|
53
|
+
throw new Error("Not authenticated. Run `sl auth login` first.");
|
|
54
|
+
}
|
|
55
|
+
return {
|
|
56
|
+
token: auth.token,
|
|
57
|
+
apiUrl: normalizeString(auth.apiUrl).replace(/\/+$/, ""),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function admissionsUrl(apiUrl, sessionId, suffix = "") {
|
|
62
|
+
return `${apiUrl}/api/v1/sessions/${encodeURIComponent(sessionId)}/admissions${suffix}`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function boundedIdempotencyKey(value, operation) {
|
|
66
|
+
const explicit = normalizeString(value);
|
|
67
|
+
if (explicit.length > 128) throw new Error("--idempotency-key must be at most 128 characters.");
|
|
68
|
+
return explicit || createSessionMutationIdempotencyKey(operation);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function mutateAdmission(
|
|
72
|
+
sessionId,
|
|
73
|
+
suffix,
|
|
74
|
+
routeId,
|
|
75
|
+
body,
|
|
76
|
+
{
|
|
77
|
+
targetPath,
|
|
78
|
+
idempotencyKey,
|
|
79
|
+
operationName,
|
|
80
|
+
origin,
|
|
81
|
+
resolveAuthSession,
|
|
82
|
+
requestMutation,
|
|
83
|
+
},
|
|
84
|
+
) {
|
|
85
|
+
const key = boundedIdempotencyKey(idempotencyKey, operationName);
|
|
86
|
+
const auth = await authContext(targetPath, resolveAuthSession);
|
|
87
|
+
const result = await requestMutation(admissionsUrl(auth.apiUrl, sessionId, suffix), {
|
|
88
|
+
method: "POST",
|
|
89
|
+
operationName,
|
|
90
|
+
idempotencyKey: key,
|
|
91
|
+
headers: createSessionMutationHeaders({
|
|
92
|
+
bearerToken: auth.token,
|
|
93
|
+
sessionId,
|
|
94
|
+
routeId,
|
|
95
|
+
idempotencyKey: key,
|
|
96
|
+
origin,
|
|
97
|
+
}),
|
|
98
|
+
body,
|
|
99
|
+
});
|
|
100
|
+
return { idempotencyKey: key, result };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export async function listSessionAdmissions(
|
|
104
|
+
sessionId,
|
|
105
|
+
{
|
|
106
|
+
status = "",
|
|
107
|
+
targetPath = process.cwd(),
|
|
108
|
+
resolveAuthSession = resolveActiveAuthSession,
|
|
109
|
+
requestRead = requestJson,
|
|
110
|
+
} = {},
|
|
111
|
+
) {
|
|
112
|
+
const sid = required(sessionId, "session id");
|
|
113
|
+
const normalizedStatus = normalizeString(status).toLowerCase();
|
|
114
|
+
if (normalizedStatus && !ADMISSION_STATUSES.includes(normalizedStatus)) {
|
|
115
|
+
throw new Error(
|
|
116
|
+
`--status must be one of: ${ADMISSION_STATUSES.join(", ")}.`,
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
const auth = await authContext(targetPath, resolveAuthSession);
|
|
120
|
+
const query = normalizedStatus ? `?status=${encodeURIComponent(normalizedStatus)}` : "";
|
|
121
|
+
return requestRead(`${admissionsUrl(auth.apiUrl, sid)}${query}`, {
|
|
122
|
+
method: "GET",
|
|
123
|
+
headers: { Authorization: `Bearer ${auth.token}` },
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export async function getSessionAdmission(
|
|
128
|
+
sessionId,
|
|
129
|
+
admissionId,
|
|
130
|
+
{
|
|
131
|
+
targetPath = process.cwd(),
|
|
132
|
+
resolveAuthSession = resolveActiveAuthSession,
|
|
133
|
+
requestRead = requestJson,
|
|
134
|
+
} = {},
|
|
135
|
+
) {
|
|
136
|
+
const sid = required(sessionId, "session id");
|
|
137
|
+
const aid = required(admissionId, "admission id");
|
|
138
|
+
const auth = await authContext(targetPath, resolveAuthSession);
|
|
139
|
+
return requestRead(admissionsUrl(auth.apiUrl, sid, `/${encodeURIComponent(aid)}`), {
|
|
140
|
+
method: "GET",
|
|
141
|
+
headers: { Authorization: `Bearer ${auth.token}` },
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export async function decideSessionAdmission(
|
|
146
|
+
sessionId,
|
|
147
|
+
admissionId,
|
|
148
|
+
{
|
|
149
|
+
decision,
|
|
150
|
+
grantedActions,
|
|
151
|
+
ttlSeconds,
|
|
152
|
+
note,
|
|
153
|
+
targetPath = process.cwd(),
|
|
154
|
+
idempotencyKey = "",
|
|
155
|
+
origin = SESSION_MUTATION_ORIGIN,
|
|
156
|
+
resolveAuthSession = resolveActiveAuthSession,
|
|
157
|
+
requestMutation = requestJsonMutation,
|
|
158
|
+
} = {},
|
|
159
|
+
) {
|
|
160
|
+
const sid = required(sessionId, "session id");
|
|
161
|
+
const aid = required(admissionId, "admission id");
|
|
162
|
+
const normalizedDecision = normalizeString(decision).toLowerCase();
|
|
163
|
+
if (!["approve", "deny"].includes(normalizedDecision)) {
|
|
164
|
+
throw new Error("decision must be approve or deny.");
|
|
165
|
+
}
|
|
166
|
+
if (normalizedDecision === "deny" && (grantedActions !== undefined || ttlSeconds !== undefined)) {
|
|
167
|
+
throw new Error("A denied admission cannot include --scope or --ttl.");
|
|
168
|
+
}
|
|
169
|
+
const body = { decision: normalizedDecision };
|
|
170
|
+
if (grantedActions !== undefined) body.grantedActions = grantedActions;
|
|
171
|
+
if (ttlSeconds !== undefined) body.ttlSeconds = ttlSeconds;
|
|
172
|
+
const normalizedNote = normalizeString(note);
|
|
173
|
+
if (normalizedNote) body.note = normalizedNote;
|
|
174
|
+
return mutateAdmission(
|
|
175
|
+
sid,
|
|
176
|
+
`/${encodeURIComponent(aid)}/decision`,
|
|
177
|
+
ROUTES.decision,
|
|
178
|
+
body,
|
|
179
|
+
{
|
|
180
|
+
targetPath,
|
|
181
|
+
idempotencyKey,
|
|
182
|
+
operationName: `session.admission_${normalizedDecision}`,
|
|
183
|
+
origin,
|
|
184
|
+
resolveAuthSession,
|
|
185
|
+
requestMutation,
|
|
186
|
+
},
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export async function revokeSessionAdmission(
|
|
191
|
+
sessionId,
|
|
192
|
+
admissionId,
|
|
193
|
+
{
|
|
194
|
+
reason,
|
|
195
|
+
targetPath = process.cwd(),
|
|
196
|
+
idempotencyKey = "",
|
|
197
|
+
origin = SESSION_MUTATION_ORIGIN,
|
|
198
|
+
resolveAuthSession = resolveActiveAuthSession,
|
|
199
|
+
requestMutation = requestJsonMutation,
|
|
200
|
+
} = {},
|
|
201
|
+
) {
|
|
202
|
+
const sid = required(sessionId, "session id");
|
|
203
|
+
const aid = required(admissionId, "admission id");
|
|
204
|
+
const normalizedReason = normalizeString(reason);
|
|
205
|
+
return mutateAdmission(
|
|
206
|
+
sid,
|
|
207
|
+
`/${encodeURIComponent(aid)}/revoke`,
|
|
208
|
+
ROUTES.revoke,
|
|
209
|
+
normalizedReason ? { reason: normalizedReason } : {},
|
|
210
|
+
{
|
|
211
|
+
targetPath,
|
|
212
|
+
idempotencyKey,
|
|
213
|
+
operationName: "session.admission_revoke",
|
|
214
|
+
origin,
|
|
215
|
+
resolveAuthSession,
|
|
216
|
+
requestMutation,
|
|
217
|
+
},
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export async function setSessionAdmissionMode(
|
|
222
|
+
sessionId,
|
|
223
|
+
mode,
|
|
224
|
+
{
|
|
225
|
+
targetPath = process.cwd(),
|
|
226
|
+
idempotencyKey = "",
|
|
227
|
+
origin = SESSION_MUTATION_ORIGIN,
|
|
228
|
+
resolveAuthSession = resolveActiveAuthSession,
|
|
229
|
+
requestMutation = requestJsonMutation,
|
|
230
|
+
} = {},
|
|
231
|
+
) {
|
|
232
|
+
const sid = required(sessionId, "session id");
|
|
233
|
+
const normalizedMode = normalizeString(mode).toLowerCase();
|
|
234
|
+
if (!["legacy", "required"].includes(normalizedMode)) {
|
|
235
|
+
throw new Error("mode must be legacy or required.");
|
|
236
|
+
}
|
|
237
|
+
const key = boundedIdempotencyKey(idempotencyKey, "session.admission_mode");
|
|
238
|
+
const auth = await authContext(targetPath, resolveAuthSession);
|
|
239
|
+
const result = await requestMutation(
|
|
240
|
+
`${auth.apiUrl}/api/v1/sessions/${encodeURIComponent(sid)}/admission-mode`,
|
|
241
|
+
{
|
|
242
|
+
method: "POST",
|
|
243
|
+
operationName: "session.admission_mode",
|
|
244
|
+
idempotencyKey: key,
|
|
245
|
+
headers: createSessionMutationHeaders({
|
|
246
|
+
bearerToken: auth.token,
|
|
247
|
+
sessionId: sid,
|
|
248
|
+
routeId: ROUTES.mode,
|
|
249
|
+
idempotencyKey: key,
|
|
250
|
+
origin,
|
|
251
|
+
}),
|
|
252
|
+
body: { mode: normalizedMode },
|
|
253
|
+
},
|
|
254
|
+
);
|
|
255
|
+
return { idempotencyKey: key, result };
|
|
256
|
+
}
|