@rayfold/server 0.1.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/LICENSE +202 -0
- package/NOTICE +10 -0
- package/README.md +70 -0
- package/args.d.ts +15 -0
- package/args.js +226 -0
- package/args.js.map +1 -0
- package/batch.d.ts +54 -0
- package/batch.js +483 -0
- package/batch.js.map +1 -0
- package/bindings.d.ts +37 -0
- package/bindings.js +284 -0
- package/bindings.js.map +1 -0
- package/capability-scope.d.ts +8 -0
- package/capability-scope.js +19 -0
- package/capability-scope.js.map +1 -0
- package/capability.d.ts +56 -0
- package/capability.js +112 -0
- package/capability.js.map +1 -0
- package/context.d.ts +74 -0
- package/context.js +109 -0
- package/context.js.map +1 -0
- package/core.d.ts +18 -0
- package/core.js +18 -0
- package/core.js.map +1 -0
- package/cost.d.ts +15 -0
- package/cost.js +110 -0
- package/cost.js.map +1 -0
- package/executor.d.ts +89 -0
- package/executor.js +695 -0
- package/executor.js.map +1 -0
- package/guard.d.ts +33 -0
- package/guard.js +65 -0
- package/guard.js.map +1 -0
- package/http.d.ts +33 -0
- package/http.js +379 -0
- package/http.js.map +1 -0
- package/index.d.ts +8 -0
- package/index.js +9 -0
- package/index.js.map +1 -0
- package/instrumentation.d.ts +36 -0
- package/instrumentation.js +2 -0
- package/instrumentation.js.map +1 -0
- package/live.d.ts +37 -0
- package/live.js +240 -0
- package/live.js.map +1 -0
- package/mcp.d.ts +55 -0
- package/mcp.js +314 -0
- package/mcp.js.map +1 -0
- package/openapi.d.ts +11 -0
- package/openapi.js +124 -0
- package/openapi.js.map +1 -0
- package/package.json +53 -0
- package/policy.d.ts +17 -0
- package/policy.js +64 -0
- package/policy.js.map +1 -0
- package/protocol.d.ts +139 -0
- package/protocol.js +98 -0
- package/protocol.js.map +1 -0
- package/server.d.ts +58 -0
- package/server.js +79 -0
- package/server.js.map +1 -0
- package/usage.d.ts +39 -0
- package/usage.js +44 -0
- package/usage.js.map +1 -0
- package/views.d.ts +33 -0
- package/views.js +108 -0
- package/views.js.map +1 -0
- package/wiring.d.ts +16 -0
- package/wiring.js +57 -0
- package/wiring.js.map +1 -0
- package/ws.d.ts +21 -0
- package/ws.js +209 -0
- package/ws.js.map +1 -0
package/bindings.js
ADDED
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
import { annotation } from "@rayfold/schema";
|
|
2
|
+
import { HTTP_STATUS, RayfoldError } from "./protocol.js";
|
|
3
|
+
import { applyCacheHeaders } from "./http.js";
|
|
4
|
+
import { BodyTooLarge, PROBLEM_TYPE_BASE, hostProblem, mediaType, originProblem, refuse, refuseBody } from "./guard.js";
|
|
5
|
+
export const QUERY_METHODS = ["GET", "QUERY"];
|
|
6
|
+
export const COMMAND_METHODS = ["POST", "PUT", "PATCH", "DELETE"];
|
|
7
|
+
/** Methods whose HTTP semantics are idempotent: a command bound to them may run without an Idempotency-Key. */
|
|
8
|
+
const IDEMPOTENT_METHODS = new Set(["PUT", "PATCH", "DELETE"]);
|
|
9
|
+
export function bindingsOf(ir) {
|
|
10
|
+
const out = [];
|
|
11
|
+
for (const op of Object.values(ir.ops)) {
|
|
12
|
+
const a = annotation(op, "http");
|
|
13
|
+
if (!a)
|
|
14
|
+
continue;
|
|
15
|
+
const method = identOrString(a.args["method"])?.toUpperCase();
|
|
16
|
+
const path = typeof a.args["path"] === "string" ? a.args["path"] : undefined;
|
|
17
|
+
if (!method || !path)
|
|
18
|
+
continue;
|
|
19
|
+
const params = [];
|
|
20
|
+
const pattern = path.replace(/[.*+?^()|[\]\\]/g, "\\$&").replace(/\{([A-Za-z_][A-Za-z0-9_]*)\}/g, (_m, name) => {
|
|
21
|
+
params.push(name);
|
|
22
|
+
return "([^/]+)";
|
|
23
|
+
});
|
|
24
|
+
const b = { op, method, path, params, regex: new RegExp(`^${pattern}$`) };
|
|
25
|
+
const body = identOrString(a.args["body"]);
|
|
26
|
+
if (body)
|
|
27
|
+
b.body = body;
|
|
28
|
+
if (typeof a.args["location"] === "string")
|
|
29
|
+
b.location = a.args["location"];
|
|
30
|
+
out.push(b);
|
|
31
|
+
}
|
|
32
|
+
return out;
|
|
33
|
+
}
|
|
34
|
+
function identOrString(v) {
|
|
35
|
+
if (typeof v === "string")
|
|
36
|
+
return v;
|
|
37
|
+
if (v && typeof v === "object" && "$ident" in v)
|
|
38
|
+
return String(v.$ident);
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
/** Returns a handler that answers bound routes and returns false for anything else. */
|
|
42
|
+
export function createBindingHandler(server, opts = {}) {
|
|
43
|
+
const bindings = bindingsOf(server.ir);
|
|
44
|
+
const prefix = opts.prefix ?? "";
|
|
45
|
+
const maxBody = opts.maxBody ?? 1_048_576;
|
|
46
|
+
return async (req, res) => {
|
|
47
|
+
const url = new URL(req.url ?? "/", "http://localhost");
|
|
48
|
+
if (!url.pathname.startsWith(prefix))
|
|
49
|
+
return false;
|
|
50
|
+
const path = url.pathname.slice(prefix.length) || "/";
|
|
51
|
+
const matches = bindings.map((b) => ({ b, m: b.regex.exec(path) })).filter((x) => x.m);
|
|
52
|
+
if (!matches.length)
|
|
53
|
+
return false;
|
|
54
|
+
res.setHeader("X-Content-Type-Options", "nosniff");
|
|
55
|
+
const safeMethod = req.method === "GET" || req.method === "HEAD" || req.method === "QUERY"; // queries only: they cannot change data
|
|
56
|
+
const refused = hostProblem(req, opts) ?? (safeMethod ? null : originProblem(req, opts));
|
|
57
|
+
if (refused) {
|
|
58
|
+
refuse(res, 403, "permission_denied", refused);
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
const hit = matches.find((x) => x.b.method === req.method);
|
|
62
|
+
if (!hit) {
|
|
63
|
+
res.setHeader("Allow", matches.map((x) => x.b.method).join(", "));
|
|
64
|
+
problem(res, 405, { code: "unimplemented", message: `${req.method} is not bound on ${path}` });
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
const { b, m } = hit;
|
|
68
|
+
try {
|
|
69
|
+
const args = {};
|
|
70
|
+
b.params.forEach((name, i) => (args[name] = fromText(b.op, name, decodePathSegment(m[i + 1], name))));
|
|
71
|
+
const shapeParam = url.searchParams.get("shape");
|
|
72
|
+
if (b.method === "GET") {
|
|
73
|
+
for (const [k, v] of url.searchParams)
|
|
74
|
+
if (k !== "shape" && !(k in args))
|
|
75
|
+
args[k] = fromText(b.op, k, v);
|
|
76
|
+
}
|
|
77
|
+
if (b.body) {
|
|
78
|
+
const raw = await readBody(req, maxBody);
|
|
79
|
+
if (raw.length) {
|
|
80
|
+
const ct = mediaType(req);
|
|
81
|
+
const accepted = b.method === "PATCH" ? ["application/merge-patch+json", "application/json"] : ["application/json"];
|
|
82
|
+
if (!accepted.includes(ct)) {
|
|
83
|
+
refuse(res, 415, "invalid_argument", `Content-Type ${ct || "(none)"} is not accepted; send ${accepted[0]}`, "unsupported_media_type");
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
86
|
+
let parsed;
|
|
87
|
+
try {
|
|
88
|
+
parsed = JSON.parse(raw);
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
throw new RayfoldError("invalid_argument", "Body is not valid JSON");
|
|
92
|
+
}
|
|
93
|
+
if (b.body === "*") {
|
|
94
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
95
|
+
throw new RayfoldError("invalid_argument", "Body must be a JSON object");
|
|
96
|
+
Object.assign(args, parsed);
|
|
97
|
+
}
|
|
98
|
+
else
|
|
99
|
+
args[b.body] = parsed;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
const op = { id: 1, op: b.op.name, args };
|
|
103
|
+
if (shapeParam)
|
|
104
|
+
op.shape = shapeParam;
|
|
105
|
+
const key = header(req, "idempotency-key");
|
|
106
|
+
if (key)
|
|
107
|
+
op.key = key;
|
|
108
|
+
const ifMatch = header(req, "if-match");
|
|
109
|
+
if (ifMatch) {
|
|
110
|
+
const v = ifMatch.replace(/^W\//, "").replace(/^"|"$/g, "");
|
|
111
|
+
op.ifVersion = /^\d+$/.test(v) ? Number(v) : v;
|
|
112
|
+
}
|
|
113
|
+
if (b.op.kind === "command" && b.method === "POST" && !key) {
|
|
114
|
+
const idem = annotation(b.op, "idempotent");
|
|
115
|
+
if (!(idem && idem.args["value"] === false)) {
|
|
116
|
+
throw new RayfoldError("invalid_argument", `POST ${b.path} requires an Idempotency-Key header (16-128 characters)`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
const envelope = { ops: [op] };
|
|
120
|
+
const viewer = opts.viewer ? await opts.viewer(req) : null;
|
|
121
|
+
const frames = [];
|
|
122
|
+
for await (const f of server.execute(envelope, { viewer, keyOptional: IDEMPOTENT_METHODS.has(b.method) }))
|
|
123
|
+
frames.push(f);
|
|
124
|
+
const folded = fold(frames);
|
|
125
|
+
if ("error" in folded) {
|
|
126
|
+
const e = folded.error;
|
|
127
|
+
const status = e.type === "VersionConflict" ? 412 : HTTP_STATUS[e.code];
|
|
128
|
+
problem(res, status, e);
|
|
129
|
+
return true;
|
|
130
|
+
}
|
|
131
|
+
const result = folded.result;
|
|
132
|
+
if (b.op.kind === "query") {
|
|
133
|
+
applyCacheHeaders(server, envelope, frames, viewer, res);
|
|
134
|
+
if (header(req, "if-none-match") && header(req, "if-none-match") === res.getHeader("ETag")) {
|
|
135
|
+
res.removeHeader("X-Content-Type-Options"); // no body to sniff; the cached response keeps its headers
|
|
136
|
+
res.writeHead(304).end();
|
|
137
|
+
return true;
|
|
138
|
+
}
|
|
139
|
+
json(res, 200, result);
|
|
140
|
+
return true;
|
|
141
|
+
}
|
|
142
|
+
res.setHeader("Cache-Control", "no-store");
|
|
143
|
+
const version = versionOf(server.ir, b.op, result);
|
|
144
|
+
if (version !== undefined)
|
|
145
|
+
res.setHeader("ETag", `"${version}"`);
|
|
146
|
+
if (folded.replay)
|
|
147
|
+
res.setHeader("Idempotent-Replayed", "true");
|
|
148
|
+
if (b.method === "POST" && b.location && result && typeof result === "object") {
|
|
149
|
+
res.setHeader("Location", prefix + b.location.replace(/\{([A-Za-z_][A-Za-z0-9_]*)\}/g, (_m, f) => encodeURIComponent(String(result[f] ?? ""))));
|
|
150
|
+
json(res, 201, result);
|
|
151
|
+
return true;
|
|
152
|
+
}
|
|
153
|
+
json(res, 200, result);
|
|
154
|
+
return true;
|
|
155
|
+
}
|
|
156
|
+
catch (e) {
|
|
157
|
+
if (e instanceof BodyTooLarge) {
|
|
158
|
+
refuseBody(res, e);
|
|
159
|
+
return true;
|
|
160
|
+
}
|
|
161
|
+
const w = e instanceof RayfoldError ? e.toWire() : { code: "internal", message: "Internal error" };
|
|
162
|
+
problem(res, HTTP_STATUS[w.code], w);
|
|
163
|
+
return true;
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
function decodePathSegment(raw, name) {
|
|
168
|
+
try {
|
|
169
|
+
return decodeURIComponent(raw);
|
|
170
|
+
}
|
|
171
|
+
catch {
|
|
172
|
+
throw new RayfoldError("invalid_argument", `Path parameter ${name} is not valid percent-encoding`);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
/** Path and query-string values are text; coerce them by the argument's declared type. */
|
|
176
|
+
function fromText(op, name, text) {
|
|
177
|
+
const def = op.args.find((a) => a.name === name);
|
|
178
|
+
if (!def || def.type.kind !== "named")
|
|
179
|
+
return text;
|
|
180
|
+
switch (def.type.name) {
|
|
181
|
+
case "Int":
|
|
182
|
+
case "Float":
|
|
183
|
+
return /^-?\d+(\.\d+)?$/.test(text) ? Number(text) : text;
|
|
184
|
+
case "Boolean":
|
|
185
|
+
return text === "true" ? true : text === "false" ? false : text;
|
|
186
|
+
case "ID":
|
|
187
|
+
case "String":
|
|
188
|
+
case "Decimal":
|
|
189
|
+
case "Date":
|
|
190
|
+
case "Instant":
|
|
191
|
+
return text;
|
|
192
|
+
default:
|
|
193
|
+
try {
|
|
194
|
+
return JSON.parse(text);
|
|
195
|
+
}
|
|
196
|
+
catch {
|
|
197
|
+
return text;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
/** The @version field of the result entity, if the op returns one. */
|
|
202
|
+
function versionOf(ir, op, result) {
|
|
203
|
+
if (!result || typeof result !== "object" || op.returns.kind !== "named")
|
|
204
|
+
return undefined;
|
|
205
|
+
const def = ir.types[op.returns.name];
|
|
206
|
+
if (!def || def.kind !== "entity")
|
|
207
|
+
return undefined;
|
|
208
|
+
const vf = def.fields.find((f) => annotation(f, "version"));
|
|
209
|
+
return vf ? result[vf.name] : undefined;
|
|
210
|
+
}
|
|
211
|
+
function fold(frames) {
|
|
212
|
+
let result = null;
|
|
213
|
+
let replay = false;
|
|
214
|
+
for (const f of frames) {
|
|
215
|
+
if ("error" in f)
|
|
216
|
+
return { error: f.error };
|
|
217
|
+
if ("ok" in f) {
|
|
218
|
+
result = f.ok;
|
|
219
|
+
replay = !!f.meta?.replay;
|
|
220
|
+
}
|
|
221
|
+
else if ("data" in f && !("at" in f))
|
|
222
|
+
result = f.data;
|
|
223
|
+
else if ("at" in f && result && typeof result === "object") {
|
|
224
|
+
let target = result;
|
|
225
|
+
if (f.at !== "")
|
|
226
|
+
for (const p of f.at.split("."))
|
|
227
|
+
target = target && typeof target === "object" ? target[p] : undefined;
|
|
228
|
+
if (target && typeof target === "object")
|
|
229
|
+
Object.assign(target, f.data);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return { result, replay };
|
|
233
|
+
}
|
|
234
|
+
function header(req, name) {
|
|
235
|
+
const v = req.headers[name];
|
|
236
|
+
return typeof v === "string" ? v : undefined;
|
|
237
|
+
}
|
|
238
|
+
/** How much of an over-limit body is drained (so the refusal can be read) before the socket is dropped. */
|
|
239
|
+
const DRAIN_LIMIT = 1_048_576;
|
|
240
|
+
function readBody(req, max) {
|
|
241
|
+
return new Promise((resolve, reject) => {
|
|
242
|
+
const chunks = [];
|
|
243
|
+
let size = 0;
|
|
244
|
+
let over = false;
|
|
245
|
+
req.on("data", (c) => {
|
|
246
|
+
size += c.length;
|
|
247
|
+
if (over) {
|
|
248
|
+
// A client that keeps streaming long after the refusal is cut off rather than read forever.
|
|
249
|
+
if (size > max + DRAIN_LIMIT)
|
|
250
|
+
req.destroy();
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
if (size > max) {
|
|
254
|
+
// Stop buffering but keep draining: destroying the socket here would lose the refusal on its way out.
|
|
255
|
+
over = true;
|
|
256
|
+
chunks.length = 0;
|
|
257
|
+
reject(new BodyTooLarge(max));
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
chunks.push(c);
|
|
261
|
+
});
|
|
262
|
+
req.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
|
|
263
|
+
req.on("error", reject);
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
function json(res, status, body) {
|
|
267
|
+
res.writeHead(status, { "Content-Type": "application/json; charset=utf-8" }).end(JSON.stringify(body));
|
|
268
|
+
}
|
|
269
|
+
/** RFC 9457 problem; typed Rayfold errors keep their `type` and `data` so REST clients can branch on them. */
|
|
270
|
+
function problem(res, status, e) {
|
|
271
|
+
const body = {
|
|
272
|
+
type: PROBLEM_TYPE_BASE + (e.type ?? e.code),
|
|
273
|
+
title: e.type ?? e.code.replace(/_/g, " "),
|
|
274
|
+
status,
|
|
275
|
+
detail: e.message,
|
|
276
|
+
code: e.code,
|
|
277
|
+
};
|
|
278
|
+
if (e.path !== undefined)
|
|
279
|
+
body["path"] = e.path;
|
|
280
|
+
if (e.data !== undefined)
|
|
281
|
+
body["data"] = e.data;
|
|
282
|
+
res.writeHead(status, { "Content-Type": "application/problem+json", "Cache-Control": "no-store" }).end(JSON.stringify(body));
|
|
283
|
+
}
|
|
284
|
+
//# sourceMappingURL=bindings.js.map
|
package/bindings.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bindings.js","sourceRoot":"","sources":["../src/bindings.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * HTTP bindings (extension `http`, spec 04 section 8): expose queries and commands on REST-shaped routes with\n * their natural methods, backed by the same contract (validation, policies, typed errors, idempotency, patches).\n *\n * query book(id: ID): Book? @http(method: GET, path: \"/books/{id}\")\n * command editReview(id: ID, input: ...) @http(method: PUT, path: \"/reviews/{id}\", body: input)\n * command updateBook(id: ID, patch: ...) @http(method: PATCH, path: \"/books/{id}\", body: patch)\n * command deleteReview(id: ID) @http(method: DELETE, path: \"/reviews/{id}\")\n * command placeOrder(input: ...) @http(method: POST, path: \"/orders\", body: input, location: \"/orders/{id}\")\n * query books(filter: ..., page: ...) @http(method: QUERY, path: \"/books\", body: \"*\")\n */\nimport type { IncomingMessage, ServerResponse } from \"node:http\";\nimport { annotation, type OpDef, type RayfoldSchemaIR } from \"@rayfold/schema\";\nimport type { RayfoldServer } from \"./server.ts\";\nimport { HTTP_STATUS, RayfoldError, type Frame, type RequestEnvelope, type RequestOp, type WireError } from \"./protocol.ts\";\nimport { applyCacheHeaders } from \"./http.ts\";\nimport { BodyTooLarge, PROBLEM_TYPE_BASE, hostProblem, mediaType, originProblem, refuse, refuseBody, type OriginOptions } from \"./guard.ts\";\n\nexport const QUERY_METHODS = [\"GET\", \"QUERY\"] as const;\nexport const COMMAND_METHODS = [\"POST\", \"PUT\", \"PATCH\", \"DELETE\"] as const;\n/** Methods whose HTTP semantics are idempotent: a command bound to them may run without an Idempotency-Key. */\nconst IDEMPOTENT_METHODS = new Set([\"PUT\", \"PATCH\", \"DELETE\"]);\n\nexport interface Binding {\n op: OpDef;\n method: string;\n path: string;\n /** name of the argument that receives the JSON body, or \"*\" to spread the body into the arguments */\n body?: string;\n /** Location template for 201 responses, filled from the result, e.g. \"/orders/{id}\" */\n location?: string;\n params: string[];\n regex: RegExp;\n}\n\nexport function bindingsOf(ir: RayfoldSchemaIR): Binding[] {\n const out: Binding[] = [];\n for (const op of Object.values(ir.ops)) {\n const a = annotation(op, \"http\");\n if (!a) continue;\n const method = identOrString(a.args[\"method\"])?.toUpperCase();\n const path = typeof a.args[\"path\"] === \"string\" ? a.args[\"path\"] : undefined;\n if (!method || !path) continue;\n const params: string[] = [];\n const pattern = path.replace(/[.*+?^()|[\\]\\\\]/g, \"\\\\$&\").replace(/\\{([A-Za-z_][A-Za-z0-9_]*)\\}/g, (_m, name: string) => {\n params.push(name);\n return \"([^/]+)\";\n });\n const b: Binding = { op, method, path, params, regex: new RegExp(`^${pattern}$`) };\n const body = identOrString(a.args[\"body\"]);\n if (body) b.body = body;\n if (typeof a.args[\"location\"] === \"string\") b.location = a.args[\"location\"];\n out.push(b);\n }\n return out;\n}\n\nfunction identOrString(v: unknown): string | undefined {\n if (typeof v === \"string\") return v;\n if (v && typeof v === \"object\" && \"$ident\" in v) return String((v as { $ident: string }).$ident);\n return undefined;\n}\n\nexport interface BindingOptions extends OriginOptions {\n /** Mount prefix, default \"\" (routes are served exactly as declared). */\n prefix?: string;\n viewer?: (req: IncomingMessage) => unknown | Promise<unknown>;\n maxBody?: number;\n}\n\n/** Returns a handler that answers bound routes and returns false for anything else. */\nexport function createBindingHandler(server: RayfoldServer, opts: BindingOptions = {}): (req: IncomingMessage, res: ServerResponse) => Promise<boolean> {\n const bindings = bindingsOf(server.ir);\n const prefix = opts.prefix ?? \"\";\n const maxBody = opts.maxBody ?? 1_048_576;\n\n return async (req, res) => {\n const url = new URL(req.url ?? \"/\", \"http://localhost\");\n if (!url.pathname.startsWith(prefix)) return false;\n const path = url.pathname.slice(prefix.length) || \"/\";\n const matches = bindings.map((b) => ({ b, m: b.regex.exec(path) })).filter((x) => x.m);\n if (!matches.length) return false;\n res.setHeader(\"X-Content-Type-Options\", \"nosniff\");\n const safeMethod = req.method === \"GET\" || req.method === \"HEAD\" || req.method === \"QUERY\"; // queries only: they cannot change data\n const refused = hostProblem(req, opts) ?? (safeMethod ? null : originProblem(req, opts));\n if (refused) {\n refuse(res, 403, \"permission_denied\", refused);\n return true;\n }\n const hit = matches.find((x) => x.b.method === req.method);\n if (!hit) {\n res.setHeader(\"Allow\", matches.map((x) => x.b.method).join(\", \"));\n problem(res, 405, { code: \"unimplemented\", message: `${req.method} is not bound on ${path}` });\n return true;\n }\n const { b, m } = hit as { b: Binding; m: RegExpExecArray };\n try {\n const args: Record<string, unknown> = {};\n b.params.forEach((name, i) => (args[name] = fromText(b.op, name, decodePathSegment(m[i + 1]!, name))));\n const shapeParam = url.searchParams.get(\"shape\");\n if (b.method === \"GET\") {\n for (const [k, v] of url.searchParams) if (k !== \"shape\" && !(k in args)) args[k] = fromText(b.op, k, v);\n }\n if (b.body) {\n const raw = await readBody(req, maxBody);\n if (raw.length) {\n const ct = mediaType(req);\n const accepted = b.method === \"PATCH\" ? [\"application/merge-patch+json\", \"application/json\"] : [\"application/json\"];\n if (!accepted.includes(ct)) {\n refuse(res, 415, \"invalid_argument\", `Content-Type ${ct || \"(none)\"} is not accepted; send ${accepted[0]}`, \"unsupported_media_type\");\n return true;\n }\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch {\n throw new RayfoldError(\"invalid_argument\", \"Body is not valid JSON\");\n }\n if (b.body === \"*\") {\n if (!parsed || typeof parsed !== \"object\" || Array.isArray(parsed)) throw new RayfoldError(\"invalid_argument\", \"Body must be a JSON object\");\n Object.assign(args, parsed);\n } else args[b.body] = parsed;\n }\n }\n const op: RequestOp = { id: 1, op: b.op.name, args };\n if (shapeParam) op.shape = shapeParam;\n const key = header(req, \"idempotency-key\");\n if (key) op.key = key;\n const ifMatch = header(req, \"if-match\");\n if (ifMatch) {\n const v = ifMatch.replace(/^W\\//, \"\").replace(/^\"|\"$/g, \"\");\n op.ifVersion = /^\\d+$/.test(v) ? Number(v) : v;\n }\n if (b.op.kind === \"command\" && b.method === \"POST\" && !key) {\n const idem = annotation(b.op, \"idempotent\");\n if (!(idem && idem.args[\"value\"] === false)) {\n throw new RayfoldError(\"invalid_argument\", `POST ${b.path} requires an Idempotency-Key header (16-128 characters)`);\n }\n }\n const envelope: RequestEnvelope = { ops: [op] };\n const viewer = opts.viewer ? await opts.viewer(req) : null;\n const frames: Frame[] = [];\n for await (const f of server.execute(envelope, { viewer, keyOptional: IDEMPOTENT_METHODS.has(b.method) })) frames.push(f);\n const folded = fold(frames);\n if (\"error\" in folded) {\n const e = folded.error;\n const status = e.type === \"VersionConflict\" ? 412 : HTTP_STATUS[e.code];\n problem(res, status, e);\n return true;\n }\n const result = folded.result;\n if (b.op.kind === \"query\") {\n applyCacheHeaders(server, envelope, frames, viewer, res);\n if (header(req, \"if-none-match\") && header(req, \"if-none-match\") === res.getHeader(\"ETag\")) {\n res.removeHeader(\"X-Content-Type-Options\"); // no body to sniff; the cached response keeps its headers\n res.writeHead(304).end();\n return true;\n }\n json(res, 200, result);\n return true;\n }\n res.setHeader(\"Cache-Control\", \"no-store\");\n const version = versionOf(server.ir, b.op, result);\n if (version !== undefined) res.setHeader(\"ETag\", `\"${version}\"`);\n if (folded.replay) res.setHeader(\"Idempotent-Replayed\", \"true\");\n if (b.method === \"POST\" && b.location && result && typeof result === \"object\") {\n res.setHeader(\"Location\", prefix + b.location.replace(/\\{([A-Za-z_][A-Za-z0-9_]*)\\}/g, (_m, f: string) => encodeURIComponent(String((result as Record<string, unknown>)[f] ?? \"\"))));\n json(res, 201, result);\n return true;\n }\n json(res, 200, result);\n return true;\n } catch (e) {\n if (e instanceof BodyTooLarge) {\n refuseBody(res, e);\n return true;\n }\n const w = e instanceof RayfoldError ? e.toWire() : { code: \"internal\" as const, message: \"Internal error\" };\n problem(res, HTTP_STATUS[w.code], w);\n return true;\n }\n };\n}\n\nfunction decodePathSegment(raw: string, name: string): string {\n try {\n return decodeURIComponent(raw);\n } catch {\n throw new RayfoldError(\"invalid_argument\", `Path parameter ${name} is not valid percent-encoding`);\n }\n}\n\n/** Path and query-string values are text; coerce them by the argument's declared type. */\nfunction fromText(op: OpDef, name: string, text: string): unknown {\n const def = op.args.find((a) => a.name === name);\n if (!def || def.type.kind !== \"named\") return text;\n switch (def.type.name) {\n case \"Int\":\n case \"Float\":\n return /^-?\\d+(\\.\\d+)?$/.test(text) ? Number(text) : text;\n case \"Boolean\":\n return text === \"true\" ? true : text === \"false\" ? false : text;\n case \"ID\":\n case \"String\":\n case \"Decimal\":\n case \"Date\":\n case \"Instant\":\n return text;\n default:\n try {\n return JSON.parse(text);\n } catch {\n return text;\n }\n }\n}\n\n/** The @version field of the result entity, if the op returns one. */\nfunction versionOf(ir: RayfoldSchemaIR, op: OpDef, result: unknown): unknown {\n if (!result || typeof result !== \"object\" || op.returns.kind !== \"named\") return undefined;\n const def = ir.types[op.returns.name];\n if (!def || def.kind !== \"entity\") return undefined;\n const vf = def.fields.find((f) => annotation(f, \"version\"));\n return vf ? (result as Record<string, unknown>)[vf.name] : undefined;\n}\n\nfunction fold(frames: Frame[]): { result: unknown; replay: boolean } | { error: WireError } {\n let result: unknown = null;\n let replay = false;\n for (const f of frames) {\n if (\"error\" in f) return { error: f.error };\n if (\"ok\" in f) {\n result = f.ok;\n replay = !!(f.meta as { replay?: boolean } | undefined)?.replay;\n } else if (\"data\" in f && !(\"at\" in f)) result = f.data;\n else if (\"at\" in f && result && typeof result === \"object\") {\n let target: unknown = result;\n if (f.at !== \"\") for (const p of f.at.split(\".\")) target = target && typeof target === \"object\" ? (target as Record<string, unknown>)[p] : undefined;\n if (target && typeof target === \"object\") Object.assign(target as object, f.data as object);\n }\n }\n return { result, replay };\n}\n\nfunction header(req: IncomingMessage, name: string): string | undefined {\n const v = req.headers[name];\n return typeof v === \"string\" ? v : undefined;\n}\n\n/** How much of an over-limit body is drained (so the refusal can be read) before the socket is dropped. */\nconst DRAIN_LIMIT = 1_048_576;\n\nfunction readBody(req: IncomingMessage, max: number): Promise<string> {\n return new Promise((resolve, reject) => {\n const chunks: Buffer[] = [];\n let size = 0;\n let over = false;\n req.on(\"data\", (c: Buffer) => {\n size += c.length;\n if (over) {\n // A client that keeps streaming long after the refusal is cut off rather than read forever.\n if (size > max + DRAIN_LIMIT) req.destroy();\n return;\n }\n if (size > max) {\n // Stop buffering but keep draining: destroying the socket here would lose the refusal on its way out.\n over = true;\n chunks.length = 0;\n reject(new BodyTooLarge(max));\n return;\n }\n chunks.push(c);\n });\n req.on(\"end\", () => resolve(Buffer.concat(chunks).toString(\"utf8\")));\n req.on(\"error\", reject);\n });\n}\n\nfunction json(res: ServerResponse, status: number, body: unknown): void {\n res.writeHead(status, { \"Content-Type\": \"application/json; charset=utf-8\" }).end(JSON.stringify(body));\n}\n\n/** RFC 9457 problem; typed Rayfold errors keep their `type` and `data` so REST clients can branch on them. */\nfunction problem(res: ServerResponse, status: number, e: WireError): void {\n const body: Record<string, unknown> = {\n type: PROBLEM_TYPE_BASE + (e.type ?? e.code),\n title: e.type ?? e.code.replace(/_/g, \" \"),\n status,\n detail: e.message,\n code: e.code,\n };\n if (e.path !== undefined) body[\"path\"] = e.path;\n if (e.data !== undefined) body[\"data\"] = e.data;\n res.writeHead(status, { \"Content-Type\": \"application/problem+json\", \"Cache-Control\": \"no-store\" }).end(JSON.stringify(body));\n}\n"]}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether a viewer holding a capability may call this operation. A viewer without `caps.ops` is not a capability
|
|
3
|
+
* holder and is left to the schema's own policies.
|
|
4
|
+
*
|
|
5
|
+
* Kept apart from capability.ts, which signs tokens with node:crypto: the batch needs only this check, and the batch
|
|
6
|
+
* has to run anywhere, browsers included.
|
|
7
|
+
*/
|
|
8
|
+
export declare function capabilityAllows(viewer: unknown, op: string): boolean;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether a viewer holding a capability may call this operation. A viewer without `caps.ops` is not a capability
|
|
3
|
+
* holder and is left to the schema's own policies.
|
|
4
|
+
*
|
|
5
|
+
* Kept apart from capability.ts, which signs tokens with node:crypto: the batch needs only this check, and the batch
|
|
6
|
+
* has to run anywhere, browsers included.
|
|
7
|
+
*/
|
|
8
|
+
export function capabilityAllows(viewer, op) {
|
|
9
|
+
if (!viewer || typeof viewer !== "object" || Array.isArray(viewer))
|
|
10
|
+
return true;
|
|
11
|
+
const caps = viewer.caps;
|
|
12
|
+
if (!caps || typeof caps !== "object" || Array.isArray(caps))
|
|
13
|
+
return true;
|
|
14
|
+
const ops = caps.ops;
|
|
15
|
+
if (!Array.isArray(ops))
|
|
16
|
+
return true;
|
|
17
|
+
return ops.includes(op);
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=capability-scope.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"capability-scope.js","sourceRoot":"","sources":["../src/capability-scope.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,MAAM,UAAU,gBAAgB,CAAC,MAAe,EAAE,EAAU;IAC1D,IAAI,CAAC,MAAM,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC;QAAE,OAAO,IAAI,CAAC;IAChF,MAAM,IAAI,GAAI,MAA6B,CAAC,IAAI,CAAC;IACjD,IAAI,CAAC,IAAI,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC;QAAE,OAAO,IAAI,CAAC;IAC1E,MAAM,GAAG,GAAI,IAA0B,CAAC,GAAG,CAAC;IAC5C,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC;QAAE,OAAO,IAAI,CAAC;IACrC,OAAO,GAAG,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC;AAC1B,CAAC","sourcesContent":["/**\n * Whether a viewer holding a capability may call this operation. A viewer without `caps.ops` is not a capability\n * holder and is left to the schema's own policies.\n *\n * Kept apart from capability.ts, which signs tokens with node:crypto: the batch needs only this check, and the batch\n * has to run anywhere, browsers included.\n */\nexport function capabilityAllows(viewer: unknown, op: string): boolean {\n if (!viewer || typeof viewer !== \"object\" || Array.isArray(viewer)) return true;\n const caps = (viewer as { caps?: unknown }).caps;\n if (!caps || typeof caps !== \"object\" || Array.isArray(caps)) return true;\n const ops = (caps as { ops?: unknown }).ops;\n if (!Array.isArray(ops)) return true;\n return ops.includes(op);\n}\n"]}
|
package/capability.d.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
export interface Capability {
|
|
2
|
+
/** the viewer this token speaks for, as the server's own viewer shape */
|
|
3
|
+
viewer: unknown;
|
|
4
|
+
/** the operations it may call; an empty list may call none */
|
|
5
|
+
ops: string[];
|
|
6
|
+
/** epoch milliseconds after which it is refused */
|
|
7
|
+
exp: number;
|
|
8
|
+
/** who issued it, for logs and revocation lists */
|
|
9
|
+
iss?: string;
|
|
10
|
+
/** extra facts the schema's policies read as `viewer.caps` */
|
|
11
|
+
caps?: Record<string, unknown>;
|
|
12
|
+
/** unique id of this token, so one can be named in a revocation list */
|
|
13
|
+
jti: string;
|
|
14
|
+
}
|
|
15
|
+
export interface MintOptions {
|
|
16
|
+
/** the operations the holder may call */
|
|
17
|
+
ops: string[];
|
|
18
|
+
/** how long it lives, in milliseconds */
|
|
19
|
+
ttlMs: number;
|
|
20
|
+
iss?: string;
|
|
21
|
+
caps?: Record<string, unknown>;
|
|
22
|
+
}
|
|
23
|
+
export interface CapabilitiesOptions {
|
|
24
|
+
/** the signing secret; anything derived from it must not leave the server */
|
|
25
|
+
secret: string | Uint8Array;
|
|
26
|
+
now?: () => number;
|
|
27
|
+
/** the longest life any token may have, minted or attenuated. Default one hour. */
|
|
28
|
+
maxTtlMs?: number;
|
|
29
|
+
}
|
|
30
|
+
/** Mints, verifies and narrows capability tokens with one secret. */
|
|
31
|
+
export declare class Capabilities {
|
|
32
|
+
private readonly secret;
|
|
33
|
+
private readonly now;
|
|
34
|
+
private readonly maxTtlMs;
|
|
35
|
+
constructor(opts: CapabilitiesOptions);
|
|
36
|
+
mint(viewer: unknown, opts: MintOptions): string;
|
|
37
|
+
/** The capability a token carries, or a refusal: unsigned, edited, malformed and expired all fail here. */
|
|
38
|
+
verify(token: string): Capability;
|
|
39
|
+
/**
|
|
40
|
+
* A narrower token derived from this one: operations must be a subset of what it already allows, and the life
|
|
41
|
+
* may only be shortened. The derived token is a token in its own right, so it can be narrowed again.
|
|
42
|
+
*/
|
|
43
|
+
attenuate(token: string, narrow: {
|
|
44
|
+
ops?: string[];
|
|
45
|
+
ttlMs?: number;
|
|
46
|
+
caps?: Record<string, unknown>;
|
|
47
|
+
}): string;
|
|
48
|
+
/**
|
|
49
|
+
* The viewer a token stands for, with what it allows attached as `caps`. Hand this to `execute` as the viewer:
|
|
50
|
+
* the schema's policies read `viewer.caps.*`, and the batch refuses operations the token does not name.
|
|
51
|
+
*/
|
|
52
|
+
viewerOf(token: string): unknown;
|
|
53
|
+
private sign;
|
|
54
|
+
private hmac;
|
|
55
|
+
}
|
|
56
|
+
export { capabilityAllows } from "./capability-scope.js";
|
package/capability.js
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Capability tokens (extension `cap`, spec 06 section 6): short-lived, scoped, delegatable references to a viewer.
|
|
3
|
+
* They let an agent or a downstream service perform exactly one class of operation without ever holding the user's
|
|
4
|
+
* credentials, and they can be narrowed further before being handed on.
|
|
5
|
+
*
|
|
6
|
+
* A token carries everything it claims and is signed, so verifying one needs no storage and no round trip:
|
|
7
|
+
*
|
|
8
|
+
* rfcap1.<payload as base64url JSON>.<HMAC-SHA256 of the payload, base64url>
|
|
9
|
+
*
|
|
10
|
+
* The payload names the viewer it speaks for, the operations it may call, when it expires and any extra facts the
|
|
11
|
+
* schema's policies may read as `viewer.caps`. Nothing in it is secret from its holder: it is a reference, not a
|
|
12
|
+
* password, and it is signed so it cannot be edited.
|
|
13
|
+
*
|
|
14
|
+
* Attenuation only ever narrows: a derived token may drop operations and shorten the life, never add or extend.
|
|
15
|
+
*/
|
|
16
|
+
import { createHmac, randomUUID, timingSafeEqual } from "node:crypto";
|
|
17
|
+
import { RayfoldError } from "./protocol.js";
|
|
18
|
+
const PREFIX = "rfcap1";
|
|
19
|
+
const b64url = (b) => b.toString("base64url");
|
|
20
|
+
const unb64url = (s) => Buffer.from(s, "base64url");
|
|
21
|
+
/** Mints, verifies and narrows capability tokens with one secret. */
|
|
22
|
+
export class Capabilities {
|
|
23
|
+
secret;
|
|
24
|
+
now;
|
|
25
|
+
maxTtlMs;
|
|
26
|
+
constructor(opts) {
|
|
27
|
+
this.secret = typeof opts.secret === "string" ? new TextEncoder().encode(opts.secret) : opts.secret;
|
|
28
|
+
if (this.secret.length < 16)
|
|
29
|
+
throw new Error("capability secret must be at least 16 bytes");
|
|
30
|
+
this.now = opts.now ?? Date.now;
|
|
31
|
+
this.maxTtlMs = opts.maxTtlMs ?? 3_600_000;
|
|
32
|
+
}
|
|
33
|
+
mint(viewer, opts) {
|
|
34
|
+
if (opts.ttlMs <= 0 || opts.ttlMs > this.maxTtlMs) {
|
|
35
|
+
throw new Error(`capability ttl must be between 1 and ${this.maxTtlMs} ms`);
|
|
36
|
+
}
|
|
37
|
+
const cap = {
|
|
38
|
+
viewer,
|
|
39
|
+
ops: [...new Set(opts.ops)].sort(),
|
|
40
|
+
exp: this.now() + opts.ttlMs,
|
|
41
|
+
jti: randomUUID().replace(/-/g, ""),
|
|
42
|
+
...(opts.iss ? { iss: opts.iss } : {}),
|
|
43
|
+
...(opts.caps ? { caps: opts.caps } : {}),
|
|
44
|
+
};
|
|
45
|
+
return this.sign(cap);
|
|
46
|
+
}
|
|
47
|
+
/** The capability a token carries, or a refusal: unsigned, edited, malformed and expired all fail here. */
|
|
48
|
+
verify(token) {
|
|
49
|
+
const parts = token.split(".");
|
|
50
|
+
if (parts.length !== 3 || parts[0] !== PREFIX)
|
|
51
|
+
throw new RayfoldError("unauthenticated", "Not a capability token");
|
|
52
|
+
const signature = unb64url(parts[2]);
|
|
53
|
+
const expected = this.hmac(parts[1]);
|
|
54
|
+
if (signature.length !== expected.length || !timingSafeEqual(signature, expected)) {
|
|
55
|
+
throw new RayfoldError("unauthenticated", "Capability signature does not match");
|
|
56
|
+
}
|
|
57
|
+
let cap;
|
|
58
|
+
try {
|
|
59
|
+
cap = JSON.parse(unb64url(parts[1]).toString("utf8"));
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
throw new RayfoldError("unauthenticated", "Capability payload is not readable");
|
|
63
|
+
}
|
|
64
|
+
if (!cap || typeof cap !== "object" || !Array.isArray(cap.ops) || typeof cap.exp !== "number") {
|
|
65
|
+
throw new RayfoldError("unauthenticated", "Capability payload is not a capability");
|
|
66
|
+
}
|
|
67
|
+
if (cap.exp <= this.now())
|
|
68
|
+
throw new RayfoldError("unauthenticated", "Capability has expired");
|
|
69
|
+
return cap;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* A narrower token derived from this one: operations must be a subset of what it already allows, and the life
|
|
73
|
+
* may only be shortened. The derived token is a token in its own right, so it can be narrowed again.
|
|
74
|
+
*/
|
|
75
|
+
attenuate(token, narrow) {
|
|
76
|
+
const cap = this.verify(token);
|
|
77
|
+
const ops = narrow.ops ? [...new Set(narrow.ops)].sort() : cap.ops;
|
|
78
|
+
const widened = ops.filter((op) => !cap.ops.includes(op));
|
|
79
|
+
if (widened.length)
|
|
80
|
+
throw new RayfoldError("permission_denied", `Cannot widen a capability: ${widened.join(", ")}`);
|
|
81
|
+
const exp = narrow.ttlMs === undefined ? cap.exp : Math.min(cap.exp, this.now() + narrow.ttlMs);
|
|
82
|
+
if (exp <= this.now())
|
|
83
|
+
throw new RayfoldError("permission_denied", "Cannot derive a capability that has expired");
|
|
84
|
+
return this.sign({
|
|
85
|
+
...cap,
|
|
86
|
+
ops,
|
|
87
|
+
exp,
|
|
88
|
+
jti: randomUUID().replace(/-/g, ""),
|
|
89
|
+
// extra facts may be replaced, never added to: a narrower token cannot claim more than the one it came from
|
|
90
|
+
...(narrow.caps ? { caps: narrow.caps } : {}),
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* The viewer a token stands for, with what it allows attached as `caps`. Hand this to `execute` as the viewer:
|
|
95
|
+
* the schema's policies read `viewer.caps.*`, and the batch refuses operations the token does not name.
|
|
96
|
+
*/
|
|
97
|
+
viewerOf(token) {
|
|
98
|
+
const cap = this.verify(token);
|
|
99
|
+
const viewer = cap.viewer;
|
|
100
|
+
const caps = { ops: cap.ops, exp: cap.exp, jti: cap.jti, ...(cap.iss ? { iss: cap.iss } : {}), ...(cap.caps ?? {}) };
|
|
101
|
+
return viewer !== null && typeof viewer === "object" && !Array.isArray(viewer) ? { ...viewer, caps } : { viewer, caps };
|
|
102
|
+
}
|
|
103
|
+
sign(cap) {
|
|
104
|
+
const payload = b64url(Buffer.from(JSON.stringify(cap), "utf8"));
|
|
105
|
+
return `${PREFIX}.${payload}.${b64url(this.hmac(payload))}`;
|
|
106
|
+
}
|
|
107
|
+
hmac(payload) {
|
|
108
|
+
return createHmac("sha256", this.secret).update(`${PREFIX}.${payload}`).digest();
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
export { capabilityAllows } from "./capability-scope.js";
|
|
112
|
+
//# sourceMappingURL=capability.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"capability.js","sourceRoot":"","sources":["../src/capability.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Capability tokens (extension `cap`, spec 06 section 6): short-lived, scoped, delegatable references to a viewer.\n * They let an agent or a downstream service perform exactly one class of operation without ever holding the user's\n * credentials, and they can be narrowed further before being handed on.\n *\n * A token carries everything it claims and is signed, so verifying one needs no storage and no round trip:\n *\n * rfcap1.<payload as base64url JSON>.<HMAC-SHA256 of the payload, base64url>\n *\n * The payload names the viewer it speaks for, the operations it may call, when it expires and any extra facts the\n * schema's policies may read as `viewer.caps`. Nothing in it is secret from its holder: it is a reference, not a\n * password, and it is signed so it cannot be edited.\n *\n * Attenuation only ever narrows: a derived token may drop operations and shorten the life, never add or extend.\n */\nimport { createHmac, randomUUID, timingSafeEqual } from \"node:crypto\";\nimport { RayfoldError } from \"./protocol.ts\";\n\nconst PREFIX = \"rfcap1\";\n\nexport interface Capability {\n /** the viewer this token speaks for, as the server's own viewer shape */\n viewer: unknown;\n /** the operations it may call; an empty list may call none */\n ops: string[];\n /** epoch milliseconds after which it is refused */\n exp: number;\n /** who issued it, for logs and revocation lists */\n iss?: string;\n /** extra facts the schema's policies read as `viewer.caps` */\n caps?: Record<string, unknown>;\n /** unique id of this token, so one can be named in a revocation list */\n jti: string;\n}\n\nexport interface MintOptions {\n /** the operations the holder may call */\n ops: string[];\n /** how long it lives, in milliseconds */\n ttlMs: number;\n iss?: string;\n caps?: Record<string, unknown>;\n}\n\nexport interface CapabilitiesOptions {\n /** the signing secret; anything derived from it must not leave the server */\n secret: string | Uint8Array;\n now?: () => number;\n /** the longest life any token may have, minted or attenuated. Default one hour. */\n maxTtlMs?: number;\n}\n\nconst b64url = (b: Buffer): string => b.toString(\"base64url\");\nconst unb64url = (s: string): Buffer => Buffer.from(s, \"base64url\");\n\n/** Mints, verifies and narrows capability tokens with one secret. */\nexport class Capabilities {\n private readonly secret: Uint8Array;\n private readonly now: () => number;\n private readonly maxTtlMs: number;\n\n constructor(opts: CapabilitiesOptions) {\n this.secret = typeof opts.secret === \"string\" ? new TextEncoder().encode(opts.secret) : opts.secret;\n if (this.secret.length < 16) throw new Error(\"capability secret must be at least 16 bytes\");\n this.now = opts.now ?? Date.now;\n this.maxTtlMs = opts.maxTtlMs ?? 3_600_000;\n }\n\n mint(viewer: unknown, opts: MintOptions): string {\n if (opts.ttlMs <= 0 || opts.ttlMs > this.maxTtlMs) {\n throw new Error(`capability ttl must be between 1 and ${this.maxTtlMs} ms`);\n }\n const cap: Capability = {\n viewer,\n ops: [...new Set(opts.ops)].sort(),\n exp: this.now() + opts.ttlMs,\n jti: randomUUID().replace(/-/g, \"\"),\n ...(opts.iss ? { iss: opts.iss } : {}),\n ...(opts.caps ? { caps: opts.caps } : {}),\n };\n return this.sign(cap);\n }\n\n /** The capability a token carries, or a refusal: unsigned, edited, malformed and expired all fail here. */\n verify(token: string): Capability {\n const parts = token.split(\".\");\n if (parts.length !== 3 || parts[0] !== PREFIX) throw new RayfoldError(\"unauthenticated\", \"Not a capability token\");\n const signature = unb64url(parts[2]!);\n const expected = this.hmac(parts[1]!);\n if (signature.length !== expected.length || !timingSafeEqual(signature, expected)) {\n throw new RayfoldError(\"unauthenticated\", \"Capability signature does not match\");\n }\n let cap: Capability;\n try {\n cap = JSON.parse(unb64url(parts[1]!).toString(\"utf8\")) as Capability;\n } catch {\n throw new RayfoldError(\"unauthenticated\", \"Capability payload is not readable\");\n }\n if (!cap || typeof cap !== \"object\" || !Array.isArray(cap.ops) || typeof cap.exp !== \"number\") {\n throw new RayfoldError(\"unauthenticated\", \"Capability payload is not a capability\");\n }\n if (cap.exp <= this.now()) throw new RayfoldError(\"unauthenticated\", \"Capability has expired\");\n return cap;\n }\n\n /**\n * A narrower token derived from this one: operations must be a subset of what it already allows, and the life\n * may only be shortened. The derived token is a token in its own right, so it can be narrowed again.\n */\n attenuate(token: string, narrow: { ops?: string[]; ttlMs?: number; caps?: Record<string, unknown> }): string {\n const cap = this.verify(token);\n const ops = narrow.ops ? [...new Set(narrow.ops)].sort() : cap.ops;\n const widened = ops.filter((op) => !cap.ops.includes(op));\n if (widened.length) throw new RayfoldError(\"permission_denied\", `Cannot widen a capability: ${widened.join(\", \")}`);\n const exp = narrow.ttlMs === undefined ? cap.exp : Math.min(cap.exp, this.now() + narrow.ttlMs);\n if (exp <= this.now()) throw new RayfoldError(\"permission_denied\", \"Cannot derive a capability that has expired\");\n return this.sign({\n ...cap,\n ops,\n exp,\n jti: randomUUID().replace(/-/g, \"\"),\n // extra facts may be replaced, never added to: a narrower token cannot claim more than the one it came from\n ...(narrow.caps ? { caps: narrow.caps } : {}),\n });\n }\n\n /**\n * The viewer a token stands for, with what it allows attached as `caps`. Hand this to `execute` as the viewer:\n * the schema's policies read `viewer.caps.*`, and the batch refuses operations the token does not name.\n */\n viewerOf(token: string): unknown {\n const cap = this.verify(token);\n const viewer = cap.viewer;\n const caps = { ops: cap.ops, exp: cap.exp, jti: cap.jti, ...(cap.iss ? { iss: cap.iss } : {}), ...(cap.caps ?? {}) };\n return viewer !== null && typeof viewer === \"object\" && !Array.isArray(viewer) ? { ...(viewer as Record<string, unknown>), caps } : { viewer, caps };\n }\n\n private sign(cap: Capability): string {\n const payload = b64url(Buffer.from(JSON.stringify(cap), \"utf8\"));\n return `${PREFIX}.${payload}.${b64url(this.hmac(payload))}`;\n }\n\n private hmac(payload: string): Buffer {\n return createHmac(\"sha256\", this.secret).update(`${PREFIX}.${payload}`).digest();\n }\n}\n\nexport { capabilityAllows } from \"./capability-scope.ts\";\n"]}
|
package/context.d.ts
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type { Expr, Shape } from "@rayfold/schema";
|
|
2
|
+
import type { RequestMeta } from "./protocol.js";
|
|
3
|
+
/** In-process event bus used for `emits` and for streams that subscribe to events. */
|
|
4
|
+
export declare class EventBus {
|
|
5
|
+
private readonly subs;
|
|
6
|
+
private seq;
|
|
7
|
+
publish(name: string, payload: Record<string, unknown>): void;
|
|
8
|
+
on(name: string, fn: (payload: unknown) => void): () => void;
|
|
9
|
+
/** Async iterator over an event; ends when `signal` aborts. Buffers between pulls. */
|
|
10
|
+
subscribe<T = unknown>(name: string, signal?: AbortSignal): AsyncIterable<T>;
|
|
11
|
+
}
|
|
12
|
+
export interface PolicyHint {
|
|
13
|
+
/** Pushable read policy for the type being loaded, if any (spec 06 §4). */
|
|
14
|
+
filter?: Expr;
|
|
15
|
+
}
|
|
16
|
+
/** What every resolver receives. `V` is the server's viewer type. */
|
|
17
|
+
export interface RayfoldContext<V = unknown> {
|
|
18
|
+
viewer: V;
|
|
19
|
+
signal: AbortSignal;
|
|
20
|
+
simulate: boolean;
|
|
21
|
+
/** Compact wire mode requested by the client (spec 04 section 1). */
|
|
22
|
+
compact?: boolean;
|
|
23
|
+
/** Expected version for a conditional command (spec 03 section 4a). */
|
|
24
|
+
ifVersion?: string | number;
|
|
25
|
+
/**
|
|
26
|
+
* Conditional-write check: throws VersionConflict when the request carried `ifVersion` and it differs
|
|
27
|
+
* from `actual`. `current` is the entity as stored; it is returned to the client in the op's shape.
|
|
28
|
+
*/
|
|
29
|
+
checkVersion(key: string, actual: unknown, current: unknown): void;
|
|
30
|
+
events: EventBus;
|
|
31
|
+
meta: RequestMeta;
|
|
32
|
+
opId: number;
|
|
33
|
+
opName: string;
|
|
34
|
+
policy: PolicyHint;
|
|
35
|
+
/** The shape this op asked for, so an adapter can plan a whole screen at once (spec 02). */
|
|
36
|
+
shape?: Shape;
|
|
37
|
+
/** Values for `$name` references in the op's shape. */
|
|
38
|
+
vars?: Record<string, unknown>;
|
|
39
|
+
/** Per-request scratch space (e.g. per-request loader caches). */
|
|
40
|
+
state: Map<string, unknown>;
|
|
41
|
+
/**
|
|
42
|
+
* Scratch space shared by every op of the batch. The executor keeps loaded field values here, so an entity one op
|
|
43
|
+
* already loaded is not loaded again by another op of the same request (spec 03 section 2).
|
|
44
|
+
*/
|
|
45
|
+
batch: Map<string, unknown>;
|
|
46
|
+
/** Wall clock, injectable for tests. */
|
|
47
|
+
now: () => number;
|
|
48
|
+
}
|
|
49
|
+
export interface IdempotencyRecord {
|
|
50
|
+
argsHash: string;
|
|
51
|
+
/** the full ok frame */
|
|
52
|
+
frame: unknown;
|
|
53
|
+
/** the same result in compact form, for retries that ask for compact frames */
|
|
54
|
+
compactFrame?: unknown;
|
|
55
|
+
at: number;
|
|
56
|
+
}
|
|
57
|
+
export interface IdempotencyStore {
|
|
58
|
+
get(scope: string, key: string): Promise<IdempotencyRecord | undefined>;
|
|
59
|
+
put(scope: string, key: string, record: IdempotencyRecord): Promise<void>;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* In-memory idempotency records. Records expire after `ttlMs`; expired ones are dropped on read and swept on every
|
|
63
|
+
* write, and past `maxEntries` the oldest go first, so memory stays bounded however many commands arrive.
|
|
64
|
+
*/
|
|
65
|
+
export declare class MemoryIdempotencyStore implements IdempotencyStore {
|
|
66
|
+
private readonly ttlMs;
|
|
67
|
+
private readonly now;
|
|
68
|
+
private readonly maxEntries;
|
|
69
|
+
private readonly map;
|
|
70
|
+
constructor(ttlMs?: number, now?: () => number, maxEntries?: number);
|
|
71
|
+
get(scope: string, key: string): Promise<IdempotencyRecord | undefined>;
|
|
72
|
+
put(scope: string, key: string, record: IdempotencyRecord): Promise<void>;
|
|
73
|
+
get size(): number;
|
|
74
|
+
}
|