@sema-agent/server 7.79.0 → 7.79.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/file-history-disclosure.d.ts +53 -0
- package/dist/file-history-disclosure.js +89 -0
- package/dist/http/admission.d.ts +56 -0
- package/dist/http/admission.js +560 -0
- package/dist/http/approval-sweeps.d.ts +21 -0
- package/dist/http/approval-sweeps.js +138 -0
- package/dist/http/dispatch.d.ts +32 -0
- package/dist/http/dispatch.js +230 -0
- package/dist/http/request-guards.d.ts +51 -0
- package/dist/http/request-guards.js +205 -0
- package/dist/http/resume-legs.d.ts +80 -0
- package/dist/http/resume-legs.js +1230 -0
- package/dist/http/route-table.d.ts +113 -0
- package/dist/http/route-table.js +235 -0
- package/dist/http/routes/admin-memory-optout.d.ts +1 -1
- package/dist/http/routes/memory-compliance.d.ts +1 -1
- package/dist/http/routes/memory-origin.d.ts +1 -1
- package/dist/http/routes/rules.js +4 -4
- package/dist/http/server.d.ts +20 -104
- package/dist/http/server.js +24 -2589
- package/dist/main.js +2 -0
- package/dist/observability/metrics.js +1 -1
- package/dist/plugins/file-history-store-sql.js +12 -11
- package/dist/trace/redact.d.ts +9 -0
- package/dist/trace/redact.js +210 -78
- package/package.json +1 -1
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { redactHead } from "../observability/run-terminal-log.js";
|
|
2
|
+
import { findParkOrigin } from "../parked-decide.js";
|
|
3
|
+
import { decodeCheckpointScope } from "../security.js";
|
|
4
|
+
import { personDecision } from "../host-decision.js";
|
|
5
|
+
import { APPROVAL_GATE_KINDS } from "../tool-approval.js";
|
|
6
|
+
import { createWallClockJumpGuard } from "../wall-clock-jump-guard.js";
|
|
7
|
+
import { hasServiceAuth } from "./dispatch.js";
|
|
8
|
+
import { gatedPrincipal } from "./principal-gate.js";
|
|
9
|
+
export function createApprovalSweeps(ctx) {
|
|
10
|
+
const { deps, resumeCheckpoint, mayDiscloseRowError } = ctx;
|
|
11
|
+
const denySweepClockGuard = createWallClockJumpGuard({
|
|
12
|
+
slaMs: deps.config.approvalTimeoutSec > 0 ? deps.config.approvalTimeoutSec * 1000 : undefined,
|
|
13
|
+
onJump: ({ jumpMs, graceMs }) => deps.logger?.warn?.("deny_sweep_wall_clock_jump", {
|
|
14
|
+
jumpMs,
|
|
15
|
+
graceMs,
|
|
16
|
+
note: jumpMs > 0
|
|
17
|
+
? "the wall clock stepped FORWARD relative to the monotonic clock; the approval SLA deny-sweep stands down for graceMs of MONOTONIC time so no pending gate is denied earlier than one full SLA of REAL time ([4232]①/[4235]). The checkpoint terminal_at_ms backstop is unaffected — nothing can hang forever."
|
|
18
|
+
: "the wall clock stepped BACKWARD relative to the monotonic clock; the deny-sweep needs no grace for this direction (a receding deadline only DELAYS a deny) but the step itself is an operational fact worth recording.",
|
|
19
|
+
}),
|
|
20
|
+
onSuppressed: ({ remainingMs }) => deps.logger?.info?.("deny_sweep_clock_grace", { remainingMs }),
|
|
21
|
+
onGraceCapped: ({ totalGraceMs }) => deps.logger?.warn?.("deny_sweep_clock_grace_capped", {
|
|
22
|
+
totalGraceMs,
|
|
23
|
+
note: "the wall clock kept stepping forward; the approval SLA deny-sweep has stood down for the maximum cumulative grace and now RESUMES on a demonstrably unstable clock — a fail-closed leg that never runs is worse than one running on a bad clock. Fix NTP on this host.",
|
|
24
|
+
}),
|
|
25
|
+
});
|
|
26
|
+
async function denyExpiredApprovals(now) {
|
|
27
|
+
const cs = deps.checkpointStore;
|
|
28
|
+
if (!cs)
|
|
29
|
+
return;
|
|
30
|
+
if (!denySweepClockGuard.admit(now))
|
|
31
|
+
return;
|
|
32
|
+
const expired = await cs.listExpiredApprovalGates(now).catch(() => []);
|
|
33
|
+
const isParkedOwned = async (sessionId) => {
|
|
34
|
+
if (deps.backgroundAgentStore === undefined && deps.workflowAgentSessionIndex === undefined)
|
|
35
|
+
return false;
|
|
36
|
+
try {
|
|
37
|
+
const token = await cs.findPendingTokenBySession(sessionId, undefined, { gateKinds: APPROVAL_GATE_KINDS });
|
|
38
|
+
if (!token)
|
|
39
|
+
return false;
|
|
40
|
+
const cp = await cs.get(token);
|
|
41
|
+
if (!cp)
|
|
42
|
+
return false;
|
|
43
|
+
return (await findParkOrigin({
|
|
44
|
+
...(deps.backgroundAgentStore !== undefined ? { agentStore: deps.backgroundAgentStore } : {}),
|
|
45
|
+
...(deps.workflowAgentSessionIndex !== undefined && deps.workflowRunStore !== undefined
|
|
46
|
+
? { workflowIndex: deps.workflowAgentSessionIndex, workflowRunStore: deps.workflowRunStore }
|
|
47
|
+
: {}),
|
|
48
|
+
}, { token, scope: cp.scope, sessionId: cp.sessionId })) !== undefined;
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
let parkedSkipped = 0;
|
|
55
|
+
for (const { sessionId } of expired) {
|
|
56
|
+
if (await isParkedOwned(sessionId)) {
|
|
57
|
+
parkedSkipped += 1;
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
await resumeCheckpoint(sessionId, "deny", "approval SLA expired — auto-denied", { decidedBy: "sla_timeout" }).then((r) => { if (r.status >= 400)
|
|
61
|
+
deps.logger?.warn?.("deny_sweep_resume_blocked", { sessionId, status: r.status, errorCode: r.body?.errorCode }); }, (e) => deps.logger?.warn?.("deny_sweep_resume_failed", { sessionId, err: redactHead(e instanceof Error ? e.message : String(e), 2000) }));
|
|
62
|
+
}
|
|
63
|
+
if (parkedSkipped > 0)
|
|
64
|
+
deps.logger?.info?.("deny_sweep_parked_skipped", { count: parkedSkipped });
|
|
65
|
+
}
|
|
66
|
+
async function redeemParkedAsk(req) {
|
|
67
|
+
const cs = deps.checkpointStore;
|
|
68
|
+
if (!cs) {
|
|
69
|
+
return { status: 404, body: { error: "approval not found", errorCode: "not_found.approval" } };
|
|
70
|
+
}
|
|
71
|
+
if (!hasServiceAuth(deps.config) && !deps.config.allowUnauthedWrites) {
|
|
72
|
+
return { status: 503, body: { error: "this worker requires a service auth token (set SERVICE_AUTH_TOKEN) before accepting task submissions", errorCode: "auth.service_token_required" } };
|
|
73
|
+
}
|
|
74
|
+
if (deps.drainState?.draining) {
|
|
75
|
+
return {
|
|
76
|
+
status: 503,
|
|
77
|
+
body: {
|
|
78
|
+
error: "draining",
|
|
79
|
+
errorCode: "draining",
|
|
80
|
+
message: "this instance is draining for shutdown/upgrade — retry against the replacement instance",
|
|
81
|
+
...(deps.drainState.reason ? { reason: deps.drainState.reason } : {}),
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
if (deps.modelReady && !deps.modelReady()) {
|
|
86
|
+
return {
|
|
87
|
+
status: 503,
|
|
88
|
+
body: {
|
|
89
|
+
error: "model_roster_pending",
|
|
90
|
+
errorCode: "state.model_roster_pending",
|
|
91
|
+
message: "this worker has no model yet (waiting for the first effective-config pull to land the roster) — retry shortly",
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
if (deps.costQuota && req.httpReq) {
|
|
96
|
+
const quotaPrincipal = gatedPrincipal(req.httpReq, deps.config);
|
|
97
|
+
if (quotaPrincipal !== undefined) {
|
|
98
|
+
const d = deps.costQuota.check(quotaPrincipal);
|
|
99
|
+
if (!d.allowed) {
|
|
100
|
+
deps.metrics?.inc("cost_quota_rejected_total");
|
|
101
|
+
return {
|
|
102
|
+
status: 429,
|
|
103
|
+
body: { error: "cost quota exceeded", errorCode: "limit.cost_quota_exceeded", usedMicroUsd: d.usedMicroUsd, limitMicroUsd: d.limitMicroUsd, retryAfterSec: d.retryAfterSec },
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
let cp;
|
|
109
|
+
try {
|
|
110
|
+
cp = await cs.get(req.gateToken);
|
|
111
|
+
}
|
|
112
|
+
catch (e) {
|
|
113
|
+
if (!(await mayDiscloseRowError(cs, req.gateToken, { principal: req.principal, explicitOperator: false }))) {
|
|
114
|
+
deps.logger?.warn?.("parked_late_decision_unreadable_row_withheld", { askId: req.askId });
|
|
115
|
+
return { status: 404, body: { error: "approval not found", errorCode: "not_found.approval" } };
|
|
116
|
+
}
|
|
117
|
+
return { status: 409, body: { error: e instanceof Error ? e.message : String(e), errorCode: "conflict.checkpoint_unreadable" } };
|
|
118
|
+
}
|
|
119
|
+
if (!cp) {
|
|
120
|
+
return { status: 404, body: { error: "approval not found", errorCode: "not_found.approval" } };
|
|
121
|
+
}
|
|
122
|
+
const cpPrincipal = decodeCheckpointScope(cp.scope);
|
|
123
|
+
if (cpPrincipal !== undefined && cpPrincipal !== req.principal) {
|
|
124
|
+
deps.logger?.warn?.("parked_late_decision_scope_mismatch", { askId: req.askId });
|
|
125
|
+
return { status: 404, body: { error: "approval not found", errorCode: "not_found.approval" } };
|
|
126
|
+
}
|
|
127
|
+
const binding = {
|
|
128
|
+
checkpointToken: req.gateToken,
|
|
129
|
+
...(req.gateBoundCallId !== null ? { boundCallId: req.gateBoundCallId } : {}),
|
|
130
|
+
...(req.gateBoundInputHash !== null ? { boundInputHash: req.gateBoundInputHash } : {}),
|
|
131
|
+
...(req.updatedInput !== undefined ? { updatedInput: req.updatedInput } : {}),
|
|
132
|
+
};
|
|
133
|
+
const out = await resumeCheckpoint(cp.sessionId, req.decision, req.note, personDecision(req.principal), req.httpReq, undefined, binding, undefined, true, { principal: req.principal, explicitOperator: false });
|
|
134
|
+
return { status: out.status, body: out.body };
|
|
135
|
+
}
|
|
136
|
+
return { denyExpiredApprovals, redeemParkedAsk };
|
|
137
|
+
}
|
|
138
|
+
//# sourceMappingURL=approval-sweeps.js.map
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 请求流水线(S-147 切片:`http/server.ts` 的 `handle()` 与它的模块级词表搬家;S-146 再把「这是什么路由」
|
|
3
|
+
* 整件事交给 `./route-table.ts`)。
|
|
4
|
+
*
|
|
5
|
+
* 住在这里的是**一条请求从进门到落地**的全过程,一件事一遍:admission 计数 / 附着登记 / CORS / /health /
|
|
6
|
+
* /metrics / 鉴权桥 / 全局服务凭据门 / 无凭据写门 / drain 与 roster-pending 两道 503 / 破坏性会话写门 /
|
|
7
|
+
* 两段分派 / 全局 404。
|
|
8
|
+
*
|
|
9
|
+
* 🔴 S-146 之后本文件**不再持有任何一份路由名单**:分派序、门侧、标签、三只门的判据全部读
|
|
10
|
+
* `ROUTES` / `ROUTE_DECLARATIONS`(见 route-table.ts 头注)。三道门也不再「按行号插在 37 只 if 中间」——
|
|
11
|
+
* 它们在门前段与门后段之间**恒定一处**执行,三段代码逐字未动,只是不再被域调用夹着。
|
|
12
|
+
*
|
|
13
|
+
* ctx 字段 4 只(`deps` / `counters` / `noteActivity` / `routeCtxBase`),机器算(closure-free-vars)与手抄一致。
|
|
14
|
+
* `openResponses` / `lastAttachEndedAt` 是 handle 自己写的外层 `let`/`Set`,随它一起进工厂 —— 这样那两行赋值
|
|
15
|
+
* 正文一个字都不用改(装盒改写会把 `lastAttachEndedAt = …` 改成 `box.x = …`,那就不是 move 了)。
|
|
16
|
+
*/
|
|
17
|
+
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
18
|
+
import type { ServiceConfig } from "../config-types.js";
|
|
19
|
+
import type { RouteCtxBase, RunRegistry } from "./route-ctx.js";
|
|
20
|
+
import type { FlatServiceDeps } from "./server.js";
|
|
21
|
+
/** 「这台部署配了任何一把 service 凭证吗」—— fail-closed 写门族的真值源。**单一属主**:`handle()` 的
|
|
22
|
+
* 凭证门、[ref] 的 parked 赎回席、以及 [ref] 的 consolidation 阀门启动期告警读同一句(手抄迟早
|
|
23
|
+
* 只改一处,而这条判据决定的是「无凭证部署上写口开不开」)。导出仅为最后一个消费点(main.ts)。 */
|
|
24
|
+
export declare function hasServiceAuth(config: ServiceConfig): boolean;
|
|
25
|
+
export declare function createDispatch(ctx: {
|
|
26
|
+
deps: FlatServiceDeps;
|
|
27
|
+
noteActivity: (url: string) => void;
|
|
28
|
+
routeCtxBase: RouteCtxBase;
|
|
29
|
+
} & Pick<RunRegistry, "counters">): {
|
|
30
|
+
handle: (req: IncomingMessage, res: ServerResponse) => Promise<void>;
|
|
31
|
+
};
|
|
32
|
+
//# sourceMappingURL=dispatch.d.ts.map
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { setSsoPrincipal, ssoVerifiedPrincipal, setSsoScope, isDestructiveSessionWrite, PRINCIPAL_TOKEN_HEADER, APPROVAL_MAC_HEADER, APPROVAL_MAC_KID_HEADER } from "../security.js";
|
|
3
|
+
import { publicStoreProbeError } from "../store-live-probe.js";
|
|
4
|
+
import { looksLikeJwt } from "@sema-agent/settings-schema/api/auth-bridge";
|
|
5
|
+
import { PRE_CREDENTIAL_ROUTES, POST_CREDENTIAL_ROUTES, routeLabel, isBillableSubmitPath, isCredentialGatedRewrite, isMethodDispatchedSubmitPath } from "./route-table.js";
|
|
6
|
+
import { sendJson, sendError, isSseResponse } from "./send.js";
|
|
7
|
+
import { authorized, systemFor } from "./principal-gate.js";
|
|
8
|
+
const PROCESS_STARTED_AT_MS = Date.now() - Math.round(process.uptime() * 1000);
|
|
9
|
+
export function hasServiceAuth(config) {
|
|
10
|
+
return Boolean(config.authToken) || Object.keys(config.authTokens ?? {}).length > 0;
|
|
11
|
+
}
|
|
12
|
+
function cors(res, req, origins, principalHeader) {
|
|
13
|
+
if (!origins || origins.length === 0)
|
|
14
|
+
return;
|
|
15
|
+
let allow;
|
|
16
|
+
if (origins.length === 1) {
|
|
17
|
+
allow = origins[0];
|
|
18
|
+
}
|
|
19
|
+
else {
|
|
20
|
+
res.setHeader("vary", "Origin");
|
|
21
|
+
const reqOrigin = typeof req.headers.origin === "string" ? req.headers.origin : undefined;
|
|
22
|
+
allow = reqOrigin !== undefined && origins.includes(reqOrigin) ? reqOrigin : undefined;
|
|
23
|
+
}
|
|
24
|
+
if (allow === undefined)
|
|
25
|
+
return;
|
|
26
|
+
res.setHeader("access-control-allow-origin", allow);
|
|
27
|
+
res.setHeader("access-control-allow-methods", "GET, POST, PUT, DELETE, OPTIONS");
|
|
28
|
+
res.setHeader("access-control-allow-headers", `content-type, authorization, last-event-id, ${principalHeader}, idempotency-key, x-detach-on-disconnect, ${PRINCIPAL_TOKEN_HEADER}, ${APPROVAL_MAC_HEADER}, ${APPROVAL_MAC_KID_HEADER}, if-none-match`);
|
|
29
|
+
res.setHeader("access-control-expose-headers", "x-task-id, retry-after");
|
|
30
|
+
}
|
|
31
|
+
export function createDispatch(ctx) {
|
|
32
|
+
const { deps, noteActivity, routeCtxBase, counters } = ctx;
|
|
33
|
+
const openResponses = new Set();
|
|
34
|
+
let lastAttachEndedAt = 0;
|
|
35
|
+
if (deps.drainState) {
|
|
36
|
+
deps.drainState.attachedStreams = () => {
|
|
37
|
+
let n = 0;
|
|
38
|
+
for (const r of openResponses)
|
|
39
|
+
if (isSseResponse(r))
|
|
40
|
+
n++;
|
|
41
|
+
return n;
|
|
42
|
+
};
|
|
43
|
+
deps.drainState.lastAttachEndedAt = () => lastAttachEndedAt;
|
|
44
|
+
}
|
|
45
|
+
async function dispatchSegment(rows, req, res, url, ctx) {
|
|
46
|
+
for (const row of rows) {
|
|
47
|
+
if (row.enabled !== undefined && !row.enabled(deps))
|
|
48
|
+
continue;
|
|
49
|
+
if (await row.handler(req, res, url, ctx))
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
async function handle(req, res) {
|
|
55
|
+
const startedAt = Date.now();
|
|
56
|
+
const method = req.method ?? "GET";
|
|
57
|
+
const url = (req.url ?? "/").split("?")[0];
|
|
58
|
+
noteActivity(url);
|
|
59
|
+
const admitted = method !== "OPTIONS" && url !== "/health" && !url.startsWith("/metrics");
|
|
60
|
+
if (admitted)
|
|
61
|
+
counters.admittedInflight++;
|
|
62
|
+
if (admitted)
|
|
63
|
+
openResponses.add(res);
|
|
64
|
+
let logged = false;
|
|
65
|
+
const reqState = { streamTaskId: undefined, streamDetached: false, source: null };
|
|
66
|
+
const ctx = { deps: routeCtxBase.deps, registry: routeCtxBase.registry, helpers: routeCtxBase.helpers, local: routeCtxBase.local, legs: routeCtxBase.legs, req: reqState };
|
|
67
|
+
const record = (viaClose) => {
|
|
68
|
+
if (logged)
|
|
69
|
+
return;
|
|
70
|
+
logged = true;
|
|
71
|
+
if (admitted)
|
|
72
|
+
counters.admittedInflight--;
|
|
73
|
+
if (openResponses.delete(res) && isSseResponse(res))
|
|
74
|
+
lastAttachEndedAt = performance.now();
|
|
75
|
+
const route = routeLabel(method, url);
|
|
76
|
+
const seconds = (Date.now() - startedAt) / 1000;
|
|
77
|
+
const aborted = viaClose && !res.writableEnded;
|
|
78
|
+
const status = aborted ? "499" : String(res.statusCode);
|
|
79
|
+
deps.metrics?.inc("http_requests_total", { route, method, status });
|
|
80
|
+
deps.metrics?.observe("http_request_duration_seconds", seconds, { route });
|
|
81
|
+
const log = route === "/health" || route === "/metrics" ? deps.logger?.debug : deps.logger?.info;
|
|
82
|
+
log?.call(deps.logger, "request", { method, route, status, ms: Date.now() - startedAt, ...(reqState.runStatus !== undefined ? { runStatus: reqState.runStatus } : {}), ...(reqState.runErrorCode !== undefined ? { errorCode: reqState.runErrorCode } : {}), ...(aborted ? { aborted: true, ...(reqState.streamTaskId ? { taskId: reqState.streamTaskId } : {}), ...(reqState.streamDetached ? { detached: true } : {}) } : {}) });
|
|
83
|
+
};
|
|
84
|
+
res.on("finish", () => record(false));
|
|
85
|
+
res.on("close", () => record(true));
|
|
86
|
+
cors(res, req, deps.config.corsOrigins, deps.config.principalHeader);
|
|
87
|
+
if (req.method === "OPTIONS") {
|
|
88
|
+
res.writeHead(204);
|
|
89
|
+
res.end();
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
if (req.method === "GET" && url === "/health") {
|
|
93
|
+
const restart = deps.restartState?.();
|
|
94
|
+
const configHash = createHash("sha256")
|
|
95
|
+
.update(JSON.stringify({ model: deps.config.model.id, sessionBackend: deps.config.sessionBackend, dbBackend: deps.config.dbBackend, dataRoot: deps.config.localDataRoot, configProvider: deps.config.configProvider, remoteExec: deps.config.remoteExec?.provider, port: deps.config.port }))
|
|
96
|
+
.digest("hex")
|
|
97
|
+
.slice(0, 16);
|
|
98
|
+
sendJson(res, 200, {
|
|
99
|
+
status: "ok",
|
|
100
|
+
...(typeof deps.capabilities?.version === "string" ? { version: deps.capabilities.version } : {}),
|
|
101
|
+
...(deps.drainState?.draining ? { draining: true, ...(deps.drainState.since ? { drainingSince: deps.drainState.since } : {}), ...(deps.drainState.reason ? { drainReason: deps.drainState.reason } : {}) } : {}),
|
|
102
|
+
...(deps.drainState?.inflight ? { inflight: deps.drainState.inflight() } : {}),
|
|
103
|
+
...(deps.storeDegraded ? { durable: false, storeDegraded: true } : {}),
|
|
104
|
+
...(deps.modelReady && !deps.modelReady() ? { ready: false, readyReason: "model roster pending (first effective-config pull)" } : {}),
|
|
105
|
+
model: deps.config.model.id,
|
|
106
|
+
sessionBackend: deps.sessionStoreLabel ?? deps.config.sessionBackend,
|
|
107
|
+
sessions: deps.runner.sessions.size,
|
|
108
|
+
...(deps.instanceId ? { instanceId: deps.instanceId } : {}),
|
|
109
|
+
...(deps.config.localDataRoot ? { dataRoot: deps.config.localDataRoot } : {}),
|
|
110
|
+
configHash,
|
|
111
|
+
pid: process.pid,
|
|
112
|
+
startedAt: PROCESS_STARTED_AT_MS,
|
|
113
|
+
port: deps.config.port,
|
|
114
|
+
...(deps.config.directDoorActive ? { directDoorActive: true } : {}),
|
|
115
|
+
...(restart ? { restartRequired: true, restart: { reasons: restart.reasons, version: restart.version, since: restart.since } } : {}),
|
|
116
|
+
...(() => {
|
|
117
|
+
const stuck = deps.planeDeferredState?.();
|
|
118
|
+
return stuck ? { modelPlaneDeferred: { version: stuck.version, since: stuck.since, noHandoff: true, ...(stuck.blocked ? { blockedReasons: stuck.blocked } : {}) } } : {};
|
|
119
|
+
})(),
|
|
120
|
+
...(() => {
|
|
121
|
+
const gen = deps.configApplyState?.();
|
|
122
|
+
if (!gen)
|
|
123
|
+
return {};
|
|
124
|
+
return {
|
|
125
|
+
configTargetVersion: gen.configTargetVersion,
|
|
126
|
+
configAppliedVersion: gen.configAppliedVersion,
|
|
127
|
+
configTargetOrdinal: gen.configTargetOrdinal,
|
|
128
|
+
configAppliedOrdinal: gen.configAppliedOrdinal,
|
|
129
|
+
...(gen.configApplyStaleMs !== undefined ? { configApplyStaleMs: gen.configApplyStaleMs } : {}),
|
|
130
|
+
};
|
|
131
|
+
})(),
|
|
132
|
+
...(() => {
|
|
133
|
+
const probe = deps.storeLiveState?.();
|
|
134
|
+
if (!probe)
|
|
135
|
+
return {};
|
|
136
|
+
return {
|
|
137
|
+
...(probe.live ? {} : { storeLive: false }),
|
|
138
|
+
storeProbe: { live: probe.live, ageMs: probe.ageMs, ...(probe.error !== undefined ? { error: publicStoreProbeError(probe.error) } : {}) },
|
|
139
|
+
};
|
|
140
|
+
})(),
|
|
141
|
+
});
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
if (req.method === "GET" && (url === "/metrics" || url === "/metrics/summary" || url === "/metrics/plan-cache")) {
|
|
145
|
+
if (!deps.metrics) {
|
|
146
|
+
sendError(res, 404, "feature.metrics_disabled", "metrics disabled");
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
const { authToken: t, metricsToken: mt } = deps.config;
|
|
150
|
+
const anyT = Boolean(t) || Object.keys(deps.config.authTokens ?? {}).length > 0;
|
|
151
|
+
const devOpen = !anyT && !mt && !deps.config.requirePrincipal;
|
|
152
|
+
const ok = devOpen || (anyT && systemFor(req, deps.config) !== undefined) || (!!mt && authorized(req, mt));
|
|
153
|
+
if (!ok) {
|
|
154
|
+
sendError(res, 401, "auth.unauthorized", "unauthorized");
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
if (url === "/metrics/summary") {
|
|
158
|
+
sendJson(res, 200, { model: deps.config.model.id, ...deps.metrics.summarize() });
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
if (url === "/metrics/plan-cache") {
|
|
162
|
+
sendJson(res, 200, { scopes: deps.planCacheProbe?.dump() ?? {} });
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
res.writeHead(200, { "content-type": "text/plain; version=0.0.4" });
|
|
166
|
+
res.end(deps.metrics.render());
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
if (deps.registryJwtVerifier) {
|
|
170
|
+
const authz = req.headers.authorization ?? "";
|
|
171
|
+
const rawBearer = /^Bearer\s/i.test(authz) ? authz.replace(/^Bearer\s+/i, "") : "";
|
|
172
|
+
if (rawBearer && looksLikeJwt(rawBearer)) {
|
|
173
|
+
const v = await deps.registryJwtVerifier.verify(rawBearer);
|
|
174
|
+
if (v.ok) {
|
|
175
|
+
setSsoPrincipal(req, v.identity.principal);
|
|
176
|
+
req.headers[deps.config.principalHeader.toLowerCase()] = v.identity.principal;
|
|
177
|
+
if (v.identity.scope)
|
|
178
|
+
setSsoScope(req, v.identity.scope);
|
|
179
|
+
deps.logger?.info?.("auth_bridge_principal_accepted", { principal: v.identity.principal, ...(v.identity.scope ? { scope: v.identity.scope } : {}) });
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
if (await dispatchSegment(PRE_CREDENTIAL_ROUTES, req, res, url, ctx))
|
|
184
|
+
return;
|
|
185
|
+
let source = null;
|
|
186
|
+
const anyServiceAuth = hasServiceAuth(deps.config);
|
|
187
|
+
if (anyServiceAuth) {
|
|
188
|
+
const sys = systemFor(req, deps.config);
|
|
189
|
+
if (sys === undefined) {
|
|
190
|
+
if (ssoVerifiedPrincipal(req)) {
|
|
191
|
+
source = "sso";
|
|
192
|
+
}
|
|
193
|
+
else {
|
|
194
|
+
sendError(res, 401, "auth.unauthorized", "unauthorized");
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
else {
|
|
199
|
+
source = sys;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
reqState.source = source;
|
|
203
|
+
if (!anyServiceAuth &&
|
|
204
|
+
!deps.config.allowUnauthedWrites &&
|
|
205
|
+
((req.method === "POST" && (isBillableSubmitPath(url) || url.startsWith("/v1/images/bakes"))) ||
|
|
206
|
+
isCredentialGatedRewrite(req.method ?? "", url))) {
|
|
207
|
+
sendError(res, 503, "auth.service_token_required", "this worker requires a service auth token (set SERVICE_AUTH_TOKEN) before accepting task submissions");
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
if (deps.drainState?.draining && req.method === "POST" && isBillableSubmitPath(url) && !isMethodDispatchedSubmitPath(url)) {
|
|
211
|
+
res.setHeader("retry-after", "15");
|
|
212
|
+
sendError(res, 503, "draining", "draining", { message: "this instance is draining for shutdown/upgrade — retry against the replacement instance", ...(deps.drainState.reason ? { reason: deps.drainState.reason } : {}) });
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
if (deps.modelReady && !deps.modelReady() && req.method === "POST" && isBillableSubmitPath(url) && !isMethodDispatchedSubmitPath(url)) {
|
|
216
|
+
res.setHeader("retry-after", "5");
|
|
217
|
+
sendError(res, 503, "state.model_roster_pending", "model_roster_pending", { message: "this worker has no model yet (waiting for the first effective-config pull to land the roster) — retry shortly" });
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
if (isDestructiveSessionWrite(req.method ?? "", url) && !anyServiceAuth && !deps.config.allowUnauthedWrites) {
|
|
221
|
+
sendError(res, 503, "auth.service_token_required", "this worker requires a service auth token (set SERVICE_AUTH_TOKEN) before accepting destructive session writes (policy / sync-import / fork / delete)");
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
if (await dispatchSegment(POST_CREDENTIAL_ROUTES, req, res, url, ctx))
|
|
225
|
+
return;
|
|
226
|
+
sendError(res, 404, "not_found.route", "not found");
|
|
227
|
+
}
|
|
228
|
+
return { handle };
|
|
229
|
+
}
|
|
230
|
+
//# sourceMappingURL=dispatch.js.map
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 请求守门族(S-147 切片:`http/server.ts` 4837–5195 + `MAX_BODY` 逐字搬家,正文一字未改)。
|
|
3
|
+
* 配额 / 租约 / 用量窗 / 限流 / owner-scope / 体读取 —— 每只守门的判据与它自己的头注原位随行。
|
|
4
|
+
* ctx 字段 1 只(`deps`):本族对外围的耦合就这一格,它也是这一族的可测性读数。
|
|
5
|
+
*/
|
|
6
|
+
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
7
|
+
import type { FlatServiceDeps } from "./server.js";
|
|
8
|
+
export declare function createRequestGuards(ctx: {
|
|
9
|
+
deps: FlatServiceDeps;
|
|
10
|
+
}): {
|
|
11
|
+
quotaExceeded: (req: IncomingMessage, res: ServerResponse) => boolean;
|
|
12
|
+
ownerCostQuotaDenied: (principal: string | undefined) => {
|
|
13
|
+
status: number;
|
|
14
|
+
body: object;
|
|
15
|
+
} | undefined;
|
|
16
|
+
ownerFleetLeaseDenied: (owner: string | undefined) => Promise<{
|
|
17
|
+
status: number;
|
|
18
|
+
body: object;
|
|
19
|
+
} | undefined>;
|
|
20
|
+
ownerLeaseDenied: (res: ServerResponse, owner: string | undefined) => Promise<boolean>;
|
|
21
|
+
ownerQuotaDenied: (res: ServerResponse, owner: string | undefined) => boolean;
|
|
22
|
+
leaseDenied: (req: IncomingMessage, res: ServerResponse) => Promise<boolean>;
|
|
23
|
+
usageWindowDenialFor: (key: string) => Promise<{
|
|
24
|
+
status: number;
|
|
25
|
+
body: {
|
|
26
|
+
error: string;
|
|
27
|
+
errorCode: string;
|
|
28
|
+
retryAfterSec: number;
|
|
29
|
+
};
|
|
30
|
+
retryAfterSec: number;
|
|
31
|
+
} | undefined>;
|
|
32
|
+
usageWindowDenied: (req: IncomingMessage, res: ServerResponse) => Promise<boolean>;
|
|
33
|
+
rateLimited: (req: IncomingMessage, res: ServerResponse) => boolean;
|
|
34
|
+
safeDecode: (seg: string) => string | null;
|
|
35
|
+
isFleetWide: (req: IncomingMessage) => boolean;
|
|
36
|
+
sessionOwnerScope: (req: IncomingMessage) => {
|
|
37
|
+
fleetWide: boolean;
|
|
38
|
+
gateOwner: string | null;
|
|
39
|
+
};
|
|
40
|
+
sessionOwnerScopeForWrite: (req: IncomingMessage) => {
|
|
41
|
+
fleetWide: boolean;
|
|
42
|
+
gateOwner: string | null;
|
|
43
|
+
};
|
|
44
|
+
runOwnerOk: (req: IncomingMessage, res: ServerResponse, owner: string | null, notFoundForm?: "run" | "session") => boolean;
|
|
45
|
+
runSessionAcceptOk: (req: IncomingMessage, res: ServerResponse, run: {
|
|
46
|
+
sessionId: string | null;
|
|
47
|
+
}, face: string, notFoundError?: string) => boolean;
|
|
48
|
+
readJson: (req: IncomingMessage) => Promise<unknown>;
|
|
49
|
+
readRawBody: (req: IncomingMessage, max: number) => Promise<Buffer>;
|
|
50
|
+
};
|
|
51
|
+
//# sourceMappingURL=request-guards.d.ts.map
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { GLOBAL_USAGE_KEY, usageRetryAfterMs } from "@sema-agent/core";
|
|
2
|
+
import { HttpError, principalFrom } from "../security.js";
|
|
3
|
+
import { RUN_NOT_FOUND_MESSAGE } from "./route-ctx.js";
|
|
4
|
+
import { sendError } from "./send.js";
|
|
5
|
+
import { authorized, systemFor, gatedPrincipal, explicitOperatorOk, runOwnerReadable, sendPrincipalRefusal } from "./principal-gate.js";
|
|
6
|
+
const MAX_BODY = 8 * 1024 * 1024;
|
|
7
|
+
export function createRequestGuards(ctx) {
|
|
8
|
+
const { deps } = ctx;
|
|
9
|
+
function quotaExceeded(req, res) {
|
|
10
|
+
if (!deps.costQuota)
|
|
11
|
+
return false;
|
|
12
|
+
const principal = gatedPrincipal(req, deps.config);
|
|
13
|
+
if (!principal)
|
|
14
|
+
return false;
|
|
15
|
+
const d = deps.costQuota.check(principal);
|
|
16
|
+
if (d.allowed)
|
|
17
|
+
return false;
|
|
18
|
+
deps.metrics?.inc("cost_quota_rejected_total");
|
|
19
|
+
res.setHeader("retry-after", String(d.retryAfterSec));
|
|
20
|
+
sendError(res, 429, "limit.cost_quota_exceeded", "cost quota exceeded", { usedMicroUsd: d.usedMicroUsd, limitMicroUsd: d.limitMicroUsd, retryAfterSec: d.retryAfterSec });
|
|
21
|
+
return true;
|
|
22
|
+
}
|
|
23
|
+
function ownerCostQuotaDenied(principal) {
|
|
24
|
+
if (!deps.costQuota || !principal)
|
|
25
|
+
return undefined;
|
|
26
|
+
const d = deps.costQuota.check(principal);
|
|
27
|
+
if (d.allowed)
|
|
28
|
+
return undefined;
|
|
29
|
+
deps.metrics?.inc("cost_quota_rejected_total");
|
|
30
|
+
return { status: 429, body: { error: "cost quota exceeded", errorCode: "limit.cost_quota_exceeded", usedMicroUsd: d.usedMicroUsd, limitMicroUsd: d.limitMicroUsd, retryAfterSec: d.retryAfterSec } };
|
|
31
|
+
}
|
|
32
|
+
async function ownerFleetLeaseDenied(owner) {
|
|
33
|
+
if (!deps.fleetLease || !owner)
|
|
34
|
+
return undefined;
|
|
35
|
+
const adm = await deps.fleetLease.admit(owner);
|
|
36
|
+
if (adm.ok)
|
|
37
|
+
return undefined;
|
|
38
|
+
deps.metrics?.inc("fleet_lease_rejected_total");
|
|
39
|
+
return { status: 429, body: { error: "budget exhausted (quota lease)", errorCode: "quota_exhausted", retryAfterSec: adm.retryAfterSec } };
|
|
40
|
+
}
|
|
41
|
+
async function ownerLeaseDenied(res, owner) {
|
|
42
|
+
if (!deps.fleetLease || !owner)
|
|
43
|
+
return false;
|
|
44
|
+
const adm = await deps.fleetLease.admit(owner);
|
|
45
|
+
if (adm.ok)
|
|
46
|
+
return false;
|
|
47
|
+
deps.metrics?.inc("fleet_lease_rejected_total");
|
|
48
|
+
res.setHeader("retry-after", String(adm.retryAfterSec));
|
|
49
|
+
sendError(res, 429, "quota_exhausted", "budget exhausted (quota lease)", { retryAfterSec: adm.retryAfterSec, ...(adm.detail ?? {}) });
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
function ownerQuotaDenied(res, owner) {
|
|
53
|
+
const denial = ownerCostQuotaDenied(owner);
|
|
54
|
+
if (!denial)
|
|
55
|
+
return false;
|
|
56
|
+
const { errorCode, error, ...extras } = denial.body;
|
|
57
|
+
res.setHeader("retry-after", String(extras.retryAfterSec));
|
|
58
|
+
sendError(res, denial.status, errorCode, error, extras);
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
async function leaseDenied(req, res) {
|
|
62
|
+
if (!deps.fleetLease)
|
|
63
|
+
return false;
|
|
64
|
+
const principal = gatedPrincipal(req, deps.config);
|
|
65
|
+
if (!principal)
|
|
66
|
+
return false;
|
|
67
|
+
const adm = await deps.fleetLease.admit(principal);
|
|
68
|
+
if (adm.ok)
|
|
69
|
+
return false;
|
|
70
|
+
deps.metrics?.inc("fleet_lease_rejected_total");
|
|
71
|
+
res.setHeader("retry-after", String(adm.retryAfterSec));
|
|
72
|
+
sendError(res, 429, "quota_exhausted", "budget exhausted (quota lease)", { retryAfterSec: adm.retryAfterSec, ...(adm.detail ?? {}) });
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
async function usageWindowDenialFor(key) {
|
|
76
|
+
const store = deps.usageWindowStore;
|
|
77
|
+
const windows = deps.config.usageWindows;
|
|
78
|
+
if (!store || !windows || windows.length === 0)
|
|
79
|
+
return undefined;
|
|
80
|
+
let retryAfterMs;
|
|
81
|
+
try {
|
|
82
|
+
retryAfterMs = usageRetryAfterMs(await store.read(key, windows, Date.now()));
|
|
83
|
+
}
|
|
84
|
+
catch (e) {
|
|
85
|
+
deps.logger?.warn?.("usage_window_admission_read_failed", { error: e instanceof Error ? e.message : String(e) });
|
|
86
|
+
return undefined;
|
|
87
|
+
}
|
|
88
|
+
if (retryAfterMs === undefined)
|
|
89
|
+
return undefined;
|
|
90
|
+
deps.metrics?.inc("usage_window_rejected_total");
|
|
91
|
+
deps.logger?.info?.("usage_window_exhausted", { hasPrincipal: key !== GLOBAL_USAGE_KEY, retryAfterMs });
|
|
92
|
+
const retryAfterSec = Math.max(1, Math.ceil(retryAfterMs / 1000));
|
|
93
|
+
return { status: 429, body: { error: "usage window exhausted (deployment token governance)", errorCode: "usage.window_exhausted", retryAfterSec }, retryAfterSec };
|
|
94
|
+
}
|
|
95
|
+
async function usageWindowDenied(req, res) {
|
|
96
|
+
const denial = await usageWindowDenialFor(gatedPrincipal(req, deps.config) || GLOBAL_USAGE_KEY);
|
|
97
|
+
if (!denial)
|
|
98
|
+
return false;
|
|
99
|
+
res.setHeader("retry-after", String(denial.retryAfterSec));
|
|
100
|
+
sendError(res, denial.status, denial.body.errorCode, denial.body.error, { retryAfterSec: denial.retryAfterSec });
|
|
101
|
+
return true;
|
|
102
|
+
}
|
|
103
|
+
function rateLimited(req, res) {
|
|
104
|
+
if (!deps.rateLimiter)
|
|
105
|
+
return false;
|
|
106
|
+
const key = (deps.config.directDoorActive ? undefined : principalFrom(req, deps.config)) ?? req.socket.remoteAddress ?? "anon";
|
|
107
|
+
const d = deps.rateLimiter.check(key);
|
|
108
|
+
if (d.allowed)
|
|
109
|
+
return false;
|
|
110
|
+
deps.metrics?.inc("rate_limited_total");
|
|
111
|
+
res.setHeader("retry-after", String(d.retryAfterSec));
|
|
112
|
+
sendError(res, 429, "limit.rate_exceeded", "rate limit exceeded", { retryAfterSec: d.retryAfterSec });
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
115
|
+
function safeDecode(seg) {
|
|
116
|
+
try {
|
|
117
|
+
return decodeURIComponent(seg);
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
function isFleetWide(req) {
|
|
124
|
+
const { authToken: at, traceToken: tt } = deps.config;
|
|
125
|
+
const anyAt = Boolean(at) || Object.keys(deps.config.authTokens ?? {}).length > 0;
|
|
126
|
+
return (!anyAt && !tt && !deps.config.requirePrincipal) || (anyAt && systemFor(req, deps.config) !== undefined) || (!!tt && authorized(req, tt));
|
|
127
|
+
}
|
|
128
|
+
function sessionOwnerScope(req) {
|
|
129
|
+
const fleetWide = isFleetWide(req);
|
|
130
|
+
const gateOwner = fleetWide ? null : (gatedPrincipal(req, deps.config) || null);
|
|
131
|
+
return { fleetWide, gateOwner };
|
|
132
|
+
}
|
|
133
|
+
function sessionOwnerScopeForWrite(req) {
|
|
134
|
+
const { authToken: at, traceToken: tt } = deps.config;
|
|
135
|
+
const anyAt = Boolean(at) || Object.keys(deps.config.authTokens ?? {}).length > 0;
|
|
136
|
+
const fleetWide = (!anyAt && !tt && !deps.config.requirePrincipal) || (anyAt && systemFor(req, deps.config) !== undefined);
|
|
137
|
+
const gateOwner = fleetWide ? null : (gatedPrincipal(req, deps.config) || null);
|
|
138
|
+
return { fleetWide, gateOwner };
|
|
139
|
+
}
|
|
140
|
+
function runOwnerOk(req, res, owner, notFoundForm = "run") {
|
|
141
|
+
const principal = gatedPrincipal(req, deps.config);
|
|
142
|
+
if (deps.config.requirePrincipal && !principal) {
|
|
143
|
+
sendPrincipalRefusal(res, req, deps.config);
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
if (!runOwnerReadable(principal, owner, deps.config)) {
|
|
147
|
+
if (notFoundForm === "session")
|
|
148
|
+
sendError(res, 404, "not_found.session", "session not found");
|
|
149
|
+
else
|
|
150
|
+
sendError(res, 404, "not_found.run", RUN_NOT_FOUND_MESSAGE);
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
return true;
|
|
154
|
+
}
|
|
155
|
+
const sessionAssertAbsentWarned = new Set();
|
|
156
|
+
function runSessionAcceptOk(req, res, run, face, notFoundError = RUN_NOT_FOUND_MESSAGE) {
|
|
157
|
+
if (!run.sessionId)
|
|
158
|
+
return true;
|
|
159
|
+
const asserted = new URL(req.url ?? "", "http://x").searchParams.get("session");
|
|
160
|
+
if (asserted === null) {
|
|
161
|
+
if (!sessionAssertAbsentWarned.has(face)) {
|
|
162
|
+
sessionAssertAbsentWarned.add(face);
|
|
163
|
+
console.warn(JSON.stringify({ ts: new Date().toISOString(), level: "warn", msg: "run_face_session_assert_absent", face, note: "caller sent no ?session= on a session-bound run — allowed in the accept phase; the enforcement train will 404 this ([1498]⑥/[1499])" }));
|
|
164
|
+
}
|
|
165
|
+
return true;
|
|
166
|
+
}
|
|
167
|
+
if (asserted === run.sessionId)
|
|
168
|
+
return true;
|
|
169
|
+
if (explicitOperatorOk(gatedPrincipal(req, deps.config), deps.config.operatorPrincipals))
|
|
170
|
+
return true;
|
|
171
|
+
sendError(res, 404, "not_found.run", notFoundError);
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
174
|
+
async function readJson(req) {
|
|
175
|
+
const chunks = [];
|
|
176
|
+
let size = 0;
|
|
177
|
+
for await (const c of req) {
|
|
178
|
+
size += c.length;
|
|
179
|
+
if (size > MAX_BODY)
|
|
180
|
+
throw new HttpError(413, "request body too large");
|
|
181
|
+
chunks.push(c);
|
|
182
|
+
}
|
|
183
|
+
if (chunks.length === 0)
|
|
184
|
+
return {};
|
|
185
|
+
try {
|
|
186
|
+
return JSON.parse(Buffer.concat(chunks).toString("utf8")) ?? {};
|
|
187
|
+
}
|
|
188
|
+
catch {
|
|
189
|
+
throw new HttpError(400, "invalid JSON body");
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
async function readRawBody(req, max) {
|
|
193
|
+
const chunks = [];
|
|
194
|
+
let size = 0;
|
|
195
|
+
for await (const c of req) {
|
|
196
|
+
size += c.length;
|
|
197
|
+
if (size > max)
|
|
198
|
+
throw new HttpError(413, "blob too large");
|
|
199
|
+
chunks.push(c);
|
|
200
|
+
}
|
|
201
|
+
return Buffer.concat(chunks);
|
|
202
|
+
}
|
|
203
|
+
return { quotaExceeded, ownerCostQuotaDenied, ownerFleetLeaseDenied, ownerLeaseDenied, ownerQuotaDenied, leaseDenied, usageWindowDenialFor, usageWindowDenied, rateLimited, safeDecode, isFleetWide, sessionOwnerScope, sessionOwnerScopeForWrite, runOwnerOk, runSessionAcceptOk, readJson, readRawBody };
|
|
204
|
+
}
|
|
205
|
+
//# sourceMappingURL=request-guards.js.map
|