@volter/twin-tunnel 1.0.0 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -1
- package/dist/journeys/coverage.json +109 -0
- package/dist/journeys/customer-life.json +588 -0
- package/dist/journeys/customer-life.outline.md +34 -0
- package/dist/journeys/customer-life.outline.verdict.json +42 -0
- package/dist/journeys/customer-life.verdict.json +51 -0
- package/dist/journeys/decisions.json +23 -0
- package/dist/journeys/demand.json +24 -0
- package/dist/journeys/score.json +134 -0
- package/dist/journeys/unreachable.json +12 -0
- package/dist/journeys/vendor-examples.json +7 -0
- package/dist/journeys/vendor-examples.verdict.json +18 -0
- package/dist/spec/SOURCE.md +30 -14
- package/dist/spec/openapi.json +47 -15
- package/dist/spec/sources.json +1 -0
- package/dist/src/index.js +4 -3
- package/dist/src/manifest.js +7 -0
- package/dist/src/semantics/around.js +42 -9
- package/dist/src/semantics/doors.js +2 -2
- package/dist/src/semantics/shared.d.ts +8 -3
- package/dist/src/semantics/shared.js +61 -3
- package/dist/src/semantics/sockets.js +97 -16
- package/journeys/coverage.json +109 -0
- package/journeys/customer-life.json +588 -0
- package/journeys/customer-life.outline.md +34 -0
- package/journeys/customer-life.outline.verdict.json +42 -0
- package/journeys/customer-life.verdict.json +51 -0
- package/journeys/decisions.json +23 -0
- package/journeys/demand.json +24 -0
- package/journeys/score.json +134 -0
- package/journeys/unreachable.json +12 -0
- package/journeys/vendor-examples.json +7 -0
- package/journeys/vendor-examples.verdict.json +18 -0
- package/package.json +3 -2
- package/spec/SOURCE.md +30 -14
- package/spec/openapi.json +47 -15
- package/spec/sources.json +1 -0
- package/src/index.ts +5 -4
- package/src/manifest.ts +7 -0
- package/src/semantics/around.ts +37 -10
- package/src/semantics/doors.ts +2 -2
- package/src/semantics/shared.ts +56 -4
- package/src/semantics/sockets.ts +72 -14
package/src/semantics/sockets.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// and the client answers with a whole `response` or a stream (`response-start`, `response-chunk`s, `response-end`),
|
|
4
4
|
// which settle the request waiting on it by its `reqId`. Frames as volter-tunnel's protocol.ts defines them.
|
|
5
5
|
import type { HandlerContext, SocketEngine, SocketSession } from '@volter/world-core';
|
|
6
|
-
import { ACCOUNT, b64decode,
|
|
6
|
+
import { ACCOUNT, b64decode, bufferedBody, claimant, requests, responseHeaders, type Row, sha256, TUNNEL, tunnelUrl } from './shared.ts';
|
|
7
7
|
|
|
8
8
|
type Streams = Map<string, ReadableStreamDefaultController<Uint8Array>>;
|
|
9
9
|
const obj = (v: unknown): Row => (v && typeof v === 'object' && !Array.isArray(v) ? v as Row : {});
|
|
@@ -19,24 +19,33 @@ function reject(s: SocketSession, message: string, code: number, reason: string)
|
|
|
19
19
|
async function register(s: SocketSession, msg: Row, ctx: HandlerContext): Promise<void> {
|
|
20
20
|
const token = typeof msg.secret === 'string' ? msg.secret : '';
|
|
21
21
|
const account = token ? ctx.rowsRaw(ACCOUNT).find((a) => a.deleted !== true && a.token_sha256 === sha256(ctx, token)) : undefined;
|
|
22
|
-
|
|
22
|
+
// source: spec:/x-reference/relay "Invalid tunnel secret"
|
|
23
|
+
if (!account) return reject(s, 'Invalid tunnel secret', 4003, 'Invalid tunnel secret');
|
|
23
24
|
// the control URL's ?id= names the tunnel (the routing relay's authority), else the register frame's, else one minted
|
|
24
25
|
// This id names a stored reservation: mint it, then record the reservation with its owner below. ctx.issue is for
|
|
25
26
|
// issued ids whose subjects the vendor does not store; its empty row would look like another account's reservation.
|
|
27
|
+
// source: spec:/x-reference/relay "Prefer the ?id= captured at accept (authoritative"
|
|
26
28
|
const id = String(s.state.tunnelId || msg.tunnelId || ctx.mint(TUNNEL));
|
|
27
|
-
if (!validId(id)) return reject(s, `Tunnel rejected: invalid tunnel id ${id}`, 4029, 'invalidId');
|
|
28
29
|
const held = ctx.row(TUNNEL, id);
|
|
29
|
-
//
|
|
30
|
-
if (held && held._owner !== account.id) return reject(s, `Tunnel
|
|
31
|
-
const live =
|
|
30
|
+
// source: spec:/x-reference/relay "Tunnel ID reserved"
|
|
31
|
+
if (held && held._owner !== account.id) return reject(s, `Tunnel ID '${id}' is reserved by another account.`, 4002, 'Tunnel ID reserved');
|
|
32
|
+
const live = claimant(ctx, id);
|
|
32
33
|
if (live && live !== s) {
|
|
33
|
-
//
|
|
34
|
-
if (msg.replace !== true) return reject(s, `Tunnel
|
|
34
|
+
// source: spec:/x-reference/relay "Pass { replace: true } to take over."
|
|
35
|
+
if (msg.replace !== true) return reject(s, `Tunnel ID '${id}' is already in use by another client. Pass { replace: true } to take over.`, 4002, 'Tunnel ID already in use');
|
|
36
|
+
// source: spec:/x-reference/relay "Tunnel replaced"
|
|
37
|
+
live.state.superseded = true;
|
|
38
|
+
live.state.disconnectReason = 'Tunnel replaced';
|
|
35
39
|
live.state.registered = false;
|
|
40
|
+
// source: spec:/x-reference/relay "Replaced by new client"
|
|
36
41
|
live.close(4001, 'Replaced by new client');
|
|
37
42
|
}
|
|
38
|
-
|
|
43
|
+
Object.assign(s.state, { registering: true, tunnelId: id, account: account.id });
|
|
44
|
+
try { if (!held) await ctx.record(TUNNEL, { _owner: account.id, reservedAt: ctx.occurredAt }, id); }
|
|
45
|
+
finally { s.state.registering = false; }
|
|
46
|
+
if (s.state.superseded === true || !ctx.sockets('control').includes(s)) return;
|
|
39
47
|
Object.assign(s.state, { registered: true, tunnelId: id, account: account.id, authRequired: msg.authRequired !== false });
|
|
48
|
+
// source: spec:/x-reference/protocol "Acknowledges a successful registration; carries the public URL + usage."
|
|
40
49
|
s.send({ type: 'registered', tunnelId: id, url: tunnelUrl(id) });
|
|
41
50
|
}
|
|
42
51
|
|
|
@@ -50,20 +59,69 @@ export const control: SocketEngine<HandlerContext> = {
|
|
|
50
59
|
if (msg.type === 'register') return register(s, msg, ctx);
|
|
51
60
|
if (s.state.registered !== true) return;
|
|
52
61
|
const reqId = String(msg.reqId ?? '');
|
|
62
|
+
const request = requests(s).get(reqId);
|
|
63
|
+
// source: spec:/x-reference/protocol "A complete (buffered) response"
|
|
53
64
|
if (msg.type === 'response') {
|
|
54
|
-
|
|
65
|
+
if (!request) return;
|
|
66
|
+
const headers = responseHeaders(headersOf(msg.headers), request);
|
|
67
|
+
const body = bufferedBody(msg.body);
|
|
68
|
+
// source: spec:/x-reference/relay "Malformed tunnel response"
|
|
69
|
+
if (body === undefined) s.fulfil(reqId, new Response('Malformed tunnel response', { status: 502, headers }));
|
|
70
|
+
else s.fulfil(reqId, new Response(request.method === 'HEAD' ? null : body, { status: Number(msg.status || 200), headers }));
|
|
71
|
+
// source: spec:/x-reference/protocol "Start of a streamed response"
|
|
55
72
|
} else if (msg.type === 'response-start') {
|
|
56
|
-
|
|
57
|
-
|
|
73
|
+
if (!request) return;
|
|
74
|
+
const status = Number(msg.status || 200);
|
|
75
|
+
const headers = responseHeaders(headersOf(msg.headers), request);
|
|
76
|
+
// source: spec:/x-reference/relay "HEAD is bodyless by contract too."
|
|
77
|
+
if (request.method === 'HEAD' || status === 204 || status === 205 || status === 304) {
|
|
78
|
+
s.fulfil(reqId, new Response(null, { status, headers }));
|
|
79
|
+
} else {
|
|
80
|
+
const body = new ReadableStream<Uint8Array>({ start: (c) => { streams(s).set(reqId, c); }, cancel: () => cancelStream(s, reqId) });
|
|
81
|
+
armStreamIdle(s, reqId, ctx);
|
|
82
|
+
s.fulfil(reqId, new Response(body, { status, headers }));
|
|
83
|
+
}
|
|
84
|
+
// source: spec:/x-reference/protocol "One streamed response body chunk."
|
|
58
85
|
} else if (msg.type === 'response-chunk') {
|
|
59
|
-
streams(s).get(reqId)
|
|
86
|
+
const stream = streams(s).get(reqId);
|
|
87
|
+
if (stream) {
|
|
88
|
+
try { stream.enqueue(b64decode(String(msg.data ?? ''))); s.fulfil(`idle:${reqId}`, null); armStreamIdle(s, reqId, ctx); }
|
|
89
|
+
catch { streams(s).delete(reqId); s.fulfil(`idle:${reqId}`, null); }
|
|
90
|
+
}
|
|
91
|
+
// source: spec:/x-reference/protocol "End of a streamed response."
|
|
60
92
|
} else if (msg.type === 'response-end') {
|
|
61
|
-
streams(s).get(reqId)
|
|
93
|
+
const stream = streams(s).get(reqId);
|
|
62
94
|
streams(s).delete(reqId);
|
|
95
|
+
s.fulfil(`idle:${reqId}`, null);
|
|
96
|
+
if (stream) { try { stream.close(); } catch { /* already closed */ } }
|
|
63
97
|
}
|
|
64
98
|
},
|
|
65
99
|
close(s) {
|
|
100
|
+
s.state.registered = false;
|
|
101
|
+
s.state.closed = true;
|
|
66
102
|
for (const c of streams(s).values()) { try { c.error(new Error('Tunnel closed')); } catch { /* already ended */ } }
|
|
67
103
|
streams(s).clear();
|
|
68
104
|
},
|
|
69
105
|
};
|
|
106
|
+
|
|
107
|
+
// source: spec:/x-reference/relay "Idle watchdog for a streaming response"
|
|
108
|
+
function armStreamIdle(s: SocketSession, reqId: string, ctx: HandlerContext): void {
|
|
109
|
+
// source: spec:/x-reference/relay "120000"
|
|
110
|
+
void s.expect(`idle:${reqId}`, 120_000).catch(() => { if (ctx.sockets('control').includes(s)) expireStream(s, reqId); });
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// source: spec:/x-reference/relay "stream idle timeout"
|
|
114
|
+
function expireStream(s: SocketSession, reqId: string): void {
|
|
115
|
+
const stream = streams(s).get(reqId);
|
|
116
|
+
if (!stream) return;
|
|
117
|
+
streams(s).delete(reqId);
|
|
118
|
+
try { stream.error(new Error('stream idle timeout')); } catch { /* already closed */ }
|
|
119
|
+
s.send({ type: 'request-abort', reqId });
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// source: spec:/x-reference/protocol "The relay asks the client to abort an in-flight request."
|
|
123
|
+
function cancelStream(s: SocketSession, reqId: string): void {
|
|
124
|
+
streams(s).delete(reqId);
|
|
125
|
+
s.fulfil(`idle:${reqId}`, null);
|
|
126
|
+
s.send({ type: 'request-abort', reqId });
|
|
127
|
+
}
|