@kya-os/checkpoint-nextjs 1.0.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/CHANGELOG.md +80 -0
- package/EDGE_RUNTIME_WASM_SETUP.md +348 -0
- package/README.md +414 -0
- package/bin/setup-edge-wasm.js +497 -0
- package/dist/.tsbuildinfo +1 -0
- package/dist/adapt.d.mts +39 -0
- package/dist/adapt.d.ts +39 -0
- package/dist/adapt.js +58 -0
- package/dist/adapt.js.map +1 -0
- package/dist/adapt.mjs +56 -0
- package/dist/adapt.mjs.map +1 -0
- package/dist/api-client.d.mts +204 -0
- package/dist/api-client.d.ts +204 -0
- package/dist/api-client.js +206 -0
- package/dist/api-client.js.map +1 -0
- package/dist/api-client.mjs +199 -0
- package/dist/api-client.mjs.map +1 -0
- package/dist/api-middleware.d.mts +156 -0
- package/dist/api-middleware.d.ts +156 -0
- package/dist/api-middleware.js +510 -0
- package/dist/api-middleware.js.map +1 -0
- package/dist/api-middleware.mjs +505 -0
- package/dist/api-middleware.mjs.map +1 -0
- package/dist/create-middleware.d.mts +17 -0
- package/dist/create-middleware.d.ts +17 -0
- package/dist/create-middleware.js +38 -0
- package/dist/create-middleware.js.map +1 -0
- package/dist/create-middleware.mjs +35 -0
- package/dist/create-middleware.mjs.map +1 -0
- package/dist/edge/index.d.mts +110 -0
- package/dist/edge/index.d.ts +110 -0
- package/dist/edge/index.js +277 -0
- package/dist/edge/index.js.map +1 -0
- package/dist/edge/index.mjs +275 -0
- package/dist/edge/index.mjs.map +1 -0
- package/dist/edge-runtime-loader.d.mts +50 -0
- package/dist/edge-runtime-loader.d.ts +50 -0
- package/dist/edge-runtime-loader.js +204 -0
- package/dist/edge-runtime-loader.js.map +1 -0
- package/dist/edge-runtime-loader.mjs +201 -0
- package/dist/edge-runtime-loader.mjs.map +1 -0
- package/dist/edge-wasm-middleware.d.mts +68 -0
- package/dist/edge-wasm-middleware.d.ts +68 -0
- package/dist/edge-wasm-middleware.js +318 -0
- package/dist/edge-wasm-middleware.js.map +1 -0
- package/dist/edge-wasm-middleware.mjs +315 -0
- package/dist/edge-wasm-middleware.mjs.map +1 -0
- package/dist/index.d.mts +25 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.js +1019 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +979 -0
- package/dist/index.mjs.map +1 -0
- package/dist/middleware-edge.d.mts +46 -0
- package/dist/middleware-edge.d.ts +46 -0
- package/dist/middleware-edge.js +134 -0
- package/dist/middleware-edge.js.map +1 -0
- package/dist/middleware-edge.mjs +129 -0
- package/dist/middleware-edge.mjs.map +1 -0
- package/dist/middleware-node.d.mts +89 -0
- package/dist/middleware-node.d.ts +89 -0
- package/dist/middleware-node.js +127 -0
- package/dist/middleware-node.js.map +1 -0
- package/dist/middleware-node.mjs +124 -0
- package/dist/middleware-node.mjs.map +1 -0
- package/dist/middleware.d.mts +36 -0
- package/dist/middleware.d.ts +36 -0
- package/dist/middleware.js +15 -0
- package/dist/middleware.js.map +1 -0
- package/dist/middleware.mjs +12 -0
- package/dist/middleware.mjs.map +1 -0
- package/dist/nodejs-wasm-loader.d.mts +25 -0
- package/dist/nodejs-wasm-loader.d.ts +25 -0
- package/dist/nodejs-wasm-loader.js +95 -0
- package/dist/nodejs-wasm-loader.js.map +1 -0
- package/dist/nodejs-wasm-loader.mjs +85 -0
- package/dist/nodejs-wasm-loader.mjs.map +1 -0
- package/dist/policy.d.mts +162 -0
- package/dist/policy.d.ts +162 -0
- package/dist/policy.js +189 -0
- package/dist/policy.js.map +1 -0
- package/dist/policy.mjs +165 -0
- package/dist/policy.mjs.map +1 -0
- package/dist/session-tracker.d.mts +55 -0
- package/dist/session-tracker.d.ts +55 -0
- package/dist/session-tracker.js +170 -0
- package/dist/session-tracker.js.map +1 -0
- package/dist/session-tracker.mjs +167 -0
- package/dist/session-tracker.mjs.map +1 -0
- package/dist/signature-verifier.d.mts +33 -0
- package/dist/signature-verifier.d.ts +33 -0
- package/dist/signature-verifier.js +386 -0
- package/dist/signature-verifier.js.map +1 -0
- package/dist/signature-verifier.mjs +362 -0
- package/dist/signature-verifier.mjs.map +1 -0
- package/dist/translate.d.mts +33 -0
- package/dist/translate.d.ts +33 -0
- package/dist/translate.js +38 -0
- package/dist/translate.js.map +1 -0
- package/dist/translate.mjs +36 -0
- package/dist/translate.mjs.map +1 -0
- package/dist/types-C-xCUNTr.d.mts +105 -0
- package/dist/types-C-xCUNTr.d.ts +105 -0
- package/dist/wasm-middleware.d.mts +63 -0
- package/dist/wasm-middleware.d.ts +63 -0
- package/dist/wasm-middleware.js +98 -0
- package/dist/wasm-middleware.js.map +1 -0
- package/dist/wasm-middleware.mjs +95 -0
- package/dist/wasm-middleware.mjs.map +1 -0
- package/dist/wasm-setup.d.mts +46 -0
- package/dist/wasm-setup.d.ts +46 -0
- package/dist/wasm-setup.js +176 -0
- package/dist/wasm-setup.js.map +1 -0
- package/dist/wasm-setup.mjs +167 -0
- package/dist/wasm-setup.mjs.map +1 -0
- package/package.json +156 -0
- package/templates/middleware-wasm-100.ts +153 -0
- package/wasm/agentshield_wasm.d.ts +479 -0
- package/wasm/agentshield_wasm.js +1536 -0
- package/wasm/agentshield_wasm_bg.wasm +0 -0
- package/wasm/package.json +30 -0
- package/wasm.d.ts +21 -0
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { initEngineEdge, verifyRequestEdge, renderDecisionAsResponse } from '@kya-os/checkpoint-wasm-runtime/orchestrator/edge';
|
|
2
|
+
export { initEngineEdge } from '@kya-os/checkpoint-wasm-runtime/orchestrator/edge';
|
|
3
|
+
import { NextResponse } from 'next/server';
|
|
4
|
+
import { acceptsHtml, encodeVerdictCookie, classifyResponseShape, BLOCKED_PATH, VERDICT_COOKIE_NAME } from '@kya-os/checkpoint-shared';
|
|
5
|
+
import '@kya-os/checkpoint-wasm-runtime/orchestrator';
|
|
6
|
+
import { makeSystemClock, makePolicyEvaluator, makeReputationOracle, makeStatusListCache, makeDidResolver } from '@kya-os/checkpoint-wasm-runtime/adapters';
|
|
7
|
+
|
|
8
|
+
// src/middleware-edge.ts
|
|
9
|
+
function adaptToNextResponse(rendered, req) {
|
|
10
|
+
const clientAcceptsHtml = acceptsHtml(req.headers);
|
|
11
|
+
const verdictCookie = encodeVerdictCookie(rendered);
|
|
12
|
+
const shape = classifyResponseShape(rendered, clientAcceptsHtml);
|
|
13
|
+
switch (shape) {
|
|
14
|
+
case "pass-through": {
|
|
15
|
+
const res = NextResponse.next();
|
|
16
|
+
applyHeaders(res, rendered.headers);
|
|
17
|
+
setVerdictCookie(res, verdictCookie);
|
|
18
|
+
return res;
|
|
19
|
+
}
|
|
20
|
+
case "redirect": {
|
|
21
|
+
const target = new URL(rendered.headers.Location);
|
|
22
|
+
const res = NextResponse.redirect(target);
|
|
23
|
+
applyHeaders(res, rendered.headers);
|
|
24
|
+
setVerdictCookie(res, verdictCookie);
|
|
25
|
+
return res;
|
|
26
|
+
}
|
|
27
|
+
case "html-block": {
|
|
28
|
+
const blockedUrl = new URL(BLOCKED_PATH, req.url);
|
|
29
|
+
const res = NextResponse.rewrite(blockedUrl, { status: 200 });
|
|
30
|
+
applyHeaders(res, rendered.headers);
|
|
31
|
+
setVerdictCookie(res, verdictCookie);
|
|
32
|
+
return res;
|
|
33
|
+
}
|
|
34
|
+
case "json-block": {
|
|
35
|
+
const body = rendered.body ?? {};
|
|
36
|
+
const res = NextResponse.json(body, { status: rendered.status });
|
|
37
|
+
applyHeaders(res, rendered.headers);
|
|
38
|
+
setVerdictCookie(res, verdictCookie);
|
|
39
|
+
return res;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
function setVerdictCookie(res, value) {
|
|
44
|
+
res.cookies.set({
|
|
45
|
+
name: VERDICT_COOKIE_NAME,
|
|
46
|
+
value,
|
|
47
|
+
path: "/",
|
|
48
|
+
sameSite: "lax",
|
|
49
|
+
httpOnly: false
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
function applyHeaders(res, headers) {
|
|
53
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
54
|
+
res.headers.set(key, value);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// src/translate.ts
|
|
59
|
+
function nextRequestToHttpLike(req) {
|
|
60
|
+
const url = new URL(req.url);
|
|
61
|
+
return {
|
|
62
|
+
method: req.method,
|
|
63
|
+
// Path + query only — orchestrator's URL parsing expects no scheme/host.
|
|
64
|
+
url: url.pathname + url.search,
|
|
65
|
+
headers: headersToRecord(req.headers),
|
|
66
|
+
// NextRequest.body is a ReadableStream; we don't drain it here.
|
|
67
|
+
// The orchestrator routes to PlainHttp when body is falsy, which
|
|
68
|
+
// is the right call for streaming middlewares that don't want to
|
|
69
|
+
// buffer the request body just to detect agents.
|
|
70
|
+
body: null,
|
|
71
|
+
remoteAddress: extractRemoteAddress(req)
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
function headersToRecord(headers) {
|
|
75
|
+
const out = {};
|
|
76
|
+
headers.forEach((value, key) => {
|
|
77
|
+
out[key.toLowerCase()] = value;
|
|
78
|
+
});
|
|
79
|
+
return out;
|
|
80
|
+
}
|
|
81
|
+
function extractRemoteAddress(req) {
|
|
82
|
+
const xff = req.headers.get("x-forwarded-for");
|
|
83
|
+
if (xff) {
|
|
84
|
+
const first = xff.split(",")[0]?.trim();
|
|
85
|
+
if (first) return first;
|
|
86
|
+
}
|
|
87
|
+
const maybeIp = req.ip;
|
|
88
|
+
return maybeIp;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// src/middleware-node.ts
|
|
92
|
+
function buildVerifyOpts(config) {
|
|
93
|
+
const overrides = config.adapters ?? {};
|
|
94
|
+
return {
|
|
95
|
+
didResolver: overrides.didResolver ?? makeDidResolver(),
|
|
96
|
+
statusListCache: overrides.statusListCache ?? makeStatusListCache(),
|
|
97
|
+
reputationOracle: overrides.reputationOracle ?? makeReputationOracle({ argusUrl: config.argusUrl }),
|
|
98
|
+
policyEvaluator: overrides.policyEvaluator ?? makePolicyEvaluator({ dashboardUrl: config.dashboardUrl }),
|
|
99
|
+
clock: makeSystemClock(),
|
|
100
|
+
tenantHost: config.tenantHost,
|
|
101
|
+
enforcementMode: config.enforcementMode ?? "enforce",
|
|
102
|
+
reputationBaseline: config.reputationBaseline,
|
|
103
|
+
argusUrl: config.argusUrl
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// src/middleware-edge.ts
|
|
108
|
+
function withCheckpoint(config) {
|
|
109
|
+
void initEngineEdge();
|
|
110
|
+
const opts = buildVerifyOpts(config);
|
|
111
|
+
return async function checkpointMiddlewareEdge(req) {
|
|
112
|
+
const httpLike = nextRequestToHttpLike(req);
|
|
113
|
+
const result = await verifyRequestEdge(httpLike, opts);
|
|
114
|
+
await dispatchOnResult(config, result, req);
|
|
115
|
+
const rendered = renderDecisionAsResponse(result);
|
|
116
|
+
return adaptToNextResponse(rendered, req);
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
async function dispatchOnResult(config, result, req) {
|
|
120
|
+
if (!config.onResult) return;
|
|
121
|
+
try {
|
|
122
|
+
await config.onResult(result, req);
|
|
123
|
+
} catch {
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export { withCheckpoint };
|
|
128
|
+
//# sourceMappingURL=middleware-edge.mjs.map
|
|
129
|
+
//# sourceMappingURL=middleware-edge.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/adapt.ts","../src/translate.ts","../src/middleware-node.ts","../src/middleware-edge.ts"],"names":["renderDecisionAsResponse"],"mappings":";;;;;;;;AA4CO,SAAS,mBAAA,CAAoB,UAA4B,GAAA,EAAgC;AAC9F,EAAA,MAAM,iBAAA,GAAoB,WAAA,CAAY,GAAA,CAAI,OAAO,CAAA;AACjD,EAAA,MAAM,aAAA,GAAgB,oBAAoB,QAAQ,CAAA;AAClD,EAAA,MAAM,KAAA,GAAQ,qBAAA,CAAsB,QAAA,EAAU,iBAAiB,CAAA;AAE/D,EAAA,QAAQ,KAAA;AAAO,IACb,KAAK,cAAA,EAAgB;AAEnB,MAAA,MAAM,GAAA,GAAM,aAAa,IAAA,EAAK;AAC9B,MAAA,YAAA,CAAa,GAAA,EAAK,SAAS,OAAO,CAAA;AAClC,MAAA,gBAAA,CAAiB,KAAK,aAAa,CAAA;AACnC,MAAA,OAAO,GAAA;AAAA,IACT;AAAA,IAEA,KAAK,UAAA,EAAY;AAEf,MAAA,MAAM,MAAA,GAAS,IAAI,GAAA,CAAI,QAAA,CAAS,QAAQ,QAAS,CAAA;AACjD,MAAA,MAAM,GAAA,GAAM,YAAA,CAAa,QAAA,CAAS,MAAM,CAAA;AACxC,MAAA,YAAA,CAAa,GAAA,EAAK,SAAS,OAAO,CAAA;AAClC,MAAA,gBAAA,CAAiB,KAAK,aAAa,CAAA;AACnC,MAAA,OAAO,GAAA;AAAA,IACT;AAAA,IAEA,KAAK,YAAA,EAAc;AAIjB,MAAA,MAAM,UAAA,GAAa,IAAI,GAAA,CAAI,YAAA,EAAc,IAAI,GAAG,CAAA;AAChD,MAAA,MAAM,MAAM,YAAA,CAAa,OAAA,CAAQ,YAAY,EAAE,MAAA,EAAQ,KAAK,CAAA;AAC5D,MAAA,YAAA,CAAa,GAAA,EAAK,SAAS,OAAO,CAAA;AAClC,MAAA,gBAAA,CAAiB,KAAK,aAAa,CAAA;AACnC,MAAA,OAAO,GAAA;AAAA,IACT;AAAA,IAEA,KAAK,YAAA,EAAc;AAKjB,MAAA,MAAM,IAAA,GAAO,QAAA,CAAS,IAAA,IAAQ,EAAC;AAC/B,MAAA,MAAM,GAAA,GAAM,aAAa,IAAA,CAAK,IAAA,EAAM,EAAE,MAAA,EAAQ,QAAA,CAAS,QAAkB,CAAA;AACzE,MAAA,YAAA,CAAa,GAAA,EAAK,SAAS,OAAO,CAAA;AAClC,MAAA,gBAAA,CAAiB,KAAK,aAAa,CAAA;AACnC,MAAA,OAAO,GAAA;AAAA,IACT;AAAA;AAEJ;AAUA,SAAS,gBAAA,CAAiB,KAAmB,KAAA,EAAqB;AAKhE,EAAA,GAAA,CAAI,QAAQ,GAAA,CAAI;AAAA,IACd,IAAA,EAAM,mBAAA;AAAA,IACN,KAAA;AAAA,IACA,IAAA,EAAM,GAAA;AAAA,IACN,QAAA,EAAU,KAAA;AAAA,IACV,QAAA,EAAU;AAAA,GACX,CAAA;AACH;AAEA,SAAS,YAAA,CAAa,KAAmB,OAAA,EAAuC;AAI9E,EAAA,KAAA,MAAW,CAAC,GAAA,EAAK,KAAK,KAAK,MAAA,CAAO,OAAA,CAAQ,OAAO,CAAA,EAAG;AAClD,IAAA,GAAA,CAAI,OAAA,CAAQ,GAAA,CAAI,GAAA,EAAK,KAAK,CAAA;AAAA,EAC5B;AACF;;;AC1FO,SAAS,sBAAsB,GAAA,EAAoC;AACxE,EAAA,MAAM,GAAA,GAAM,IAAI,GAAA,CAAI,GAAA,CAAI,GAAG,CAAA;AAC3B,EAAA,OAAO;AAAA,IACL,QAAQ,GAAA,CAAI,MAAA;AAAA;AAAA,IAEZ,GAAA,EAAK,GAAA,CAAI,QAAA,GAAW,GAAA,CAAI,MAAA;AAAA,IACxB,OAAA,EAAS,eAAA,CAAgB,GAAA,CAAI,OAAO,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAKpC,IAAA,EAAM,IAAA;AAAA,IACN,aAAA,EAAe,qBAAqB,GAAG;AAAA,GACzC;AACF;AAUA,SAAS,gBAAgB,OAAA,EAA0C;AACjE,EAAA,MAAM,MAA8B,EAAC;AACrC,EAAA,OAAA,CAAQ,OAAA,CAAQ,CAAC,KAAA,EAAO,GAAA,KAAQ;AAC9B,IAAA,GAAA,CAAI,GAAA,CAAI,WAAA,EAAa,CAAA,GAAI,KAAA;AAAA,EAC3B,CAAC,CAAA;AACD,EAAA,OAAO,GAAA;AACT;AAWA,SAAS,qBAAqB,GAAA,EAAsC;AAClE,EAAA,MAAM,GAAA,GAAM,GAAA,CAAI,OAAA,CAAQ,GAAA,CAAI,iBAAiB,CAAA;AAC7C,EAAA,IAAI,GAAA,EAAK;AACP,IAAA,MAAM,QAAQ,GAAA,CAAI,KAAA,CAAM,GAAG,CAAA,CAAE,CAAC,GAAG,IAAA,EAAK;AACtC,IAAA,IAAI,OAAO,OAAO,KAAA;AAAA,EACpB;AAGA,EAAA,MAAM,UAAW,GAAA,CAAmC,EAAA;AACpD,EAAA,OAAO,OAAA;AACT;;;ACoDA,SAAS,gBAAgB,MAAA,EAA0B;AACjD,EAAA,MAAM,SAAA,GAAY,MAAA,CAAO,QAAA,IAAY,EAAC;AACtC,EAAA,OAAO;AAAA,IACL,WAAA,EAAa,SAAA,CAAU,WAAA,IAAe,eAAA,EAAgB;AAAA,IACtD,eAAA,EAAiB,SAAA,CAAU,eAAA,IAAmB,mBAAA,EAAoB;AAAA,IAClE,gBAAA,EACE,UAAU,gBAAA,IAAoB,oBAAA,CAAqB,EAAE,QAAA,EAAU,MAAA,CAAO,UAAU,CAAA;AAAA,IAClF,eAAA,EACE,UAAU,eAAA,IAAmB,mBAAA,CAAoB,EAAE,YAAA,EAAc,MAAA,CAAO,cAAc,CAAA;AAAA,IACxF,OAAO,eAAA,EAAgB;AAAA,IACvB,YAAY,MAAA,CAAO,UAAA;AAAA,IACnB,eAAA,EAAiB,OAAO,eAAA,IAAmB,SAAA;AAAA,IAC3C,oBAAoB,MAAA,CAAO,kBAAA;AAAA,IAC3B,UAAU,MAAA,CAAO;AAAA,GACnB;AACF;;;AC/FO,SAAS,eACd,MAAA,EAC6C;AAK7C,EAAA,KAAK,cAAA,EAAe;AAEpB,EAAA,MAAM,IAAA,GAAO,gBAAiB,MAAM,CAAA;AACpC,EAAA,OAAO,eAAe,yBAAyB,GAAA,EAAyC;AACtF,IAAA,MAAM,QAAA,GAAW,sBAAsB,GAAG,CAAA;AAC1C,IAAA,MAAM,MAAA,GAAS,MAAM,iBAAA,CAAkB,QAAA,EAAU,IAAI,CAAA;AACrD,IAAA,MAAM,gBAAA,CAAiB,MAAA,EAAQ,MAAA,EAAQ,GAAG,CAAA;AAC1C,IAAA,MAAM,QAAA,GAAWA,yBAAyB,MAAM,CAAA;AAChD,IAAA,OAAO,mBAAA,CAAoB,UAAU,GAAG,CAAA;AAAA,EAC1C,CAAA;AACF;AAEA,eAAe,gBAAA,CACb,MAAA,EACA,MAAA,EACA,GAAA,EACe;AACf,EAAA,IAAI,CAAC,OAAO,QAAA,EAAU;AACtB,EAAA,IAAI;AACF,IAAA,MAAM,MAAA,CAAO,QAAA,CAAS,MAAA,EAAQ,GAAG,CAAA;AAAA,EACnC,CAAA,CAAA,MAAQ;AAAA,EAER;AACF","file":"middleware-edge.mjs","sourcesContent":["/**\n * D.3 — `RenderedResponse` → `NextResponse` adapter.\n *\n * The host wrapper's *only* job on the outbound path: take the\n * transport-agnostic `RenderedResponse` Phase C's\n * `renderDecisionAsResponse` produces and translate it to a\n * `NextResponse`. Zero verdict decisions, zero engine I/O.\n *\n * Shared between the Node-runtime and Edge-runtime entries. The\n * branching here is identical in both — Next.js `NextResponse` has the\n * same API surface across runtimes; only the underlying response\n * primitive differs (Node http.ServerResponse vs Edge `Response`).\n *\n * Architectural pins per architect § 4.3 / § 4.4:\n *\n * 1. **Verdict-cookie format is contract.** Sites-1's Sonner toast\n * depends on `__checkpoint_verdict=%7B%22verdict%22%3A%22<v>%22...\n * %7D` (single URL-encoded JSON). Byte-format pinned by adapt.test.\n *\n * 2. **HTML-accepting clients → `/blocked` rewrite at status 200**\n * (so the page renders with the verdict cookie set; Sonner picks\n * up the cookie and shows the toast). Non-HTML clients → JSON 4xx.\n *\n * 3. **`X-Checkpoint-Engine` carries `result.engineInfo.name`** —\n * `checkpoint-engine-wasm` after Phase D ships. Brian's Sites-2\n * deviation note confirmed the `X-Checkpoint-*` prefix is canon.\n */\n\nimport { type NextRequest, NextResponse } from 'next/server';\n\nimport type { RenderedResponse } from '@kya-os/checkpoint-wasm-runtime/orchestrator';\nimport {\n VERDICT_COOKIE_NAME,\n BLOCKED_PATH,\n encodeVerdictCookie,\n acceptsHtml,\n classifyResponseShape,\n} from '@kya-os/checkpoint-shared';\n\n/**\n * Convert the engine's transport-agnostic `RenderedResponse` into a\n * `NextResponse`. Sites-1's Playwright suite is the regression gate;\n * any drift here is caught downstream.\n */\nexport function adaptToNextResponse(rendered: RenderedResponse, req: NextRequest): NextResponse {\n const clientAcceptsHtml = acceptsHtml(req.headers);\n const verdictCookie = encodeVerdictCookie(rendered);\n const shape = classifyResponseShape(rendered, clientAcceptsHtml);\n\n switch (shape) {\n case 'pass-through': {\n // Permit OR Observe-mode any-verdict.\n const res = NextResponse.next();\n applyHeaders(res, rendered.headers);\n setVerdictCookie(res, verdictCookie);\n return res;\n }\n\n case 'redirect': {\n // Decision::Redirect → 302 + Location.\n const target = new URL(rendered.headers.Location!);\n const res = NextResponse.redirect(target);\n applyHeaders(res, rendered.headers);\n setVerdictCookie(res, verdictCookie);\n return res;\n }\n\n case 'html-block': {\n // Sites-1 contract: HTML clients (browsers) need a renderable page\n // to show the rejection UI. The verdict cookie carries the reason;\n // the /blocked route reads it and renders the toast.\n const blockedUrl = new URL(BLOCKED_PATH, req.url);\n const res = NextResponse.rewrite(blockedUrl, { status: 200 });\n applyHeaders(res, rendered.headers);\n setVerdictCookie(res, verdictCookie);\n return res;\n }\n\n case 'json-block': {\n // The orchestrator's RenderedResponse already supplies the correct\n // status (401/403/422/...); we just need to materialise the body.\n // application/problem+json (Instruct) uses the Content-Type from\n // rendered.headers; defaults to application/json for everything else.\n const body = rendered.body ?? {};\n const res = NextResponse.json(body, { status: rendered.status as number });\n applyHeaders(res, rendered.headers);\n setVerdictCookie(res, verdictCookie);\n return res;\n }\n }\n}\n\n// -----------------------------------------------------------------------------\n// Helpers — Next.js-specific glue. The framework-agnostic primitives\n// (encodeVerdictCookie, acceptsHtml, classifyResponseShape,\n// VERDICT_COOKIE_NAME, BLOCKED_PATH) live in `@kya-os/checkpoint-shared`\n// so checkpoint-express + future host wrappers produce byte-identical\n// cookies and route HTML/JSON branching the same way.\n// -----------------------------------------------------------------------------\n\nfunction setVerdictCookie(res: NextResponse, value: string): void {\n // Path / SameSite / HttpOnly chosen for the Sonner-bridge use case:\n // path=/ so any route can read it, SameSite=Lax so first-party\n // navigations carry it, HttpOnly=false so the client-side toast JS\n // can read it (it's verdict UX, not a session token).\n res.cookies.set({\n name: VERDICT_COOKIE_NAME,\n value,\n path: '/',\n sameSite: 'lax',\n httpOnly: false,\n });\n}\n\nfunction applyHeaders(res: NextResponse, headers: Record<string, string>): void {\n // NextResponse.next() / rewrite() / json() return responses with\n // some default headers; orchestrator headers (X-Checkpoint-*, Location)\n // override. We don't strip pre-existing headers — only set new ones.\n for (const [key, value] of Object.entries(headers)) {\n res.headers.set(key, value);\n }\n}\n","/**\n * D.2 — `NextRequest` → `IncomingHttpLike` translator.\n *\n * The host wrapper's *only* job on the inbound path: take Next.js's\n * native request shape and produce the transport-agnostic\n * `IncomingHttpLike` Phase C's orchestrator consumes. Zero verification\n * logic, zero adapter calls, zero engine I/O.\n *\n * Shared between the Node-runtime and Edge-runtime entries. Next.js\n * `NextRequest` is the same shape in both runtimes — `req.headers` is\n * a `Headers` instance, `req.body` is a `ReadableStream`, `req.ip` is\n * a getter (only present in some deployment surfaces; fall back to\n * `x-forwarded-for` first IP).\n */\n\nimport type { NextRequest } from 'next/server';\n\nimport type { IncomingHttpLike } from '@kya-os/checkpoint-wasm-runtime/orchestrator';\n\n/**\n * Translate a Next.js `NextRequest` into the orchestrator's\n * `IncomingHttpLike` shape.\n *\n * The body is passed through as-is — the orchestrator's\n * `buildAgentRequest` decides whether to parse JSON (looking for an\n * MCP-I `_meta.proof.jws` envelope) or treat the request as PlainHttp.\n * On Next.js middleware the body is typically not pre-parsed; consumers\n * who want to inspect the body for routing decisions should `await\n * req.json()` themselves and pass the parsed result via a second\n * `verifyRequest` call (not common).\n */\nexport function nextRequestToHttpLike(req: NextRequest): IncomingHttpLike {\n const url = new URL(req.url);\n return {\n method: req.method,\n // Path + query only — orchestrator's URL parsing expects no scheme/host.\n url: url.pathname + url.search,\n headers: headersToRecord(req.headers),\n // NextRequest.body is a ReadableStream; we don't drain it here.\n // The orchestrator routes to PlainHttp when body is falsy, which\n // is the right call for streaming middlewares that don't want to\n // buffer the request body just to detect agents.\n body: null,\n remoteAddress: extractRemoteAddress(req),\n };\n}\n\n/**\n * Convert a `Headers` instance into a lowercase-keyed plain object.\n * HTTP header names are case-insensitive (RFC 9110 § 5.1); the\n * orchestrator does case-sensitive lookups, so we normalise to\n * lowercase here. Multi-value headers (Set-Cookie, Accept) are\n * surfaced as their `Headers.get()` view — a single string with\n * comma-joined values, matching what other host adapters produce.\n */\nfunction headersToRecord(headers: Headers): Record<string, string> {\n const out: Record<string, string> = {};\n headers.forEach((value, key) => {\n out[key.toLowerCase()] = value;\n });\n return out;\n}\n\n/**\n * Pull the originating client IP, preferring `x-forwarded-for`'s first\n * entry over `NextRequest.ip` (the latter is only populated on Vercel-\n * hosted deployments and is missing on self-hosted Next.js + nginx /\n * Fly.io / docker-compose surfaces). The `x-forwarded-for` first IP is\n * the closest the request has come to a load balancer's \"trust this is\n * the real client\" attestation — same convention as nginx, Caddy,\n * Cloudflare.\n */\nfunction extractRemoteAddress(req: NextRequest): string | undefined {\n const xff = req.headers.get('x-forwarded-for');\n if (xff) {\n const first = xff.split(',')[0]?.trim();\n if (first) return first;\n }\n // `req.ip` is typed but may be undefined off-Vercel.\n // Use `unknown` cast to avoid the type-narrowing optimism.\n const maybeIp = (req as unknown as { ip?: string }).ip;\n return maybeIp;\n}\n","/**\n * D.1 + D.3 — Node-runtime Next.js middleware entry.\n *\n * The host wrapper that composes Phase B adapters + Phase C\n * `verifyRequest` (sync engine) + Phase D translate/adapt into the\n * `withCheckpoint(config)` factory. Mounted under Vercel Node-runtime\n * serverless functions and long-lived Node servers.\n *\n * For Vercel Edge runtime (the Next.js middleware default), customers\n * import from `./edge` or `@kya-os/checkpoint-nextjs/edge` — that\n * variant uses `verifyRequestEdge` (async-init) and is otherwise\n * structurally identical. Both share `translate.ts` + `adapt.ts`.\n *\n * **Public API contract (architect § 4.1 — preserved):**\n *\n * - `withCheckpoint(config)` — factory returning the middleware.\n * - `CheckpointConfig` — the config shape; new fields are additive.\n *\n * Internal implementation gutted, external contract held. Sites-1's\n * Playwright suite is the regression gate.\n */\n\nimport { type NextRequest, type NextResponse } from 'next/server';\n\nimport {\n renderDecisionAsResponse,\n verifyRequest,\n} from '@kya-os/checkpoint-wasm-runtime/orchestrator';\nimport {\n makeDidResolver,\n makePolicyEvaluator,\n makeReputationOracle,\n makeStatusListCache,\n makeSystemClock,\n type DidResolverAdapter,\n type PolicyEvaluatorAdapter,\n type ReputationOracleAdapter,\n type StatusListCacheAdapter,\n} from '@kya-os/checkpoint-wasm-runtime/adapters';\nimport type { EnforcementMode, VerifyResult } from '@kya-os/checkpoint-wasm-runtime/engine';\n\nimport { adaptToNextResponse } from './adapt';\nimport { nextRequestToHttpLike } from './translate';\n\n/**\n * Configuration for `withCheckpoint`.\n *\n * The new minimal shape Phase D's middleware needs. Legacy\n * `AgentShieldMiddlewareConfig` (from `./api-middleware`) remains\n * exported during the deprecation window — see D.4 cutover.\n */\nexport interface CheckpointConfig {\n /**\n * Tenant identifier — typically the customer's dashboard hostname\n * (e.g. `acme.checkpoint.example`). The PolicyEvaluator uses this\n * to look up tenant policy from the dashboard.\n */\n tenantHost: string;\n\n /**\n * `'enforce'` (default) blocks; `'observe'` passes everything\n * through with `X-Checkpoint-Would-Have-Been` headers. Per Phase 0.2.\n */\n enforcementMode?: EnforcementMode;\n\n /**\n * Argus reputation oracle base URL. Omit to use the trust-by-default\n * baseline (reputation defaults to 1.0; orchestrator logs a one-shot\n * warning at first request).\n */\n argusUrl?: string;\n\n /**\n * Dashboard base URL for the PolicyEvaluator to fetch tenant policy\n * from. Omit to use the open-by-default tenant policy.\n */\n dashboardUrl?: string;\n\n /**\n * Returned to the PolicyEvaluator for anonymous requests (no agent\n * DID). Default 1.0 (trust-by-default).\n */\n reputationBaseline?: number;\n\n /**\n * Pre-built adapter instances. Production deployments use the\n * factory-built defaults from `@kya-os/checkpoint-wasm-runtime/adapters`;\n * tests use stubs. The factory composes any provided overrides over\n * defaults — partial overrides are supported.\n */\n adapters?: Partial<{\n didResolver: DidResolverAdapter;\n statusListCache: StatusListCacheAdapter;\n reputationOracle: ReputationOracleAdapter;\n policyEvaluator: PolicyEvaluatorAdapter;\n }>;\n\n /**\n * Optional callback for the post-verdict path — fires after every\n * verification, regardless of permit/block, with the full\n * `VerifyResult`. Use for logging, dashboards, telemetry. Errors\n * thrown here are swallowed so user code can't break the middleware\n * response.\n */\n onResult?: (result: VerifyResult, req: NextRequest) => void | Promise<void>;\n}\n\n/**\n * Build the Checkpoint middleware. Returns a function `(req) => NextResponse`\n * suitable for `export default withCheckpoint({...})` in `middleware.ts`.\n *\n * Every verification decision flows through the Rust `kya-os-engine`\n * via WASM. The TS layer translates request shape, calls\n * `verifyRequest`, and translates the verdict to `NextResponse`. No\n * verification logic lives in this file.\n */\nexport function withCheckpoint(\n config: CheckpointConfig\n): (req: NextRequest) => Promise<NextResponse> {\n const opts = buildVerifyOpts(config);\n return async function checkpointMiddleware(req: NextRequest): Promise<NextResponse> {\n const httpLike = nextRequestToHttpLike(req);\n const result = await verifyRequest(httpLike, opts);\n await dispatchOnResult(config, result, req);\n const rendered = renderDecisionAsResponse(result);\n return adaptToNextResponse(rendered, req);\n };\n}\n\n/**\n * Compose adapter defaults with caller-supplied overrides. Factored\n * out so the Edge entry (which uses the same composition) can reuse\n * the shape.\n */\nfunction buildVerifyOpts(config: CheckpointConfig) {\n const overrides = config.adapters ?? {};\n return {\n didResolver: overrides.didResolver ?? makeDidResolver(),\n statusListCache: overrides.statusListCache ?? makeStatusListCache(),\n reputationOracle:\n overrides.reputationOracle ?? makeReputationOracle({ argusUrl: config.argusUrl }),\n policyEvaluator:\n overrides.policyEvaluator ?? makePolicyEvaluator({ dashboardUrl: config.dashboardUrl }),\n clock: makeSystemClock(),\n tenantHost: config.tenantHost,\n enforcementMode: config.enforcementMode ?? 'enforce',\n reputationBaseline: config.reputationBaseline,\n argusUrl: config.argusUrl,\n };\n}\n\nasync function dispatchOnResult(\n config: CheckpointConfig,\n result: VerifyResult,\n req: NextRequest\n): Promise<void> {\n if (!config.onResult) return;\n try {\n await config.onResult(result, req);\n } catch {\n // Swallow — onResult is observability, not verdict-critical.\n // Verdict already computed; let the response proceed.\n }\n}\n\n// Re-export the shared opts builder for the Edge entry. Internal seam;\n// not part of the public surface.\nexport { buildVerifyOpts as _buildVerifyOpts };\n","/**\n * D.3 — Edge-runtime Next.js middleware entry.\n *\n * The async-init equivalent of `./middleware-node.ts`. Mounted under\n * Vercel Edge runtime (the Next.js middleware default) and Cloudflare\n * Workers when Next.js targets the Edge.\n *\n * Differs from the Node entry in exactly two places:\n *\n * 1. Imports `verifyRequestEdge` + `initEngineEdge` from the\n * orchestrator's `./edge` subpath (Edge-WASM-2 from D.1.5)\n * instead of `verifyRequest` from the Node orchestrator entry.\n * 2. Calls `initEngineEdge()` once at module load (eagerly, before\n * any request hits the middleware) so the first request's cold-\n * boot latency is amortised onto deploy time. Subsequent calls\n * to `initEngineEdge` are idempotent.\n *\n * Adapter composition (`buildVerifyOpts`), translate.ts, adapt.ts,\n * verdict-cookie format, X-Checkpoint-* headers — all shared with\n * the Node entry. Cross-runtime parity verified by Phase F's CI gate\n * (D.5 ships the Next.js-specific half).\n *\n * **Public API contract — preserved:** `withCheckpoint(config)`,\n * `CheckpointConfig`. Same exports as Node, same signatures.\n */\n\nimport { type NextRequest, type NextResponse } from 'next/server';\n\nimport {\n initEngineEdge,\n renderDecisionAsResponse,\n verifyRequestEdge,\n} from '@kya-os/checkpoint-wasm-runtime/orchestrator/edge';\nimport type { VerifyResult } from '@kya-os/checkpoint-wasm-runtime/engine';\n\nimport { adaptToNextResponse } from './adapt';\nimport { _buildVerifyOpts, type CheckpointConfig } from './middleware-node';\nimport { nextRequestToHttpLike } from './translate';\n\n// Re-export the config type so consumers can `import type` from the\n// edge entry without a second import line.\nexport type { CheckpointConfig } from './middleware-node';\n\n/**\n * Build the Checkpoint middleware for Edge runtime. Returns a function\n * `(req) => Promise<NextResponse>` suitable for\n * `export default withCheckpoint({...})` in `middleware.ts` under\n * `export const config = { runtime: 'edge' }`.\n *\n * Idempotent eager init: the first call to `withCheckpoint` kicks off\n * `initEngineEdge()` so the wasm artifact loads while the rest of the\n * factory closure is being built. The first request awaits the same\n * promise; subsequent requests resolve sync.\n */\nexport function withCheckpoint(\n config: CheckpointConfig\n): (req: NextRequest) => Promise<NextResponse> {\n // Eager init — fire-and-forget. The first request will await the\n // same promise via the orchestrator's lazy init path. Eager-init\n // hosts that want to await the init explicitly can call\n // `initEngineEdge()` themselves at startup.\n void initEngineEdge();\n\n const opts = _buildVerifyOpts(config);\n return async function checkpointMiddlewareEdge(req: NextRequest): Promise<NextResponse> {\n const httpLike = nextRequestToHttpLike(req);\n const result = await verifyRequestEdge(httpLike, opts);\n await dispatchOnResult(config, result, req);\n const rendered = renderDecisionAsResponse(result);\n return adaptToNextResponse(rendered, req);\n };\n}\n\nasync function dispatchOnResult(\n config: CheckpointConfig,\n result: VerifyResult,\n req: NextRequest\n): Promise<void> {\n if (!config.onResult) return;\n try {\n await config.onResult(result, req);\n } catch {\n // Swallow — onResult is observability, not verdict-critical.\n }\n}\n\n// Re-export `initEngineEdge` so eager-init hosts that want to warm the\n// wasm load at process startup can do so without a second import line.\nexport { initEngineEdge };\n"]}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import * as _kya_os_checkpoint_wasm_runtime_adapters from '@kya-os/checkpoint-wasm-runtime/adapters';
|
|
2
|
+
import { DidResolverAdapter, StatusListCacheAdapter, ReputationOracleAdapter, PolicyEvaluatorAdapter } from '@kya-os/checkpoint-wasm-runtime/adapters';
|
|
3
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
4
|
+
import { EnforcementMode, VerifyResult } from '@kya-os/checkpoint-wasm-runtime/engine';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Configuration for `withCheckpoint`.
|
|
8
|
+
*
|
|
9
|
+
* The new minimal shape Phase D's middleware needs. Legacy
|
|
10
|
+
* `AgentShieldMiddlewareConfig` (from `./api-middleware`) remains
|
|
11
|
+
* exported during the deprecation window — see D.4 cutover.
|
|
12
|
+
*/
|
|
13
|
+
interface CheckpointConfig {
|
|
14
|
+
/**
|
|
15
|
+
* Tenant identifier — typically the customer's dashboard hostname
|
|
16
|
+
* (e.g. `acme.checkpoint.example`). The PolicyEvaluator uses this
|
|
17
|
+
* to look up tenant policy from the dashboard.
|
|
18
|
+
*/
|
|
19
|
+
tenantHost: string;
|
|
20
|
+
/**
|
|
21
|
+
* `'enforce'` (default) blocks; `'observe'` passes everything
|
|
22
|
+
* through with `X-Checkpoint-Would-Have-Been` headers. Per Phase 0.2.
|
|
23
|
+
*/
|
|
24
|
+
enforcementMode?: EnforcementMode;
|
|
25
|
+
/**
|
|
26
|
+
* Argus reputation oracle base URL. Omit to use the trust-by-default
|
|
27
|
+
* baseline (reputation defaults to 1.0; orchestrator logs a one-shot
|
|
28
|
+
* warning at first request).
|
|
29
|
+
*/
|
|
30
|
+
argusUrl?: string;
|
|
31
|
+
/**
|
|
32
|
+
* Dashboard base URL for the PolicyEvaluator to fetch tenant policy
|
|
33
|
+
* from. Omit to use the open-by-default tenant policy.
|
|
34
|
+
*/
|
|
35
|
+
dashboardUrl?: string;
|
|
36
|
+
/**
|
|
37
|
+
* Returned to the PolicyEvaluator for anonymous requests (no agent
|
|
38
|
+
* DID). Default 1.0 (trust-by-default).
|
|
39
|
+
*/
|
|
40
|
+
reputationBaseline?: number;
|
|
41
|
+
/**
|
|
42
|
+
* Pre-built adapter instances. Production deployments use the
|
|
43
|
+
* factory-built defaults from `@kya-os/checkpoint-wasm-runtime/adapters`;
|
|
44
|
+
* tests use stubs. The factory composes any provided overrides over
|
|
45
|
+
* defaults — partial overrides are supported.
|
|
46
|
+
*/
|
|
47
|
+
adapters?: Partial<{
|
|
48
|
+
didResolver: DidResolverAdapter;
|
|
49
|
+
statusListCache: StatusListCacheAdapter;
|
|
50
|
+
reputationOracle: ReputationOracleAdapter;
|
|
51
|
+
policyEvaluator: PolicyEvaluatorAdapter;
|
|
52
|
+
}>;
|
|
53
|
+
/**
|
|
54
|
+
* Optional callback for the post-verdict path — fires after every
|
|
55
|
+
* verification, regardless of permit/block, with the full
|
|
56
|
+
* `VerifyResult`. Use for logging, dashboards, telemetry. Errors
|
|
57
|
+
* thrown here are swallowed so user code can't break the middleware
|
|
58
|
+
* response.
|
|
59
|
+
*/
|
|
60
|
+
onResult?: (result: VerifyResult, req: NextRequest) => void | Promise<void>;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Build the Checkpoint middleware. Returns a function `(req) => NextResponse`
|
|
64
|
+
* suitable for `export default withCheckpoint({...})` in `middleware.ts`.
|
|
65
|
+
*
|
|
66
|
+
* Every verification decision flows through the Rust `kya-os-engine`
|
|
67
|
+
* via WASM. The TS layer translates request shape, calls
|
|
68
|
+
* `verifyRequest`, and translates the verdict to `NextResponse`. No
|
|
69
|
+
* verification logic lives in this file.
|
|
70
|
+
*/
|
|
71
|
+
declare function withCheckpoint(config: CheckpointConfig): (req: NextRequest) => Promise<NextResponse>;
|
|
72
|
+
/**
|
|
73
|
+
* Compose adapter defaults with caller-supplied overrides. Factored
|
|
74
|
+
* out so the Edge entry (which uses the same composition) can reuse
|
|
75
|
+
* the shape.
|
|
76
|
+
*/
|
|
77
|
+
declare function buildVerifyOpts(config: CheckpointConfig): {
|
|
78
|
+
didResolver: DidResolverAdapter;
|
|
79
|
+
statusListCache: StatusListCacheAdapter;
|
|
80
|
+
reputationOracle: ReputationOracleAdapter;
|
|
81
|
+
policyEvaluator: PolicyEvaluatorAdapter;
|
|
82
|
+
clock: _kya_os_checkpoint_wasm_runtime_adapters.ClockAdapter;
|
|
83
|
+
tenantHost: string;
|
|
84
|
+
enforcementMode: EnforcementMode;
|
|
85
|
+
reputationBaseline: number | undefined;
|
|
86
|
+
argusUrl: string | undefined;
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
export { type CheckpointConfig, buildVerifyOpts as _buildVerifyOpts, withCheckpoint };
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import * as _kya_os_checkpoint_wasm_runtime_adapters from '@kya-os/checkpoint-wasm-runtime/adapters';
|
|
2
|
+
import { DidResolverAdapter, StatusListCacheAdapter, ReputationOracleAdapter, PolicyEvaluatorAdapter } from '@kya-os/checkpoint-wasm-runtime/adapters';
|
|
3
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
4
|
+
import { EnforcementMode, VerifyResult } from '@kya-os/checkpoint-wasm-runtime/engine';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Configuration for `withCheckpoint`.
|
|
8
|
+
*
|
|
9
|
+
* The new minimal shape Phase D's middleware needs. Legacy
|
|
10
|
+
* `AgentShieldMiddlewareConfig` (from `./api-middleware`) remains
|
|
11
|
+
* exported during the deprecation window — see D.4 cutover.
|
|
12
|
+
*/
|
|
13
|
+
interface CheckpointConfig {
|
|
14
|
+
/**
|
|
15
|
+
* Tenant identifier — typically the customer's dashboard hostname
|
|
16
|
+
* (e.g. `acme.checkpoint.example`). The PolicyEvaluator uses this
|
|
17
|
+
* to look up tenant policy from the dashboard.
|
|
18
|
+
*/
|
|
19
|
+
tenantHost: string;
|
|
20
|
+
/**
|
|
21
|
+
* `'enforce'` (default) blocks; `'observe'` passes everything
|
|
22
|
+
* through with `X-Checkpoint-Would-Have-Been` headers. Per Phase 0.2.
|
|
23
|
+
*/
|
|
24
|
+
enforcementMode?: EnforcementMode;
|
|
25
|
+
/**
|
|
26
|
+
* Argus reputation oracle base URL. Omit to use the trust-by-default
|
|
27
|
+
* baseline (reputation defaults to 1.0; orchestrator logs a one-shot
|
|
28
|
+
* warning at first request).
|
|
29
|
+
*/
|
|
30
|
+
argusUrl?: string;
|
|
31
|
+
/**
|
|
32
|
+
* Dashboard base URL for the PolicyEvaluator to fetch tenant policy
|
|
33
|
+
* from. Omit to use the open-by-default tenant policy.
|
|
34
|
+
*/
|
|
35
|
+
dashboardUrl?: string;
|
|
36
|
+
/**
|
|
37
|
+
* Returned to the PolicyEvaluator for anonymous requests (no agent
|
|
38
|
+
* DID). Default 1.0 (trust-by-default).
|
|
39
|
+
*/
|
|
40
|
+
reputationBaseline?: number;
|
|
41
|
+
/**
|
|
42
|
+
* Pre-built adapter instances. Production deployments use the
|
|
43
|
+
* factory-built defaults from `@kya-os/checkpoint-wasm-runtime/adapters`;
|
|
44
|
+
* tests use stubs. The factory composes any provided overrides over
|
|
45
|
+
* defaults — partial overrides are supported.
|
|
46
|
+
*/
|
|
47
|
+
adapters?: Partial<{
|
|
48
|
+
didResolver: DidResolverAdapter;
|
|
49
|
+
statusListCache: StatusListCacheAdapter;
|
|
50
|
+
reputationOracle: ReputationOracleAdapter;
|
|
51
|
+
policyEvaluator: PolicyEvaluatorAdapter;
|
|
52
|
+
}>;
|
|
53
|
+
/**
|
|
54
|
+
* Optional callback for the post-verdict path — fires after every
|
|
55
|
+
* verification, regardless of permit/block, with the full
|
|
56
|
+
* `VerifyResult`. Use for logging, dashboards, telemetry. Errors
|
|
57
|
+
* thrown here are swallowed so user code can't break the middleware
|
|
58
|
+
* response.
|
|
59
|
+
*/
|
|
60
|
+
onResult?: (result: VerifyResult, req: NextRequest) => void | Promise<void>;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Build the Checkpoint middleware. Returns a function `(req) => NextResponse`
|
|
64
|
+
* suitable for `export default withCheckpoint({...})` in `middleware.ts`.
|
|
65
|
+
*
|
|
66
|
+
* Every verification decision flows through the Rust `kya-os-engine`
|
|
67
|
+
* via WASM. The TS layer translates request shape, calls
|
|
68
|
+
* `verifyRequest`, and translates the verdict to `NextResponse`. No
|
|
69
|
+
* verification logic lives in this file.
|
|
70
|
+
*/
|
|
71
|
+
declare function withCheckpoint(config: CheckpointConfig): (req: NextRequest) => Promise<NextResponse>;
|
|
72
|
+
/**
|
|
73
|
+
* Compose adapter defaults with caller-supplied overrides. Factored
|
|
74
|
+
* out so the Edge entry (which uses the same composition) can reuse
|
|
75
|
+
* the shape.
|
|
76
|
+
*/
|
|
77
|
+
declare function buildVerifyOpts(config: CheckpointConfig): {
|
|
78
|
+
didResolver: DidResolverAdapter;
|
|
79
|
+
statusListCache: StatusListCacheAdapter;
|
|
80
|
+
reputationOracle: ReputationOracleAdapter;
|
|
81
|
+
policyEvaluator: PolicyEvaluatorAdapter;
|
|
82
|
+
clock: _kya_os_checkpoint_wasm_runtime_adapters.ClockAdapter;
|
|
83
|
+
tenantHost: string;
|
|
84
|
+
enforcementMode: EnforcementMode;
|
|
85
|
+
reputationBaseline: number | undefined;
|
|
86
|
+
argusUrl: string | undefined;
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
export { type CheckpointConfig, buildVerifyOpts as _buildVerifyOpts, withCheckpoint };
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var orchestrator = require('@kya-os/checkpoint-wasm-runtime/orchestrator');
|
|
4
|
+
var adapters = require('@kya-os/checkpoint-wasm-runtime/adapters');
|
|
5
|
+
var server = require('next/server');
|
|
6
|
+
var checkpointShared = require('@kya-os/checkpoint-shared');
|
|
7
|
+
|
|
8
|
+
// src/middleware-node.ts
|
|
9
|
+
function adaptToNextResponse(rendered, req) {
|
|
10
|
+
const clientAcceptsHtml = checkpointShared.acceptsHtml(req.headers);
|
|
11
|
+
const verdictCookie = checkpointShared.encodeVerdictCookie(rendered);
|
|
12
|
+
const shape = checkpointShared.classifyResponseShape(rendered, clientAcceptsHtml);
|
|
13
|
+
switch (shape) {
|
|
14
|
+
case "pass-through": {
|
|
15
|
+
const res = server.NextResponse.next();
|
|
16
|
+
applyHeaders(res, rendered.headers);
|
|
17
|
+
setVerdictCookie(res, verdictCookie);
|
|
18
|
+
return res;
|
|
19
|
+
}
|
|
20
|
+
case "redirect": {
|
|
21
|
+
const target = new URL(rendered.headers.Location);
|
|
22
|
+
const res = server.NextResponse.redirect(target);
|
|
23
|
+
applyHeaders(res, rendered.headers);
|
|
24
|
+
setVerdictCookie(res, verdictCookie);
|
|
25
|
+
return res;
|
|
26
|
+
}
|
|
27
|
+
case "html-block": {
|
|
28
|
+
const blockedUrl = new URL(checkpointShared.BLOCKED_PATH, req.url);
|
|
29
|
+
const res = server.NextResponse.rewrite(blockedUrl, { status: 200 });
|
|
30
|
+
applyHeaders(res, rendered.headers);
|
|
31
|
+
setVerdictCookie(res, verdictCookie);
|
|
32
|
+
return res;
|
|
33
|
+
}
|
|
34
|
+
case "json-block": {
|
|
35
|
+
const body = rendered.body ?? {};
|
|
36
|
+
const res = server.NextResponse.json(body, { status: rendered.status });
|
|
37
|
+
applyHeaders(res, rendered.headers);
|
|
38
|
+
setVerdictCookie(res, verdictCookie);
|
|
39
|
+
return res;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
function setVerdictCookie(res, value) {
|
|
44
|
+
res.cookies.set({
|
|
45
|
+
name: checkpointShared.VERDICT_COOKIE_NAME,
|
|
46
|
+
value,
|
|
47
|
+
path: "/",
|
|
48
|
+
sameSite: "lax",
|
|
49
|
+
httpOnly: false
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
function applyHeaders(res, headers) {
|
|
53
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
54
|
+
res.headers.set(key, value);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// src/translate.ts
|
|
59
|
+
function nextRequestToHttpLike(req) {
|
|
60
|
+
const url = new URL(req.url);
|
|
61
|
+
return {
|
|
62
|
+
method: req.method,
|
|
63
|
+
// Path + query only — orchestrator's URL parsing expects no scheme/host.
|
|
64
|
+
url: url.pathname + url.search,
|
|
65
|
+
headers: headersToRecord(req.headers),
|
|
66
|
+
// NextRequest.body is a ReadableStream; we don't drain it here.
|
|
67
|
+
// The orchestrator routes to PlainHttp when body is falsy, which
|
|
68
|
+
// is the right call for streaming middlewares that don't want to
|
|
69
|
+
// buffer the request body just to detect agents.
|
|
70
|
+
body: null,
|
|
71
|
+
remoteAddress: extractRemoteAddress(req)
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
function headersToRecord(headers) {
|
|
75
|
+
const out = {};
|
|
76
|
+
headers.forEach((value, key) => {
|
|
77
|
+
out[key.toLowerCase()] = value;
|
|
78
|
+
});
|
|
79
|
+
return out;
|
|
80
|
+
}
|
|
81
|
+
function extractRemoteAddress(req) {
|
|
82
|
+
const xff = req.headers.get("x-forwarded-for");
|
|
83
|
+
if (xff) {
|
|
84
|
+
const first = xff.split(",")[0]?.trim();
|
|
85
|
+
if (first) return first;
|
|
86
|
+
}
|
|
87
|
+
const maybeIp = req.ip;
|
|
88
|
+
return maybeIp;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// src/middleware-node.ts
|
|
92
|
+
function withCheckpoint(config) {
|
|
93
|
+
const opts = buildVerifyOpts(config);
|
|
94
|
+
return async function checkpointMiddleware(req) {
|
|
95
|
+
const httpLike = nextRequestToHttpLike(req);
|
|
96
|
+
const result = await orchestrator.verifyRequest(httpLike, opts);
|
|
97
|
+
await dispatchOnResult(config, result, req);
|
|
98
|
+
const rendered = orchestrator.renderDecisionAsResponse(result);
|
|
99
|
+
return adaptToNextResponse(rendered, req);
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
function buildVerifyOpts(config) {
|
|
103
|
+
const overrides = config.adapters ?? {};
|
|
104
|
+
return {
|
|
105
|
+
didResolver: overrides.didResolver ?? adapters.makeDidResolver(),
|
|
106
|
+
statusListCache: overrides.statusListCache ?? adapters.makeStatusListCache(),
|
|
107
|
+
reputationOracle: overrides.reputationOracle ?? adapters.makeReputationOracle({ argusUrl: config.argusUrl }),
|
|
108
|
+
policyEvaluator: overrides.policyEvaluator ?? adapters.makePolicyEvaluator({ dashboardUrl: config.dashboardUrl }),
|
|
109
|
+
clock: adapters.makeSystemClock(),
|
|
110
|
+
tenantHost: config.tenantHost,
|
|
111
|
+
enforcementMode: config.enforcementMode ?? "enforce",
|
|
112
|
+
reputationBaseline: config.reputationBaseline,
|
|
113
|
+
argusUrl: config.argusUrl
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
async function dispatchOnResult(config, result, req) {
|
|
117
|
+
if (!config.onResult) return;
|
|
118
|
+
try {
|
|
119
|
+
await config.onResult(result, req);
|
|
120
|
+
} catch {
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
exports._buildVerifyOpts = buildVerifyOpts;
|
|
125
|
+
exports.withCheckpoint = withCheckpoint;
|
|
126
|
+
//# sourceMappingURL=middleware-node.js.map
|
|
127
|
+
//# sourceMappingURL=middleware-node.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/adapt.ts","../src/translate.ts","../src/middleware-node.ts"],"names":["acceptsHtml","encodeVerdictCookie","classifyResponseShape","NextResponse","BLOCKED_PATH","VERDICT_COOKIE_NAME","verifyRequest","renderDecisionAsResponse","makeDidResolver","makeStatusListCache","makeReputationOracle","makePolicyEvaluator","makeSystemClock"],"mappings":"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","file":"middleware-node.js","sourcesContent":["/**\n * D.3 — `RenderedResponse` → `NextResponse` adapter.\n *\n * The host wrapper's *only* job on the outbound path: take the\n * transport-agnostic `RenderedResponse` Phase C's\n * `renderDecisionAsResponse` produces and translate it to a\n * `NextResponse`. Zero verdict decisions, zero engine I/O.\n *\n * Shared between the Node-runtime and Edge-runtime entries. The\n * branching here is identical in both — Next.js `NextResponse` has the\n * same API surface across runtimes; only the underlying response\n * primitive differs (Node http.ServerResponse vs Edge `Response`).\n *\n * Architectural pins per architect § 4.3 / § 4.4:\n *\n * 1. **Verdict-cookie format is contract.** Sites-1's Sonner toast\n * depends on `__checkpoint_verdict=%7B%22verdict%22%3A%22<v>%22...\n * %7D` (single URL-encoded JSON). Byte-format pinned by adapt.test.\n *\n * 2. **HTML-accepting clients → `/blocked` rewrite at status 200**\n * (so the page renders with the verdict cookie set; Sonner picks\n * up the cookie and shows the toast). Non-HTML clients → JSON 4xx.\n *\n * 3. **`X-Checkpoint-Engine` carries `result.engineInfo.name`** —\n * `checkpoint-engine-wasm` after Phase D ships. Brian's Sites-2\n * deviation note confirmed the `X-Checkpoint-*` prefix is canon.\n */\n\nimport { type NextRequest, NextResponse } from 'next/server';\n\nimport type { RenderedResponse } from '@kya-os/checkpoint-wasm-runtime/orchestrator';\nimport {\n VERDICT_COOKIE_NAME,\n BLOCKED_PATH,\n encodeVerdictCookie,\n acceptsHtml,\n classifyResponseShape,\n} from '@kya-os/checkpoint-shared';\n\n/**\n * Convert the engine's transport-agnostic `RenderedResponse` into a\n * `NextResponse`. Sites-1's Playwright suite is the regression gate;\n * any drift here is caught downstream.\n */\nexport function adaptToNextResponse(rendered: RenderedResponse, req: NextRequest): NextResponse {\n const clientAcceptsHtml = acceptsHtml(req.headers);\n const verdictCookie = encodeVerdictCookie(rendered);\n const shape = classifyResponseShape(rendered, clientAcceptsHtml);\n\n switch (shape) {\n case 'pass-through': {\n // Permit OR Observe-mode any-verdict.\n const res = NextResponse.next();\n applyHeaders(res, rendered.headers);\n setVerdictCookie(res, verdictCookie);\n return res;\n }\n\n case 'redirect': {\n // Decision::Redirect → 302 + Location.\n const target = new URL(rendered.headers.Location!);\n const res = NextResponse.redirect(target);\n applyHeaders(res, rendered.headers);\n setVerdictCookie(res, verdictCookie);\n return res;\n }\n\n case 'html-block': {\n // Sites-1 contract: HTML clients (browsers) need a renderable page\n // to show the rejection UI. The verdict cookie carries the reason;\n // the /blocked route reads it and renders the toast.\n const blockedUrl = new URL(BLOCKED_PATH, req.url);\n const res = NextResponse.rewrite(blockedUrl, { status: 200 });\n applyHeaders(res, rendered.headers);\n setVerdictCookie(res, verdictCookie);\n return res;\n }\n\n case 'json-block': {\n // The orchestrator's RenderedResponse already supplies the correct\n // status (401/403/422/...); we just need to materialise the body.\n // application/problem+json (Instruct) uses the Content-Type from\n // rendered.headers; defaults to application/json for everything else.\n const body = rendered.body ?? {};\n const res = NextResponse.json(body, { status: rendered.status as number });\n applyHeaders(res, rendered.headers);\n setVerdictCookie(res, verdictCookie);\n return res;\n }\n }\n}\n\n// -----------------------------------------------------------------------------\n// Helpers — Next.js-specific glue. The framework-agnostic primitives\n// (encodeVerdictCookie, acceptsHtml, classifyResponseShape,\n// VERDICT_COOKIE_NAME, BLOCKED_PATH) live in `@kya-os/checkpoint-shared`\n// so checkpoint-express + future host wrappers produce byte-identical\n// cookies and route HTML/JSON branching the same way.\n// -----------------------------------------------------------------------------\n\nfunction setVerdictCookie(res: NextResponse, value: string): void {\n // Path / SameSite / HttpOnly chosen for the Sonner-bridge use case:\n // path=/ so any route can read it, SameSite=Lax so first-party\n // navigations carry it, HttpOnly=false so the client-side toast JS\n // can read it (it's verdict UX, not a session token).\n res.cookies.set({\n name: VERDICT_COOKIE_NAME,\n value,\n path: '/',\n sameSite: 'lax',\n httpOnly: false,\n });\n}\n\nfunction applyHeaders(res: NextResponse, headers: Record<string, string>): void {\n // NextResponse.next() / rewrite() / json() return responses with\n // some default headers; orchestrator headers (X-Checkpoint-*, Location)\n // override. We don't strip pre-existing headers — only set new ones.\n for (const [key, value] of Object.entries(headers)) {\n res.headers.set(key, value);\n }\n}\n","/**\n * D.2 — `NextRequest` → `IncomingHttpLike` translator.\n *\n * The host wrapper's *only* job on the inbound path: take Next.js's\n * native request shape and produce the transport-agnostic\n * `IncomingHttpLike` Phase C's orchestrator consumes. Zero verification\n * logic, zero adapter calls, zero engine I/O.\n *\n * Shared between the Node-runtime and Edge-runtime entries. Next.js\n * `NextRequest` is the same shape in both runtimes — `req.headers` is\n * a `Headers` instance, `req.body` is a `ReadableStream`, `req.ip` is\n * a getter (only present in some deployment surfaces; fall back to\n * `x-forwarded-for` first IP).\n */\n\nimport type { NextRequest } from 'next/server';\n\nimport type { IncomingHttpLike } from '@kya-os/checkpoint-wasm-runtime/orchestrator';\n\n/**\n * Translate a Next.js `NextRequest` into the orchestrator's\n * `IncomingHttpLike` shape.\n *\n * The body is passed through as-is — the orchestrator's\n * `buildAgentRequest` decides whether to parse JSON (looking for an\n * MCP-I `_meta.proof.jws` envelope) or treat the request as PlainHttp.\n * On Next.js middleware the body is typically not pre-parsed; consumers\n * who want to inspect the body for routing decisions should `await\n * req.json()` themselves and pass the parsed result via a second\n * `verifyRequest` call (not common).\n */\nexport function nextRequestToHttpLike(req: NextRequest): IncomingHttpLike {\n const url = new URL(req.url);\n return {\n method: req.method,\n // Path + query only — orchestrator's URL parsing expects no scheme/host.\n url: url.pathname + url.search,\n headers: headersToRecord(req.headers),\n // NextRequest.body is a ReadableStream; we don't drain it here.\n // The orchestrator routes to PlainHttp when body is falsy, which\n // is the right call for streaming middlewares that don't want to\n // buffer the request body just to detect agents.\n body: null,\n remoteAddress: extractRemoteAddress(req),\n };\n}\n\n/**\n * Convert a `Headers` instance into a lowercase-keyed plain object.\n * HTTP header names are case-insensitive (RFC 9110 § 5.1); the\n * orchestrator does case-sensitive lookups, so we normalise to\n * lowercase here. Multi-value headers (Set-Cookie, Accept) are\n * surfaced as their `Headers.get()` view — a single string with\n * comma-joined values, matching what other host adapters produce.\n */\nfunction headersToRecord(headers: Headers): Record<string, string> {\n const out: Record<string, string> = {};\n headers.forEach((value, key) => {\n out[key.toLowerCase()] = value;\n });\n return out;\n}\n\n/**\n * Pull the originating client IP, preferring `x-forwarded-for`'s first\n * entry over `NextRequest.ip` (the latter is only populated on Vercel-\n * hosted deployments and is missing on self-hosted Next.js + nginx /\n * Fly.io / docker-compose surfaces). The `x-forwarded-for` first IP is\n * the closest the request has come to a load balancer's \"trust this is\n * the real client\" attestation — same convention as nginx, Caddy,\n * Cloudflare.\n */\nfunction extractRemoteAddress(req: NextRequest): string | undefined {\n const xff = req.headers.get('x-forwarded-for');\n if (xff) {\n const first = xff.split(',')[0]?.trim();\n if (first) return first;\n }\n // `req.ip` is typed but may be undefined off-Vercel.\n // Use `unknown` cast to avoid the type-narrowing optimism.\n const maybeIp = (req as unknown as { ip?: string }).ip;\n return maybeIp;\n}\n","/**\n * D.1 + D.3 — Node-runtime Next.js middleware entry.\n *\n * The host wrapper that composes Phase B adapters + Phase C\n * `verifyRequest` (sync engine) + Phase D translate/adapt into the\n * `withCheckpoint(config)` factory. Mounted under Vercel Node-runtime\n * serverless functions and long-lived Node servers.\n *\n * For Vercel Edge runtime (the Next.js middleware default), customers\n * import from `./edge` or `@kya-os/checkpoint-nextjs/edge` — that\n * variant uses `verifyRequestEdge` (async-init) and is otherwise\n * structurally identical. Both share `translate.ts` + `adapt.ts`.\n *\n * **Public API contract (architect § 4.1 — preserved):**\n *\n * - `withCheckpoint(config)` — factory returning the middleware.\n * - `CheckpointConfig` — the config shape; new fields are additive.\n *\n * Internal implementation gutted, external contract held. Sites-1's\n * Playwright suite is the regression gate.\n */\n\nimport { type NextRequest, type NextResponse } from 'next/server';\n\nimport {\n renderDecisionAsResponse,\n verifyRequest,\n} from '@kya-os/checkpoint-wasm-runtime/orchestrator';\nimport {\n makeDidResolver,\n makePolicyEvaluator,\n makeReputationOracle,\n makeStatusListCache,\n makeSystemClock,\n type DidResolverAdapter,\n type PolicyEvaluatorAdapter,\n type ReputationOracleAdapter,\n type StatusListCacheAdapter,\n} from '@kya-os/checkpoint-wasm-runtime/adapters';\nimport type { EnforcementMode, VerifyResult } from '@kya-os/checkpoint-wasm-runtime/engine';\n\nimport { adaptToNextResponse } from './adapt';\nimport { nextRequestToHttpLike } from './translate';\n\n/**\n * Configuration for `withCheckpoint`.\n *\n * The new minimal shape Phase D's middleware needs. Legacy\n * `AgentShieldMiddlewareConfig` (from `./api-middleware`) remains\n * exported during the deprecation window — see D.4 cutover.\n */\nexport interface CheckpointConfig {\n /**\n * Tenant identifier — typically the customer's dashboard hostname\n * (e.g. `acme.checkpoint.example`). The PolicyEvaluator uses this\n * to look up tenant policy from the dashboard.\n */\n tenantHost: string;\n\n /**\n * `'enforce'` (default) blocks; `'observe'` passes everything\n * through with `X-Checkpoint-Would-Have-Been` headers. Per Phase 0.2.\n */\n enforcementMode?: EnforcementMode;\n\n /**\n * Argus reputation oracle base URL. Omit to use the trust-by-default\n * baseline (reputation defaults to 1.0; orchestrator logs a one-shot\n * warning at first request).\n */\n argusUrl?: string;\n\n /**\n * Dashboard base URL for the PolicyEvaluator to fetch tenant policy\n * from. Omit to use the open-by-default tenant policy.\n */\n dashboardUrl?: string;\n\n /**\n * Returned to the PolicyEvaluator for anonymous requests (no agent\n * DID). Default 1.0 (trust-by-default).\n */\n reputationBaseline?: number;\n\n /**\n * Pre-built adapter instances. Production deployments use the\n * factory-built defaults from `@kya-os/checkpoint-wasm-runtime/adapters`;\n * tests use stubs. The factory composes any provided overrides over\n * defaults — partial overrides are supported.\n */\n adapters?: Partial<{\n didResolver: DidResolverAdapter;\n statusListCache: StatusListCacheAdapter;\n reputationOracle: ReputationOracleAdapter;\n policyEvaluator: PolicyEvaluatorAdapter;\n }>;\n\n /**\n * Optional callback for the post-verdict path — fires after every\n * verification, regardless of permit/block, with the full\n * `VerifyResult`. Use for logging, dashboards, telemetry. Errors\n * thrown here are swallowed so user code can't break the middleware\n * response.\n */\n onResult?: (result: VerifyResult, req: NextRequest) => void | Promise<void>;\n}\n\n/**\n * Build the Checkpoint middleware. Returns a function `(req) => NextResponse`\n * suitable for `export default withCheckpoint({...})` in `middleware.ts`.\n *\n * Every verification decision flows through the Rust `kya-os-engine`\n * via WASM. The TS layer translates request shape, calls\n * `verifyRequest`, and translates the verdict to `NextResponse`. No\n * verification logic lives in this file.\n */\nexport function withCheckpoint(\n config: CheckpointConfig\n): (req: NextRequest) => Promise<NextResponse> {\n const opts = buildVerifyOpts(config);\n return async function checkpointMiddleware(req: NextRequest): Promise<NextResponse> {\n const httpLike = nextRequestToHttpLike(req);\n const result = await verifyRequest(httpLike, opts);\n await dispatchOnResult(config, result, req);\n const rendered = renderDecisionAsResponse(result);\n return adaptToNextResponse(rendered, req);\n };\n}\n\n/**\n * Compose adapter defaults with caller-supplied overrides. Factored\n * out so the Edge entry (which uses the same composition) can reuse\n * the shape.\n */\nfunction buildVerifyOpts(config: CheckpointConfig) {\n const overrides = config.adapters ?? {};\n return {\n didResolver: overrides.didResolver ?? makeDidResolver(),\n statusListCache: overrides.statusListCache ?? makeStatusListCache(),\n reputationOracle:\n overrides.reputationOracle ?? makeReputationOracle({ argusUrl: config.argusUrl }),\n policyEvaluator:\n overrides.policyEvaluator ?? makePolicyEvaluator({ dashboardUrl: config.dashboardUrl }),\n clock: makeSystemClock(),\n tenantHost: config.tenantHost,\n enforcementMode: config.enforcementMode ?? 'enforce',\n reputationBaseline: config.reputationBaseline,\n argusUrl: config.argusUrl,\n };\n}\n\nasync function dispatchOnResult(\n config: CheckpointConfig,\n result: VerifyResult,\n req: NextRequest\n): Promise<void> {\n if (!config.onResult) return;\n try {\n await config.onResult(result, req);\n } catch {\n // Swallow — onResult is observability, not verdict-critical.\n // Verdict already computed; let the response proceed.\n }\n}\n\n// Re-export the shared opts builder for the Edge entry. Internal seam;\n// not part of the public surface.\nexport { buildVerifyOpts as _buildVerifyOpts };\n"]}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { verifyRequest, renderDecisionAsResponse } from '@kya-os/checkpoint-wasm-runtime/orchestrator';
|
|
2
|
+
import { makeSystemClock, makePolicyEvaluator, makeReputationOracle, makeStatusListCache, makeDidResolver } from '@kya-os/checkpoint-wasm-runtime/adapters';
|
|
3
|
+
import { NextResponse } from 'next/server';
|
|
4
|
+
import { acceptsHtml, encodeVerdictCookie, classifyResponseShape, BLOCKED_PATH, VERDICT_COOKIE_NAME } from '@kya-os/checkpoint-shared';
|
|
5
|
+
|
|
6
|
+
// src/middleware-node.ts
|
|
7
|
+
function adaptToNextResponse(rendered, req) {
|
|
8
|
+
const clientAcceptsHtml = acceptsHtml(req.headers);
|
|
9
|
+
const verdictCookie = encodeVerdictCookie(rendered);
|
|
10
|
+
const shape = classifyResponseShape(rendered, clientAcceptsHtml);
|
|
11
|
+
switch (shape) {
|
|
12
|
+
case "pass-through": {
|
|
13
|
+
const res = NextResponse.next();
|
|
14
|
+
applyHeaders(res, rendered.headers);
|
|
15
|
+
setVerdictCookie(res, verdictCookie);
|
|
16
|
+
return res;
|
|
17
|
+
}
|
|
18
|
+
case "redirect": {
|
|
19
|
+
const target = new URL(rendered.headers.Location);
|
|
20
|
+
const res = NextResponse.redirect(target);
|
|
21
|
+
applyHeaders(res, rendered.headers);
|
|
22
|
+
setVerdictCookie(res, verdictCookie);
|
|
23
|
+
return res;
|
|
24
|
+
}
|
|
25
|
+
case "html-block": {
|
|
26
|
+
const blockedUrl = new URL(BLOCKED_PATH, req.url);
|
|
27
|
+
const res = NextResponse.rewrite(blockedUrl, { status: 200 });
|
|
28
|
+
applyHeaders(res, rendered.headers);
|
|
29
|
+
setVerdictCookie(res, verdictCookie);
|
|
30
|
+
return res;
|
|
31
|
+
}
|
|
32
|
+
case "json-block": {
|
|
33
|
+
const body = rendered.body ?? {};
|
|
34
|
+
const res = NextResponse.json(body, { status: rendered.status });
|
|
35
|
+
applyHeaders(res, rendered.headers);
|
|
36
|
+
setVerdictCookie(res, verdictCookie);
|
|
37
|
+
return res;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function setVerdictCookie(res, value) {
|
|
42
|
+
res.cookies.set({
|
|
43
|
+
name: VERDICT_COOKIE_NAME,
|
|
44
|
+
value,
|
|
45
|
+
path: "/",
|
|
46
|
+
sameSite: "lax",
|
|
47
|
+
httpOnly: false
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
function applyHeaders(res, headers) {
|
|
51
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
52
|
+
res.headers.set(key, value);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// src/translate.ts
|
|
57
|
+
function nextRequestToHttpLike(req) {
|
|
58
|
+
const url = new URL(req.url);
|
|
59
|
+
return {
|
|
60
|
+
method: req.method,
|
|
61
|
+
// Path + query only — orchestrator's URL parsing expects no scheme/host.
|
|
62
|
+
url: url.pathname + url.search,
|
|
63
|
+
headers: headersToRecord(req.headers),
|
|
64
|
+
// NextRequest.body is a ReadableStream; we don't drain it here.
|
|
65
|
+
// The orchestrator routes to PlainHttp when body is falsy, which
|
|
66
|
+
// is the right call for streaming middlewares that don't want to
|
|
67
|
+
// buffer the request body just to detect agents.
|
|
68
|
+
body: null,
|
|
69
|
+
remoteAddress: extractRemoteAddress(req)
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
function headersToRecord(headers) {
|
|
73
|
+
const out = {};
|
|
74
|
+
headers.forEach((value, key) => {
|
|
75
|
+
out[key.toLowerCase()] = value;
|
|
76
|
+
});
|
|
77
|
+
return out;
|
|
78
|
+
}
|
|
79
|
+
function extractRemoteAddress(req) {
|
|
80
|
+
const xff = req.headers.get("x-forwarded-for");
|
|
81
|
+
if (xff) {
|
|
82
|
+
const first = xff.split(",")[0]?.trim();
|
|
83
|
+
if (first) return first;
|
|
84
|
+
}
|
|
85
|
+
const maybeIp = req.ip;
|
|
86
|
+
return maybeIp;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// src/middleware-node.ts
|
|
90
|
+
function withCheckpoint(config) {
|
|
91
|
+
const opts = buildVerifyOpts(config);
|
|
92
|
+
return async function checkpointMiddleware(req) {
|
|
93
|
+
const httpLike = nextRequestToHttpLike(req);
|
|
94
|
+
const result = await verifyRequest(httpLike, opts);
|
|
95
|
+
await dispatchOnResult(config, result, req);
|
|
96
|
+
const rendered = renderDecisionAsResponse(result);
|
|
97
|
+
return adaptToNextResponse(rendered, req);
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
function buildVerifyOpts(config) {
|
|
101
|
+
const overrides = config.adapters ?? {};
|
|
102
|
+
return {
|
|
103
|
+
didResolver: overrides.didResolver ?? makeDidResolver(),
|
|
104
|
+
statusListCache: overrides.statusListCache ?? makeStatusListCache(),
|
|
105
|
+
reputationOracle: overrides.reputationOracle ?? makeReputationOracle({ argusUrl: config.argusUrl }),
|
|
106
|
+
policyEvaluator: overrides.policyEvaluator ?? makePolicyEvaluator({ dashboardUrl: config.dashboardUrl }),
|
|
107
|
+
clock: makeSystemClock(),
|
|
108
|
+
tenantHost: config.tenantHost,
|
|
109
|
+
enforcementMode: config.enforcementMode ?? "enforce",
|
|
110
|
+
reputationBaseline: config.reputationBaseline,
|
|
111
|
+
argusUrl: config.argusUrl
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
async function dispatchOnResult(config, result, req) {
|
|
115
|
+
if (!config.onResult) return;
|
|
116
|
+
try {
|
|
117
|
+
await config.onResult(result, req);
|
|
118
|
+
} catch {
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export { buildVerifyOpts as _buildVerifyOpts, withCheckpoint };
|
|
123
|
+
//# sourceMappingURL=middleware-node.mjs.map
|
|
124
|
+
//# sourceMappingURL=middleware-node.mjs.map
|