@uniflowed/router 0.37.0 → 0.38.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/internal/closure-seal.js +417 -0
- package/package.json +4 -4
- package/rsc.js +63 -0
|
@@ -0,0 +1,417 @@
|
|
|
1
|
+
// @noflow
|
|
2
|
+
//
|
|
3
|
+
// Synchronous ChaCha20-Poly1305 (RFC 8439) for an inline server closure.
|
|
4
|
+
//
|
|
5
|
+
// The seal has to be synchronous: WebCrypto's `subtle` is async, and awaiting
|
|
6
|
+
// it would make `bindServerClosure` a promise the component cannot return as a
|
|
7
|
+
// prop. `node:crypto` is synchronous and is not there on a worker, so this
|
|
8
|
+
// file is the cipher. It imports nothing. The build id is a key and never
|
|
9
|
+
// appears in an error, a log, or the blob's associated data — the action id
|
|
10
|
+
// does, so a blob sealed for one action is refused by another.
|
|
11
|
+
|
|
12
|
+
const REFUSED = "server action closure was refused";
|
|
13
|
+
const BUILD_ID_SHORT = "UF_BUILD_ID is missing or shorter than 8 bytes";
|
|
14
|
+
const BUILD_ID_LONG = "UF_BUILD_ID is longer than 256 bytes";
|
|
15
|
+
const P = (1n << 130n) - 5n;
|
|
16
|
+
const MOD_128 = (1n << 128n) - 1n;
|
|
17
|
+
const BASE64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
18
|
+
|
|
19
|
+
const SHA256_K = new Uint32Array([
|
|
20
|
+
0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
|
|
21
|
+
0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
|
|
22
|
+
0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
|
|
23
|
+
0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
|
|
24
|
+
0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
|
|
25
|
+
0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
|
|
26
|
+
0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
|
|
27
|
+
0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2,
|
|
28
|
+
]);
|
|
29
|
+
|
|
30
|
+
/** @param {Uint8Array} key @param {Uint8Array} nonce @param {Uint8Array} aad @param {Uint8Array} plaintext */
|
|
31
|
+
export function aeadSeal(key, nonce, aad, plaintext) {
|
|
32
|
+
requireLength("key", key, 32);
|
|
33
|
+
requireLength("nonce", nonce, 12);
|
|
34
|
+
const ciphertext = chachaXor(key, 1, nonce, plaintext);
|
|
35
|
+
const tag = poly1305(oneTimeKey(key, nonce), macData(aad, ciphertext));
|
|
36
|
+
return { ciphertext, tag };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Seal `plaintext` for one action. The result is standard base64 of
|
|
41
|
+
* `nonce || ciphertext || tag` and does not contain the build id.
|
|
42
|
+
*
|
|
43
|
+
* @param {string} buildId
|
|
44
|
+
* @param {string} actionId
|
|
45
|
+
* @param {string} plaintext
|
|
46
|
+
*/
|
|
47
|
+
export function sealCaptures(buildId, actionId, plaintext) {
|
|
48
|
+
const nonce = new Uint8Array(12);
|
|
49
|
+
crypto.getRandomValues(nonce);
|
|
50
|
+
return sealWithNonce(buildId, actionId, plaintext, nonce);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* `sealCaptures` with the nonce supplied, so a test can match another
|
|
55
|
+
* implementation byte for byte.
|
|
56
|
+
*
|
|
57
|
+
* @param {string} buildId
|
|
58
|
+
* @param {string} actionId
|
|
59
|
+
* @param {string} plaintext
|
|
60
|
+
* @param {Uint8Array} nonce
|
|
61
|
+
*/
|
|
62
|
+
export function sealWithNonce(buildId, actionId, plaintext, nonce) {
|
|
63
|
+
const key = buildKey(buildId);
|
|
64
|
+
requireLength("nonce", nonce, 12);
|
|
65
|
+
const sealed = aeadSeal(key, nonce, utf8(String(actionId)), utf8(String(plaintext)));
|
|
66
|
+
return encodeBase64(concat([nonce, sealed.ciphertext, sealed.tag]));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* The plaintext, or a refusal that names nothing about the blob or the key.
|
|
71
|
+
*
|
|
72
|
+
* A missing or over-long build id is a different error: the process is
|
|
73
|
+
* misconfigured, and saying so does not publish the id.
|
|
74
|
+
*
|
|
75
|
+
* @param {string} buildId
|
|
76
|
+
* @param {string} actionId
|
|
77
|
+
* @param {string} sealed
|
|
78
|
+
*/
|
|
79
|
+
export function openCaptures(buildId, actionId, sealed) {
|
|
80
|
+
const key = buildKey(buildId);
|
|
81
|
+
const bytes = decodeBase64(sealed);
|
|
82
|
+
if (bytes == null || bytes.length < 12 + 16) throw refused();
|
|
83
|
+
const nonce = bytes.subarray(0, 12);
|
|
84
|
+
const tag = bytes.subarray(bytes.length - 16);
|
|
85
|
+
const ciphertext = bytes.subarray(12, bytes.length - 16);
|
|
86
|
+
const aad = utf8(String(actionId));
|
|
87
|
+
const expect = poly1305(oneTimeKey(key, nonce), macData(aad, ciphertext));
|
|
88
|
+
if (!sameBytes(expect, tag)) throw refused();
|
|
89
|
+
const plain = chachaXor(key, 1, nonce, ciphertext);
|
|
90
|
+
try {
|
|
91
|
+
return new TextDecoder("utf-8", { fatal: true }).decode(plain);
|
|
92
|
+
} catch {
|
|
93
|
+
throw refused();
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** @param {string | undefined} buildId */
|
|
98
|
+
function buildKey(buildId) {
|
|
99
|
+
if (typeof buildId !== "string" || buildId.length === 0) throw new Error(BUILD_ID_SHORT);
|
|
100
|
+
const bytes = utf8(buildId);
|
|
101
|
+
if (bytes.length < 8) throw new Error(BUILD_ID_SHORT);
|
|
102
|
+
if (bytes.length > 256) throw new Error(BUILD_ID_LONG);
|
|
103
|
+
return sha256(bytes);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function refused() {
|
|
107
|
+
return new Error(REFUSED);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** @param {Uint8Array} key @param {Uint8Array} nonce */
|
|
111
|
+
function oneTimeKey(key, nonce) {
|
|
112
|
+
return chachaBlock(key, 0, nonce).subarray(0, 32);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** @param {Uint8Array} aad @param {Uint8Array} ciphertext */
|
|
116
|
+
function macData(aad, ciphertext) {
|
|
117
|
+
return concat([pad16(aad), pad16(ciphertext), le64(aad.length), le64(ciphertext.length)]);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** @param {Uint8Array} data */
|
|
121
|
+
function pad16(data) {
|
|
122
|
+
const extra = (16 - (data.length % 16)) % 16;
|
|
123
|
+
if (extra === 0) return data;
|
|
124
|
+
const out = new Uint8Array(data.length + extra);
|
|
125
|
+
out.set(data);
|
|
126
|
+
return out;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** @param {number} length */
|
|
130
|
+
function le64(length) {
|
|
131
|
+
const out = new Uint8Array(8);
|
|
132
|
+
let value = BigInt(length);
|
|
133
|
+
for (let index = 0; index < 8; index += 1) {
|
|
134
|
+
out[index] = Number(value & 0xffn);
|
|
135
|
+
value >>= 8n;
|
|
136
|
+
}
|
|
137
|
+
return out;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* @param {Uint8Array} key
|
|
142
|
+
* @param {number} counter
|
|
143
|
+
* @param {Uint8Array} nonce
|
|
144
|
+
*/
|
|
145
|
+
function chachaBlock(key, counter, nonce) {
|
|
146
|
+
const state = new Uint32Array(16);
|
|
147
|
+
state[0] = 0x61707865;
|
|
148
|
+
state[1] = 0x3320646e;
|
|
149
|
+
state[2] = 0x79622d32;
|
|
150
|
+
state[3] = 0x6b206574;
|
|
151
|
+
for (let index = 0; index < 8; index += 1) state[4 + index] = readU32(key, index * 4);
|
|
152
|
+
state[12] = counter >>> 0;
|
|
153
|
+
state[13] = readU32(nonce, 0);
|
|
154
|
+
state[14] = readU32(nonce, 4);
|
|
155
|
+
state[15] = readU32(nonce, 8);
|
|
156
|
+
const working = state.slice();
|
|
157
|
+
for (let round = 0; round < 10; round += 1) {
|
|
158
|
+
quarter(working, 0, 4, 8, 12);
|
|
159
|
+
quarter(working, 1, 5, 9, 13);
|
|
160
|
+
quarter(working, 2, 6, 10, 14);
|
|
161
|
+
quarter(working, 3, 7, 11, 15);
|
|
162
|
+
quarter(working, 0, 5, 10, 15);
|
|
163
|
+
quarter(working, 1, 6, 11, 12);
|
|
164
|
+
quarter(working, 2, 7, 8, 13);
|
|
165
|
+
quarter(working, 3, 4, 9, 14);
|
|
166
|
+
}
|
|
167
|
+
const out = new Uint8Array(64);
|
|
168
|
+
for (let index = 0; index < 16; index += 1) {
|
|
169
|
+
writeU32(out, index * 4, (working[index] + state[index]) >>> 0);
|
|
170
|
+
}
|
|
171
|
+
return out;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** @param {Uint32Array} state @param {number} a @param {number} b @param {number} c @param {number} d */
|
|
175
|
+
function quarter(state, a, b, c, d) {
|
|
176
|
+
state[a] = (state[a] + state[b]) >>> 0;
|
|
177
|
+
state[d] = rotl(state[d] ^ state[a], 16);
|
|
178
|
+
state[c] = (state[c] + state[d]) >>> 0;
|
|
179
|
+
state[b] = rotl(state[b] ^ state[c], 12);
|
|
180
|
+
state[a] = (state[a] + state[b]) >>> 0;
|
|
181
|
+
state[d] = rotl(state[d] ^ state[a], 8);
|
|
182
|
+
state[c] = (state[c] + state[d]) >>> 0;
|
|
183
|
+
state[b] = rotl(state[b] ^ state[c], 7);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** @param {number} value @param {number} bits */
|
|
187
|
+
function rotl(value, bits) {
|
|
188
|
+
return ((value << bits) | (value >>> (32 - bits))) >>> 0;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* @param {Uint8Array} key
|
|
193
|
+
* @param {number} counter
|
|
194
|
+
* @param {Uint8Array} nonce
|
|
195
|
+
* @param {Uint8Array} data
|
|
196
|
+
*/
|
|
197
|
+
function chachaXor(key, counter, nonce, data) {
|
|
198
|
+
const out = new Uint8Array(data.length);
|
|
199
|
+
let offset = 0;
|
|
200
|
+
let block = counter >>> 0;
|
|
201
|
+
while (offset < data.length) {
|
|
202
|
+
const stream = chachaBlock(key, block, nonce);
|
|
203
|
+
const take = Math.min(64, data.length - offset);
|
|
204
|
+
for (let index = 0; index < take; index += 1)
|
|
205
|
+
out[offset + index] = data[offset + index] ^ stream[index];
|
|
206
|
+
offset += take;
|
|
207
|
+
block = (block + 1) >>> 0;
|
|
208
|
+
}
|
|
209
|
+
return out;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/** @param {Uint8Array} key @param {Uint8Array} data */
|
|
213
|
+
function poly1305(key, data) {
|
|
214
|
+
const r = clamped(key.subarray(0, 16));
|
|
215
|
+
const s = little(key.subarray(16, 32));
|
|
216
|
+
let acc = 0n;
|
|
217
|
+
for (let offset = 0; offset < data.length; offset += 16) {
|
|
218
|
+
const take = Math.min(16, data.length - offset);
|
|
219
|
+
const block = new Uint8Array(take + 1);
|
|
220
|
+
block.set(data.subarray(offset, offset + take));
|
|
221
|
+
block[take] = 1;
|
|
222
|
+
acc = ((acc + little(block)) * r) % P;
|
|
223
|
+
}
|
|
224
|
+
return bytesOf((acc + s) & MOD_128, 16);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** @param {Uint8Array} bytes */
|
|
228
|
+
function clamped(bytes) {
|
|
229
|
+
const copy = bytes.slice();
|
|
230
|
+
copy[3] &= 15;
|
|
231
|
+
copy[7] &= 15;
|
|
232
|
+
copy[11] &= 15;
|
|
233
|
+
copy[15] &= 15;
|
|
234
|
+
copy[4] &= 252;
|
|
235
|
+
copy[8] &= 252;
|
|
236
|
+
copy[12] &= 252;
|
|
237
|
+
return little(copy);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/** @param {Uint8Array} bytes */
|
|
241
|
+
function little(bytes) {
|
|
242
|
+
let value = 0n;
|
|
243
|
+
for (let index = bytes.length - 1; index >= 0; index -= 1) {
|
|
244
|
+
value = (value << 8n) | BigInt(bytes[index]);
|
|
245
|
+
}
|
|
246
|
+
return value;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/** @param {bigint} value @param {number} length */
|
|
250
|
+
function bytesOf(value, length) {
|
|
251
|
+
const out = new Uint8Array(length);
|
|
252
|
+
let rest = value;
|
|
253
|
+
for (let index = 0; index < length; index += 1) {
|
|
254
|
+
out[index] = Number(rest & 0xffn);
|
|
255
|
+
rest >>= 8n;
|
|
256
|
+
}
|
|
257
|
+
return out;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/** @param {Uint8Array} bytes */
|
|
261
|
+
function sha256(bytes) {
|
|
262
|
+
const bitLength = BigInt(bytes.length) * 8n;
|
|
263
|
+
const withBit = bytes.length + 1;
|
|
264
|
+
const zeroPad = (64 + 56 - (withBit % 64)) % 64;
|
|
265
|
+
const padded = new Uint8Array(withBit + zeroPad + 8);
|
|
266
|
+
padded.set(bytes);
|
|
267
|
+
padded[bytes.length] = 0x80;
|
|
268
|
+
const view = new DataView(padded.buffer);
|
|
269
|
+
view.setUint32(padded.length - 8, Number((bitLength >> 32n) & 0xffffffffn));
|
|
270
|
+
view.setUint32(padded.length - 4, Number(bitLength & 0xffffffffn));
|
|
271
|
+
|
|
272
|
+
const hash = new Uint32Array([
|
|
273
|
+
0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19,
|
|
274
|
+
]);
|
|
275
|
+
const words = new Uint32Array(64);
|
|
276
|
+
for (let offset = 0; offset < padded.length; offset += 64) {
|
|
277
|
+
for (let index = 0; index < 16; index += 1) words[index] = view.getUint32(offset + index * 4);
|
|
278
|
+
for (let index = 16; index < 64; index += 1) {
|
|
279
|
+
const s0 =
|
|
280
|
+
rotr(words[index - 15], 7) ^ rotr(words[index - 15], 18) ^ (words[index - 15] >>> 3);
|
|
281
|
+
const s1 =
|
|
282
|
+
rotr(words[index - 2], 17) ^ rotr(words[index - 2], 19) ^ (words[index - 2] >>> 10);
|
|
283
|
+
words[index] = (words[index - 16] + s0 + words[index - 7] + s1) >>> 0;
|
|
284
|
+
}
|
|
285
|
+
let a = hash[0];
|
|
286
|
+
let b = hash[1];
|
|
287
|
+
let c = hash[2];
|
|
288
|
+
let d = hash[3];
|
|
289
|
+
let e = hash[4];
|
|
290
|
+
let f = hash[5];
|
|
291
|
+
let g = hash[6];
|
|
292
|
+
let h = hash[7];
|
|
293
|
+
for (let index = 0; index < 64; index += 1) {
|
|
294
|
+
const s1 = rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25);
|
|
295
|
+
const ch = (e & f) ^ (~e & g);
|
|
296
|
+
const temp1 = (h + s1 + ch + SHA256_K[index] + words[index]) >>> 0;
|
|
297
|
+
const s0 = rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22);
|
|
298
|
+
const maj = (a & b) ^ (a & c) ^ (b & c);
|
|
299
|
+
const temp2 = (s0 + maj) >>> 0;
|
|
300
|
+
h = g;
|
|
301
|
+
g = f;
|
|
302
|
+
f = e;
|
|
303
|
+
e = (d + temp1) >>> 0;
|
|
304
|
+
d = c;
|
|
305
|
+
c = b;
|
|
306
|
+
b = a;
|
|
307
|
+
a = (temp1 + temp2) >>> 0;
|
|
308
|
+
}
|
|
309
|
+
hash[0] = (hash[0] + a) >>> 0;
|
|
310
|
+
hash[1] = (hash[1] + b) >>> 0;
|
|
311
|
+
hash[2] = (hash[2] + c) >>> 0;
|
|
312
|
+
hash[3] = (hash[3] + d) >>> 0;
|
|
313
|
+
hash[4] = (hash[4] + e) >>> 0;
|
|
314
|
+
hash[5] = (hash[5] + f) >>> 0;
|
|
315
|
+
hash[6] = (hash[6] + g) >>> 0;
|
|
316
|
+
hash[7] = (hash[7] + h) >>> 0;
|
|
317
|
+
}
|
|
318
|
+
const out = new Uint8Array(32);
|
|
319
|
+
const outView = new DataView(out.buffer);
|
|
320
|
+
for (let index = 0; index < 8; index += 1) outView.setUint32(index * 4, hash[index]);
|
|
321
|
+
return out;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/** @param {number} value @param {number} bits */
|
|
325
|
+
function rotr(value, bits) {
|
|
326
|
+
return ((value >>> bits) | (value << (32 - bits))) >>> 0;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/** @param {Uint8Array} bytes @param {number} offset */
|
|
330
|
+
function readU32(bytes, offset) {
|
|
331
|
+
return (
|
|
332
|
+
(bytes[offset] |
|
|
333
|
+
(bytes[offset + 1] << 8) |
|
|
334
|
+
(bytes[offset + 2] << 16) |
|
|
335
|
+
(bytes[offset + 3] << 24)) >>>
|
|
336
|
+
0
|
|
337
|
+
);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/** @param {Uint8Array} bytes @param {number} offset @param {number} value */
|
|
341
|
+
function writeU32(bytes, offset, value) {
|
|
342
|
+
bytes[offset] = value & 0xff;
|
|
343
|
+
bytes[offset + 1] = (value >>> 8) & 0xff;
|
|
344
|
+
bytes[offset + 2] = (value >>> 16) & 0xff;
|
|
345
|
+
bytes[offset + 3] = (value >>> 24) & 0xff;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/** @param {string} text */
|
|
349
|
+
function utf8(text) {
|
|
350
|
+
return new TextEncoder().encode(text);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/** @param {Array<Uint8Array>} parts */
|
|
354
|
+
function concat(parts) {
|
|
355
|
+
let size = 0;
|
|
356
|
+
for (const part of parts) size += part.length;
|
|
357
|
+
const out = new Uint8Array(size);
|
|
358
|
+
let offset = 0;
|
|
359
|
+
for (const part of parts) {
|
|
360
|
+
out.set(part, offset);
|
|
361
|
+
offset += part.length;
|
|
362
|
+
}
|
|
363
|
+
return out;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/** @param {Uint8Array} bytes */
|
|
367
|
+
function encodeBase64(bytes) {
|
|
368
|
+
let out = "";
|
|
369
|
+
for (let index = 0; index < bytes.length; index += 3) {
|
|
370
|
+
const b0 = bytes[index];
|
|
371
|
+
const b1 = index + 1 < bytes.length ? bytes[index + 1] : 0;
|
|
372
|
+
const b2 = index + 2 < bytes.length ? bytes[index + 2] : 0;
|
|
373
|
+
const triple = (b0 << 16) | (b1 << 8) | b2;
|
|
374
|
+
out += BASE64[(triple >> 18) & 63];
|
|
375
|
+
out += BASE64[(triple >> 12) & 63];
|
|
376
|
+
out += index + 1 < bytes.length ? BASE64[(triple >> 6) & 63] : "=";
|
|
377
|
+
out += index + 2 < bytes.length ? BASE64[triple & 63] : "=";
|
|
378
|
+
}
|
|
379
|
+
return out;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/** @param {string} text */
|
|
383
|
+
function decodeBase64(text) {
|
|
384
|
+
if (typeof text !== "string" || text.length === 0 || text.length % 4 !== 0) return null;
|
|
385
|
+
const out = [];
|
|
386
|
+
for (let index = 0; index < text.length; index += 4) {
|
|
387
|
+
const c0 = BASE64.indexOf(text[index]);
|
|
388
|
+
const c1 = BASE64.indexOf(text[index + 1]);
|
|
389
|
+
const pad2 = text[index + 2] === "=";
|
|
390
|
+
const pad3 = text[index + 3] === "=";
|
|
391
|
+
const c2 = pad2 ? 0 : BASE64.indexOf(text[index + 2]);
|
|
392
|
+
const c3 = pad3 ? 0 : BASE64.indexOf(text[index + 3]);
|
|
393
|
+
if (c0 < 0 || c1 < 0 || c2 < 0 || c3 < 0) return null;
|
|
394
|
+
if ((pad2 || pad3) && index + 4 !== text.length) return null;
|
|
395
|
+
if (pad2 && !pad3) return null;
|
|
396
|
+
const triple = (c0 << 18) | (c1 << 12) | (c2 << 6) | c3;
|
|
397
|
+
out.push((triple >> 16) & 255);
|
|
398
|
+
if (!pad2) out.push((triple >> 8) & 255);
|
|
399
|
+
if (!pad3) out.push(triple & 255);
|
|
400
|
+
}
|
|
401
|
+
return Uint8Array.from(out);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/** @param {Uint8Array} left @param {Uint8Array} right */
|
|
405
|
+
function sameBytes(left, right) {
|
|
406
|
+
if (left.length !== right.length) return false;
|
|
407
|
+
let diff = 0;
|
|
408
|
+
for (let index = 0; index < left.length; index += 1) diff |= left[index] ^ right[index];
|
|
409
|
+
return diff === 0;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/** @param {string} name @param {Uint8Array} bytes @param {number} length */
|
|
413
|
+
function requireLength(name, bytes, length) {
|
|
414
|
+
if (!(bytes instanceof Uint8Array) || bytes.length !== length) {
|
|
415
|
+
throw new Error(`${name} must be ${String(length)} bytes`);
|
|
416
|
+
}
|
|
417
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uniflowed/router",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.38.0",
|
|
4
4
|
"description": "The file-system router for Flow React applications: matching, layouts, loaders, navigation, server rendering and hydration.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -73,8 +73,8 @@
|
|
|
73
73
|
}
|
|
74
74
|
},
|
|
75
75
|
"dependencies": {
|
|
76
|
-
"@uniflowed/hooks": "0.
|
|
77
|
-
"@uniflowed/server": "0.
|
|
78
|
-
"@uniflowed/validator": "0.
|
|
76
|
+
"@uniflowed/hooks": "0.38.0",
|
|
77
|
+
"@uniflowed/server": "0.38.0",
|
|
78
|
+
"@uniflowed/validator": "0.38.0"
|
|
79
79
|
}
|
|
80
80
|
}
|
package/rsc.js
CHANGED
|
@@ -44,6 +44,8 @@
|
|
|
44
44
|
// in this graph, and a boundary module without the directive is refused here,
|
|
45
45
|
// with its file named, rather than by React with a sentence about functions.
|
|
46
46
|
|
|
47
|
+
import { decodeActionArguments, encodeActionArguments } from "./internal/action-wire.js";
|
|
48
|
+
import { openCaptures, sealCaptures } from "./internal/closure-seal.js";
|
|
47
49
|
import { traceLoader } from "./internal/server-instrumentation.js";
|
|
48
50
|
|
|
49
51
|
import * as React from "react";
|
|
@@ -119,6 +121,67 @@ export function registerServerFunction<T>(fn: T, id: string, name: string): T {
|
|
|
119
121
|
return fn;
|
|
120
122
|
}
|
|
121
123
|
|
|
124
|
+
/**
|
|
125
|
+
* Bind an inline `"use server"` closure to the captures it closed over.
|
|
126
|
+
*
|
|
127
|
+
* The rewrite in `uf_rsc` hoists the closure to `$$ufActionN` and leaves this
|
|
128
|
+
* call where the closure was written. The values are sealed with the build id
|
|
129
|
+
* before the function is handed to a Client Component, and the hoisted export
|
|
130
|
+
* opens them. What the browser receives is the sealed blob, as a bound
|
|
131
|
+
* argument, never the values and never the build id.
|
|
132
|
+
*
|
|
133
|
+
* The return is untyped on purpose. The captured names keep whatever type the
|
|
134
|
+
* body already had only after `unsealServerCaptures`, and this call cannot
|
|
135
|
+
* see those types: they are not arguments of the closure.
|
|
136
|
+
*/
|
|
137
|
+
function closureBuildId(): string {
|
|
138
|
+
// A Worker has no `process` until the generated entry installs one. Reading
|
|
139
|
+
// the global unconditionally throws before the seal can say the id is missing.
|
|
140
|
+
if (typeof process === "undefined") return "";
|
|
141
|
+
const named = process.env.UF_BUILD_ID;
|
|
142
|
+
return typeof named === "string" ? named : "";
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function bindServerClosure(
|
|
146
|
+
fn : (...args: $ReadOnlyArray<mixed>) => mixed,
|
|
147
|
+
id : string,
|
|
148
|
+
captures: $ReadOnlyArray<mixed>,
|
|
149
|
+
): $FlowFixMe {
|
|
150
|
+
if (typeof fn !== "function") return fn;
|
|
151
|
+
const buildId = closureBuildId();
|
|
152
|
+
const sealed = sealCaptures(buildId, id, encodeActionArguments(captures));
|
|
153
|
+
// Register first. React's server-reference `bind` records the sealed blob on
|
|
154
|
+
// `$$bound`, which is what Flight sends and the client posts back. A native
|
|
155
|
+
// bind made before registration leaves `$$bound` null, so the hoisted export
|
|
156
|
+
// is called without the blob.
|
|
157
|
+
const registered = registerServerFunction(fn, id, id);
|
|
158
|
+
return registered.bind(null, sealed);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Open the blob `bindServerClosure` sealed for this action.
|
|
163
|
+
*
|
|
164
|
+
* Called from the hoisted export, including when the closure captured
|
|
165
|
+
* nothing, so a tampered blob is refused before the body runs. A missing
|
|
166
|
+
* build id is reported as itself. Every other failure is one sentence that
|
|
167
|
+
* contains neither the blob nor a captured value.
|
|
168
|
+
*/
|
|
169
|
+
export function unsealServerCaptures(sealed: string, actionId: string): $FlowFixMe {
|
|
170
|
+
const buildId = closureBuildId();
|
|
171
|
+
let plaintext;
|
|
172
|
+
try {
|
|
173
|
+
plaintext = openCaptures(buildId, actionId, sealed);
|
|
174
|
+
} catch (error) {
|
|
175
|
+
if (error instanceof Error && error.message.startsWith("UF_BUILD_ID")) throw error;
|
|
176
|
+
throw new Error("server action closure was refused");
|
|
177
|
+
}
|
|
178
|
+
try {
|
|
179
|
+
return decodeActionArguments(plaintext);
|
|
180
|
+
} catch {
|
|
181
|
+
throw new Error("server action closure was refused");
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
122
185
|
/** What a host may tell the renderer about one render. */
|
|
123
186
|
export type FlightOptions = {|
|
|
124
187
|
/**
|