codex-agent-view 0.4.6 → 0.4.8
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/.codex-plugin/plugin.json +1 -1
- package/README.ko.md +10 -10
- package/README.md +10 -10
- package/bin/codex-agent-view.mjs +297 -11
- package/package.json +1 -1
- package/public/app.js +339 -29
- package/skills/show-agents/SKILL.md +40 -64
- package/src/core/monitor-store.mjs +37 -0
- package/src/runtime/server.mjs +371 -14
|
@@ -232,6 +232,27 @@ function addActivity(session, event, status, limit) {
|
|
|
232
232
|
}
|
|
233
233
|
}
|
|
234
234
|
|
|
235
|
+
function refineUnresolvedStartActivity(
|
|
236
|
+
session,
|
|
237
|
+
{ type, idField, id, status },
|
|
238
|
+
) {
|
|
239
|
+
const unresolvedStatuses = new Set([
|
|
240
|
+
"running",
|
|
241
|
+
"completion_not_observed",
|
|
242
|
+
"interrupted",
|
|
243
|
+
]);
|
|
244
|
+
for (const activity of session.recent_activities) {
|
|
245
|
+
if (
|
|
246
|
+
activity.type === type &&
|
|
247
|
+
activity[idField] === id &&
|
|
248
|
+
unresolvedStatuses.has(activity.status)
|
|
249
|
+
) {
|
|
250
|
+
activity.status = status;
|
|
251
|
+
break;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
235
256
|
function applySessionEvent(session, event, limits) {
|
|
236
257
|
const lifecycle = session.lifecycle;
|
|
237
258
|
if (event.type === "session_started") {
|
|
@@ -377,6 +398,14 @@ function applySubagentEvent(session, event, limits) {
|
|
|
377
398
|
agent.stopped_at_ms = event.received_at_ms;
|
|
378
399
|
agent.status = agent.start_observed ? "stopped" : "stopped_without_start";
|
|
379
400
|
agent.has_out_of_order_events = !agent.start_observed;
|
|
401
|
+
if (agent.start_observed) {
|
|
402
|
+
refineUnresolvedStartActivity(session, {
|
|
403
|
+
type: "subagent_started",
|
|
404
|
+
idField: "agent_id",
|
|
405
|
+
id: event.agent_id,
|
|
406
|
+
status: "stopped",
|
|
407
|
+
});
|
|
408
|
+
}
|
|
380
409
|
}
|
|
381
410
|
|
|
382
411
|
agent.agent_type = event.agent_type;
|
|
@@ -430,6 +459,14 @@ function applyToolEvent(session, event, limits) {
|
|
|
430
459
|
tool.status = tool.start_observed ? "completed" : "completed_without_start";
|
|
431
460
|
tool.has_out_of_order_events = !tool.start_observed;
|
|
432
461
|
activityStatus = tool.status;
|
|
462
|
+
if (tool.start_observed) {
|
|
463
|
+
refineUnresolvedStartActivity(session, {
|
|
464
|
+
type: "tool_started",
|
|
465
|
+
idField: "tool_use_id",
|
|
466
|
+
id: event.tool_use_id,
|
|
467
|
+
status: "completed",
|
|
468
|
+
});
|
|
469
|
+
}
|
|
433
470
|
|
|
434
471
|
const permission = session.permission;
|
|
435
472
|
if (
|
package/src/runtime/server.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createServer } from "node:http";
|
|
2
2
|
import { readFile } from "node:fs/promises";
|
|
3
|
-
import { timingSafeEqual } from "node:crypto";
|
|
3
|
+
import { createHmac, timingSafeEqual } from "node:crypto";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
|
|
6
6
|
import { createMonitorStore } from "../core/index.mjs";
|
|
@@ -29,6 +29,20 @@ const SECURITY_HEADERS = {
|
|
|
29
29
|
"x-frame-options": "DENY",
|
|
30
30
|
};
|
|
31
31
|
|
|
32
|
+
const RECOVERY_HEADER = "x-codex-agent-view-recovery";
|
|
33
|
+
const ACCESS_HEADER = "x-codex-agent-view-access";
|
|
34
|
+
const BOOTSTRAP_SCOPE = "viewer_bootstrap";
|
|
35
|
+
const RECOVERY_SCOPE = "viewer_recovery";
|
|
36
|
+
const ACCESS_SCOPE = "viewer_access";
|
|
37
|
+
const BOOTSTRAP_TTL_MS = 60 * 1_000;
|
|
38
|
+
const RECOVERY_TTL_MS = 30 * 60 * 1_000;
|
|
39
|
+
const ACCESS_TTL_MS = 15 * 60 * 1_000;
|
|
40
|
+
const MAX_SIGNED_CREDENTIAL_LENGTH = 1_024;
|
|
41
|
+
const MAX_USED_BOOTSTRAP_GRANTS = 256;
|
|
42
|
+
const OWNERSHIP_PROOF_DOMAIN = "codex-agent-view/runtime-ownership/v1";
|
|
43
|
+
const OWNERSHIP_NONCE_PATTERN = /^[A-Za-z0-9_-]{43}$/;
|
|
44
|
+
const CANONICAL_SESSION_ID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
|
|
45
|
+
|
|
32
46
|
function sendJson(response, statusCode, value, extraHeaders = {}) {
|
|
33
47
|
response.writeHead(statusCode, {
|
|
34
48
|
...SECURITY_HEADERS,
|
|
@@ -38,14 +52,12 @@ function sendJson(response, statusCode, value, extraHeaders = {}) {
|
|
|
38
52
|
response.end(`${JSON.stringify(value)}\n`);
|
|
39
53
|
}
|
|
40
54
|
|
|
41
|
-
function
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
: value.split(":", 1)[0];
|
|
48
|
-
return hostname === LOOPBACK_HOST || hostname === "localhost" || hostname === "::1";
|
|
55
|
+
function isOriginFormRequestTarget(value) {
|
|
56
|
+
return (
|
|
57
|
+
typeof value === "string" &&
|
|
58
|
+
/^\/(?!\/)/.test(value) &&
|
|
59
|
+
!value.includes("\\")
|
|
60
|
+
);
|
|
49
61
|
}
|
|
50
62
|
|
|
51
63
|
function hasToken(request, token) {
|
|
@@ -58,6 +70,120 @@ function hasToken(request, token) {
|
|
|
58
70
|
return supplied.length === expected.length && timingSafeEqual(supplied, expected);
|
|
59
71
|
}
|
|
60
72
|
|
|
73
|
+
function signCredential(payload, signingToken) {
|
|
74
|
+
const encodedPayload = Buffer.from(JSON.stringify(payload)).toString("base64url");
|
|
75
|
+
const signature = createHmac("sha256", signingToken)
|
|
76
|
+
.update(encodedPayload)
|
|
77
|
+
.digest("base64url");
|
|
78
|
+
return `${encodedPayload}.${signature}`;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function createSignedCredential({
|
|
82
|
+
audience,
|
|
83
|
+
excludeSessionId = null,
|
|
84
|
+
grantId,
|
|
85
|
+
familyExpiresAtMs,
|
|
86
|
+
nowMs,
|
|
87
|
+
scope,
|
|
88
|
+
ttlMs,
|
|
89
|
+
signingToken,
|
|
90
|
+
}) {
|
|
91
|
+
const expiresAtMs = Math.min(nowMs + ttlMs, familyExpiresAtMs);
|
|
92
|
+
const payload = {
|
|
93
|
+
aud: audience,
|
|
94
|
+
exclude_session_id: excludeSessionId,
|
|
95
|
+
exp: expiresAtMs,
|
|
96
|
+
family_exp: familyExpiresAtMs,
|
|
97
|
+
scope,
|
|
98
|
+
v: 1,
|
|
99
|
+
};
|
|
100
|
+
if (scope === BOOTSTRAP_SCOPE) {
|
|
101
|
+
payload.jti = grantId;
|
|
102
|
+
}
|
|
103
|
+
return {
|
|
104
|
+
credential: signCredential(payload, signingToken),
|
|
105
|
+
expiresAtMs,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function validateSignedCredential(
|
|
110
|
+
credential,
|
|
111
|
+
{ audience, nowMs, scope, ttlMs, signingToken },
|
|
112
|
+
) {
|
|
113
|
+
if (
|
|
114
|
+
typeof credential !== "string" ||
|
|
115
|
+
credential.length === 0 ||
|
|
116
|
+
credential.length > MAX_SIGNED_CREDENTIAL_LENGTH
|
|
117
|
+
) {
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
const parts = credential.split(".");
|
|
121
|
+
if (parts.length !== 2 || !parts.every((part) => /^[A-Za-z0-9_-]+$/.test(part))) {
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
const [encodedPayload, suppliedSignature] = parts;
|
|
125
|
+
const expectedSignature = createHmac("sha256", signingToken)
|
|
126
|
+
.update(encodedPayload)
|
|
127
|
+
.digest("base64url");
|
|
128
|
+
const supplied = Buffer.from(suppliedSignature);
|
|
129
|
+
const expected = Buffer.from(expectedSignature);
|
|
130
|
+
if (supplied.length !== expected.length || !timingSafeEqual(supplied, expected)) {
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
let payload;
|
|
135
|
+
try {
|
|
136
|
+
payload = JSON.parse(Buffer.from(encodedPayload, "base64url").toString("utf8"));
|
|
137
|
+
} catch {
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
const expectedKeys = scope === BOOTSTRAP_SCOPE
|
|
141
|
+
? "aud,exclude_session_id,exp,family_exp,jti,scope,v"
|
|
142
|
+
: "aud,exclude_session_id,exp,family_exp,scope,v";
|
|
143
|
+
if (
|
|
144
|
+
payload === null ||
|
|
145
|
+
typeof payload !== "object" ||
|
|
146
|
+
Array.isArray(payload) ||
|
|
147
|
+
Object.keys(payload).sort().join(",") !== expectedKeys ||
|
|
148
|
+
payload.v !== 1 ||
|
|
149
|
+
payload.scope !== scope ||
|
|
150
|
+
payload.aud !== audience ||
|
|
151
|
+
!(
|
|
152
|
+
payload.exclude_session_id === null ||
|
|
153
|
+
(
|
|
154
|
+
typeof payload.exclude_session_id === "string" &&
|
|
155
|
+
CANONICAL_SESSION_ID_PATTERN.test(payload.exclude_session_id)
|
|
156
|
+
)
|
|
157
|
+
) ||
|
|
158
|
+
(scope === BOOTSTRAP_SCOPE && !/^[A-Za-z0-9_-]{43}$/.test(payload.jti)) ||
|
|
159
|
+
!Number.isSafeInteger(payload.exp) ||
|
|
160
|
+
!Number.isSafeInteger(payload.family_exp) ||
|
|
161
|
+
payload.exp <= nowMs ||
|
|
162
|
+
payload.exp > nowMs + ttlMs ||
|
|
163
|
+
payload.family_exp <= nowMs ||
|
|
164
|
+
payload.exp > payload.family_exp
|
|
165
|
+
) {
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
return payload;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function createOwnershipProof(nonce, runtimeToken) {
|
|
172
|
+
return createHmac("sha256", runtimeToken)
|
|
173
|
+
.update(OWNERSHIP_PROOF_DOMAIN)
|
|
174
|
+
.update("\0")
|
|
175
|
+
.update(nonce)
|
|
176
|
+
.digest("base64url");
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function bearerValue(request) {
|
|
180
|
+
const authorization = request.headers.authorization;
|
|
181
|
+
if (typeof authorization !== "string" || !authorization.startsWith("Bearer ")) {
|
|
182
|
+
return "";
|
|
183
|
+
}
|
|
184
|
+
return authorization.slice("Bearer ".length);
|
|
185
|
+
}
|
|
186
|
+
|
|
61
187
|
async function readJsonBody(request) {
|
|
62
188
|
const chunks = [];
|
|
63
189
|
let bytes = 0;
|
|
@@ -97,28 +223,259 @@ export async function startMonitorServer({
|
|
|
97
223
|
throw new Error(`monitor server must bind to ${LOOPBACK_HOST}`);
|
|
98
224
|
}
|
|
99
225
|
|
|
226
|
+
const usedBootstrapGrants = new Map();
|
|
100
227
|
const server = createServer(async (request, response) => {
|
|
101
228
|
try {
|
|
102
|
-
|
|
103
|
-
|
|
229
|
+
const address = server.address();
|
|
230
|
+
if (address === null || typeof address === "string") {
|
|
231
|
+
sendJson(response, 503, { error: "monitor unavailable" });
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
const audience = `http://${LOOPBACK_HOST}:${address.port}`;
|
|
235
|
+
const exactHost = `${LOOPBACK_HOST}:${address.port}`;
|
|
236
|
+
if (
|
|
237
|
+
request.headers.host !== exactHost ||
|
|
238
|
+
!isOriginFormRequestTarget(request.url)
|
|
239
|
+
) {
|
|
240
|
+
sendJson(response, 421, { error: "exact monitor authority required" });
|
|
104
241
|
return;
|
|
105
242
|
}
|
|
106
243
|
|
|
107
|
-
const requestUrl = new URL(request.url
|
|
244
|
+
const requestUrl = new URL(request.url, audience);
|
|
245
|
+
const nowMs = now();
|
|
108
246
|
if (request.method === "GET" && requestUrl.pathname === "/api/health") {
|
|
109
247
|
sendJson(response, 200, { ok: true });
|
|
110
248
|
return;
|
|
111
249
|
}
|
|
112
250
|
|
|
251
|
+
if (
|
|
252
|
+
request.method === "POST" &&
|
|
253
|
+
requestUrl.pathname === "/api/internal/ownership-proof"
|
|
254
|
+
) {
|
|
255
|
+
if (request.headers["content-type"] !== "application/json") {
|
|
256
|
+
sendJson(response, 415, { error: "application/json required" });
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
const payload = await readJsonBody(request);
|
|
260
|
+
if (
|
|
261
|
+
payload === null ||
|
|
262
|
+
typeof payload !== "object" ||
|
|
263
|
+
Array.isArray(payload) ||
|
|
264
|
+
Object.keys(payload).join(",") !== "nonce" ||
|
|
265
|
+
!OWNERSHIP_NONCE_PATTERN.test(payload.nonce)
|
|
266
|
+
) {
|
|
267
|
+
sendJson(response, 400, { error: "invalid ownership challenge" });
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
sendJson(response, 200, {
|
|
271
|
+
proof: createOwnershipProof(payload.nonce, token),
|
|
272
|
+
status: "owned",
|
|
273
|
+
});
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if (
|
|
278
|
+
request.method === "POST" &&
|
|
279
|
+
requestUrl.pathname === "/api/internal/viewer-grant"
|
|
280
|
+
) {
|
|
281
|
+
if (!hasToken(request, token)) {
|
|
282
|
+
sendJson(response, 401, { error: "authorization required" });
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
if (request.headers["content-type"] !== "application/json") {
|
|
286
|
+
sendJson(response, 415, { error: "application/json required" });
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
const payload = await readJsonBody(request);
|
|
290
|
+
if (
|
|
291
|
+
payload === null ||
|
|
292
|
+
typeof payload !== "object" ||
|
|
293
|
+
Array.isArray(payload) ||
|
|
294
|
+
Object.keys(payload).join(",") !== "exclude_session_id" ||
|
|
295
|
+
!(
|
|
296
|
+
payload.exclude_session_id === null ||
|
|
297
|
+
(
|
|
298
|
+
typeof payload.exclude_session_id === "string" &&
|
|
299
|
+
CANONICAL_SESSION_ID_PATTERN.test(payload.exclude_session_id)
|
|
300
|
+
)
|
|
301
|
+
)
|
|
302
|
+
) {
|
|
303
|
+
sendJson(response, 400, { error: "invalid viewer grant request" });
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
const familyExpiresAtMs = nowMs + RECOVERY_TTL_MS;
|
|
307
|
+
const bootstrap = createSignedCredential({
|
|
308
|
+
audience,
|
|
309
|
+
excludeSessionId: payload.exclude_session_id,
|
|
310
|
+
familyExpiresAtMs,
|
|
311
|
+
grantId: createRuntimeToken(),
|
|
312
|
+
nowMs,
|
|
313
|
+
scope: BOOTSTRAP_SCOPE,
|
|
314
|
+
ttlMs: BOOTSTRAP_TTL_MS,
|
|
315
|
+
signingToken: token,
|
|
316
|
+
});
|
|
317
|
+
sendJson(response, 201, {
|
|
318
|
+
bootstrap_credential: bootstrap.credential,
|
|
319
|
+
expires_in_ms: BOOTSTRAP_TTL_MS,
|
|
320
|
+
status: "granted",
|
|
321
|
+
});
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
|
|
113
325
|
if (
|
|
114
326
|
request.method === "GET" &&
|
|
115
327
|
requestUrl.pathname === "/api/state"
|
|
116
328
|
) {
|
|
117
|
-
|
|
329
|
+
const suppliedBearer = bearerValue(request);
|
|
330
|
+
const accessPayload = validateSignedCredential(suppliedBearer, {
|
|
331
|
+
audience,
|
|
332
|
+
nowMs,
|
|
333
|
+
scope: ACCESS_SCOPE,
|
|
334
|
+
ttlMs: ACCESS_TTL_MS,
|
|
335
|
+
signingToken: viewerToken,
|
|
336
|
+
});
|
|
337
|
+
const runtimeAuthorized = hasToken(request, token);
|
|
338
|
+
const rootViewerAuthorized = hasToken(request, viewerToken);
|
|
339
|
+
if (
|
|
340
|
+
!runtimeAuthorized &&
|
|
341
|
+
!rootViewerAuthorized &&
|
|
342
|
+
!accessPayload
|
|
343
|
+
) {
|
|
118
344
|
sendJson(response, 401, { error: "authorization required" });
|
|
119
345
|
return;
|
|
120
346
|
}
|
|
121
|
-
|
|
347
|
+
const requestedExclusion = request.headers["x-codex-agent-view-exclude-session"];
|
|
348
|
+
const excludeSessionId = accessPayload?.exclude_session_id ?? (
|
|
349
|
+
rootViewerAuthorized &&
|
|
350
|
+
typeof requestedExclusion === "string" &&
|
|
351
|
+
CANONICAL_SESSION_ID_PATTERN.test(requestedExclusion)
|
|
352
|
+
? requestedExclusion
|
|
353
|
+
: null
|
|
354
|
+
);
|
|
355
|
+
const extraHeaders = {};
|
|
356
|
+
if (rootViewerAuthorized || accessPayload) {
|
|
357
|
+
const familyExpiresAtMs = accessPayload?.family_exp ?? (
|
|
358
|
+
nowMs + RECOVERY_TTL_MS
|
|
359
|
+
);
|
|
360
|
+
const access = createSignedCredential({
|
|
361
|
+
audience,
|
|
362
|
+
excludeSessionId,
|
|
363
|
+
familyExpiresAtMs,
|
|
364
|
+
nowMs,
|
|
365
|
+
scope: ACCESS_SCOPE,
|
|
366
|
+
ttlMs: ACCESS_TTL_MS,
|
|
367
|
+
signingToken: viewerToken,
|
|
368
|
+
});
|
|
369
|
+
extraHeaders[ACCESS_HEADER] = access.credential;
|
|
370
|
+
}
|
|
371
|
+
if (rootViewerAuthorized) {
|
|
372
|
+
const familyExpiresAtMs = nowMs + RECOVERY_TTL_MS;
|
|
373
|
+
const recovery = createSignedCredential({
|
|
374
|
+
audience,
|
|
375
|
+
excludeSessionId,
|
|
376
|
+
familyExpiresAtMs,
|
|
377
|
+
nowMs,
|
|
378
|
+
scope: RECOVERY_SCOPE,
|
|
379
|
+
ttlMs: RECOVERY_TTL_MS,
|
|
380
|
+
signingToken: viewerToken,
|
|
381
|
+
});
|
|
382
|
+
extraHeaders[RECOVERY_HEADER] = recovery.credential;
|
|
383
|
+
}
|
|
384
|
+
sendJson(response, 200, store.getSnapshot(), {
|
|
385
|
+
...extraHeaders,
|
|
386
|
+
});
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
if (
|
|
391
|
+
request.method === "POST" &&
|
|
392
|
+
requestUrl.pathname === "/api/viewer/exchange"
|
|
393
|
+
) {
|
|
394
|
+
if (
|
|
395
|
+
request.headers.origin !== audience ||
|
|
396
|
+
(
|
|
397
|
+
request.headers["sec-fetch-site"] !== undefined &&
|
|
398
|
+
request.headers["sec-fetch-site"] !== "same-origin"
|
|
399
|
+
)
|
|
400
|
+
) {
|
|
401
|
+
sendJson(response, 403, { error: "same-origin request required" });
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
if (request.headers["content-type"] !== "application/json") {
|
|
405
|
+
sendJson(response, 415, { error: "application/json required" });
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
const payload = await readJsonBody(request);
|
|
409
|
+
const exactPayload =
|
|
410
|
+
payload === null ||
|
|
411
|
+
typeof payload !== "object" ||
|
|
412
|
+
Array.isArray(payload) ||
|
|
413
|
+
Object.keys(payload).join(",") !== "credential";
|
|
414
|
+
const bootstrapPayload = exactPayload ? null : validateSignedCredential(
|
|
415
|
+
payload.credential,
|
|
416
|
+
{
|
|
417
|
+
audience,
|
|
418
|
+
nowMs,
|
|
419
|
+
scope: BOOTSTRAP_SCOPE,
|
|
420
|
+
ttlMs: BOOTSTRAP_TTL_MS,
|
|
421
|
+
signingToken: token,
|
|
422
|
+
},
|
|
423
|
+
);
|
|
424
|
+
const recoveryPayload = exactPayload || bootstrapPayload
|
|
425
|
+
? null
|
|
426
|
+
: validateSignedCredential(payload.credential, {
|
|
427
|
+
audience,
|
|
428
|
+
nowMs,
|
|
429
|
+
scope: RECOVERY_SCOPE,
|
|
430
|
+
ttlMs: RECOVERY_TTL_MS,
|
|
431
|
+
signingToken: viewerToken,
|
|
432
|
+
});
|
|
433
|
+
const credentialPayload = bootstrapPayload || recoveryPayload;
|
|
434
|
+
if (!credentialPayload) {
|
|
435
|
+
sendJson(response, 401, { error: "viewer exchange authorization required" });
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
if (bootstrapPayload) {
|
|
439
|
+
for (const [grantId, expiresAtMs] of usedBootstrapGrants) {
|
|
440
|
+
if (expiresAtMs <= nowMs) usedBootstrapGrants.delete(grantId);
|
|
441
|
+
}
|
|
442
|
+
if (usedBootstrapGrants.has(bootstrapPayload.jti)) {
|
|
443
|
+
sendJson(response, 409, { error: "viewer grant already used" });
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
if (usedBootstrapGrants.size >= MAX_USED_BOOTSTRAP_GRANTS) {
|
|
447
|
+
sendJson(response, 503, { error: "viewer grant capacity reached" });
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
usedBootstrapGrants.set(bootstrapPayload.jti, bootstrapPayload.exp);
|
|
451
|
+
}
|
|
452
|
+
const excludeSessionId = credentialPayload.exclude_session_id;
|
|
453
|
+
const access = createSignedCredential({
|
|
454
|
+
audience,
|
|
455
|
+
excludeSessionId,
|
|
456
|
+
familyExpiresAtMs: credentialPayload.family_exp,
|
|
457
|
+
nowMs,
|
|
458
|
+
scope: ACCESS_SCOPE,
|
|
459
|
+
ttlMs: ACCESS_TTL_MS,
|
|
460
|
+
signingToken: viewerToken,
|
|
461
|
+
});
|
|
462
|
+
const recovery = createSignedCredential({
|
|
463
|
+
audience,
|
|
464
|
+
excludeSessionId,
|
|
465
|
+
familyExpiresAtMs: credentialPayload.family_exp,
|
|
466
|
+
nowMs,
|
|
467
|
+
scope: RECOVERY_SCOPE,
|
|
468
|
+
ttlMs: RECOVERY_TTL_MS,
|
|
469
|
+
signingToken: viewerToken,
|
|
470
|
+
});
|
|
471
|
+
sendJson(response, 200, {
|
|
472
|
+
access_credential: access.credential,
|
|
473
|
+
access_expires_in_ms: access.expiresAtMs - nowMs,
|
|
474
|
+
excluded_session_id: excludeSessionId,
|
|
475
|
+
recovery_credential: recovery.credential,
|
|
476
|
+
recovery_expires_in_ms: recovery.expiresAtMs - nowMs,
|
|
477
|
+
status: "exchanged",
|
|
478
|
+
});
|
|
122
479
|
return;
|
|
123
480
|
}
|
|
124
481
|
|