@fougere/transport-http 0.24.0-alpha.0 → 0.26.0-alpha.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/dist/client.js +2 -2
- package/dist/client.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/receive.d.ts +1 -1
- package/dist/receive.d.ts.map +1 -1
- package/dist/receive.js +9 -39
- package/dist/receive.js.map +1 -1
- package/dist/serve/RunningReceiver.d.ts +1 -1
- package/dist/serve/RunningReceiver.d.ts.map +1 -1
- package/dist/serve/RunningReceiver.js +33 -13
- package/dist/serve/RunningReceiver.js.map +1 -1
- package/dist/serve/ServeOptions.d.ts +5 -0
- package/dist/serve/ServeOptions.d.ts.map +1 -1
- package/dist/server.d.ts +33 -2
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +144 -80
- package/dist/server.js.map +1 -1
- package/package.json +5 -5
- package/src/client.ts +2 -2
- package/src/index.ts +1 -1
- package/src/receive.ts +9 -45
- package/src/serve/RunningReceiver.ts +33 -14
- package/src/serve/ServeOptions.ts +5 -0
- package/src/server.ts +177 -83
package/dist/client.js
CHANGED
|
@@ -54,9 +54,9 @@ export function createHttpTransport(baseUrl, options = {}) {
|
|
|
54
54
|
// receiver choosing between a proof and a claim about the same thing.
|
|
55
55
|
// `caller` is dropped on every hop: it names who signed THIS call, so carrying the
|
|
56
56
|
// one this process was handed would make `shop → catalog → billing` read `shop`.
|
|
57
|
-
// It travels outside the envelope anyway, so a stale one would be unsigned too. `
|
|
57
|
+
// It travels outside the envelope anyway, so a stale one would be unsigned too. `fromAnotherProcess`
|
|
58
58
|
// is the next receiver's to write, for the same reason.
|
|
59
|
-
const { caller: _established,
|
|
59
|
+
const { caller: _established, fromAnotherProcess: _received, ...forwarded } = invocation;
|
|
60
60
|
const sent = options.sign
|
|
61
61
|
? { ...forwarded, state: {}, identity: await options.sign({ ...call, ...invocation }) }
|
|
62
62
|
: forwarded;
|
package/dist/client.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA,kEAAkE;AAClE,OAAO,EAAE,YAAY,EAAE,SAAS,EAAE,YAAY,EAAE,aAAa,EAA2E,MAAM,wBAAwB,CAAC;AAMvK,0CAA0C;AAC1C,MAAM,UAAU,SAAS,CAAC,IAAe,EAAE,UAA6B,EAAE,EAAU;IAClF,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,EAAE,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC,OAAO,IAAI,IAAI,CAAC,EAAE,EAAE,EAAE,MAAM,EAAE,UAAU,EAAE,CAAC;AAC1F,CAAC;AAED,oFAAoF;AACpF,MAAM,UAAU,eAAe,CAAC,QAAiB,EAAE,IAAe;IAChE,IAAI,CAAC,QAAQ,IAAI,OAAO,QAAQ,KAAK,QAAQ,IAAI,CAAC,CAAC,QAAQ,IAAI,QAAQ,IAAI,OAAO,IAAI,QAAQ,CAAC,EAAE,CAAC;QAChG,MAAM,IAAI,YAAY,CAAC;YACrB,IAAI,EAAE,SAAS,CAAC,WAAW;YAC3B,OAAO,EAAE,kEAAkE;YAC3E,OAAO,EAAE,IAAI,CAAC,OAAO;YACrB,SAAS,EAAE,IAAI,CAAC,EAAE;SACnB,CAAC,CAAC;IACL,CAAC;IACD,IAAI,OAAO,IAAI,QAAQ,EAAE,CAAC;QACxB,MAAM,KAAK,GAAG,QAAQ,CAAC,KAAsC,CAAC;QAC9D,IAAI,CAAC,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;YACxC,MAAM,IAAI,YAAY,CAAC;gBACrB,IAAI,EAAE,SAAS,CAAC,WAAW;gBAC3B,OAAO,EAAE,uDAAuD;gBAChE,OAAO,EAAE,IAAI,CAAC,OAAO;gBACrB,SAAS,EAAE,IAAI,CAAC,EAAE;aACnB,CAAC,CAAC;QACL,CAAC;QACD,IAAI,KAAK,CAAC,IAAI,KAAK,SAAS;YAAE,MAAM,YAAY,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QACtE,MAAM,IAAI,YAAY,CAAC;YACrB,IAAI,EAAE,SAAS,CAAC,cAAc;YAC9B,OAAO,EAAE,GAAG,KAAK,CAAC,OAAO,SAAS,KAAK,CAAC,IAAI,GAAG;YAC/C,OAAO,EAAE,IAAI,CAAC,OAAO;YACrB,SAAS,EAAE,IAAI,CAAC,EAAE;SACnB,CAAC,CAAC;IACL,CAAC;IAED,OAAO,QAAQ,CAAC,MAAM,CAAC;AACzB,CAAC;AAeD,+EAA+E;AAC/E,MAAM,mBAAmB,GAAG,IAAI,GAAG,CAAC,CAAC,cAAc,EAAE,WAAW,EAAE,WAAW,CAAC,CAAC,CAAC;AAEhF,MAAM,UAAU,mBAAmB,CAAC,OAAe,EAAE,OAAO,GAAyB,EAAE;IACrF,MAAM,GAAG,GAAG,GAAG,OAAO,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,gBAAgB,CAAC;IAC1D,wFAAwF;IACxF,qFAAqF;IACrF,kFAAkF;IAClF,uFAAuF;IACvF,sFAAsF;IACtF,sBAAsB;IACtB,MAAM,IAAI,GAAG,OAAO,CAAC,KAAK,IAAI,KAAK,CAAC;IACpC,MAAM,SAAS,GAAG,OAAO,CAAC,SAAS,IAAI,MAAM,CAAC;IAC9C,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,IAAI,CAAC,CAAC;IACrC,IAAI,MAAM,GAAG,CAAC,CAAC;IAEf,OAAO,KAAK,EAAE,IAAI,EAAE,UAAU,EAAE,EAAE;QAChC,6EAA6E;QAC7E,sEAAsE;QACtE,mFAAmF;QACnF,iFAAiF;QACjF,
|
|
1
|
+
{"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { createHttpTransport, frameCall, unframeResponse } from './client.js';
|
|
2
|
-
export { handleRpc } from './server.js';
|
|
2
|
+
export { handleRpc, pageDoor, peerDoor, type PageDoorOptions, type ReceiveOptions, type RpcCall } from './server.js';
|
|
3
3
|
export { receive } from './receive.js';
|
|
4
4
|
export { maxFrameBytes } from './policy.js';
|
|
5
5
|
export { serve, type RunningReceiver } from './serve/RunningReceiver.js';
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,SAAS,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAC9E,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,SAAS,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAC9E,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,QAAQ,EAAE,KAAK,eAAe,EAAE,KAAK,cAAc,EAAE,KAAK,OAAO,EAAE,MAAM,aAAa,CAAC;AACrH,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AACvC,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAC5C,OAAO,EAAE,KAAK,EAAE,KAAK,eAAe,EAAE,MAAM,4BAA4B,CAAC;AACzE,OAAO,EAAE,WAAW,EAAE,MAAM,4BAA4B,CAAC;AACzD,YAAY,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { createHttpTransport, frameCall, unframeResponse } from './client.js';
|
|
2
|
-
export { handleRpc } from './server.js';
|
|
2
|
+
export { handleRpc, pageDoor, peerDoor } from './server.js';
|
|
3
3
|
export { receive } from './receive.js';
|
|
4
4
|
export { maxFrameBytes } from './policy.js';
|
|
5
5
|
export { serve } from './serve/RunningReceiver.js';
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,SAAS,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAC9E,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,SAAS,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAC9E,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,QAAQ,EAA2D,MAAM,aAAa,CAAC;AACrH,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AACvC,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAC5C,OAAO,EAAE,KAAK,EAAwB,MAAM,4BAA4B,CAAC;AACzE,OAAO,EAAE,WAAW,EAAE,MAAM,4BAA4B,CAAC"}
|
package/dist/receive.d.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* baseline, measured 2026-08-22.
|
|
7
7
|
*/
|
|
8
8
|
import { type ReceiveOptions } from './server.js';
|
|
9
|
-
import type
|
|
9
|
+
import { type Transport } from '@fougere/core/contract';
|
|
10
10
|
export interface ReceiveHttpOptions extends ReceiveOptions {
|
|
11
11
|
/** The path this facade answers. Default: `/_fougere/call`. */
|
|
12
12
|
path?: string;
|
package/dist/receive.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"receive.d.ts","sourceRoot":"","sources":["../src/receive.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,EAAa,KAAK,cAAc,EAAE,MAAM,aAAa,CAAC;AAE7D,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"receive.d.ts","sourceRoot":"","sources":["../src/receive.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,EAAa,KAAK,cAAc,EAAE,MAAM,aAAa,CAAC;AAE7D,OAAO,EAAuC,KAAK,SAAS,EAAE,MAAM,wBAAwB,CAAC;AAE7F,MAAM,WAAW,kBAAmB,SAAQ,cAAc;IACxD,+DAA+D;IAC/D,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,yCAAyC;IACzC,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB;AAKD,wBAAgB,OAAO,CACrB,MAAM,EAAE,SAAS,EACjB,OAAO,GAAE,kBAAuB,GAC/B,CAAC,OAAO,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,CAAC,CAmCzC"}
|
package/dist/receive.js
CHANGED
|
@@ -7,44 +7,8 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { handleRpc } from './server.js';
|
|
9
9
|
import { maxFrameBytes, CALL_PATH, parseError, tooLarge } from './policy.js';
|
|
10
|
+
import { ErrorCode, FougereError, readWithin } from '@fougere/core/contract';
|
|
10
11
|
const json = (body, status = 200) => new Response(JSON.stringify(body), { status, headers: { 'content-type': 'application/json' } });
|
|
11
|
-
/** Distinguishes "the body was too large" from any body a caller could actually send. */
|
|
12
|
-
const TOO_LARGE = Symbol('too large');
|
|
13
|
-
/**
|
|
14
|
-
* The declared length is a cheap refusal; the count is the real one. A `content-length` may
|
|
15
|
-
* be absent, and it may lie — the copy that trusted it alone had no cap at all in that case.
|
|
16
|
-
*/
|
|
17
|
-
async function bodyWithin(request, max) {
|
|
18
|
-
const declared = Number(request.headers.get('content-length'));
|
|
19
|
-
if (Number.isFinite(declared) && declared > max)
|
|
20
|
-
return TOO_LARGE;
|
|
21
|
-
const reader = request.body?.getReader();
|
|
22
|
-
if (!reader)
|
|
23
|
-
return '';
|
|
24
|
-
const chunks = [];
|
|
25
|
-
let size = 0;
|
|
26
|
-
for (;;) {
|
|
27
|
-
const { done, value } = await reader.read();
|
|
28
|
-
if (done)
|
|
29
|
-
break;
|
|
30
|
-
size += value.byteLength;
|
|
31
|
-
if (size > max) {
|
|
32
|
-
await reader.cancel();
|
|
33
|
-
return TOO_LARGE;
|
|
34
|
-
}
|
|
35
|
-
chunks.push(value);
|
|
36
|
-
}
|
|
37
|
-
return new TextDecoder().decode(concat(chunks, size));
|
|
38
|
-
}
|
|
39
|
-
function concat(chunks, size) {
|
|
40
|
-
const all = new Uint8Array(size);
|
|
41
|
-
let at = 0;
|
|
42
|
-
for (const chunk of chunks) {
|
|
43
|
-
all.set(chunk, at);
|
|
44
|
-
at += chunk.byteLength;
|
|
45
|
-
}
|
|
46
|
-
return all;
|
|
47
|
-
}
|
|
48
12
|
export function receive(runner, options = {}) {
|
|
49
13
|
// At CONSTRUCTION and not per call, for the reason `serve` refuses at bind: a receiver
|
|
50
14
|
// that starts and then rejects everything is found in production.
|
|
@@ -57,9 +21,15 @@ export function receive(runner, options = {}) {
|
|
|
57
21
|
if (request.method !== 'POST' || new URL(request.url).pathname !== path) {
|
|
58
22
|
return new Response(null, { status: 404 });
|
|
59
23
|
}
|
|
60
|
-
|
|
61
|
-
|
|
24
|
+
let raw;
|
|
25
|
+
try {
|
|
26
|
+
raw = await readWithin(request.body, maxFrameBytes(), request.headers.get('content-length'));
|
|
27
|
+
}
|
|
28
|
+
catch (refused) {
|
|
29
|
+
if (!(refused instanceof FougereError && refused.code === ErrorCode.PAYLOAD_TOO_LARGE))
|
|
30
|
+
throw refused;
|
|
62
31
|
return json(tooLarge(), 413);
|
|
32
|
+
}
|
|
63
33
|
let parsed;
|
|
64
34
|
try {
|
|
65
35
|
parsed = JSON.parse(raw);
|
package/dist/receive.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"receive.js","sourceRoot":"","sources":["../src/receive.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,EAAE,SAAS,EAAuB,MAAM,aAAa,CAAC;AAC7D,OAAO,EAAE,aAAa,EAAE,SAAS,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"receive.js","sourceRoot":"","sources":["../src/receive.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,EAAE,SAAS,EAAuB,MAAM,aAAa,CAAC;AAC7D,OAAO,EAAE,aAAa,EAAE,SAAS,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAC7E,OAAO,EAAE,SAAS,EAAE,YAAY,EAAE,UAAU,EAAkB,MAAM,wBAAwB,CAAC;AAS7F,MAAM,IAAI,GAAG,CAAC,IAAa,EAAE,MAAM,GAAG,GAAG,EAAE,EAAE,CAC3C,IAAI,QAAQ,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE,EAAE,CAAC,CAAC;AAElG,MAAM,UAAU,OAAO,CACrB,MAAiB,EACjB,OAAO,GAAuB,EAAE;IAEhC,uFAAuF;IACvF,kEAAkE;IAClE,IAAI,CAAC,OAAO,CAAC,MAAM,IAAI,CAAC,OAAO,CAAC,aAAa,EAAE,CAAC;QAC9C,MAAM,IAAI,KAAK,CACb,gHAAgH;cAC9G,iGAAiG,CACpG,CAAC;IACJ,CAAC;IAED,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,IAAI,SAAS,CAAC;IAEvC,OAAO,KAAK,EAAE,OAAO,EAAE,EAAE;QACvB,IAAI,OAAO,CAAC,MAAM,KAAK,MAAM,IAAI,IAAI,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,QAAQ,KAAK,IAAI,EAAE,CAAC;YACxE,OAAO,IAAI,QAAQ,CAAC,IAAI,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAAC;QAC7C,CAAC;QAED,IAAI,GAAW,CAAC;QAChB,IAAI,CAAC;YACH,GAAG,GAAG,MAAM,UAAU,CAAC,OAAO,CAAC,IAAI,EAAE,aAAa,EAAE,EAAE,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,gBAAgB,CAAC,CAAC,CAAC;QAC/F,CAAC;QAAC,OAAO,OAAO,EAAE,CAAC;YACjB,IAAI,CAAC,CAAC,OAAO,YAAY,YAAY,IAAI,OAAO,CAAC,IAAI,KAAK,SAAS,CAAC,iBAAiB,CAAC;gBAAE,MAAM,OAAO,CAAC;YAEtG,OAAO,IAAI,CAAC,QAAQ,EAAE,EAAE,GAAG,CAAC,CAAC;QAC/B,CAAC;QAED,IAAI,MAAe,CAAC;QACpB,IAAI,CAAC;YACH,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QAC3B,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,IAAI,CAAC,UAAU,EAAE,CAAC,CAAC;QAC5B,CAAC;QAED,OAAO,IAAI,CAAC,MAAM,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IACxD,CAAC,CAAC;AACJ,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"RunningReceiver.d.ts","sourceRoot":"","sources":["../../src/serve/RunningReceiver.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"RunningReceiver.d.ts","sourceRoot":"","sources":["../../src/serve/RunningReceiver.ts"],"names":[],"mappings":"AAEA,OAAO,EAAuC,KAAK,SAAS,EAAE,MAAM,eAAe,CAAC;AAIpF,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAEtD,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB;AAED,qDAAqD;AACrD,eAAO,MAAM,cAAc,UAAoC,CAAC;AAEhE,wBAAgB,KAAK,CAAC,MAAM,EAAE,SAAS,EAAE,OAAO,GAAE,YAAiB,GAAG,OAAO,CAAC,eAAe,CAAC,CAM7F"}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { createServer } from 'node:http';
|
|
2
|
+
import { Readable } from 'node:stream';
|
|
3
|
+
import { ErrorCode, FougereError, readWithin } from '@fougere/core';
|
|
2
4
|
import { handleRpc } from '../server.js';
|
|
3
5
|
import { maxFrameBytes, CALL_PATH, parseError, tooLarge } from '../policy.js';
|
|
4
6
|
/** What a frond binds when nobody says otherwise. */
|
|
@@ -34,24 +36,27 @@ function whyNotHere(host, options) {
|
|
|
34
36
|
function answering(runner, options) {
|
|
35
37
|
return async (req, res) => {
|
|
36
38
|
if (req.method !== 'POST' || req.url !== CALL_PATH) {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
const maxBodyBytes = maxFrameBytes();
|
|
41
|
-
const chunks = [];
|
|
42
|
-
let size = 0;
|
|
43
|
-
for await (const value of req) {
|
|
44
|
-
const chunk = Buffer.from(value);
|
|
45
|
-
size += chunk.length;
|
|
46
|
-
if (size > maxBodyBytes) {
|
|
47
|
-
res.writeHead(413, { 'content-type': 'application/json' }).end(JSON.stringify(tooLarge()));
|
|
39
|
+
const answered = options.beside && await options.beside(webRequestOf(req));
|
|
40
|
+
if (!answered) {
|
|
41
|
+
res.writeHead(404).end();
|
|
48
42
|
return;
|
|
49
43
|
}
|
|
50
|
-
|
|
44
|
+
res.writeHead(answered.status, Object.fromEntries(answered.headers)).end(Buffer.from(await answered.arrayBuffer()));
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
let raw;
|
|
48
|
+
try {
|
|
49
|
+
raw = await readWithin(req, maxFrameBytes(), req.headers['content-length']);
|
|
50
|
+
}
|
|
51
|
+
catch (refused) {
|
|
52
|
+
if (!(refused instanceof FougereError && refused.code === ErrorCode.PAYLOAD_TOO_LARGE))
|
|
53
|
+
throw refused;
|
|
54
|
+
res.writeHead(413, { 'content-type': 'application/json' }).end(JSON.stringify(tooLarge()));
|
|
55
|
+
return;
|
|
51
56
|
}
|
|
52
57
|
let response;
|
|
53
58
|
try {
|
|
54
|
-
response = await handleRpc(runner, JSON.parse(
|
|
59
|
+
response = await handleRpc(runner, JSON.parse(raw), options);
|
|
55
60
|
}
|
|
56
61
|
catch {
|
|
57
62
|
response = parseError();
|
|
@@ -59,6 +64,21 @@ function answering(runner, options) {
|
|
|
59
64
|
res.writeHead(200, { 'content-type': 'application/json' }).end(JSON.stringify(response));
|
|
60
65
|
};
|
|
61
66
|
}
|
|
67
|
+
/** A Node request as a Web one, its body still a stream — built only for what is not a call. */
|
|
68
|
+
function webRequestOf(req) {
|
|
69
|
+
const headers = new Headers();
|
|
70
|
+
for (const [name, value] of Object.entries(req.headers)) {
|
|
71
|
+
if (value !== undefined)
|
|
72
|
+
headers.set(name, Array.isArray(value) ? value.join(', ') : value);
|
|
73
|
+
}
|
|
74
|
+
// A message carries a body when it says so — a length, or chunks — and a POST with neither has none.
|
|
75
|
+
const reads = Number(req.headers['content-length']) > 0 || req.headers['transfer-encoding'] !== undefined;
|
|
76
|
+
return new Request(`http://${req.headers.host ?? 'localhost'}${req.url ?? '/'}`, {
|
|
77
|
+
method: req.method,
|
|
78
|
+
headers,
|
|
79
|
+
...(reads && { body: Readable.toWeb(req), duplex: 'half' }),
|
|
80
|
+
});
|
|
81
|
+
}
|
|
62
82
|
/** The port a receiver ended up on — asked of the server, since 0 means "whatever is free". */
|
|
63
83
|
function listening(server, host, options) {
|
|
64
84
|
return new Promise((resolve, reject) => {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"RunningReceiver.js","sourceRoot":"","sources":["../../src/serve/RunningReceiver.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAA0D,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"RunningReceiver.js","sourceRoot":"","sources":["../../src/serve/RunningReceiver.ts"],"names":[],"mappings":"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"}
|
|
@@ -10,5 +10,10 @@ export interface ServeOptions extends ReceiveOptions {
|
|
|
10
10
|
allowUnsigned?: boolean;
|
|
11
11
|
/** Time allowed to receive a request. Default: 15 seconds. */
|
|
12
12
|
requestTimeoutMs?: number;
|
|
13
|
+
/**
|
|
14
|
+
* What answers a request that is not a call — the REST door of the fronds this process hosts.
|
|
15
|
+
* `undefined` from it, or no `beside` at all, is a 404.
|
|
16
|
+
*/
|
|
17
|
+
beside?: (request: Request) => Promise<Response | undefined>;
|
|
13
18
|
}
|
|
14
19
|
//# sourceMappingURL=ServeOptions.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ServeOptions.d.ts","sourceRoot":"","sources":["../../src/serve/ServeOptions.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,cAAc,EAAE,MAAM,cAAc,CAAC;AAEnD,MAAM,WAAW,YAAa,SAAQ,cAAc;IAClD,uDAAuD;IACvD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,4CAA4C;IAC5C,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;IACjB,4EAA4E;IAC5E,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,0DAA0D;IAC1D,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,8DAA8D;IAC9D,gBAAgB,CAAC,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"ServeOptions.d.ts","sourceRoot":"","sources":["../../src/serve/ServeOptions.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,cAAc,EAAE,MAAM,cAAc,CAAC;AAEnD,MAAM,WAAW,YAAa,SAAQ,cAAc;IAClD,uDAAuD;IACvD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,4CAA4C;IAC5C,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;IACjB,4EAA4E;IAC5E,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,0DAA0D;IAC1D,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,8DAA8D;IAC9D,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B;;;OAGG;IACH,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,GAAG,SAAS,CAAC,CAAC;CAC9D"}
|
package/dist/server.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
/** Receiving half — unframe the call, run it, frame what comes out. */
|
|
2
|
-
import { type SignedCall, type Transport } from '@fougere/core/contract';
|
|
1
|
+
/** Receiving half — unframe the call, run it, frame what comes out: a door, in JSON-RPC terms. */
|
|
2
|
+
import { type Answering, type Door, type InvocationContext, type SignedCall, type Transport } from '@fougere/core/contract';
|
|
3
3
|
import { type RpcResponse } from './jsonrpc/RpcResponse.js';
|
|
4
4
|
/** What a receiver does with the caller's envelope. */
|
|
5
5
|
export interface ReceiveOptions {
|
|
@@ -12,6 +12,37 @@ export interface ReceiveOptions {
|
|
|
12
12
|
* Refuse a call carrying no verifiable identity — the whole of "secure by default" at the wire.
|
|
13
13
|
*/
|
|
14
14
|
requireIdentity?: boolean;
|
|
15
|
+
/** The app's cycle, which watches the request; a receiver holding only a runner runs it unwatched. */
|
|
16
|
+
answer?: Answering;
|
|
15
17
|
}
|
|
18
|
+
/** One call, unframed. */
|
|
19
|
+
export interface RpcCall {
|
|
20
|
+
address: string;
|
|
21
|
+
op: string;
|
|
22
|
+
invocation: InvocationContext;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* The door a process calls another through. The caller is a peer: its state is established by
|
|
26
|
+
* `verify` when it signed, and the call is marked `fromAnotherProcess` so the receiver does not judge again
|
|
27
|
+
* what the sender's own boundary already judged.
|
|
28
|
+
*/
|
|
29
|
+
export declare function peerDoor(runner: Transport, options?: ReceiveOptions): Door<unknown, RpcCall, RpcResponse>;
|
|
30
|
+
/**
|
|
31
|
+
* The door a page calls through — the same wire, another trust boundary. The browser sits outside
|
|
32
|
+
* the topology: its state is what the host resolved server-side, never what the payload claims,
|
|
33
|
+
* and nothing it sends is taken as already judged. Nor does it write `args`, which are taken as
|
|
34
|
+
* written — a peer's right — and `[id, draft, { id: 'admin' }]` would hand the handler a user no
|
|
35
|
+
* collector read.
|
|
36
|
+
*/
|
|
37
|
+
export interface PageDoorOptions {
|
|
38
|
+
/** What the host resolved of the caller, server-side. */
|
|
39
|
+
state: Record<string, unknown>;
|
|
40
|
+
/** The page's own HTTP request — what a cookie-kept state arrives and leaves in, written back by the host. */
|
|
41
|
+
carrier?: unknown;
|
|
42
|
+
/** Whether an announcement calls this op — one a page may not name. */
|
|
43
|
+
listens?: (address: string, op: string) => boolean | Promise<boolean>;
|
|
44
|
+
}
|
|
45
|
+
export declare function pageDoor(runner: Transport, { state, carrier, listens }: PageDoorOptions): Door<unknown, RpcCall, RpcResponse>;
|
|
46
|
+
/** A peer's call, through the app's cycle when the receiver holds one. */
|
|
16
47
|
export declare function handleRpc(runner: Transport, raw: unknown, options?: ReceiveOptions): Promise<RpcResponse>;
|
|
17
48
|
//# sourceMappingURL=server.d.ts.map
|
package/dist/server.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA,
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA,kGAAkG;AAClG,OAAO,EAC8B,KAAK,SAAS,EAAE,KAAK,IAAI,EAAE,KAAK,iBAAiB,EAAE,KAAK,UAAU,EAAE,KAAK,SAAS,EACtH,MAAM,wBAAwB,CAAC;AAGhC,OAAO,EAAE,KAAK,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAE5D,uDAAuD;AACvD,MAAM,WAAW,cAAc;IAC7B,yCAAyC;IACzC,MAAM,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,UAAU,KAAK,OAAO,CAAC;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;KAAE,CAAC,CAAC;IAClH;;OAEG;IACH,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,sGAAsG;IACtG,MAAM,CAAC,EAAE,SAAS,CAAC;CACpB;AAED,0BAA0B;AAC1B,MAAM,WAAW,OAAO;IACtB,OAAO,EAAE,MAAM,CAAC;IAChB,EAAE,EAAE,MAAM,CAAC;IACX,UAAU,EAAE,iBAAiB,CAAC;CAC/B;AAED;;;;GAIG;AACH,wBAAgB,QAAQ,CAAC,MAAM,EAAE,SAAS,EAAE,OAAO,GAAE,cAAmB,GAAG,IAAI,CAAC,OAAO,EAAE,OAAO,EAAE,WAAW,CAAC,CA+C7G;AAED;;;;;;GAMG;AACH,MAAM,WAAW,eAAe;IAC9B,yDAAyD;IACzD,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC/B,8GAA8G;IAC9G,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,uEAAuE;IACvE,OAAO,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,KAAK,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CACvE;AAED,wBAAgB,QAAQ,CAAC,MAAM,EAAE,SAAS,EAAE,EAAE,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,EAAE,eAAe,GAAG,IAAI,CAAC,OAAO,EAAE,OAAO,EAAE,WAAW,CAAC,CAgC7H;AAED,0EAA0E;AAC1E,wBAAgB,SAAS,CAAC,MAAM,EAAE,SAAS,EAAE,GAAG,EAAE,OAAO,EAAE,OAAO,GAAE,cAAmB,GAAG,OAAO,CAAC,WAAW,CAAC,CAI7G"}
|
package/dist/server.js
CHANGED
|
@@ -1,20 +1,115 @@
|
|
|
1
|
-
/** Receiving half — unframe the call, run it, frame what comes out. */
|
|
2
|
-
import { FougereError, ErrorCode,
|
|
1
|
+
/** Receiving half — unframe the call, run it, frame what comes out: a door, in JSON-RPC terms. */
|
|
2
|
+
import { answerAt, FougereError, ErrorCode, } from '@fougere/core/contract';
|
|
3
3
|
import { APP_ERROR, INVALID_REQUEST } from './jsonrpc/RpcErrorShape.js';
|
|
4
|
-
|
|
4
|
+
/**
|
|
5
|
+
* The door a process calls another through. The caller is a peer: its state is established by
|
|
6
|
+
* `verify` when it signed, and the call is marked `fromAnotherProcess` so the receiver does not judge again
|
|
7
|
+
* what the sender's own boundary already judged.
|
|
8
|
+
*/
|
|
9
|
+
export function peerDoor(runner, options = {}) {
|
|
10
|
+
return {
|
|
11
|
+
name: 'rpc',
|
|
12
|
+
decode: async (raw) => {
|
|
13
|
+
const { address, op, sent } = envelopeOf(raw);
|
|
14
|
+
/** State is ESTABLISHED here, or it is only claimed. */
|
|
15
|
+
let state = sent.state ?? {};
|
|
16
|
+
let caller;
|
|
17
|
+
if (options.verify && sent.identity) {
|
|
18
|
+
try {
|
|
19
|
+
// What ARRIVED, never what we would rather it had been — the comparison is the point.
|
|
20
|
+
({ caller, state } = await options.verify(sent.identity, { address, op,
|
|
21
|
+
params: sent.params ?? {},
|
|
22
|
+
query: sent.query ?? {},
|
|
23
|
+
input: sent.input,
|
|
24
|
+
args: sent.args,
|
|
25
|
+
runAt: sent.runAt,
|
|
26
|
+
}));
|
|
27
|
+
}
|
|
28
|
+
catch (err) {
|
|
29
|
+
throw unauthorized(err?.message ?? 'unverifiable identity', address, op);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
else if (options.requireIdentity) {
|
|
33
|
+
throw unauthorized('this receiver takes signed calls only', address, op);
|
|
34
|
+
}
|
|
35
|
+
// Built field by field, never spread from `sent`: everything here is either validated
|
|
36
|
+
// above or carried deliberately, and `caller` is ours to write alone.
|
|
37
|
+
return {
|
|
38
|
+
address,
|
|
39
|
+
op,
|
|
40
|
+
invocation: {
|
|
41
|
+
params: sent.params ?? {},
|
|
42
|
+
query: sent.query ?? {},
|
|
43
|
+
input: sent.input,
|
|
44
|
+
...(sent.args === undefined ? {} : { args: sent.args }),
|
|
45
|
+
state,
|
|
46
|
+
trace: sent.trace,
|
|
47
|
+
...(sent.runAt === undefined ? {} : { runAt: sent.runAt }),
|
|
48
|
+
...(caller ? { caller } : {}),
|
|
49
|
+
fromAnotherProcess: true,
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
},
|
|
53
|
+
run: ({ address, op, invocation }) => called(runner, address, op, invocation),
|
|
54
|
+
encode: framed,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
export function pageDoor(runner, { state, carrier, listens }) {
|
|
58
|
+
return {
|
|
59
|
+
name: 'rpc',
|
|
60
|
+
...(carrier !== undefined && { carries: 'http', carrier: () => carrier }),
|
|
61
|
+
decode: async (raw) => {
|
|
62
|
+
const { address, op, sent } = envelopeOf(raw);
|
|
63
|
+
if (sent.args !== undefined) {
|
|
64
|
+
throw new FougereError({
|
|
65
|
+
code: ErrorCode.BAD_REQUEST,
|
|
66
|
+
message: `${address}.${op}: a page sends params, query and input, not \`args\` (only fronds send \`args\`).`,
|
|
67
|
+
address,
|
|
68
|
+
operation: op,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
if (await listens?.(address, op)) {
|
|
72
|
+
throw new FougereError({
|
|
73
|
+
code: ErrorCode.NOT_FOUND,
|
|
74
|
+
message: `${address}.${op} is called by an announcement, never by a page: call the command that announces it.`,
|
|
75
|
+
address,
|
|
76
|
+
operation: op,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
return {
|
|
80
|
+
address,
|
|
81
|
+
op,
|
|
82
|
+
invocation: { params: sent.params ?? {}, query: sent.query ?? {}, input: sent.input, state, trace: sent.trace },
|
|
83
|
+
};
|
|
84
|
+
},
|
|
85
|
+
run: ({ address, op, invocation }) => called(runner, address, op, invocation),
|
|
86
|
+
encode: framed,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
/** A peer's call, through the app's cycle when the receiver holds one. */
|
|
90
|
+
export function handleRpc(runner, raw, options = {}) {
|
|
91
|
+
const door = peerDoor(runner, options);
|
|
92
|
+
return options.answer ? options.answer(door, raw) : answerAt(door, raw);
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* The members a caller may send. `caller` and `fromAnotherProcess` are written by the receiver and never claimed, and
|
|
96
|
+
* a key outside the list is refused by name: `body` used to be dropped, and the call then ran
|
|
97
|
+
* with no input and no validation.
|
|
98
|
+
*/
|
|
99
|
+
const SENT = {
|
|
100
|
+
params: true, query: true, input: true, args: true, state: true, trace: true, identity: true, runAt: true, caller: false,
|
|
101
|
+
fromAnotherProcess: false,
|
|
102
|
+
};
|
|
103
|
+
/** Where a refusal of the envelope itself says which JSON-RPC code it is, rather than an app's. */
|
|
104
|
+
const ENVELOPE = 'jsonrpc';
|
|
105
|
+
function envelopeOf(raw) {
|
|
5
106
|
const req = raw;
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
return { jsonrpc: '2.0', id, error: { code: INVALID_REQUEST, message: 'Invalid JSON-RPC 2.0 request' } };
|
|
107
|
+
if (!req || req.jsonrpc !== '2.0' || typeof req.method !== 'string' || idOf(raw) === null) {
|
|
108
|
+
throw invalid('Invalid JSON-RPC 2.0 request');
|
|
9
109
|
}
|
|
10
110
|
const dot = req.method.indexOf('.');
|
|
11
|
-
if (dot <= 0 || dot === req.method.length - 1)
|
|
12
|
-
|
|
13
|
-
jsonrpc: '2.0',
|
|
14
|
-
id,
|
|
15
|
-
error: { code: INVALID_REQUEST, message: `Invalid method '${req.method}' — expected 'address.op'` },
|
|
16
|
-
};
|
|
17
|
-
}
|
|
111
|
+
if (dot <= 0 || dot === req.method.length - 1)
|
|
112
|
+
throw invalid(`Invalid method '${req.method}' — expected 'address.op'`);
|
|
18
113
|
const address = req.method.slice(0, dot);
|
|
19
114
|
const op = req.method.slice(dot + 1);
|
|
20
115
|
// Fresh objects — middlewares deposit into state, nothing may be shared.
|
|
@@ -22,81 +117,50 @@ export async function handleRpc(runner, raw, options = {}) {
|
|
|
22
117
|
const strangers = Object.entries(sent)
|
|
23
118
|
.filter(([key, value]) => value !== undefined && !SENT[key])
|
|
24
119
|
.map(([key]) => key);
|
|
25
|
-
if (strangers.length > 0)
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
({ caller, state } = await options.verify(sent.identity, { address, op,
|
|
34
|
-
params: sent.params ?? {},
|
|
35
|
-
query: sent.query ?? {},
|
|
36
|
-
input: sent.input,
|
|
37
|
-
args: sent.args,
|
|
38
|
-
runAt: sent.runAt,
|
|
39
|
-
}));
|
|
40
|
-
}
|
|
41
|
-
catch (err) {
|
|
42
|
-
return refused(id, err?.message ?? 'unverifiable identity', address, op);
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
else if (options.requireIdentity) {
|
|
46
|
-
return refused(id, 'this receiver takes signed calls only', address, op);
|
|
120
|
+
if (strangers.length > 0) {
|
|
121
|
+
const admitted = Object.keys(SENT).filter((key) => SENT[key]);
|
|
122
|
+
throw new FougereError({
|
|
123
|
+
code: ErrorCode.BAD_REQUEST,
|
|
124
|
+
message: `Unknown invocation member ${strangers.map((key) => `'${key}'`).join(', ')} — one of ${admitted.join(', ')}.`,
|
|
125
|
+
address,
|
|
126
|
+
operation: op,
|
|
127
|
+
});
|
|
47
128
|
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
query: sent.query ?? {},
|
|
53
|
-
input: sent.input,
|
|
54
|
-
...(sent.args === undefined ? {} : { args: sent.args }),
|
|
55
|
-
state,
|
|
56
|
-
trace: sent.trace,
|
|
57
|
-
...(sent.runAt === undefined ? {} : { runAt: sent.runAt }),
|
|
58
|
-
...(caller ? { caller } : {}),
|
|
59
|
-
crossed: true,
|
|
60
|
-
};
|
|
129
|
+
return { address, op, sent };
|
|
130
|
+
}
|
|
131
|
+
/** An accident names the op it happened in, so the receiver's log says where. */
|
|
132
|
+
async function called(runner, address, op, invocation) {
|
|
61
133
|
try {
|
|
62
|
-
return
|
|
134
|
+
return await runner({ address, op }, invocation);
|
|
63
135
|
}
|
|
64
136
|
catch (err) {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
address,
|
|
71
|
-
operation: op,
|
|
72
|
-
cause: err,
|
|
73
|
-
});
|
|
74
|
-
const data = toPublicError(failure);
|
|
75
|
-
return { jsonrpc: '2.0', id, error: { code: APP_ERROR, message: data.message, data } };
|
|
137
|
+
if (err instanceof FougereError)
|
|
138
|
+
throw err;
|
|
139
|
+
throw new FougereError({
|
|
140
|
+
code: ErrorCode.INTERNAL_ERROR, message: err?.message ?? 'Internal error', address, operation: op, cause: err,
|
|
141
|
+
});
|
|
76
142
|
}
|
|
77
143
|
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
function malformed(id, strangers, address, op) {
|
|
88
|
-
const admitted = Object.keys(SENT).filter((key) => SENT[key]);
|
|
89
|
-
const data = toPublicError(new FougereError({
|
|
90
|
-
code: ErrorCode.BAD_REQUEST,
|
|
91
|
-
message: `Unknown invocation member ${strangers.map((key) => `'${key}'`).join(', ')} — one of ${admitted.join(', ')}.`,
|
|
92
|
-
address,
|
|
93
|
-
operation: op,
|
|
94
|
-
}));
|
|
144
|
+
function framed(outcome, raw) {
|
|
145
|
+
const id = idOf(raw);
|
|
146
|
+
if ('answered' in outcome)
|
|
147
|
+
return { jsonrpc: '2.0', id: id, result: outcome.answered };
|
|
148
|
+
const { refused } = outcome;
|
|
149
|
+
const envelope = refused.details?.[ENVELOPE];
|
|
150
|
+
if (typeof envelope === 'number')
|
|
151
|
+
return { jsonrpc: '2.0', id, error: { code: envelope, message: refused.message } };
|
|
152
|
+
const data = refused.toJSON();
|
|
95
153
|
return { jsonrpc: '2.0', id, error: { code: APP_ERROR, message: data.message, data } };
|
|
96
154
|
}
|
|
155
|
+
function idOf(raw) {
|
|
156
|
+
const id = raw?.id;
|
|
157
|
+
return typeof id === 'number' || typeof id === 'string' ? id : null;
|
|
158
|
+
}
|
|
159
|
+
function invalid(message) {
|
|
160
|
+
return new FougereError({ code: ErrorCode.BAD_REQUEST, message, details: { [ENVELOPE]: INVALID_REQUEST } });
|
|
161
|
+
}
|
|
97
162
|
/** An admission refusal, framed like any other failure so a caller reads one vocabulary. */
|
|
98
|
-
function
|
|
99
|
-
|
|
100
|
-
return { jsonrpc: '2.0', id, error: { code: APP_ERROR, message: data.message, data } };
|
|
163
|
+
function unauthorized(why, address, op) {
|
|
164
|
+
return new FougereError({ code: ErrorCode.UNAUTHORIZED, message: `Refused: ${why}`, address, operation: op });
|
|
101
165
|
}
|
|
102
166
|
//# sourceMappingURL=server.js.map
|
package/dist/server.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA,
|
|
1
|
+
{"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fougere/transport-http",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.26.0-alpha.0",
|
|
4
4
|
"description": "The call contract on the wire, in JSON-RPC 2.0 (server + client).",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"fougere",
|
|
@@ -38,13 +38,13 @@
|
|
|
38
38
|
"src"
|
|
39
39
|
],
|
|
40
40
|
"dependencies": {
|
|
41
|
-
"@fougere/core": "0.
|
|
41
|
+
"@fougere/core": "0.26.0-alpha.0"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
|
-
"@fougere/schema": "0.
|
|
44
|
+
"@fougere/schema": "0.26.0-alpha.0",
|
|
45
45
|
"jiti": "^2.6.1",
|
|
46
|
-
"@fougere/container": "0.
|
|
47
|
-
"@fougere/compiler": "0.
|
|
46
|
+
"@fougere/container": "0.26.0-alpha.0",
|
|
47
|
+
"@fougere/compiler": "0.26.0-alpha.0"
|
|
48
48
|
},
|
|
49
49
|
"publishConfig": {
|
|
50
50
|
"access": "public"
|
package/src/client.ts
CHANGED
|
@@ -76,9 +76,9 @@ export function createHttpTransport(baseUrl: string, options: HttpTransportOptio
|
|
|
76
76
|
// receiver choosing between a proof and a claim about the same thing.
|
|
77
77
|
// `caller` is dropped on every hop: it names who signed THIS call, so carrying the
|
|
78
78
|
// one this process was handed would make `shop → catalog → billing` read `shop`.
|
|
79
|
-
// It travels outside the envelope anyway, so a stale one would be unsigned too. `
|
|
79
|
+
// It travels outside the envelope anyway, so a stale one would be unsigned too. `fromAnotherProcess`
|
|
80
80
|
// is the next receiver's to write, for the same reason.
|
|
81
|
-
const { caller: _established,
|
|
81
|
+
const { caller: _established, fromAnotherProcess: _received, ...forwarded } = invocation;
|
|
82
82
|
const sent = options.sign
|
|
83
83
|
? { ...forwarded, state: {}, identity: await options.sign({ ...call, ...invocation }) }
|
|
84
84
|
: forwarded;
|
package/src/index.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { createHttpTransport, frameCall, unframeResponse } from './client.js';
|
|
2
|
-
export { handleRpc } from './server.js';
|
|
2
|
+
export { handleRpc, pageDoor, peerDoor, type PageDoorOptions, type ReceiveOptions, type RpcCall } from './server.js';
|
|
3
3
|
export { receive } from './receive.js';
|
|
4
4
|
export { maxFrameBytes } from './policy.js';
|
|
5
5
|
export { serve, type RunningReceiver } from './serve/RunningReceiver.js';
|
package/src/receive.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { handleRpc, type ReceiveOptions } from './server.js';
|
|
9
9
|
import { maxFrameBytes, CALL_PATH, parseError, tooLarge } from './policy.js';
|
|
10
|
-
import type
|
|
10
|
+
import { ErrorCode, FougereError, readWithin, type Transport } from '@fougere/core/contract';
|
|
11
11
|
|
|
12
12
|
export interface ReceiveHttpOptions extends ReceiveOptions {
|
|
13
13
|
/** The path this facade answers. Default: `/_fougere/call`. */
|
|
@@ -19,48 +19,6 @@ export interface ReceiveHttpOptions extends ReceiveOptions {
|
|
|
19
19
|
const json = (body: unknown, status = 200) =>
|
|
20
20
|
new Response(JSON.stringify(body), { status, headers: { 'content-type': 'application/json' } });
|
|
21
21
|
|
|
22
|
-
/** Distinguishes "the body was too large" from any body a caller could actually send. */
|
|
23
|
-
const TOO_LARGE = Symbol('too large');
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* The declared length is a cheap refusal; the count is the real one. A `content-length` may
|
|
27
|
-
* be absent, and it may lie — the copy that trusted it alone had no cap at all in that case.
|
|
28
|
-
*/
|
|
29
|
-
async function bodyWithin(request: Request, max: number): Promise<string | typeof TOO_LARGE> {
|
|
30
|
-
const declared = Number(request.headers.get('content-length'));
|
|
31
|
-
if (Number.isFinite(declared) && declared > max) return TOO_LARGE;
|
|
32
|
-
|
|
33
|
-
const reader = request.body?.getReader();
|
|
34
|
-
if (!reader) return '';
|
|
35
|
-
|
|
36
|
-
const chunks: Uint8Array[] = [];
|
|
37
|
-
let size = 0;
|
|
38
|
-
for (;;) {
|
|
39
|
-
const { done, value } = await reader.read();
|
|
40
|
-
if (done) break;
|
|
41
|
-
size += value.byteLength;
|
|
42
|
-
if (size > max) {
|
|
43
|
-
await reader.cancel();
|
|
44
|
-
|
|
45
|
-
return TOO_LARGE;
|
|
46
|
-
}
|
|
47
|
-
chunks.push(value);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
return new TextDecoder().decode(concat(chunks, size));
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function concat(chunks: readonly Uint8Array[], size: number): Uint8Array {
|
|
54
|
-
const all = new Uint8Array(size);
|
|
55
|
-
let at = 0;
|
|
56
|
-
for (const chunk of chunks) {
|
|
57
|
-
all.set(chunk, at);
|
|
58
|
-
at += chunk.byteLength;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
return all;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
22
|
export function receive(
|
|
65
23
|
runner: Transport,
|
|
66
24
|
options: ReceiveHttpOptions = {},
|
|
@@ -81,8 +39,14 @@ export function receive(
|
|
|
81
39
|
return new Response(null, { status: 404 });
|
|
82
40
|
}
|
|
83
41
|
|
|
84
|
-
|
|
85
|
-
|
|
42
|
+
let raw: string;
|
|
43
|
+
try {
|
|
44
|
+
raw = await readWithin(request.body, maxFrameBytes(), request.headers.get('content-length'));
|
|
45
|
+
} catch (refused) {
|
|
46
|
+
if (!(refused instanceof FougereError && refused.code === ErrorCode.PAYLOAD_TOO_LARGE)) throw refused;
|
|
47
|
+
|
|
48
|
+
return json(tooLarge(), 413);
|
|
49
|
+
}
|
|
86
50
|
|
|
87
51
|
let parsed: unknown;
|
|
88
52
|
try {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createServer, type IncomingMessage, type Server, type ServerResponse } from 'node:http';
|
|
2
|
-
import
|
|
2
|
+
import { Readable } from 'node:stream';
|
|
3
|
+
import { ErrorCode, FougereError, readWithin, type Transport } from '@fougere/core';
|
|
3
4
|
import type { RpcResponse } from '../jsonrpc/RpcResponse.js';
|
|
4
5
|
import { handleRpc } from '../server.js';
|
|
5
6
|
import { maxFrameBytes, CALL_PATH, parseError, tooLarge } from '../policy.js';
|
|
@@ -52,28 +53,30 @@ function whyNotHere(host: string | undefined, options: ServeOptions): Error | un
|
|
|
52
53
|
function answering(runner: Transport, options: ServeOptions) {
|
|
53
54
|
return async (req: IncomingMessage, res: ServerResponse): Promise<void> => {
|
|
54
55
|
if (req.method !== 'POST' || req.url !== CALL_PATH) {
|
|
55
|
-
|
|
56
|
+
const answered = options.beside && await options.beside(webRequestOf(req));
|
|
57
|
+
if (!answered) {
|
|
58
|
+
res.writeHead(404).end();
|
|
59
|
+
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
res.writeHead(answered.status, Object.fromEntries(answered.headers)).end(Buffer.from(await answered.arrayBuffer()));
|
|
56
63
|
|
|
57
64
|
return;
|
|
58
65
|
}
|
|
59
66
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
if (size > maxBodyBytes) {
|
|
67
|
-
res.writeHead(413, { 'content-type': 'application/json' }).end(JSON.stringify(tooLarge()));
|
|
67
|
+
let raw: string;
|
|
68
|
+
try {
|
|
69
|
+
raw = await readWithin(req, maxFrameBytes(), req.headers['content-length']);
|
|
70
|
+
} catch (refused) {
|
|
71
|
+
if (!(refused instanceof FougereError && refused.code === ErrorCode.PAYLOAD_TOO_LARGE)) throw refused;
|
|
72
|
+
res.writeHead(413, { 'content-type': 'application/json' }).end(JSON.stringify(tooLarge()));
|
|
68
73
|
|
|
69
|
-
|
|
70
|
-
}
|
|
71
|
-
chunks.push(chunk);
|
|
74
|
+
return;
|
|
72
75
|
}
|
|
73
76
|
|
|
74
77
|
let response: RpcResponse;
|
|
75
78
|
try {
|
|
76
|
-
response = await handleRpc(runner, JSON.parse(
|
|
79
|
+
response = await handleRpc(runner, JSON.parse(raw), options);
|
|
77
80
|
} catch {
|
|
78
81
|
response = parseError();
|
|
79
82
|
}
|
|
@@ -82,6 +85,22 @@ function answering(runner: Transport, options: ServeOptions) {
|
|
|
82
85
|
};
|
|
83
86
|
}
|
|
84
87
|
|
|
88
|
+
/** A Node request as a Web one, its body still a stream — built only for what is not a call. */
|
|
89
|
+
function webRequestOf(req: IncomingMessage): Request {
|
|
90
|
+
const headers = new Headers();
|
|
91
|
+
for (const [name, value] of Object.entries(req.headers)) {
|
|
92
|
+
if (value !== undefined) headers.set(name, Array.isArray(value) ? value.join(', ') : value);
|
|
93
|
+
}
|
|
94
|
+
// A message carries a body when it says so — a length, or chunks — and a POST with neither has none.
|
|
95
|
+
const reads = Number(req.headers['content-length']) > 0 || req.headers['transfer-encoding'] !== undefined;
|
|
96
|
+
|
|
97
|
+
return new Request(`http://${req.headers.host ?? 'localhost'}${req.url ?? '/'}`, {
|
|
98
|
+
method: req.method,
|
|
99
|
+
headers,
|
|
100
|
+
...(reads && { body: Readable.toWeb(req) as ReadableStream<Uint8Array>, duplex: 'half' }),
|
|
101
|
+
} as RequestInit);
|
|
102
|
+
}
|
|
103
|
+
|
|
85
104
|
/** The port a receiver ended up on — asked of the server, since 0 means "whatever is free". */
|
|
86
105
|
function listening(server: Server, host: string, options: ServeOptions): Promise<RunningReceiver> {
|
|
87
106
|
return new Promise((resolve, reject) => {
|
|
@@ -11,4 +11,9 @@ export interface ServeOptions extends ReceiveOptions {
|
|
|
11
11
|
allowUnsigned?: boolean;
|
|
12
12
|
/** Time allowed to receive a request. Default: 15 seconds. */
|
|
13
13
|
requestTimeoutMs?: number;
|
|
14
|
+
/**
|
|
15
|
+
* What answers a request that is not a call — the REST door of the fronds this process hosts.
|
|
16
|
+
* `undefined` from it, or no `beside` at all, is a 404.
|
|
17
|
+
*/
|
|
18
|
+
beside?: (request: Request) => Promise<Response | undefined>;
|
|
14
19
|
}
|
package/src/server.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
/** Receiving half — unframe the call, run it, frame what comes out. */
|
|
2
|
-
import {
|
|
1
|
+
/** Receiving half — unframe the call, run it, frame what comes out: a door, in JSON-RPC terms. */
|
|
2
|
+
import {
|
|
3
|
+
answerAt, FougereError, ErrorCode, type Answering, type Door, type InvocationContext, type SignedCall, type Transport,
|
|
4
|
+
} from '@fougere/core/contract';
|
|
3
5
|
import { APP_ERROR, INVALID_REQUEST } from './jsonrpc/RpcErrorShape.js';
|
|
4
6
|
import { type RpcRequest } from './jsonrpc/RpcRequest.js';
|
|
5
7
|
import { type RpcResponse } from './jsonrpc/RpcResponse.js';
|
|
@@ -12,24 +14,149 @@ export interface ReceiveOptions {
|
|
|
12
14
|
* Refuse a call carrying no verifiable identity — the whole of "secure by default" at the wire.
|
|
13
15
|
*/
|
|
14
16
|
requireIdentity?: boolean;
|
|
17
|
+
/** The app's cycle, which watches the request; a receiver holding only a runner runs it unwatched. */
|
|
18
|
+
answer?: Answering;
|
|
15
19
|
}
|
|
16
20
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
21
|
+
/** One call, unframed. */
|
|
22
|
+
export interface RpcCall {
|
|
23
|
+
address: string;
|
|
24
|
+
op: string;
|
|
25
|
+
invocation: InvocationContext;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* The door a process calls another through. The caller is a peer: its state is established by
|
|
30
|
+
* `verify` when it signed, and the call is marked `fromAnotherProcess` so the receiver does not judge again
|
|
31
|
+
* what the sender's own boundary already judged.
|
|
32
|
+
*/
|
|
33
|
+
export function peerDoor(runner: Transport, options: ReceiveOptions = {}): Door<unknown, RpcCall, RpcResponse> {
|
|
34
|
+
return {
|
|
35
|
+
name: 'rpc',
|
|
36
|
+
decode: async (raw) => {
|
|
37
|
+
const { address, op, sent } = envelopeOf(raw);
|
|
38
|
+
|
|
39
|
+
/** State is ESTABLISHED here, or it is only claimed. */
|
|
40
|
+
let state: Record<string, unknown> = sent.state ?? {};
|
|
41
|
+
let caller: string | undefined;
|
|
42
|
+
if (options.verify && sent.identity) {
|
|
43
|
+
try {
|
|
44
|
+
// What ARRIVED, never what we would rather it had been — the comparison is the point.
|
|
45
|
+
({ caller, state } = await options.verify(sent.identity, { address, op,
|
|
46
|
+
params: sent.params ?? {},
|
|
47
|
+
query: sent.query ?? {},
|
|
48
|
+
input: sent.input,
|
|
49
|
+
args: sent.args,
|
|
50
|
+
runAt: sent.runAt,
|
|
51
|
+
}));
|
|
52
|
+
} catch (err) {
|
|
53
|
+
throw unauthorized((err as Error)?.message ?? 'unverifiable identity', address, op);
|
|
54
|
+
}
|
|
55
|
+
} else if (options.requireIdentity) {
|
|
56
|
+
throw unauthorized('this receiver takes signed calls only', address, op);
|
|
57
|
+
}
|
|
20
58
|
|
|
21
|
-
|
|
22
|
-
|
|
59
|
+
// Built field by field, never spread from `sent`: everything here is either validated
|
|
60
|
+
// above or carried deliberately, and `caller` is ours to write alone.
|
|
61
|
+
return {
|
|
62
|
+
address,
|
|
63
|
+
op,
|
|
64
|
+
invocation: {
|
|
65
|
+
params: sent.params ?? {},
|
|
66
|
+
query: sent.query ?? {},
|
|
67
|
+
input: sent.input,
|
|
68
|
+
...(sent.args === undefined ? {} : { args: sent.args }),
|
|
69
|
+
state,
|
|
70
|
+
trace: sent.trace,
|
|
71
|
+
...(sent.runAt === undefined ? {} : { runAt: sent.runAt }),
|
|
72
|
+
...(caller ? { caller } : {}),
|
|
73
|
+
fromAnotherProcess: true,
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
},
|
|
77
|
+
run: ({ address, op, invocation }) => called(runner, address, op, invocation),
|
|
78
|
+
encode: framed,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* The door a page calls through — the same wire, another trust boundary. The browser sits outside
|
|
84
|
+
* the topology: its state is what the host resolved server-side, never what the payload claims,
|
|
85
|
+
* and nothing it sends is taken as already judged. Nor does it write `args`, which are taken as
|
|
86
|
+
* written — a peer's right — and `[id, draft, { id: 'admin' }]` would hand the handler a user no
|
|
87
|
+
* collector read.
|
|
88
|
+
*/
|
|
89
|
+
export interface PageDoorOptions {
|
|
90
|
+
/** What the host resolved of the caller, server-side. */
|
|
91
|
+
state: Record<string, unknown>;
|
|
92
|
+
/** The page's own HTTP request — what a cookie-kept state arrives and leaves in, written back by the host. */
|
|
93
|
+
carrier?: unknown;
|
|
94
|
+
/** Whether an announcement calls this op — one a page may not name. */
|
|
95
|
+
listens?: (address: string, op: string) => boolean | Promise<boolean>;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function pageDoor(runner: Transport, { state, carrier, listens }: PageDoorOptions): Door<unknown, RpcCall, RpcResponse> {
|
|
99
|
+
return {
|
|
100
|
+
name: 'rpc',
|
|
101
|
+
...(carrier !== undefined && { carries: 'http', carrier: () => carrier }),
|
|
102
|
+
decode: async (raw) => {
|
|
103
|
+
const { address, op, sent } = envelopeOf(raw);
|
|
104
|
+
if (sent.args !== undefined) {
|
|
105
|
+
throw new FougereError({
|
|
106
|
+
code: ErrorCode.BAD_REQUEST,
|
|
107
|
+
message: `${address}.${op}: a page sends params, query and input, not \`args\` (only fronds send \`args\`).`,
|
|
108
|
+
address,
|
|
109
|
+
operation: op,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
if (await listens?.(address, op)) {
|
|
113
|
+
throw new FougereError({
|
|
114
|
+
code: ErrorCode.NOT_FOUND,
|
|
115
|
+
message: `${address}.${op} is called by an announcement, never by a page: call the command that announces it.`,
|
|
116
|
+
address,
|
|
117
|
+
operation: op,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return {
|
|
122
|
+
address,
|
|
123
|
+
op,
|
|
124
|
+
invocation: { params: sent.params ?? {}, query: sent.query ?? {}, input: sent.input, state, trace: sent.trace },
|
|
125
|
+
};
|
|
126
|
+
},
|
|
127
|
+
run: ({ address, op, invocation }) => called(runner, address, op, invocation),
|
|
128
|
+
encode: framed,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** A peer's call, through the app's cycle when the receiver holds one. */
|
|
133
|
+
export function handleRpc(runner: Transport, raw: unknown, options: ReceiveOptions = {}): Promise<RpcResponse> {
|
|
134
|
+
const door = peerDoor(runner, options);
|
|
135
|
+
|
|
136
|
+
return options.answer ? options.answer(door, raw) : answerAt(door, raw);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* The members a caller may send. `caller` and `fromAnotherProcess` are written by the receiver and never claimed, and
|
|
141
|
+
* a key outside the list is refused by name: `body` used to be dropped, and the call then ran
|
|
142
|
+
* with no input and no validation.
|
|
143
|
+
*/
|
|
144
|
+
const SENT = {
|
|
145
|
+
params: true, query: true, input: true, args: true, state: true, trace: true, identity: true, runAt: true, caller: false,
|
|
146
|
+
fromAnotherProcess: false,
|
|
147
|
+
} satisfies Record<keyof InvocationContext, boolean>;
|
|
148
|
+
|
|
149
|
+
/** Where a refusal of the envelope itself says which JSON-RPC code it is, rather than an app's. */
|
|
150
|
+
const ENVELOPE = 'jsonrpc';
|
|
151
|
+
|
|
152
|
+
function envelopeOf(raw: unknown): { address: string; op: string; sent: Partial<InvocationContext> } {
|
|
153
|
+
const req = raw as Partial<RpcRequest> | null;
|
|
154
|
+
if (!req || req.jsonrpc !== '2.0' || typeof req.method !== 'string' || idOf(raw) === null) {
|
|
155
|
+
throw invalid('Invalid JSON-RPC 2.0 request');
|
|
23
156
|
}
|
|
24
157
|
|
|
25
158
|
const dot = req.method.indexOf('.');
|
|
26
|
-
if (dot <= 0 || dot === req.method.length - 1) {
|
|
27
|
-
return {
|
|
28
|
-
jsonrpc: '2.0',
|
|
29
|
-
id,
|
|
30
|
-
error: { code: INVALID_REQUEST, message: `Invalid method '${req.method}' — expected 'address.op'` },
|
|
31
|
-
};
|
|
32
|
-
}
|
|
159
|
+
if (dot <= 0 || dot === req.method.length - 1) throw invalid(`Invalid method '${req.method}' — expected 'address.op'`);
|
|
33
160
|
const address = req.method.slice(0, dot);
|
|
34
161
|
const op = req.method.slice(dot + 1);
|
|
35
162
|
|
|
@@ -38,87 +165,54 @@ export async function handleRpc(runner: Transport, raw: unknown, options: Receiv
|
|
|
38
165
|
const strangers = Object.entries(sent)
|
|
39
166
|
.filter(([key, value]) => value !== undefined && !SENT[key as keyof InvocationContext])
|
|
40
167
|
.map(([key]) => key);
|
|
41
|
-
if (strangers.length > 0)
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
({ caller, state } = await options.verify(sent.identity, { address, op,
|
|
50
|
-
params: sent.params ?? {},
|
|
51
|
-
query: sent.query ?? {},
|
|
52
|
-
input: sent.input,
|
|
53
|
-
args: sent.args,
|
|
54
|
-
runAt: sent.runAt,
|
|
55
|
-
}));
|
|
56
|
-
} catch (err) {
|
|
57
|
-
return refused(id, (err as Error)?.message ?? 'unverifiable identity', address, op);
|
|
58
|
-
}
|
|
59
|
-
} else if (options.requireIdentity) {
|
|
60
|
-
return refused(id, 'this receiver takes signed calls only', address, op);
|
|
168
|
+
if (strangers.length > 0) {
|
|
169
|
+
const admitted = Object.keys(SENT).filter((key) => SENT[key as keyof InvocationContext]);
|
|
170
|
+
throw new FougereError({
|
|
171
|
+
code: ErrorCode.BAD_REQUEST,
|
|
172
|
+
message: `Unknown invocation member ${strangers.map((key) => `'${key}'`).join(', ')} — one of ${admitted.join(', ')}.`,
|
|
173
|
+
address,
|
|
174
|
+
operation: op,
|
|
175
|
+
});
|
|
61
176
|
}
|
|
62
177
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
const invocation: InvocationContext = {
|
|
66
|
-
params: sent.params ?? {},
|
|
67
|
-
query: sent.query ?? {},
|
|
68
|
-
input: sent.input,
|
|
69
|
-
...(sent.args === undefined ? {} : { args: sent.args }),
|
|
70
|
-
state,
|
|
71
|
-
trace: sent.trace,
|
|
72
|
-
...(sent.runAt === undefined ? {} : { runAt: sent.runAt }),
|
|
73
|
-
...(caller ? { caller } : {}),
|
|
74
|
-
crossed: true,
|
|
75
|
-
};
|
|
178
|
+
return { address, op, sent };
|
|
179
|
+
}
|
|
76
180
|
|
|
181
|
+
/** An accident names the op it happened in, so the receiver's log says where. */
|
|
182
|
+
async function called(runner: Transport, address: string, op: string, invocation: InvocationContext): Promise<unknown> {
|
|
77
183
|
try {
|
|
78
|
-
return
|
|
184
|
+
return await runner({ address, op }, invocation);
|
|
79
185
|
} catch (err) {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
:
|
|
83
|
-
|
|
84
|
-
message: (err as Error)?.message ?? 'Internal error',
|
|
85
|
-
address,
|
|
86
|
-
operation: op,
|
|
87
|
-
cause: err,
|
|
88
|
-
});
|
|
89
|
-
const data = toPublicError(failure);
|
|
90
|
-
|
|
91
|
-
return { jsonrpc: '2.0', id, error: { code: APP_ERROR, message: data.message, data } };
|
|
186
|
+
if (err instanceof FougereError) throw err;
|
|
187
|
+
throw new FougereError({
|
|
188
|
+
code: ErrorCode.INTERNAL_ERROR, message: (err as Error)?.message ?? 'Internal error', address, operation: op, cause: err,
|
|
189
|
+
});
|
|
92
190
|
}
|
|
93
191
|
}
|
|
94
192
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
* with no input and no validation.
|
|
99
|
-
*/
|
|
100
|
-
const SENT = {
|
|
101
|
-
params: true, query: true, input: true, args: true, state: true, trace: true, identity: true, runAt: true, caller: false,
|
|
102
|
-
crossed: false,
|
|
103
|
-
} satisfies Record<keyof InvocationContext, boolean>;
|
|
193
|
+
function framed(outcome: { answered: unknown } | { refused: FougereError }, raw: unknown): RpcResponse {
|
|
194
|
+
const id = idOf(raw);
|
|
195
|
+
if ('answered' in outcome) return { jsonrpc: '2.0', id: id!, result: outcome.answered };
|
|
104
196
|
|
|
105
|
-
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
message: `Unknown invocation member ${strangers.map((key) => `'${key}'`).join(', ')} — one of ${admitted.join(', ')}.`,
|
|
110
|
-
address,
|
|
111
|
-
operation: op,
|
|
112
|
-
}));
|
|
197
|
+
const { refused } = outcome;
|
|
198
|
+
const envelope = (refused.details as Record<string, unknown> | undefined)?.[ENVELOPE];
|
|
199
|
+
if (typeof envelope === 'number') return { jsonrpc: '2.0', id, error: { code: envelope, message: refused.message } };
|
|
200
|
+
const data = refused.toJSON();
|
|
113
201
|
|
|
114
202
|
return { jsonrpc: '2.0', id, error: { code: APP_ERROR, message: data.message, data } };
|
|
115
203
|
}
|
|
116
204
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
const data = toPublicError(
|
|
120
|
-
new FougereError({ code: ErrorCode.UNAUTHORIZED, message: `Refused: ${why}`, address, operation: op }),
|
|
121
|
-
);
|
|
205
|
+
function idOf(raw: unknown): string | number | null {
|
|
206
|
+
const id = (raw as Partial<RpcRequest> | null)?.id;
|
|
122
207
|
|
|
123
|
-
return
|
|
208
|
+
return typeof id === 'number' || typeof id === 'string' ? id : null;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function invalid(message: string): FougereError {
|
|
212
|
+
return new FougereError({ code: ErrorCode.BAD_REQUEST, message, details: { [ENVELOPE]: INVALID_REQUEST } });
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** An admission refusal, framed like any other failure so a caller reads one vocabulary. */
|
|
216
|
+
function unauthorized(why: string, address: string, op: string): FougereError {
|
|
217
|
+
return new FougereError({ code: ErrorCode.UNAUTHORIZED, message: `Refused: ${why}`, address, operation: op });
|
|
124
218
|
}
|