@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
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ACCOUNT, b64decode,
|
|
1
|
+
import { ACCOUNT, b64decode, bufferedBody, claimant, requests, responseHeaders, sha256, TUNNEL, tunnelUrl } from "./shared.js";
|
|
2
2
|
const obj = (v) => (v && typeof v === 'object' && !Array.isArray(v) ? v : {});
|
|
3
3
|
const streams = (s) => (s.state.streams ??= new Map());
|
|
4
4
|
const headersOf = (v) => { const h = new Headers(); for (const [k, x] of Object.entries(obj(v)))
|
|
@@ -11,29 +11,42 @@ function reject(s, message, code, reason) {
|
|
|
11
11
|
async function register(s, msg, ctx) {
|
|
12
12
|
const token = typeof msg.secret === 'string' ? msg.secret : '';
|
|
13
13
|
const account = token ? ctx.rowsRaw(ACCOUNT).find((a) => a.deleted !== true && a.token_sha256 === sha256(ctx, token)) : undefined;
|
|
14
|
+
// source: spec:/x-reference/relay "Invalid tunnel secret"
|
|
14
15
|
if (!account)
|
|
15
|
-
return reject(s, '
|
|
16
|
+
return reject(s, 'Invalid tunnel secret', 4003, 'Invalid tunnel secret');
|
|
16
17
|
// the control URL's ?id= names the tunnel (the routing relay's authority), else the register frame's, else one minted
|
|
17
18
|
// This id names a stored reservation: mint it, then record the reservation with its owner below. ctx.issue is for
|
|
18
19
|
// issued ids whose subjects the vendor does not store; its empty row would look like another account's reservation.
|
|
20
|
+
// source: spec:/x-reference/relay "Prefer the ?id= captured at accept (authoritative"
|
|
19
21
|
const id = String(s.state.tunnelId || msg.tunnelId || ctx.mint(TUNNEL));
|
|
20
|
-
if (!validId(id))
|
|
21
|
-
return reject(s, `Tunnel rejected: invalid tunnel id ${id}`, 4029, 'invalidId');
|
|
22
22
|
const held = ctx.row(TUNNEL, id);
|
|
23
|
-
//
|
|
23
|
+
// source: spec:/x-reference/relay "Tunnel ID reserved"
|
|
24
24
|
if (held && held._owner !== account.id)
|
|
25
|
-
return reject(s, `Tunnel
|
|
26
|
-
const live =
|
|
25
|
+
return reject(s, `Tunnel ID '${id}' is reserved by another account.`, 4002, 'Tunnel ID reserved');
|
|
26
|
+
const live = claimant(ctx, id);
|
|
27
27
|
if (live && live !== s) {
|
|
28
|
-
//
|
|
28
|
+
// source: spec:/x-reference/relay "Pass { replace: true } to take over."
|
|
29
29
|
if (msg.replace !== true)
|
|
30
|
-
return reject(s, `Tunnel
|
|
30
|
+
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');
|
|
31
|
+
// source: spec:/x-reference/relay "Tunnel replaced"
|
|
32
|
+
live.state.superseded = true;
|
|
33
|
+
live.state.disconnectReason = 'Tunnel replaced';
|
|
31
34
|
live.state.registered = false;
|
|
35
|
+
// source: spec:/x-reference/relay "Replaced by new client"
|
|
32
36
|
live.close(4001, 'Replaced by new client');
|
|
33
37
|
}
|
|
34
|
-
|
|
35
|
-
|
|
38
|
+
Object.assign(s.state, { registering: true, tunnelId: id, account: account.id });
|
|
39
|
+
try {
|
|
40
|
+
if (!held)
|
|
41
|
+
await ctx.record(TUNNEL, { _owner: account.id, reservedAt: ctx.occurredAt }, id);
|
|
42
|
+
}
|
|
43
|
+
finally {
|
|
44
|
+
s.state.registering = false;
|
|
45
|
+
}
|
|
46
|
+
if (s.state.superseded === true || !ctx.sockets('control').includes(s))
|
|
47
|
+
return;
|
|
36
48
|
Object.assign(s.state, { registered: true, tunnelId: id, account: account.id, authRequired: msg.authRequired !== false });
|
|
49
|
+
// source: spec:/x-reference/protocol "Acknowledges a successful registration; carries the public URL + usage."
|
|
37
50
|
s.send({ type: 'registered', tunnelId: id, url: tunnelUrl(id) });
|
|
38
51
|
}
|
|
39
52
|
export const control = {
|
|
@@ -53,22 +66,66 @@ export const control = {
|
|
|
53
66
|
if (s.state.registered !== true)
|
|
54
67
|
return;
|
|
55
68
|
const reqId = String(msg.reqId ?? '');
|
|
69
|
+
const request = requests(s).get(reqId);
|
|
70
|
+
// source: spec:/x-reference/protocol "A complete (buffered) response"
|
|
56
71
|
if (msg.type === 'response') {
|
|
57
|
-
|
|
72
|
+
if (!request)
|
|
73
|
+
return;
|
|
74
|
+
const headers = responseHeaders(headersOf(msg.headers), request);
|
|
75
|
+
const body = bufferedBody(msg.body);
|
|
76
|
+
// source: spec:/x-reference/relay "Malformed tunnel response"
|
|
77
|
+
if (body === undefined)
|
|
78
|
+
s.fulfil(reqId, new Response('Malformed tunnel response', { status: 502, headers }));
|
|
79
|
+
else
|
|
80
|
+
s.fulfil(reqId, new Response(request.method === 'HEAD' ? null : body, { status: Number(msg.status || 200), headers }));
|
|
81
|
+
// source: spec:/x-reference/protocol "Start of a streamed response"
|
|
58
82
|
}
|
|
59
83
|
else if (msg.type === 'response-start') {
|
|
60
|
-
|
|
61
|
-
|
|
84
|
+
if (!request)
|
|
85
|
+
return;
|
|
86
|
+
const status = Number(msg.status || 200);
|
|
87
|
+
const headers = responseHeaders(headersOf(msg.headers), request);
|
|
88
|
+
// source: spec:/x-reference/relay "HEAD is bodyless by contract too."
|
|
89
|
+
if (request.method === 'HEAD' || status === 204 || status === 205 || status === 304) {
|
|
90
|
+
s.fulfil(reqId, new Response(null, { status, headers }));
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
const body = new ReadableStream({ start: (c) => { streams(s).set(reqId, c); }, cancel: () => cancelStream(s, reqId) });
|
|
94
|
+
armStreamIdle(s, reqId, ctx);
|
|
95
|
+
s.fulfil(reqId, new Response(body, { status, headers }));
|
|
96
|
+
}
|
|
97
|
+
// source: spec:/x-reference/protocol "One streamed response body chunk."
|
|
62
98
|
}
|
|
63
99
|
else if (msg.type === 'response-chunk') {
|
|
64
|
-
streams(s).get(reqId)
|
|
100
|
+
const stream = streams(s).get(reqId);
|
|
101
|
+
if (stream) {
|
|
102
|
+
try {
|
|
103
|
+
stream.enqueue(b64decode(String(msg.data ?? '')));
|
|
104
|
+
s.fulfil(`idle:${reqId}`, null);
|
|
105
|
+
armStreamIdle(s, reqId, ctx);
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
streams(s).delete(reqId);
|
|
109
|
+
s.fulfil(`idle:${reqId}`, null);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
// source: spec:/x-reference/protocol "End of a streamed response."
|
|
65
113
|
}
|
|
66
114
|
else if (msg.type === 'response-end') {
|
|
67
|
-
streams(s).get(reqId)
|
|
115
|
+
const stream = streams(s).get(reqId);
|
|
68
116
|
streams(s).delete(reqId);
|
|
117
|
+
s.fulfil(`idle:${reqId}`, null);
|
|
118
|
+
if (stream) {
|
|
119
|
+
try {
|
|
120
|
+
stream.close();
|
|
121
|
+
}
|
|
122
|
+
catch { /* already closed */ }
|
|
123
|
+
}
|
|
69
124
|
}
|
|
70
125
|
},
|
|
71
126
|
close(s) {
|
|
127
|
+
s.state.registered = false;
|
|
128
|
+
s.state.closed = true;
|
|
72
129
|
for (const c of streams(s).values()) {
|
|
73
130
|
try {
|
|
74
131
|
c.error(new Error('Tunnel closed'));
|
|
@@ -78,3 +135,27 @@ export const control = {
|
|
|
78
135
|
streams(s).clear();
|
|
79
136
|
},
|
|
80
137
|
};
|
|
138
|
+
// source: spec:/x-reference/relay "Idle watchdog for a streaming response"
|
|
139
|
+
function armStreamIdle(s, reqId, ctx) {
|
|
140
|
+
// source: spec:/x-reference/relay "120000"
|
|
141
|
+
void s.expect(`idle:${reqId}`, 120_000).catch(() => { if (ctx.sockets('control').includes(s))
|
|
142
|
+
expireStream(s, reqId); });
|
|
143
|
+
}
|
|
144
|
+
// source: spec:/x-reference/relay "stream idle timeout"
|
|
145
|
+
function expireStream(s, reqId) {
|
|
146
|
+
const stream = streams(s).get(reqId);
|
|
147
|
+
if (!stream)
|
|
148
|
+
return;
|
|
149
|
+
streams(s).delete(reqId);
|
|
150
|
+
try {
|
|
151
|
+
stream.error(new Error('stream idle timeout'));
|
|
152
|
+
}
|
|
153
|
+
catch { /* already closed */ }
|
|
154
|
+
s.send({ type: 'request-abort', reqId });
|
|
155
|
+
}
|
|
156
|
+
// source: spec:/x-reference/protocol "The relay asks the client to abort an in-flight request."
|
|
157
|
+
function cancelStream(s, reqId) {
|
|
158
|
+
streams(s).delete(reqId);
|
|
159
|
+
s.fulfil(`idle:${reqId}`, null);
|
|
160
|
+
s.send({ type: 'request-abort', reqId });
|
|
161
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
{
|
|
2
|
+
"vendor": "tunnel",
|
|
3
|
+
"missing": 9,
|
|
4
|
+
"stale": [],
|
|
5
|
+
"code": {
|
|
6
|
+
"lines": 255,
|
|
7
|
+
"run": 242,
|
|
8
|
+
"recorded": 4,
|
|
9
|
+
"missing": 9
|
|
10
|
+
},
|
|
11
|
+
"state": {
|
|
12
|
+
"declared": 0,
|
|
13
|
+
"reached": 0,
|
|
14
|
+
"recorded": 0,
|
|
15
|
+
"missing": [],
|
|
16
|
+
"undeclared": []
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
{
|
|
20
|
+
"file": "semantics/doors.ts",
|
|
21
|
+
"lines": {
|
|
22
|
+
"found": 13,
|
|
23
|
+
"hit": 13
|
|
24
|
+
},
|
|
25
|
+
"missing": []
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"file": "semantics/sockets.ts",
|
|
29
|
+
"lines": {
|
|
30
|
+
"found": 82,
|
|
31
|
+
"hit": 74
|
|
32
|
+
},
|
|
33
|
+
"missing": [
|
|
34
|
+
{
|
|
35
|
+
"from": 114,
|
|
36
|
+
"to": 118,
|
|
37
|
+
"fn": "expireStream"
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
"from": 123,
|
|
41
|
+
"to": 125,
|
|
42
|
+
"fn": "cancelStream"
|
|
43
|
+
}
|
|
44
|
+
]
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
"file": "semantics/shared.ts",
|
|
48
|
+
"lines": {
|
|
49
|
+
"found": 46,
|
|
50
|
+
"hit": 46
|
|
51
|
+
},
|
|
52
|
+
"missing": []
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
"file": "semantics/around.ts",
|
|
56
|
+
"lines": {
|
|
57
|
+
"found": 40,
|
|
58
|
+
"hit": 39
|
|
59
|
+
},
|
|
60
|
+
"missing": [
|
|
61
|
+
{
|
|
62
|
+
"from": 66,
|
|
63
|
+
"to": 66,
|
|
64
|
+
"fn": "timeout"
|
|
65
|
+
}
|
|
66
|
+
]
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
"file": "semantics/index.ts",
|
|
70
|
+
"lines": {
|
|
71
|
+
"found": 2,
|
|
72
|
+
"hit": 2
|
|
73
|
+
},
|
|
74
|
+
"missing": []
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
"file": "manifest.ts",
|
|
78
|
+
"lines": {
|
|
79
|
+
"found": 43,
|
|
80
|
+
"hit": 43
|
|
81
|
+
},
|
|
82
|
+
"missing": []
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
"file": "index.ts",
|
|
86
|
+
"lines": {
|
|
87
|
+
"found": 7,
|
|
88
|
+
"hit": 7
|
|
89
|
+
},
|
|
90
|
+
"missing": []
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
"file": "server.ts",
|
|
94
|
+
"lines": {
|
|
95
|
+
"found": 6,
|
|
96
|
+
"hit": 2
|
|
97
|
+
},
|
|
98
|
+
"missing": []
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
"file": "fetch.ts",
|
|
102
|
+
"lines": {
|
|
103
|
+
"found": 16,
|
|
104
|
+
"hit": 16
|
|
105
|
+
},
|
|
106
|
+
"missing": []
|
|
107
|
+
}
|
|
108
|
+
]
|
|
109
|
+
}
|