iterate 0.2.7 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +86 -76
- package/THIRD_PARTY_NOTICES.md +55 -0
- package/bin/iterate.js +11 -3
- package/dist/api-url-B6404M82.mjs +17 -0
- package/dist/api-url-B6404M82.mjs.map +1 -0
- package/dist/app-ref-BipL0feU.mjs +35 -0
- package/dist/app-ref-BipL0feU.mjs.map +1 -0
- package/dist/app-ref-C1CrgXqX.mjs +7 -0
- package/dist/app-ref-C1CrgXqX.mjs.map +1 -0
- package/dist/app-ref-DYai_om1.mjs +7 -0
- package/dist/app-ref-DYai_om1.mjs.map +1 -0
- package/dist/cli-D0c-pDL_.mjs +1010 -0
- package/dist/cli-D0c-pDL_.mjs.map +1 -0
- package/dist/client.d.ts +3 -0
- package/dist/client.mjs +4 -0
- package/dist/cloudflare-BTm90gQ4.mjs +951 -0
- package/dist/cloudflare-BTm90gQ4.mjs.map +1 -0
- package/dist/contract-s4FW4eES.mjs +309 -0
- package/dist/contract-s4FW4eES.mjs.map +1 -0
- package/dist/document-review/index.d.ts +5 -0
- package/dist/document-review/types.d.ts +107 -0
- package/dist/document-review.mjs +7015 -0
- package/dist/document-review.mjs.map +1 -0
- package/dist/durable-object-processor-durability-CNsTjAJS.mjs +205 -0
- package/dist/durable-object-processor-durability-CNsTjAJS.mjs.map +1 -0
- package/dist/idempotency-DleloJNt.mjs +28 -0
- package/dist/idempotency-DleloJNt.mjs.map +1 -0
- package/dist/index.mjs +1 -1
- package/dist/itx/api-url.d.ts +6 -0
- package/dist/itx/itx-node-client.d.ts +65 -0
- package/dist/itx/itx-session.d.ts +215 -0
- package/dist/itx/owned-rpc-session.d.ts +14 -0
- package/dist/itx/query-client.d.ts +10 -0
- package/dist/itx-api.generated.d.ts +6195 -0
- package/dist/itx-session-sjud8GiT.mjs +534 -0
- package/dist/itx-session-sjud8GiT.mjs.map +1 -0
- package/dist/live-state-BJNqOwFw.mjs +299 -0
- package/dist/live-state-BJNqOwFw.mjs.map +1 -0
- package/dist/next/api.d.ts +479 -0
- package/dist/next/api.mjs +0 -0
- package/dist/next/app-server.d.ts +44 -0
- package/dist/next/app-server.mjs +479 -0
- package/dist/next/app-server.mjs.map +1 -0
- package/dist/next/app-session.d.ts +49 -0
- package/dist/next/app-session.mjs +238 -0
- package/dist/next/app-session.mjs.map +1 -0
- package/dist/next/app.d.ts +29 -0
- package/dist/next/app.mjs +141 -0
- package/dist/next/app.mjs.map +1 -0
- package/dist/next/client/live-state.d.ts +63 -0
- package/dist/next/client/oauth.d.ts +12 -0
- package/dist/next/client/react.d.ts +109 -0
- package/dist/next/client/socket.d.ts +6 -0
- package/dist/next/client.mjs +156 -0
- package/dist/next/client.mjs.map +1 -0
- package/dist/next/expression.d.ts +146 -0
- package/dist/next/expression.mjs +399 -0
- package/dist/next/expression.mjs.map +1 -0
- package/dist/next/lib.d.ts +56 -0
- package/dist/next/lib.mjs +199 -0
- package/dist/next/lib.mjs.map +1 -0
- package/dist/next/oauth-scopes.d.ts +32 -0
- package/dist/next/oauth-scopes.mjs +40 -0
- package/dist/next/oauth-scopes.mjs.map +1 -0
- package/dist/next/oauth.mjs +29 -0
- package/dist/next/oauth.mjs.map +1 -0
- package/dist/next/principal.d.ts +64 -0
- package/dist/next/principal.mjs +98 -0
- package/dist/next/principal.mjs.map +1 -0
- package/dist/next/project-ingress.d.ts +37 -0
- package/dist/next/project-ingress.mjs +75 -0
- package/dist/next/project-ingress.mjs.map +1 -0
- package/dist/next/react.mjs +285 -0
- package/dist/next/react.mjs.map +1 -0
- package/dist/next/sdk/auth.d.ts +5 -0
- package/dist/next/sdk/index.d.ts +112 -0
- package/dist/next/sdk.mjs +139 -0
- package/dist/next/sdk.mjs.map +1 -0
- package/dist/next/stream/processor.d.ts +378 -0
- package/dist/next/stream/processor.mjs +582 -0
- package/dist/next/stream/processor.mjs.map +1 -0
- package/dist/next/stream/run.d.ts +58 -0
- package/dist/next/stream/run.mjs +40 -0
- package/dist/next/stream/run.mjs.map +1 -0
- package/dist/next-node.d.ts +15 -0
- package/dist/next-node.mjs +51 -0
- package/dist/next-node.mjs.map +1 -0
- package/dist/node.d.ts +3 -0
- package/dist/node.mjs +185 -0
- package/dist/node.mjs.map +1 -0
- package/dist/processor-host-capabilities-BMFH3KTM.mjs +56 -0
- package/dist/processor-host-capabilities-BMFH3KTM.mjs.map +1 -0
- package/dist/processors/cloudflare.d.ts +3 -0
- package/dist/processors/durable-object-processor-durability.d.ts +79 -0
- package/dist/processors/event-consumption-metrics.d.ts +82 -0
- package/dist/processors/idempotency.d.ts +13 -0
- package/dist/processors/index.d.ts +12 -0
- package/dist/processors/processor-contracts.d.ts +342 -0
- package/dist/processors/processor-facet.d.ts +186 -0
- package/dist/processors/processor-host-capabilities.d.ts +60 -0
- package/dist/processors/prompt-sections.d.ts +17 -0
- package/dist/processors/rpc-types.d.ts +515 -0
- package/dist/processors/schemas.d.ts +102 -0
- package/dist/processors/stream-handle.d.ts +45 -0
- package/dist/processors/stream-processor-keepalive.d.ts +95 -0
- package/dist/processors/stream-processor-registry.d.ts +233 -0
- package/dist/processors/stream-processor-runner.d.ts +289 -0
- package/dist/processors/stream-processor.d.ts +339 -0
- package/dist/processors/stream-runtime-metrics.d.ts +107 -0
- package/dist/processors/testing.d.ts +302 -0
- package/dist/processors-BoNyeBfQ.mjs +10 -0
- package/dist/processors-BoNyeBfQ.mjs.map +1 -0
- package/dist/processors-cloudflare.mjs +3 -0
- package/dist/processors-testing.mjs +435 -0
- package/dist/processors-testing.mjs.map +1 -0
- package/dist/processors.mjs +52 -0
- package/dist/processors.mjs.map +1 -0
- package/dist/protocol-DnK_f2m6.mjs +251 -0
- package/dist/protocol-DnK_f2m6.mjs.map +1 -0
- package/dist/sdk/capnweb/index.d.ts +2 -0
- package/dist/sdk/capnweb/live-state/compact.d.ts +5 -0
- package/dist/sdk/capnweb/live-state/diff.d.ts +41 -0
- package/dist/sdk/capnweb/live-state/engine.d.ts +44 -0
- package/dist/sdk/capnweb/live-state/index.d.ts +41 -0
- package/dist/sdk/capnweb/live-state/protocol.d.ts +87 -0
- package/dist/sdk/capnweb/live-state/retain.d.ts +23 -0
- package/dist/sdk/capnweb/live-state/store.d.ts +20 -0
- package/dist/sdk/capnweb/live-state/types.d.ts +11 -0
- package/dist/sdk/capnweb/react.d.ts +45 -0
- package/dist/sdk/capnweb/react.mjs +316 -0
- package/dist/sdk/capnweb/react.mjs.map +1 -0
- package/dist/sdk/capnweb.mjs +4 -0
- package/dist/sdk/itx/react.d.ts +191 -0
- package/dist/sdk/itx/react.mjs +383 -0
- package/dist/sdk/itx/react.mjs.map +1 -0
- package/dist/sdk-DMB-IM11.mjs +933 -0
- package/dist/sdk-DMB-IM11.mjs.map +1 -0
- package/dist/sdk.d.ts +339 -0
- package/dist/sdk.mjs +2 -0
- package/dist/serve-itx.d.ts +46 -0
- package/dist/starter-apps/flake-dashboard/app-ref.d.ts +31 -0
- package/dist/starter-apps/flake-dashboard/configured-worker.mjs +1055 -0
- package/dist/starter-apps/flake-dashboard/configured-worker.mjs.map +1 -0
- package/dist/starter-apps/flake-dashboard/contract.d.ts +4839 -0
- package/dist/starter-apps/flake-dashboard/contract.mjs +2 -0
- package/dist/starter-apps/flake-dashboard/index.d.ts +17 -0
- package/dist/starter-apps/flake-dashboard/index.mjs +56 -0
- package/dist/starter-apps/flake-dashboard/index.mjs.map +1 -0
- package/dist/starter-apps/flake-dashboard/worker.d.ts +4607 -0
- package/dist/starter-apps/github-ai-linter/ai-linter.d.ts +8914 -0
- package/dist/starter-apps/github-ai-linter/configured-worker.mjs +17987 -0
- package/dist/starter-apps/github-ai-linter/configured-worker.mjs.map +1 -0
- package/dist/starter-apps/github-ai-linter/contract.d.ts +9193 -0
- package/dist/starter-apps/github-ai-linter/index.d.ts +10 -0
- package/dist/starter-apps/github-ai-linter/index.mjs +36 -0
- package/dist/starter-apps/github-ai-linter/index.mjs.map +1 -0
- package/dist/starter-apps/github-ai-linter/prompt.d.ts +13 -0
- package/dist/starter-apps/github-ai-linter/review-bot.d.ts +808 -0
- package/dist/starter-apps/github-ai-linter/rules.d.ts +34 -0
- package/dist/starter-apps/github-ai-linter/worker-ref.d.ts +19 -0
- package/dist/starter-apps/github-ai-linter/worker.d.ts +19 -0
- package/dist/starter-apps/github-ai-linter/worker.mjs +947 -0
- package/dist/starter-apps/github-ai-linter/worker.mjs.map +1 -0
- package/dist/starter-apps/guestbook/app-ref.d.ts +27 -0
- package/dist/starter-apps/guestbook/client.d.ts +7 -0
- package/dist/starter-apps/guestbook/client.mjs +59 -0
- package/dist/starter-apps/guestbook/configured-worker.mjs +205 -0
- package/dist/starter-apps/guestbook/configured-worker.mjs.map +1 -0
- package/dist/starter-apps/guestbook/index.d.ts +9 -0
- package/dist/starter-apps/guestbook/index.mjs +31 -0
- package/dist/starter-apps/guestbook/index.mjs.map +1 -0
- package/dist/starter-apps/guestbook/processor.d.ts +2267 -0
- package/dist/starter-apps/guestbook/worker.d.ts +26 -0
- package/dist/starter-apps/guestbook/worker.mjs +191 -0
- package/dist/starter-apps/guestbook/worker.mjs.map +1 -0
- package/dist/starter-apps/media/configured-worker.mjs +577 -0
- package/dist/starter-apps/media/configured-worker.mjs.map +1 -0
- package/dist/starter-apps/media/index.mjs +36 -0
- package/dist/starter-apps/media/index.mjs.map +1 -0
- package/dist/starter-apps/media/ref.mjs +20 -0
- package/dist/starter-apps/media/ref.mjs.map +1 -0
- package/dist/starter-apps/media/worker.mjs +579 -0
- package/dist/starter-apps/media/worker.mjs.map +1 -0
- package/dist/starter-apps/notes/configured-worker.mjs +6134 -0
- package/dist/starter-apps/notes/configured-worker.mjs.map +1 -0
- package/dist/starter-apps/notes/index.mjs +23 -0
- package/dist/starter-apps/notes/index.mjs.map +1 -0
- package/dist/starter-apps/notes/ref.mjs +21 -0
- package/dist/starter-apps/notes/ref.mjs.map +1 -0
- package/dist/starter-apps/notes/worker.mjs +427 -0
- package/dist/starter-apps/notes/worker.mjs.map +1 -0
- package/dist/starter-apps/todo/client.mjs +59 -0
- package/dist/starter-apps/todo/configured-worker.mjs +2864 -0
- package/dist/starter-apps/todo/configured-worker.mjs.map +1 -0
- package/dist/starter-apps/todo/index.d.ts +8 -0
- package/dist/starter-apps/todo/index.mjs +29 -0
- package/dist/starter-apps/todo/index.mjs.map +1 -0
- package/dist/stream-processor-keepalive-DAQTP6m3.mjs +2082 -0
- package/dist/stream-processor-keepalive-DAQTP6m3.mjs.map +1 -0
- package/dist/usingCtx-inzbY1Qz.mjs +57 -0
- package/dist/usingCtx-mZx5nsAW.mjs +11800 -0
- package/dist/usingCtx-mZx5nsAW.mjs.map +1 -0
- package/dist/worker-ref-DZxPDmb_.mjs +390 -0
- package/dist/worker-ref-DZxPDmb_.mjs.map +1 -0
- package/menubar/Iterate.entitlements +12 -0
- package/menubar/Iterate.swift +914 -0
- package/menubar/IterateIcon.swift +145 -0
- package/menubar/README.md +28 -0
- package/menubar/build-menubar-app.sh +59 -0
- package/package.json +235 -18
- package/dist/cli-DMS4kJph.mjs +0 -868
- package/dist/cli-DMS4kJph.mjs.map +0 -1
- package/dist/config-DtnR7Lv7.mjs +0 -170
- package/dist/config-DtnR7Lv7.mjs.map +0 -1
- package/dist/index.d.mts.map +0 -1
- package/dist/stream-tui/agent-chat-terminal.d.mts +0 -1
- package/dist/stream-tui/agent-chat-terminal.mjs +0 -933
- package/dist/stream-tui/agent-chat-terminal.mjs.map +0 -1
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
//#region src/next/lib.ts
|
|
2
|
+
/** A plain Error carrying `code` (+ optional `data`) as own enumerable properties. */
|
|
3
|
+
function codedError(code, message, data) {
|
|
4
|
+
return Object.assign(new Error(message), data === void 0 ? { code } : {
|
|
5
|
+
code,
|
|
6
|
+
data
|
|
7
|
+
});
|
|
8
|
+
}
|
|
9
|
+
/** The code of an error that crossed any number of hops — undefined for uncoded errors. */
|
|
10
|
+
function errorCode(error) {
|
|
11
|
+
return typeof error === "object" && error && "code" in error ? error.code : void 0;
|
|
12
|
+
}
|
|
13
|
+
const MAX = {
|
|
14
|
+
message: 1024,
|
|
15
|
+
stack: 16384,
|
|
16
|
+
string: 256,
|
|
17
|
+
attributeKeys: 32
|
|
18
|
+
};
|
|
19
|
+
/** Print ONE bounded console.error line for an unexpected failure; never throws. */
|
|
20
|
+
function reportIssue(failureSite, caught, attributes) {
|
|
21
|
+
try {
|
|
22
|
+
const bounded = {};
|
|
23
|
+
for (const [key, value] of Object.entries(attributes || {}).slice(0, MAX.attributeKeys)) {
|
|
24
|
+
if (value === void 0) continue;
|
|
25
|
+
bounded[key.slice(0, MAX.string)] = typeof value === "string" ? value.slice(0, MAX.string) : value;
|
|
26
|
+
}
|
|
27
|
+
const code = errorCode(caught);
|
|
28
|
+
const error = caught instanceof Error ? {
|
|
29
|
+
type: (caught.name || "Error").slice(0, MAX.string),
|
|
30
|
+
message: caught.message.slice(0, MAX.message),
|
|
31
|
+
...caught.stack && { stack: caught.stack.slice(0, MAX.stack) }
|
|
32
|
+
} : typeof caught === "object" && caught ? { type: "ObjectThrown" } : {
|
|
33
|
+
type: `${typeof caught}Thrown`,
|
|
34
|
+
message: String(caught).slice(0, MAX.message)
|
|
35
|
+
};
|
|
36
|
+
console.error({
|
|
37
|
+
...bounded,
|
|
38
|
+
event: "issue",
|
|
39
|
+
failureSite: failureSite.slice(0, MAX.string),
|
|
40
|
+
code,
|
|
41
|
+
error
|
|
42
|
+
});
|
|
43
|
+
} catch {}
|
|
44
|
+
}
|
|
45
|
+
const isRecord = (v) => typeof v === "object" && !!v && !Array.isArray(v);
|
|
46
|
+
const escape = (seg) => String(seg).replaceAll("~", "~0").replaceAll("/", "~1");
|
|
47
|
+
/** Structural deep-equal over plain JSON values — order-insensitive, unbudgeted (JSON is acyclic).
|
|
48
|
+
* THE one deep-equal: the live-state diff's "don't emit" test AND the idempotency-body compare
|
|
49
|
+
* (re-exported through stream/processor.ts). The `Object.hasOwn(b, k)` guard is load-bearing — without
|
|
50
|
+
* it, two objects with the same key COUNT but different key SETS compare equal. */
|
|
51
|
+
function jsonEqual(a, b) {
|
|
52
|
+
if (Object.is(a, b)) return true;
|
|
53
|
+
if (Array.isArray(a) && Array.isArray(b)) return a.length === b.length && a.every((v, i) => jsonEqual(v, b[i]));
|
|
54
|
+
if (isRecord(a) && isRecord(b)) {
|
|
55
|
+
const ka = Object.keys(a);
|
|
56
|
+
return ka.length === Object.keys(b).length && ka.every((k) => Object.hasOwn(b, k) && jsonEqual(a[k], b[k]));
|
|
57
|
+
}
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
/** Structural diff `a → b`, or undefined when deep-equal (the producer's "don't emit" signal).
|
|
61
|
+
* Objects recurse per key; arrays get the chat-log fast paths (pure append → `add …/-` ops,
|
|
62
|
+
* pure tail-truncate → `remove` ops) and are replaced wholesale on any middle divergence —
|
|
63
|
+
* the LiveView trade: optimize the growing log, full-render the rewrite.
|
|
64
|
+
* Both sides are JSON-normalized first — the wire is JSON, so the diff must see exactly what
|
|
65
|
+
* the wire will carry: undefined-valued keys vanish, Dates become their ISO strings, array
|
|
66
|
+
* holes become null. Diffing what you didn't normalize is how a Date change goes silent. */
|
|
67
|
+
function diff(a, b) {
|
|
68
|
+
const json = (v) => {
|
|
69
|
+
const s = JSON.stringify(v);
|
|
70
|
+
return s ? JSON.parse(s) : void 0;
|
|
71
|
+
};
|
|
72
|
+
return walk(json(a), json(b), "");
|
|
73
|
+
}
|
|
74
|
+
function walk(a, b, path) {
|
|
75
|
+
if (jsonEqual(a, b)) return void 0;
|
|
76
|
+
if (Array.isArray(a) && Array.isArray(b)) {
|
|
77
|
+
const shared = Math.min(a.length, b.length);
|
|
78
|
+
let p = 0;
|
|
79
|
+
while (p < shared && jsonEqual(a[p], b[p])) p++;
|
|
80
|
+
if (p === a.length) return b.slice(p).map((value) => ({
|
|
81
|
+
op: "add",
|
|
82
|
+
path: `${path}/-`,
|
|
83
|
+
value
|
|
84
|
+
}));
|
|
85
|
+
if (p === b.length) return a.slice(p).map((_, i) => ({
|
|
86
|
+
op: "remove",
|
|
87
|
+
path: `${path}/${a.length - 1 - i}`
|
|
88
|
+
}));
|
|
89
|
+
return [{
|
|
90
|
+
op: "replace",
|
|
91
|
+
path,
|
|
92
|
+
value: b
|
|
93
|
+
}];
|
|
94
|
+
}
|
|
95
|
+
if (isRecord(a) && isRecord(b)) {
|
|
96
|
+
const ops = [];
|
|
97
|
+
for (const k of Object.keys(a)) if (!Object.hasOwn(b, k)) ops.push({
|
|
98
|
+
op: "remove",
|
|
99
|
+
path: `${path}/${escape(k)}`
|
|
100
|
+
});
|
|
101
|
+
for (const k of Object.keys(b)) if (!Object.hasOwn(a, k)) ops.push({
|
|
102
|
+
op: "add",
|
|
103
|
+
path: `${path}/${escape(k)}`,
|
|
104
|
+
value: b[k]
|
|
105
|
+
});
|
|
106
|
+
else ops.push(...walk(a[k], b[k], `${path}/${escape(k)}`) ?? []);
|
|
107
|
+
return ops.length ? ops : void 0;
|
|
108
|
+
}
|
|
109
|
+
return [{
|
|
110
|
+
op: "replace",
|
|
111
|
+
path,
|
|
112
|
+
value: b
|
|
113
|
+
}];
|
|
114
|
+
}
|
|
115
|
+
/** Apply a patch non-mutatingly (clone-then-mutate). The client half of `diff` — exported
|
|
116
|
+
* through the SDK so subscribers need no third-party json-patch dependency. */
|
|
117
|
+
function applyPatch(doc, ops) {
|
|
118
|
+
let root = structuredClone(doc);
|
|
119
|
+
for (const op of ops) {
|
|
120
|
+
if (op.path === "") {
|
|
121
|
+
if (op.op === "remove") throw new Error("applyPatch: cannot remove the document root");
|
|
122
|
+
root = op.value;
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
const segs = op.path.slice(1).split("/").map((s) => s.replaceAll("~1", "/").replaceAll("~0", "~"));
|
|
126
|
+
for (const s of segs) if (s === "__proto__") throw new Error(`applyPatch: refusing __proto__ in path ${op.path}`);
|
|
127
|
+
const last = segs.pop();
|
|
128
|
+
let parent = root;
|
|
129
|
+
for (const s of segs) {
|
|
130
|
+
parent = Array.isArray(parent) ? parent[Number(s)] : isRecord(parent) && Object.hasOwn(parent, s) ? parent[s] : void 0;
|
|
131
|
+
if (parent === void 0) throw new Error(`applyPatch: missing path ${op.path}`);
|
|
132
|
+
}
|
|
133
|
+
if (Array.isArray(parent)) if (op.op === "add") if (last === "-") parent.push(op.value);
|
|
134
|
+
else parent.splice(Number(last), 0, op.value);
|
|
135
|
+
else if (op.op === "replace") parent[Number(last)] = op.value;
|
|
136
|
+
else parent.splice(Number(last), 1);
|
|
137
|
+
else if (isRecord(parent)) if (op.op === "remove") delete parent[last];
|
|
138
|
+
else parent[last] = op.value;
|
|
139
|
+
else throw new Error(`applyPatch: path ${op.path} traverses a non-container`);
|
|
140
|
+
}
|
|
141
|
+
return root;
|
|
142
|
+
}
|
|
143
|
+
async function withTimeout(promise, ms, what) {
|
|
144
|
+
let timer;
|
|
145
|
+
try {
|
|
146
|
+
return await Promise.race([promise, new Promise((_, reject) => {
|
|
147
|
+
timer = setTimeout(() => reject(codedError("TIMEOUT", `${typeof what === "function" ? what() : what}: no answer in ${ms / 1e3}s`)), ms);
|
|
148
|
+
})]);
|
|
149
|
+
} finally {
|
|
150
|
+
if (timer) clearTimeout(timer);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
/** Whether `request` may spend the cookies it carries: its `Origin` header is this origin, or absent
|
|
154
|
+
* (a non-browser client — curl, a script). A browser stamps the page's origin on every WebSocket
|
|
155
|
+
* handshake, every cross-site fetch and every form POST, so a foreign origin means a foreign site
|
|
156
|
+
* drove the request with the visitor's cookie riding along. A malformed `Origin` (the literal
|
|
157
|
+
* `null` of a sandboxed document included) is foreign. */
|
|
158
|
+
function isSameOriginBrowserRequest(request) {
|
|
159
|
+
const origin = request.headers.get("origin");
|
|
160
|
+
if (origin === null) return true;
|
|
161
|
+
try {
|
|
162
|
+
return new URL(origin).origin === new URL(request.url).origin;
|
|
163
|
+
} catch {
|
|
164
|
+
return false;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
/** `next` as a path on `origin`, else "/" — a redirect never leaves the host: `//evil.example`,
|
|
168
|
+
* `/\evil.example` and an absolute URL all resolve to a foreign origin and fall back to "/". The
|
|
169
|
+
* issuer's login redirect uses it too (os-next issuer-pages.ts). */
|
|
170
|
+
function sameOriginPath(next, origin) {
|
|
171
|
+
try {
|
|
172
|
+
const url = new URL(next, origin);
|
|
173
|
+
return url.origin === origin ? url.pathname + url.search : "/";
|
|
174
|
+
} catch {
|
|
175
|
+
return "/";
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
/** Only plain HTTP loopback origins use development login and client registration. */
|
|
179
|
+
function isLocalOrigin(origin) {
|
|
180
|
+
const url = new URL(origin);
|
|
181
|
+
return url.protocol === "http:" && (url.hostname === "localhost" || url.hostname.endsWith(".localhost") || url.hostname === "127.0.0.1");
|
|
182
|
+
}
|
|
183
|
+
/** Resolve a `cd` target against a context's own path — the one resolver every `cd` (the edge
|
|
184
|
+
* method, the built-in root, the library's relative handles) shares. Absolute ("/agents/x") stands alone; relative
|
|
185
|
+
* ("agents/x", "../inbox", ".") joins onto `base`. `.` and `..` resolve; the root cannot be
|
|
186
|
+
* escaped ("/.." is "/"). The result is canonical: leading slash, no trailing slash but for "/". */
|
|
187
|
+
function resolveContextPath(basePath, contextPath) {
|
|
188
|
+
const segments = [];
|
|
189
|
+
for (const seg of `${contextPath.startsWith("/") ? "" : basePath}/${contextPath}`.split("/")) {
|
|
190
|
+
if (seg === "" || seg === ".") continue;
|
|
191
|
+
if (seg === "..") segments.pop();
|
|
192
|
+
else segments.push(seg);
|
|
193
|
+
}
|
|
194
|
+
return `/${segments.join("/")}`;
|
|
195
|
+
}
|
|
196
|
+
//#endregion
|
|
197
|
+
export { applyPatch, codedError, diff, errorCode, isLocalOrigin, isSameOriginBrowserRequest, jsonEqual, reportIssue, resolveContextPath, sameOriginPath, withTimeout };
|
|
198
|
+
|
|
199
|
+
//# sourceMappingURL=lib.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lib.mjs","names":[],"sources":["../../src/next/lib.ts"],"sourcesContent":["// lib.ts — the pure helpers every layer shares; four concepts, one platform-neutral file (it rides\n// the SDK bundle and the node unit lane, so no cloudflare:workers here):\n// errors — `codedError` / `errorCode` / `reportIssue`: THE machine-readable error channel\n// patch — `diff` / `applyPatch` / `jsonEqual`: the live-state delta (an RFC 6902 subset)\n// timeout — `withTimeout`: a promise raced against a deadline (code TIMEOUT)\n// origin — `isSameOriginBrowserRequest`: may a request spend the cookies it carries\n\n// ── errors ── THE machine-readable error channel, after cloudflare-os\n// (workshop-shared/src/api.ts: plain Error + a `code` own-property via Object.assign, read with\n// `\"code\" in error`). Why this shape and no other: capnweb coerces custom error NAMES to a\n// builtin whitelist and drops subclass identity, but preserves ALL own enumerable properties\n// across the wire (verified at runtime against @iterate-com/capnweb 0.10.0) — and with\n// enhanced_error_serialization (our compat date) own props survive native Workers-RPC hops too.\n// So: never classify by `name`, `instanceof`, or message regex across a hop; check the code.\n// Human messages stay verbatim and greppable — the code rides beside them, never instead.\n// workerd's own stamped flags (`.retryable`, `.overloaded`, `.durableObjectReset`) ride the same\n// own-property channel; honor them rather than inventing a retry taxonomy.\n\n/** The stable machine-readable codes — SCREAMING_SNAKE, defined once, both ends import this. */\ntype ErrorCode =\n | \"SCHEDULE_LIMIT\" // bounded pending/failed scheduled batches per context\n | \"INVALID_INPUT\"\n | \"IDENTITY_CONFLICT\" // verified login cannot replace another linked identity\n | \"GRANT_NOT_FOUND\" // a caller may revoke only a grant in their own inventory\n | \"NO_ITX_EXPRESSION_MATCH\" // no rewrite rule matches the call (default-deny)\n | \"IDEMPOTENCY_CONFLICT\"\n | \"OFFSET_CONFLICT\" // an input's expected `offset` is not the offset it would land at\n | \"EVENT_TOO_LARGE\" // one event's serialized body is over the append ceiling (stream.ts EVENT_BODY_MAX_CHARS)\n | \"REDUCE_CHECKPOINT_TOO_LARGE\" // a reduce's state would not fit one storage cell (stream/processor.ts)\n | \"EVENT_UNREADABLE\" // a stored row's body is not JSON — `data.offset` names it (stream.ts read)\n | \"STREAM_PAUSED\"\n | \"INVALID_CONTEXT\" // a context name / project id the codec refuses (iterate-context.ts `DurableObjectNameCodec`) — coded, so it survives the hop\n | \"EXPRESSION_TOO_LONG\" // a STRING itx expression over ITX_EXPRESSION_STRING_MAX_CHARS — pass the parsed form\n | \"FACET_SOURCE_TOO_LARGE\" // a facet's literal source over FACET_SOURCE_MAX_CHARS (worker-loader.ts) — refused at the door\n | \"INVALID_CREDENTIALS\" // authenticate(): the admin secret did not match, or the credentials named no known kind\n | \"UNAUTHENTICATED\" // authenticate({ type: \"from-server-cookie\" }): no session cookie on the request, or a cross-origin browser's request\n | \"FORBIDDEN\" // projects.get(project): outside the session's reach (a grant narrowed to other projects, or the user is no member of its org); create on a narrowed grant; grants/consent on a session that carries none; organizations.get outside the session's memberships; `cd` in the global namespace (a global context is reached by identity, never by path)\n | \"PROJECT_NAME_TAKEN\" // projects.create({ project }): a project of that slug exists in another org\n | \"RPC_STUB_OFFLINE\" // the rpc stub a row names is neither borrowed nor pager-backed right now — or its lend ended mid-call (recalled, returned, broken; the relay re-codes)\n | \"NOT_A_METHOD\" // the dotted path's terminal segment is not callable on the target\n | \"NO_FACET\" // no facet of that name has been loaded into this context\n | \"FACET_NO_UPGRADE\" // a WebSocket upgrade aimed at a facet: a facet answers RPC and plain HTTP, never a socket — sockets terminate at the edge (apps/os-next context/facet-host.ts)\n | \"WAIT_TIMEOUT\" // waitForEvent expired with no matching event committed\n | \"TIMEOUT\"; // lib.ts withTimeout: the call did not answer within its deadline\n// (There is no separate boundary-validation library: the append door's own runtime guards\n// throw plain Errors; a client is JUST capnweb, so malformed args surface as ordinary errors.)\n\n/** A plain Error carrying `code` (+ optional `data`) as own enumerable properties. */\nexport function codedError(code: ErrorCode, message: string, data?: unknown): Error {\n return Object.assign(new Error(message), data === undefined ? { code } : { code, data });\n}\n\n/** The code of an error that crossed any number of hops — undefined for uncoded errors. */\nexport function errorCode(error: unknown): ErrorCode | undefined {\n return typeof error === \"object\" && error && \"code\" in error\n ? ((error as { code: unknown }).code as ErrorCode)\n : undefined;\n}\n\n// reportIssue — the ONE exit for unexpected failures (cloudflare-os error-reporting.ts, minus\n// its private Reporter Worker): one bounded console.error line; query event=\"issue\" in Workers\n// Logs. The Reporter seam stayed out on purpose — when one exists, check an optional env\n// binding HERE and waitUntil the dispatch; capture sites never change. Deliberately NO\n// cloudflare:workers import: this file rides the platform-neutral SDK bundle. Reporting must\n// never disturb the caller — armored end to end; worst case it prints nothing.\n\n// Bounds verbatim from cloudflare-os: hostile strings get clipped, never explode a log line.\nconst MAX = { message: 1024, stack: 16_384, string: 256, attributeKeys: 32 } as const;\ntype Scalar = string | number | boolean | null; // attribute values stay queryable scalars\n\n/** Print ONE bounded console.error line for an unexpected failure; never throws. */\nexport function reportIssue(\n failureSite: string,\n caught: unknown,\n attributes?: Record<string, Scalar | undefined>,\n): void {\n try {\n const bounded: Record<string, Scalar> = {};\n for (const [key, value] of Object.entries(attributes || {}).slice(0, MAX.attributeKeys)) {\n if (value === undefined) continue;\n bounded[key.slice(0, MAX.string)] =\n typeof value === \"string\" ? value.slice(0, MAX.string) : value;\n }\n const code = errorCode(caught);\n // The thrown value, bounded — name/message/stack read off an Error; an arbitrary object is\n // never walked.\n const error =\n caught instanceof Error\n ? {\n type: (caught.name || \"Error\").slice(0, MAX.string),\n message: caught.message.slice(0, MAX.message),\n ...(caught.stack && { stack: caught.stack.slice(0, MAX.stack) }),\n }\n : typeof caught === \"object\" && caught\n ? { type: \"ObjectThrown\" }\n : { type: `${typeof caught}Thrown`, message: String(caught).slice(0, MAX.message) };\n console.error({\n ...bounded, // fixed keys spread last so an attribute can never shadow them\n event: \"issue\",\n failureSite: failureSite.slice(0, MAX.string),\n code,\n error,\n });\n } catch {\n // Reporting must never disturb the caller — swallow and move on.\n }\n}\n\n// ── patch ── the LiveView-style delta that rides every live-state change event. `diff` runs at\n// the PRODUCER (the processor's reduce, the mini-app helper's set) where old and new value are\n// both in hand; the stream forwards the ops verbatim; only CLIENTS apply them. The format is an\n// RFC 6902 subset (add / replace / remove, JSON-Pointer paths) so any off-the-shelf json-patch\n// library applies our patch payloads — we invent no wire format, we just never emit the exotic ops.\n\nexport type PatchOp =\n | { op: \"add\"; path: string; value: unknown }\n | { op: \"replace\"; path: string; value: unknown }\n | { op: \"remove\"; path: string };\n\nconst isRecord = (v: unknown): v is Record<string, unknown> =>\n typeof v === \"object\" && !!v && !Array.isArray(v);\n\nconst escape = (seg: string | number): string =>\n String(seg).replaceAll(\"~\", \"~0\").replaceAll(\"/\", \"~1\");\n\n/** Structural deep-equal over plain JSON values — order-insensitive, unbudgeted (JSON is acyclic).\n * THE one deep-equal: the live-state diff's \"don't emit\" test AND the idempotency-body compare\n * (re-exported through stream/processor.ts). The `Object.hasOwn(b, k)` guard is load-bearing — without\n * it, two objects with the same key COUNT but different key SETS compare equal. */\nexport function jsonEqual(a: unknown, b: unknown): boolean {\n if (Object.is(a, b)) return true;\n if (Array.isArray(a) && Array.isArray(b))\n return a.length === b.length && a.every((v, i) => jsonEqual(v, b[i]));\n if (isRecord(a) && isRecord(b)) {\n const ka = Object.keys(a);\n return (\n ka.length === Object.keys(b).length &&\n ka.every((k) => Object.hasOwn(b, k) && jsonEqual(a[k], b[k]))\n );\n }\n return false;\n}\n\n/** Structural diff `a → b`, or undefined when deep-equal (the producer's \"don't emit\" signal).\n * Objects recurse per key; arrays get the chat-log fast paths (pure append → `add …/-` ops,\n * pure tail-truncate → `remove` ops) and are replaced wholesale on any middle divergence —\n * the LiveView trade: optimize the growing log, full-render the rewrite.\n * Both sides are JSON-normalized first — the wire is JSON, so the diff must see exactly what\n * the wire will carry: undefined-valued keys vanish, Dates become their ISO strings, array\n * holes become null. Diffing what you didn't normalize is how a Date change goes silent. */\nexport function diff(a: unknown, b: unknown): PatchOp[] | undefined {\n // stringify can itself return undefined (a bare function, toJSON() → undefined) — parse\n // would then throw SyntaxError('\"undefined\" is not valid JSON'); treat those as absent.\n const json = (v: unknown) => {\n const s = JSON.stringify(v);\n return s ? (JSON.parse(s) as unknown) : undefined;\n };\n return walk(json(a), json(b), \"\");\n}\n\nfunction walk(a: unknown, b: unknown, path: string): PatchOp[] | undefined {\n if (jsonEqual(a, b)) return undefined;\n if (Array.isArray(a) && Array.isArray(b)) {\n const shared = Math.min(a.length, b.length);\n let p = 0;\n while (p < shared && jsonEqual(a[p], b[p])) p++;\n if (p === a.length) return b.slice(p).map((value) => ({ op: \"add\", path: `${path}/-`, value }));\n if (p === b.length)\n return a\n .slice(p)\n .map((_, i) => ({ op: \"remove\", path: `${path}/${a.length - 1 - i}` }) as const);\n return [{ op: \"replace\", path, value: b }];\n }\n if (isRecord(a) && isRecord(b)) {\n const ops: PatchOp[] = [];\n for (const k of Object.keys(a))\n if (!Object.hasOwn(b, k)) ops.push({ op: \"remove\", path: `${path}/${escape(k)}` });\n for (const k of Object.keys(b)) {\n if (!Object.hasOwn(a, k)) ops.push({ op: \"add\", path: `${path}/${escape(k)}`, value: b[k] });\n else ops.push(...(walk(a[k], b[k], `${path}/${escape(k)}`) ?? []));\n }\n return ops.length ? ops : undefined;\n }\n return [{ op: \"replace\", path, value: b }];\n}\n\n/** Apply a patch non-mutatingly (clone-then-mutate). The client half of `diff` — exported\n * through the SDK so subscribers need no third-party json-patch dependency. */\nexport function applyPatch<T>(doc: T, ops: PatchOp[]): T {\n let root: unknown = structuredClone(doc);\n for (const op of ops) {\n if (op.path === \"\") {\n if (op.op === \"remove\") throw new Error(\"applyPatch: cannot remove the document root\");\n root = op.value;\n continue;\n }\n const segs = op.path\n .slice(1)\n .split(\"/\")\n .map((s) => s.replaceAll(\"~1\", \"/\").replaceAll(\"~0\", \"~\"));\n // Patches arrive over the wire. \"__proto__\" is refused outright (assigning it invokes the\n // prototype setter, not a property write); other prototype members (\"constructor\", …) are\n // ordinary keys — traversal below is own-property-only, so the chain is never walked.\n for (const s of segs)\n if (s === \"__proto__\") throw new Error(`applyPatch: refusing __proto__ in path ${op.path}`);\n const last = segs.pop()!;\n let parent: unknown = root;\n for (const s of segs) {\n parent = Array.isArray(parent)\n ? parent[Number(s)]\n : isRecord(parent) && Object.hasOwn(parent, s)\n ? parent[s]\n : undefined;\n if (parent === undefined) throw new Error(`applyPatch: missing path ${op.path}`);\n }\n if (Array.isArray(parent)) {\n if (op.op === \"add\") {\n if (last === \"-\") parent.push(op.value);\n else parent.splice(Number(last), 0, op.value);\n } else if (op.op === \"replace\") parent[Number(last)] = op.value;\n else parent.splice(Number(last), 1);\n } else if (isRecord(parent)) {\n if (op.op === \"remove\") delete parent[last];\n else parent[last] = op.value;\n } else throw new Error(`applyPatch: path ${op.path} traverses a non-container`);\n }\n return root as T;\n}\n\n// ── timeout ── race a promise against a deadline. The timer is CLEARED on every exit: a leaked\n// timer per call would pin a Durable Object awake. A loss rejects with code TIMEOUT so a caller can\n// tell \"took too long\" from the call's own failure (the DO's facet watchdog aborts the facet on it).\n//\n// `what` may be a THUNK: a label whose construction is expensive (the facet watchdog prints the whole\n// pushed batch) is built ONLY when the deadline is actually lost, off the per-call hot path.\n\nexport async function withTimeout<T>(\n promise: Promise<T>,\n ms: number,\n what: string | (() => string),\n): Promise<T> {\n let timer: ReturnType<typeof setTimeout> | undefined;\n try {\n return await Promise.race([\n promise,\n new Promise<never>((_, reject) => {\n timer = setTimeout(\n () =>\n reject(\n codedError(\n \"TIMEOUT\",\n `${typeof what === \"function\" ? what() : what}: no answer in ${ms / 1000}s`,\n ),\n ),\n ms,\n );\n }),\n ]);\n } finally {\n if (timer) clearTimeout(timer);\n }\n}\n\n// ── origin ── the one check that makes an ambient cookie safe to honour (session.ts\n// `from-server-cookie`, the issuer's form posts in os-next issuer-pages.ts).\n\n/** Whether `request` may spend the cookies it carries: its `Origin` header is this origin, or absent\n * (a non-browser client — curl, a script). A browser stamps the page's origin on every WebSocket\n * handshake, every cross-site fetch and every form POST, so a foreign origin means a foreign site\n * drove the request with the visitor's cookie riding along. A malformed `Origin` (the literal\n * `null` of a sandboxed document included) is foreign. */\nexport function isSameOriginBrowserRequest(request: Pick<Request, \"url\" | \"headers\">): boolean {\n const origin = request.headers.get(\"origin\");\n // oxlint-disable-next-line iterate/simple-truthiness-check -- security: only a truly absent Origin (non-browser client) is trusted; an empty \"\" Origin header must stay foreign, which a truthiness check would wrongly accept\n if (origin === null) return true;\n try {\n return new URL(origin).origin === new URL(request.url).origin;\n } catch {\n return false;\n }\n}\n\n/** `next` as a path on `origin`, else \"/\" — a redirect never leaves the host: `//evil.example`,\n * `/\\evil.example` and an absolute URL all resolve to a foreign origin and fall back to \"/\". The\n * issuer's login redirect uses it too (os-next issuer-pages.ts). */\nexport function sameOriginPath(next: string, origin: string): string {\n try {\n const url = new URL(next, origin);\n return url.origin === origin ? url.pathname + url.search : \"/\";\n } catch {\n return \"/\";\n }\n}\n\n/** Only plain HTTP loopback origins use development login and client registration. */\nexport function isLocalOrigin(origin: string) {\n const url = new URL(origin);\n return (\n url.protocol === \"http:\" &&\n (url.hostname === \"localhost\" ||\n url.hostname.endsWith(\".localhost\") ||\n url.hostname === \"127.0.0.1\")\n );\n}\n\n/** Resolve a `cd` target against a context's own path — the one resolver every `cd` (the edge\n * method, the built-in root, the library's relative handles) shares. Absolute (\"/agents/x\") stands alone; relative\n * (\"agents/x\", \"../inbox\", \".\") joins onto `base`. `.` and `..` resolve; the root cannot be\n * escaped (\"/..\" is \"/\"). The result is canonical: leading slash, no trailing slash but for \"/\". */\nexport function resolveContextPath(basePath: string, contextPath: string): string {\n const segments: string[] = [];\n for (const seg of `${contextPath.startsWith(\"/\") ? \"\" : basePath}/${contextPath}`.split(\"/\")) {\n if (seg === \"\" || seg === \".\") continue;\n if (seg === \"..\") segments.pop();\n else segments.push(seg);\n }\n return `/${segments.join(\"/\")}`;\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/** The platform's OAuth scopes — what an app may ask for and what a consent grants:
|
|
3
|
+
* - `iterate` — reach the projects the person grants (every app; implied, always granted)
|
|
4
|
+
* - `account` — manage the person's sessions and personal access tokens
|
|
5
|
+
* - `organizations:write` — the person's organizations: list every one they belong to, create new ones
|
|
6
|
+
* Consent is task-based (the shape Cloudflare's own OAuth consent took in August 2026: a client
|
|
7
|
+
* requests a set, the person may deselect the optional ones, the token carries what was granted):
|
|
8
|
+
* `iterate` is required, every other requested scope is optional on the consent page, and an app
|
|
9
|
+
* reads the granted set from `session.info().scopes` rather than assuming its request. */
|
|
10
|
+
export declare const OAuthScope: z.ZodEnum<{
|
|
11
|
+
iterate: "iterate";
|
|
12
|
+
account: "account";
|
|
13
|
+
"organizations:write": "organizations:write";
|
|
14
|
+
}>;
|
|
15
|
+
export type OAuthScope = z.infer<typeof OAuthScope>;
|
|
16
|
+
export declare const OAuthScopes: z.ZodPipe<z.ZodArray<z.ZodEnum<{
|
|
17
|
+
iterate: "iterate";
|
|
18
|
+
account: "account";
|
|
19
|
+
"organizations:write": "organizations:write";
|
|
20
|
+
}>>, z.ZodTransform<string[], ("iterate" | "account" | "organizations:write")[]>>;
|
|
21
|
+
/** A requested scope as the consent page shows it: its name, what it means to the person, and
|
|
22
|
+
* whether they may untick it (`iterate` never). */
|
|
23
|
+
export interface ConsentScope {
|
|
24
|
+
name: OAuthScope;
|
|
25
|
+
title: string;
|
|
26
|
+
note: string;
|
|
27
|
+
required: boolean;
|
|
28
|
+
}
|
|
29
|
+
/** What each scope means to the person — the consent page's copy, sent with `consent.describe` so
|
|
30
|
+
* the page renders what it is given: a scope added to `OAuthScope` is described here or it does
|
|
31
|
+
* not compile. */
|
|
32
|
+
export declare const OAuthScopeDescriptions: Record<OAuthScope, Omit<ConsentScope, "name">>;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
//#region src/next/oauth-scopes.ts
|
|
3
|
+
/** The platform's OAuth scopes — what an app may ask for and what a consent grants:
|
|
4
|
+
* - `iterate` — reach the projects the person grants (every app; implied, always granted)
|
|
5
|
+
* - `account` — manage the person's sessions and personal access tokens
|
|
6
|
+
* - `organizations:write` — the person's organizations: list every one they belong to, create new ones
|
|
7
|
+
* Consent is task-based (the shape Cloudflare's own OAuth consent took in August 2026: a client
|
|
8
|
+
* requests a set, the person may deselect the optional ones, the token carries what was granted):
|
|
9
|
+
* `iterate` is required, every other requested scope is optional on the consent page, and an app
|
|
10
|
+
* reads the granted set from `session.info().scopes` rather than assuming its request. */
|
|
11
|
+
const OAuthScope = z.enum([
|
|
12
|
+
"iterate",
|
|
13
|
+
"account",
|
|
14
|
+
"organizations:write"
|
|
15
|
+
]);
|
|
16
|
+
const OAuthScopes = z.array(OAuthScope).transform((scopes) => [...new Set(["iterate", ...scopes])]);
|
|
17
|
+
/** What each scope means to the person — the consent page's copy, sent with `consent.describe` so
|
|
18
|
+
* the page renders what it is given: a scope added to `OAuthScope` is described here or it does
|
|
19
|
+
* not compile. */
|
|
20
|
+
const OAuthScopeDescriptions = {
|
|
21
|
+
iterate: {
|
|
22
|
+
title: "Read and make changes in the projects you grant it",
|
|
23
|
+
note: "Required — what the app is for.",
|
|
24
|
+
required: true
|
|
25
|
+
},
|
|
26
|
+
account: {
|
|
27
|
+
title: "See and end your sessions, and mint personal access tokens",
|
|
28
|
+
note: "Optional.",
|
|
29
|
+
required: false
|
|
30
|
+
},
|
|
31
|
+
"organizations:write": {
|
|
32
|
+
title: "See all your organizations and create new ones",
|
|
33
|
+
note: "Optional.",
|
|
34
|
+
required: false
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
//#endregion
|
|
38
|
+
export { OAuthScope, OAuthScopeDescriptions, OAuthScopes };
|
|
39
|
+
|
|
40
|
+
//# sourceMappingURL=oauth-scopes.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"oauth-scopes.mjs","names":[],"sources":["../../src/next/oauth-scopes.ts"],"sourcesContent":["import { z } from \"zod\";\n\n/** The platform's OAuth scopes — what an app may ask for and what a consent grants:\n * - `iterate` — reach the projects the person grants (every app; implied, always granted)\n * - `account` — manage the person's sessions and personal access tokens\n * - `organizations:write` — the person's organizations: list every one they belong to, create new ones\n * Consent is task-based (the shape Cloudflare's own OAuth consent took in August 2026: a client\n * requests a set, the person may deselect the optional ones, the token carries what was granted):\n * `iterate` is required, every other requested scope is optional on the consent page, and an app\n * reads the granted set from `session.info().scopes` rather than assuming its request. */\nexport const OAuthScope = z.enum([\"iterate\", \"account\", \"organizations:write\"]);\nexport type OAuthScope = z.infer<typeof OAuthScope>;\n\nexport const OAuthScopes = z\n .array(OAuthScope)\n .transform((scopes) => [...new Set([\"iterate\", ...scopes])]);\n\n/** A requested scope as the consent page shows it: its name, what it means to the person, and\n * whether they may untick it (`iterate` never). */\nexport interface ConsentScope {\n name: OAuthScope;\n title: string;\n note: string;\n required: boolean;\n}\n\n/** What each scope means to the person — the consent page's copy, sent with `consent.describe` so\n * the page renders what it is given: a scope added to `OAuthScope` is described here or it does\n * not compile. */\nexport const OAuthScopeDescriptions: Record<OAuthScope, Omit<ConsentScope, \"name\">> = {\n iterate: {\n title: \"Read and make changes in the projects you grant it\",\n note: \"Required — what the app is for.\",\n required: true,\n },\n account: {\n title: \"See and end your sessions, and mint personal access tokens\",\n note: \"Optional.\",\n required: false,\n },\n \"organizations:write\": {\n title: \"See all your organizations and create new ones\",\n note: \"Optional.\",\n required: false,\n },\n};\n"],"mappings":";;;;;;;;;;AAUA,MAAa,aAAa,EAAE,KAAK;CAAC;CAAW;CAAW;AAAqB,CAAC;AAG9E,MAAa,cAAc,EACxB,MAAM,UAAU,CAAC,CACjB,WAAW,WAAW,CAAC,GAAG,IAAI,IAAI,CAAC,WAAW,GAAG,MAAM,CAAC,CAAC,CAAC;;;;AAc7D,MAAa,yBAAyE;CACpF,SAAS;EACP,OAAO;EACP,MAAM;EACN,UAAU;CACZ;CACA,SAAS;EACP,OAAO;EACP,MAAM;EACN,UAAU;CACZ;CACA,uBAAuB;EACrB,OAAO;EACP,MAAM;EACN,UAAU;CACZ;AACF"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { OAuthScopes } from "./oauth-scopes.mjs";
|
|
2
|
+
import * as oauth from "oauth4webapi";
|
|
3
|
+
//#region src/next/client/oauth.ts
|
|
4
|
+
/** The same code/PKCE parameters for browser login and a console-minted token. */
|
|
5
|
+
async function authorizationCodeRequest(input) {
|
|
6
|
+
const verifier = oauth.generateRandomCodeVerifier();
|
|
7
|
+
const challenge = await oauth.calculatePKCECodeChallenge(verifier);
|
|
8
|
+
const state = oauth.generateRandomState();
|
|
9
|
+
const url = new URL("/oauth2/auth", input.issuer);
|
|
10
|
+
url.search = new URLSearchParams({
|
|
11
|
+
response_type: "code",
|
|
12
|
+
client_id: input.clientId,
|
|
13
|
+
redirect_uri: input.redirectUri,
|
|
14
|
+
scope: OAuthScopes.parse(input.scopes || []).join(" "),
|
|
15
|
+
state,
|
|
16
|
+
code_challenge: challenge,
|
|
17
|
+
code_challenge_method: "S256"
|
|
18
|
+
}).toString();
|
|
19
|
+
for (const resource of input.resources) url.searchParams.append("resource", resource);
|
|
20
|
+
return {
|
|
21
|
+
url,
|
|
22
|
+
state,
|
|
23
|
+
verifier
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
//#endregion
|
|
27
|
+
export { authorizationCodeRequest };
|
|
28
|
+
|
|
29
|
+
//# sourceMappingURL=oauth.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"oauth.mjs","names":[],"sources":["../../src/next/client/oauth.ts"],"sourcesContent":["import * as oauth from \"oauth4webapi\";\nimport { OAuthScopes } from \"../oauth-scopes.ts\";\n\n/** The same code/PKCE parameters for browser login and a console-minted token. */\nexport async function authorizationCodeRequest(input: {\n issuer: string;\n clientId: string;\n redirectUri: string;\n resources: string[];\n scopes?: string[];\n}) {\n const verifier = oauth.generateRandomCodeVerifier();\n const challenge = await oauth.calculatePKCECodeChallenge(verifier);\n const state = oauth.generateRandomState();\n const url = new URL(\"/oauth2/auth\", input.issuer);\n url.search = new URLSearchParams({\n response_type: \"code\",\n client_id: input.clientId,\n redirect_uri: input.redirectUri,\n scope: OAuthScopes.parse(input.scopes || []).join(\" \"),\n state,\n code_challenge: challenge,\n code_challenge_method: \"S256\",\n }).toString();\n for (const resource of input.resources) url.searchParams.append(\"resource\", resource);\n return { url, state, verifier };\n}\n"],"mappings":";;;;AAIA,eAAsB,yBAAyB,OAM5C;CACD,MAAM,WAAW,MAAM,2BAA2B;CAClD,MAAM,YAAY,MAAM,MAAM,2BAA2B,QAAQ;CACjE,MAAM,QAAQ,MAAM,oBAAoB;CACxC,MAAM,MAAM,IAAI,IAAI,gBAAgB,MAAM,MAAM;CAChD,IAAI,SAAS,IAAI,gBAAgB;EAC/B,eAAe;EACf,WAAW,MAAM;EACjB,cAAc,MAAM;EACpB,OAAO,YAAY,MAAM,MAAM,UAAU,CAAC,CAAC,CAAC,CAAC,KAAK,GAAG;EACrD;EACA,gBAAgB;EAChB,uBAAuB;CACzB,CAAC,CAAC,CAAC,SAAS;CACZ,KAAK,MAAM,YAAY,MAAM,WAAW,IAAI,aAAa,OAAO,YAAY,QAAQ;CACpF,OAAO;EAAE;EAAK;EAAO;CAAS;AAChC"}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/** Who is acting: a stable actor id (the control plane's user id) and, when known, an email. */
|
|
2
|
+
export type Principal = {
|
|
3
|
+
actor: string;
|
|
4
|
+
email?: string;
|
|
5
|
+
};
|
|
6
|
+
/** WHO is making a call: the acting principal (null = anonymous). The one thing carried through every
|
|
7
|
+
* dispatch and every sibling hop (`invoke(call, args, caller)`). Set ONLY by trusted code — the edge
|
|
8
|
+
* after admission, the kernel — never by a client: the DO's `invoke` is a Workers-RPC verb, never
|
|
9
|
+
* capnweb-exposed, so a client cannot supply its own `Caller`. Authority is inferred FROM the
|
|
10
|
+
* principal (no separate scopes/trust field). Global contexts remain non-navigable through
|
|
11
|
+
* `cd`, including calls without a principal. */
|
|
12
|
+
export type Caller = {
|
|
13
|
+
principal: Principal | null;
|
|
14
|
+
/** THE CONNECTION the principal acts through: the OAuth grant's id — one per connected
|
|
15
|
+
* client (a Claude Code install, a dash sign-in, a personal token), the same across every call it
|
|
16
|
+
* makes. Absent for the admin secret and the kernel. */
|
|
17
|
+
grant?: string;
|
|
18
|
+
/** The context the call ORIGINATED at — stamped by the first `cd` hop and forwarded by every later
|
|
19
|
+
* one, so a relative path resolved after a hop (`repos.get('./x')` answered at the root) still
|
|
20
|
+
* means the caller's `./x`. Absent until a hop. Its presence also means the complete input
|
|
21
|
+
* expression was already admitted: a receiving resolver must not recheck owner-written
|
|
22
|
+
* rewrites as loaded code's input. Fresh env.ITX calls never inherit this stamp. */
|
|
23
|
+
path?: string;
|
|
24
|
+
/** Set when the caller is LOADED CODE — a worker, a facet, a script — holding a context through
|
|
25
|
+
* `env.ITX`. Under it the resolver refuses the fixed point (`itx.builtins…`) and any `cd` above
|
|
26
|
+
* the caller's own context on the INPUT expression; rewrites the owner wrote are never subject. */
|
|
27
|
+
app?: true;
|
|
28
|
+
/** THE PLATFORM ORIGIN the caller reached the platform on (os-next platform-origin.ts) — what a
|
|
29
|
+
* public URL is composed from (`itx.url`, a signed file URL). Absent for a caller with none (a
|
|
30
|
+
* loaded worker's `env.ITX`, the kernel); the context then uses the last one it was reached on. */
|
|
31
|
+
platformOrigin?: string | null;
|
|
32
|
+
};
|
|
33
|
+
/** The header the edge sets on a Request it forwards on a principal's behalf — the ingress after
|
|
34
|
+
* the cookie check, a session's terminal `fetch` — and strips from every inbound Request. */
|
|
35
|
+
export declare const ITX_PRINCIPAL_HEADER = "x-itx-principal";
|
|
36
|
+
/** The grant's header beside it (the caller's `grant`), set and stripped exactly where the
|
|
37
|
+
* principal's is. */
|
|
38
|
+
export declare const ITX_GRANT_HEADER = "x-itx-grant";
|
|
39
|
+
/** The header a loaded worker's `env.ITX.fetch` sets on the Request it forwards, so the context's
|
|
40
|
+
* fetch runs the call as app code; stripped from every Request that arrives from outside. */
|
|
41
|
+
export declare const ITX_APP_HEADER = "x-itx-app";
|
|
42
|
+
/** Originating context of a native fetch forwarded by a trusted context. */
|
|
43
|
+
export declare const ITX_CALLER_PATH_HEADER = "x-itx-caller-path";
|
|
44
|
+
/** The event as the log stores it: `source.principal` and `source.grant` are the platform's — set
|
|
45
|
+
* from the admitted caller, client-supplied ones dropped (an anonymous session's event carries
|
|
46
|
+
* neither, the kernel's none). */
|
|
47
|
+
export declare function stampCaller<E extends {
|
|
48
|
+
source?: Record<string, unknown>;
|
|
49
|
+
}>(event: E, caller: Caller): E;
|
|
50
|
+
/** Sign JSON claims: the bounded Google login flow's cookie (identity.ts). */
|
|
51
|
+
export declare function signClaims(claims: unknown, secret: string): Promise<string>;
|
|
52
|
+
/** The claims of a token that is well-formed and signed with `secret` — else null (no reason: a
|
|
53
|
+
* caller answers every bad token the same way). A blank secret verifies nothing. The caller checks
|
|
54
|
+
* the claims' SHAPE and expiry. */
|
|
55
|
+
export declare function verifyClaims(token: string, secret: string): Promise<unknown>;
|
|
56
|
+
/** The principal the admin secret grants — `{ actor: "admin" }`, every project — when `candidate`
|
|
57
|
+
* IS `secret` (`APP_CONFIG_ADMIN_API_SECRET`: at `authenticate({ type: "admin-secret" })`, as a
|
|
58
|
+
* lane's bearer and on `/mcp`), else null. Both are SHA-256 hashed and the digests compared in
|
|
59
|
+
* constant time. A blank secret matches nothing. */
|
|
60
|
+
export declare function verifyAdminSecret(candidate: string, secret: string): Promise<{
|
|
61
|
+
actor: "admin";
|
|
62
|
+
} | null>;
|
|
63
|
+
/** The value of the cookie `name` in a `Cookie` header, or null. */
|
|
64
|
+
export declare function cookieValueOf(cookieHeader: string | null, name: string): string | null;
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
//#region src/next/principal.ts
|
|
2
|
+
/** The header the edge sets on a Request it forwards on a principal's behalf — the ingress after
|
|
3
|
+
* the cookie check, a session's terminal `fetch` — and strips from every inbound Request. */
|
|
4
|
+
const ITX_PRINCIPAL_HEADER = "x-itx-principal";
|
|
5
|
+
/** The grant's header beside it (the caller's `grant`), set and stripped exactly where the
|
|
6
|
+
* principal's is. */
|
|
7
|
+
const ITX_GRANT_HEADER = "x-itx-grant";
|
|
8
|
+
/** The header a loaded worker's `env.ITX.fetch` sets on the Request it forwards, so the context's
|
|
9
|
+
* fetch runs the call as app code; stripped from every Request that arrives from outside. */
|
|
10
|
+
const ITX_APP_HEADER = "x-itx-app";
|
|
11
|
+
/** Originating context of a native fetch forwarded by a trusted context. */
|
|
12
|
+
const ITX_CALLER_PATH_HEADER = "x-itx-caller-path";
|
|
13
|
+
/** The event as the log stores it: `source.principal` and `source.grant` are the platform's — set
|
|
14
|
+
* from the admitted caller, client-supplied ones dropped (an anonymous session's event carries
|
|
15
|
+
* neither, the kernel's none). */
|
|
16
|
+
function stampCaller(event, caller) {
|
|
17
|
+
const { principal: _clientPrincipal, grant: _clientGrant, ...source } = event.source || {};
|
|
18
|
+
if (caller.principal) {
|
|
19
|
+
const stamped = {
|
|
20
|
+
...source,
|
|
21
|
+
principal: caller.principal
|
|
22
|
+
};
|
|
23
|
+
if (caller.grant) stamped.grant = caller.grant;
|
|
24
|
+
return {
|
|
25
|
+
...event,
|
|
26
|
+
source: stamped
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
return Object.keys(source).length > 0 ? {
|
|
30
|
+
...event,
|
|
31
|
+
source
|
|
32
|
+
} : (({ source: _dropped, ...rest }) => rest)(event);
|
|
33
|
+
}
|
|
34
|
+
const encoder = new TextEncoder();
|
|
35
|
+
const decoder = new TextDecoder();
|
|
36
|
+
const base64url = (bytes) => btoa(String.fromCharCode(...bytes)).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/, "");
|
|
37
|
+
const bytesFromBase64url = (text) => Uint8Array.from(atob(text.replaceAll("-", "+").replaceAll("_", "/").padEnd(Math.ceil(text.length / 4) * 4, "=")), (c) => c.charCodeAt(0));
|
|
38
|
+
async function hmacKey(secret, usage) {
|
|
39
|
+
return crypto.subtle.importKey("raw", encoder.encode(secret), {
|
|
40
|
+
name: "HMAC",
|
|
41
|
+
hash: "SHA-256"
|
|
42
|
+
}, false, [usage]);
|
|
43
|
+
}
|
|
44
|
+
/** Sign JSON claims: the bounded Google login flow's cookie (identity.ts). */
|
|
45
|
+
async function signClaims(claims, secret) {
|
|
46
|
+
const payload = base64url(encoder.encode(JSON.stringify(claims)));
|
|
47
|
+
const signature = await crypto.subtle.sign("HMAC", await hmacKey(secret, "sign"), encoder.encode(payload));
|
|
48
|
+
return `${payload}.${base64url(new Uint8Array(signature))}`;
|
|
49
|
+
}
|
|
50
|
+
/** The claims of a token that is well-formed and signed with `secret` — else null (no reason: a
|
|
51
|
+
* caller answers every bad token the same way). A blank secret verifies nothing. The caller checks
|
|
52
|
+
* the claims' SHAPE and expiry. */
|
|
53
|
+
async function verifyClaims(token, secret) {
|
|
54
|
+
if (!secret) return null;
|
|
55
|
+
const dot = token.indexOf(".");
|
|
56
|
+
if (dot <= 0) return null;
|
|
57
|
+
const payload = token.slice(0, dot);
|
|
58
|
+
let signature;
|
|
59
|
+
let claims;
|
|
60
|
+
try {
|
|
61
|
+
signature = bytesFromBase64url(token.slice(dot + 1));
|
|
62
|
+
claims = JSON.parse(decoder.decode(bytesFromBase64url(payload)));
|
|
63
|
+
} catch {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
return await crypto.subtle.verify("HMAC", await hmacKey(secret, "verify"), signature, encoder.encode(payload)) ? claims : null;
|
|
67
|
+
}
|
|
68
|
+
const sha256 = async (text) => new Uint8Array(await crypto.subtle.digest("SHA-256", encoder.encode(text)));
|
|
69
|
+
/** Whether two digests are the same bytes — every byte compared, no early exit, so neither a
|
|
70
|
+
* matching prefix nor its length leaks by timing. */
|
|
71
|
+
const digestsEqual = (a, b) => {
|
|
72
|
+
if (a.length !== b.length) return false;
|
|
73
|
+
let difference = 0;
|
|
74
|
+
for (let i = 0; i < a.length; i++) difference |= a[i] ^ b[i];
|
|
75
|
+
return difference === 0;
|
|
76
|
+
};
|
|
77
|
+
/** The principal the admin secret grants — `{ actor: "admin" }`, every project — when `candidate`
|
|
78
|
+
* IS `secret` (`APP_CONFIG_ADMIN_API_SECRET`: at `authenticate({ type: "admin-secret" })`, as a
|
|
79
|
+
* lane's bearer and on `/mcp`), else null. Both are SHA-256 hashed and the digests compared in
|
|
80
|
+
* constant time. A blank secret matches nothing. */
|
|
81
|
+
async function verifyAdminSecret(candidate, secret) {
|
|
82
|
+
if (!secret) return null;
|
|
83
|
+
const [candidateDigest, secretDigest] = await Promise.all([sha256(candidate), sha256(secret)]);
|
|
84
|
+
return digestsEqual(candidateDigest, secretDigest) ? { actor: "admin" } : null;
|
|
85
|
+
}
|
|
86
|
+
/** The value of the cookie `name` in a `Cookie` header, or null. */
|
|
87
|
+
function cookieValueOf(cookieHeader, name) {
|
|
88
|
+
for (const part of (cookieHeader || "").split(";")) {
|
|
89
|
+
const separator = part.indexOf("=");
|
|
90
|
+
if (separator < 0) continue;
|
|
91
|
+
if (part.slice(0, separator).trim() === name) return part.slice(separator + 1).trim();
|
|
92
|
+
}
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
//#endregion
|
|
96
|
+
export { ITX_APP_HEADER, ITX_CALLER_PATH_HEADER, ITX_GRANT_HEADER, ITX_PRINCIPAL_HEADER, cookieValueOf, signClaims, stampCaller, verifyAdminSecret, verifyClaims };
|
|
97
|
+
|
|
98
|
+
//# sourceMappingURL=principal.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"principal.mjs","names":[],"sources":["../../src/next/principal.ts"],"sourcesContent":["// Verified attribution, the signed-claims codec (the login flow's cookie) and the admin secret's compare.\n\n/** Who is acting: a stable actor id (the control plane's user id) and, when known, an email. */\nexport type Principal = { actor: string; email?: string };\n\n/** WHO is making a call: the acting principal (null = anonymous). The one thing carried through every\n * dispatch and every sibling hop (`invoke(call, args, caller)`). Set ONLY by trusted code — the edge\n * after admission, the kernel — never by a client: the DO's `invoke` is a Workers-RPC verb, never\n * capnweb-exposed, so a client cannot supply its own `Caller`. Authority is inferred FROM the\n * principal (no separate scopes/trust field). Global contexts remain non-navigable through\n * `cd`, including calls without a principal. */\nexport type Caller = {\n principal: Principal | null;\n /** THE CONNECTION the principal acts through: the OAuth grant's id — one per connected\n * client (a Claude Code install, a dash sign-in, a personal token), the same across every call it\n * makes. Absent for the admin secret and the kernel. */\n grant?: string;\n /** The context the call ORIGINATED at — stamped by the first `cd` hop and forwarded by every later\n * one, so a relative path resolved after a hop (`repos.get('./x')` answered at the root) still\n * means the caller's `./x`. Absent until a hop. Its presence also means the complete input\n * expression was already admitted: a receiving resolver must not recheck owner-written\n * rewrites as loaded code's input. Fresh env.ITX calls never inherit this stamp. */\n path?: string;\n /** Set when the caller is LOADED CODE — a worker, a facet, a script — holding a context through\n * `env.ITX`. Under it the resolver refuses the fixed point (`itx.builtins…`) and any `cd` above\n * the caller's own context on the INPUT expression; rewrites the owner wrote are never subject. */\n app?: true;\n /** THE PLATFORM ORIGIN the caller reached the platform on (os-next platform-origin.ts) — what a\n * public URL is composed from (`itx.url`, a signed file URL). Absent for a caller with none (a\n * loaded worker's `env.ITX`, the kernel); the context then uses the last one it was reached on. */\n platformOrigin?: string | null;\n};\n/** The header the edge sets on a Request it forwards on a principal's behalf — the ingress after\n * the cookie check, a session's terminal `fetch` — and strips from every inbound Request. */\nexport const ITX_PRINCIPAL_HEADER = \"x-itx-principal\";\n/** The grant's header beside it (the caller's `grant`), set and stripped exactly where the\n * principal's is. */\nexport const ITX_GRANT_HEADER = \"x-itx-grant\";\n/** The header a loaded worker's `env.ITX.fetch` sets on the Request it forwards, so the context's\n * fetch runs the call as app code; stripped from every Request that arrives from outside. */\nexport const ITX_APP_HEADER = \"x-itx-app\";\n/** Originating context of a native fetch forwarded by a trusted context. */\nexport const ITX_CALLER_PATH_HEADER = \"x-itx-caller-path\";\n\n/** The event as the log stores it: `source.principal` and `source.grant` are the platform's — set\n * from the admitted caller, client-supplied ones dropped (an anonymous session's event carries\n * neither, the kernel's none). */\nexport function stampCaller<E extends { source?: Record<string, unknown> }>(\n event: E,\n caller: Caller,\n): E {\n const { principal: _clientPrincipal, grant: _clientGrant, ...source } = event.source || {};\n if (caller.principal) {\n const stamped: Record<string, unknown> = { ...source, principal: caller.principal };\n if (caller.grant) stamped.grant = caller.grant;\n return { ...event, source: stamped };\n }\n return Object.keys(source).length > 0\n ? { ...event, source }\n : (({ source: _dropped, ...rest }) => rest as E)(event);\n}\nconst encoder = new TextEncoder();\nconst decoder = new TextDecoder();\nconst base64url = (bytes: Uint8Array): string =>\n btoa(String.fromCharCode(...bytes))\n .replaceAll(\"+\", \"-\")\n .replaceAll(\"/\", \"_\")\n .replace(/=+$/, \"\");\nconst bytesFromBase64url = (text: string): Uint8Array<ArrayBuffer> =>\n Uint8Array.from(\n atob(\n text\n .replaceAll(\"-\", \"+\")\n .replaceAll(\"_\", \"/\")\n .padEnd(Math.ceil(text.length / 4) * 4, \"=\"),\n ),\n (c) => c.charCodeAt(0),\n );\n\nasync function hmacKey(secret: string, usage: \"sign\" | \"verify\"): Promise<CryptoKey> {\n return crypto.subtle.importKey(\n \"raw\",\n encoder.encode(secret),\n { name: \"HMAC\", hash: \"SHA-256\" },\n false,\n [usage],\n );\n}\n\n/** Sign JSON claims: the bounded Google login flow's cookie (identity.ts). */\nexport async function signClaims(claims: unknown, secret: string): Promise<string> {\n const payload = base64url(encoder.encode(JSON.stringify(claims)));\n const signature = await crypto.subtle.sign(\n \"HMAC\",\n await hmacKey(secret, \"sign\"),\n encoder.encode(payload),\n );\n return `${payload}.${base64url(new Uint8Array(signature))}`;\n}\n\n/** The claims of a token that is well-formed and signed with `secret` — else null (no reason: a\n * caller answers every bad token the same way). A blank secret verifies nothing. The caller checks\n * the claims' SHAPE and expiry. */\nexport async function verifyClaims(token: string, secret: string): Promise<unknown> {\n if (!secret) return null;\n const dot = token.indexOf(\".\");\n if (dot <= 0) return null;\n const payload = token.slice(0, dot);\n let signature: Uint8Array<ArrayBuffer>;\n let claims: unknown;\n try {\n signature = bytesFromBase64url(token.slice(dot + 1));\n claims = JSON.parse(decoder.decode(bytesFromBase64url(payload)));\n } catch {\n return null;\n }\n const valid = await crypto.subtle.verify(\n \"HMAC\",\n await hmacKey(secret, \"verify\"),\n signature,\n encoder.encode(payload),\n );\n return valid ? claims : null;\n}\n\nconst sha256 = async (text: string): Promise<Uint8Array> =>\n new Uint8Array(await crypto.subtle.digest(\"SHA-256\", encoder.encode(text)));\n\n/** Whether two digests are the same bytes — every byte compared, no early exit, so neither a\n * matching prefix nor its length leaks by timing. */\nconst digestsEqual = (a: Uint8Array, b: Uint8Array): boolean => {\n if (a.length !== b.length) return false;\n let difference = 0;\n for (let i = 0; i < a.length; i++) difference |= a[i]! ^ b[i]!;\n return difference === 0;\n};\n\n/** The principal the admin secret grants — `{ actor: \"admin\" }`, every project — when `candidate`\n * IS `secret` (`APP_CONFIG_ADMIN_API_SECRET`: at `authenticate({ type: \"admin-secret\" })`, as a\n * lane's bearer and on `/mcp`), else null. Both are SHA-256 hashed and the digests compared in\n * constant time. A blank secret matches nothing. */\nexport async function verifyAdminSecret(\n candidate: string,\n secret: string,\n): Promise<{ actor: \"admin\" } | null> {\n if (!secret) return null;\n const [candidateDigest, secretDigest] = await Promise.all([sha256(candidate), sha256(secret)]);\n return digestsEqual(candidateDigest, secretDigest) ? { actor: \"admin\" } : null;\n}\n\n/** The value of the cookie `name` in a `Cookie` header, or null. */\nexport function cookieValueOf(cookieHeader: string | null, name: string): string | null {\n for (const part of (cookieHeader || \"\").split(\";\")) {\n const separator = part.indexOf(\"=\");\n if (separator < 0) continue;\n if (part.slice(0, separator).trim() === name) return part.slice(separator + 1).trim();\n }\n return null;\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/** How projects are reached over HTTP; null ⇒ no ingress (`/api` and `/mcp` still answer). */
|
|
2
|
+
export type IngressRouting = {
|
|
3
|
+
type: "subdomains";
|
|
4
|
+
hostname: string;
|
|
5
|
+
} | {
|
|
6
|
+
type: "paths";
|
|
7
|
+
} | null;
|
|
8
|
+
/** What a request names: the project (its slug, as written — whether it EXISTS is the directory's
|
|
9
|
+
* answer), the app (null ⇒ the apex: the project's config worker answers), and the path prefix the
|
|
10
|
+
* edge strips before the app sees the URL ("" under subdomains; "/projects/<project>" or
|
|
11
|
+
* "/projects/<project>/<app>" under paths). */
|
|
12
|
+
export type ProjectAddress = {
|
|
13
|
+
project: string;
|
|
14
|
+
app: string | null;
|
|
15
|
+
basePath: string;
|
|
16
|
+
};
|
|
17
|
+
/** The project + app `url` names under `routing`, or null when it names none. Pure. */
|
|
18
|
+
export declare function projectAddressOf(routing: IngressRouting, url: URL, platformOrigin: string): ProjectAddress | null;
|
|
19
|
+
/** A CUSTOM HOSTNAME — one of a deployment's own that IS a project's apex (os-next
|
|
20
|
+
* `urls.temporaryCustomHostnames`, `{ "iterate2.com": "iterate" }`): the apex shape, `app: null`, so the
|
|
21
|
+
* project's config worker `fetch` answers exactly as it does on `<project>.<hostname>`. Null for a
|
|
22
|
+
* hostname the map does not name — the map's spelling, case and a trailing dot forgiven, no wildcard
|
|
23
|
+
* under it. Pure. */
|
|
24
|
+
export declare function customProjectHostOf(hostname: string, hostnames: Record<string, string>): {
|
|
25
|
+
app: null;
|
|
26
|
+
project: string;
|
|
27
|
+
} | null;
|
|
28
|
+
/** The URL of `app` (null ⇒ the apex, the config worker) in `project` under `routing`, at `path`
|
|
29
|
+
* (default "/", must start with "/"). Null when there is no ingress, or when the result would not
|
|
30
|
+
* parse back to the same address (a bad label; a `path` that climbs out of its app). subdomains:
|
|
31
|
+
* the protocol and port are `platformOrigin`'s (local dev is `http://localhost:8788`, so
|
|
32
|
+
* `http://<app>--<project>.localhost:8788/…`); paths: `<platformOrigin>/projects/<project>[/<app>]<path>`. Pure. */
|
|
33
|
+
export declare function projectUrlOf(routing: IngressRouting, platformOrigin: string, target: {
|
|
34
|
+
project: string;
|
|
35
|
+
app?: string | null;
|
|
36
|
+
path?: string;
|
|
37
|
+
}): URL | null;
|