@farm.js/islx 0.0.6 → 0.0.7
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/CHANGELOG.md +17 -0
- package/README.md +1 -1
- package/SPEC.md +7 -4
- package/dist/_chunks/{actions-BuS5jDi0.mjs → actions-CvQ0-zjg.mjs} +15 -14
- package/dist/react/server.mjs +7 -3
- package/dist/server.d.mts +4 -4
- package/dist/server.mjs +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.0.7
|
|
4
|
+
|
|
5
|
+
Cloudflare Workers start, and sealed captured values name their key with 4 bytes.
|
|
6
|
+
|
|
7
|
+
- `@farm.js/islx/react/server` no longer generates random values while it loads. It made the
|
|
8
|
+
promise-name prefix with `crypto.getRandomValues` at module scope, which Workers reject
|
|
9
|
+
("Disallowed operation called within global scope"), so no islx app started on Cloudflare
|
|
10
|
+
since promise props (0.0.4). The prefix is made at the first render now.
|
|
11
|
+
- An inline `"use server"` function's captured values are sealed with format version 2: the key id
|
|
12
|
+
is the first 4 bytes of the key's SHA-256 instead of the first byte. With one byte, a deploy that
|
|
13
|
+
changed keys without keeping the old one gave the new key the same id one time in 256, and
|
|
14
|
+
values from pages rendered before it failed as invalid (`400`) instead of stale (the page
|
|
15
|
+
reloads). An app that does not set `ISLX_ACTIONS_KEY` gets a new key on every build, so this hit
|
|
16
|
+
about one deploy in 256.
|
|
17
|
+
- Values sealed by 0.0.6 or earlier (format version 1) are stale: forms on pages open across the
|
|
18
|
+
upgrade reload once.
|
|
19
|
+
|
|
3
20
|
## 0.0.6
|
|
4
21
|
|
|
5
22
|
Server action interceptor, early clicks on TanStack Start, library islands under pnpm.
|
package/README.md
CHANGED
|
@@ -13,7 +13,7 @@ islx started as [Farm.js](https://github.com/farming-labs/farm.js)'s islands mod
|
|
|
13
13
|
replaces React Server Components' Flight payload. On a 300-page docs app it ships 26% less
|
|
14
14
|
JavaScript, and a client navigation sends 2.5 KB instead of 12 KB.
|
|
15
15
|
|
|
16
|
-
> Status: 0.0.
|
|
16
|
+
> Status: 0.0.7. React, Vite and TanStack Start are supported; other renderers and framework
|
|
17
17
|
> integrations follow. The wire format is in [SPEC.md](./SPEC.md), changes in
|
|
18
18
|
> [CHANGELOG.md](./CHANGELOG.md).
|
|
19
19
|
|
package/SPEC.md
CHANGED
|
@@ -198,14 +198,17 @@ travel with the action, sealed, so the browser can neither read nor change them:
|
|
|
198
198
|
sealed = base64url( version ‖ key id ‖ IV ‖ AES-256-GCM(ciphertext ‖ tag) )
|
|
199
199
|
```
|
|
200
200
|
|
|
201
|
-
- `version` is `
|
|
201
|
+
- `version` is `2` (one byte); `key id` is the first 4 bytes of SHA-256 of the key, big-endian;
|
|
202
|
+
the IV is 12 random bytes;
|
|
202
203
|
- the additional data is `islx:action:<id>`, so values sealed for one action never open for
|
|
203
204
|
another;
|
|
204
205
|
- the plaintext is the captured values as a tagged JSON array.
|
|
205
206
|
|
|
206
|
-
A value sealed with a key the server does not have is **stale** (the
|
|
207
|
-
does not open is **invalid** (`400`). Servers may hold previous keys,
|
|
208
|
-
key rotation keep working. Every server instance must share the keys.
|
|
207
|
+
A value sealed with a key the server does not have, or in another `version`, is **stale** (the
|
|
208
|
+
page reloads); anything that does not open is **invalid** (`400`). Servers may hold previous keys,
|
|
209
|
+
so pages rendered before a key rotation keep working. Every server instance must share the keys.
|
|
210
|
+
Version 1 named the key with one byte, so one key rotation in 256 made values from open pages
|
|
211
|
+
invalid instead of stale; its values are stale now.
|
|
209
212
|
|
|
210
213
|
## Integration model
|
|
211
214
|
|
|
@@ -24,10 +24,11 @@ import { a as encode, r as decode, t as EncodeError } from "./codec-C8to-RQW.mjs
|
|
|
24
24
|
* Sealing is asynchronous. React's server renderer waits for it: a form whose action's values
|
|
25
25
|
* are still sealing suspends, and renders once they are ready.
|
|
26
26
|
*
|
|
27
|
-
* sealed = base64url( version (1 byte) ‖ key id (
|
|
27
|
+
* sealed = base64url( version (1 byte) ‖ key id (4 bytes) ‖ IV (12 bytes) ‖ ciphertext+tag )
|
|
28
28
|
*/
|
|
29
|
-
const VERSION =
|
|
29
|
+
const VERSION = 2;
|
|
30
30
|
const IV_BYTES = 12;
|
|
31
|
+
const HEADER_BYTES = 5;
|
|
31
32
|
const KEY_BYTES = 32;
|
|
32
33
|
/** Thrown when a sealed value does not open: changed, made for another action, or stale. */
|
|
33
34
|
var SealError = class extends Error {
|
|
@@ -58,7 +59,7 @@ function fromBase64url(text) {
|
|
|
58
59
|
return null;
|
|
59
60
|
}
|
|
60
61
|
}
|
|
61
|
-
/** A new random key, as base64url: what `
|
|
62
|
+
/** A new random key, as base64url: what `ISLX_ACTIONS_KEY` holds. */
|
|
62
63
|
function generateActionKey() {
|
|
63
64
|
return base64url(crypto.getRandomValues(new Uint8Array(KEY_BYTES)));
|
|
64
65
|
}
|
|
@@ -67,10 +68,10 @@ async function importKey(secret) {
|
|
|
67
68
|
if (!bytes || bytes.length !== KEY_BYTES) throw new TypeError("an actions key must be 32 random bytes as base64url; create one with generateActionKey() from @farm.js/islx/server");
|
|
68
69
|
return crypto.subtle.importKey("raw", bytes, "AES-GCM", false, ["encrypt", "decrypt"]);
|
|
69
70
|
}
|
|
70
|
-
/** The key's id:
|
|
71
|
+
/** The key's id: the first 4 bytes of its SHA-256, so a value names the key that sealed it. */
|
|
71
72
|
async function keyId(secret) {
|
|
72
73
|
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(secret.trim()));
|
|
73
|
-
return new
|
|
74
|
+
return new DataView(digest).getUint32(0);
|
|
74
75
|
}
|
|
75
76
|
/**
|
|
76
77
|
* The keys values are sealed and opened with. `current` seals; `previous` keys still open
|
|
@@ -128,11 +129,11 @@ async function sealBound(keys, actionId, values, options = {}) {
|
|
|
128
129
|
iv,
|
|
129
130
|
additionalData: additionalData(actionId)
|
|
130
131
|
}, keys.current, new TextEncoder().encode(json)));
|
|
131
|
-
const sealed = new Uint8Array(
|
|
132
|
+
const sealed = new Uint8Array(17 + ciphertext.length);
|
|
132
133
|
sealed[0] = VERSION;
|
|
133
|
-
sealed
|
|
134
|
-
sealed.set(iv,
|
|
135
|
-
sealed.set(ciphertext,
|
|
134
|
+
new DataView(sealed.buffer).setUint32(1, keys.currentId);
|
|
135
|
+
sealed.set(iv, HEADER_BYTES);
|
|
136
|
+
sealed.set(ciphertext, 17);
|
|
136
137
|
return base64url(sealed);
|
|
137
138
|
}
|
|
138
139
|
/**
|
|
@@ -141,17 +142,17 @@ async function sealBound(keys, actionId, values, options = {}) {
|
|
|
141
142
|
*/
|
|
142
143
|
async function unsealBound(keys, actionId, sealed, options = {}) {
|
|
143
144
|
const bytes = fromBase64url(sealed);
|
|
144
|
-
if (
|
|
145
|
-
if (bytes
|
|
146
|
-
const candidates = keys.byId.get(bytes
|
|
145
|
+
if (bytes && bytes.length > 0 && bytes[0] !== VERSION) throw new SealError("stale", "sealed action values use another format version");
|
|
146
|
+
if (!bytes || bytes.length < 33) throw new SealError("invalid", "sealed action values are malformed");
|
|
147
|
+
const candidates = keys.byId.get(new DataView(bytes.buffer).getUint32(1));
|
|
147
148
|
if (!candidates) throw new SealError("stale", "sealed action values use a key this server does not have");
|
|
148
149
|
let plaintext;
|
|
149
150
|
for (const key of candidates) try {
|
|
150
151
|
plaintext = await crypto.subtle.decrypt({
|
|
151
152
|
name: "AES-GCM",
|
|
152
|
-
iv: bytes.subarray(
|
|
153
|
+
iv: bytes.subarray(HEADER_BYTES, 17),
|
|
153
154
|
additionalData: additionalData(actionId)
|
|
154
|
-
}, key, bytes.subarray(
|
|
155
|
+
}, key, bytes.subarray(17));
|
|
155
156
|
break;
|
|
156
157
|
} catch {}
|
|
157
158
|
if (!plaintext) throw new SealError("invalid", "sealed action values do not open for this action");
|
package/dist/react/server.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { a as encode } from "../_chunks/codec-C8to-RQW.mjs";
|
|
2
2
|
import { f as FORM_STATE_SCRIPT_TYPE } from "../_chunks/wire-QEQWBr5s.mjs";
|
|
3
3
|
import { r as PREHYDRATION_SCRIPT } from "../_chunks/prehydration-B1GLTLdk.mjs";
|
|
4
|
-
import { i as encodeActionReference } from "../_chunks/actions-
|
|
4
|
+
import { i as encodeActionReference } from "../_chunks/actions-CvQ0-zjg.mjs";
|
|
5
5
|
import { t as getBuildDefaults } from "../_chunks/build-COIcs7wm.mjs";
|
|
6
6
|
import { a as parseHeldValues, n as addressValues, o as selectValuesToSend, r as escapeScriptJson } from "../_chunks/values-37UIzxM0.mjs";
|
|
7
7
|
import { c as setIslandServerHook, i as getContexts, l as setRenderStateResolver, n as createIsland, o as isolateIslands, r as getClientReference, s as runtimeTagged, t as IslandsProvider, u as withinClientRoot } from "../_chunks/island-DVOr-OUw.mjs";
|
|
@@ -12,8 +12,12 @@ import * as React from "react";
|
|
|
12
12
|
* @farm.js/islx/react/server: rendering a React page with islands. Importing it gives islands the
|
|
13
13
|
* server code they need (values, action references); `renderToStream` renders a page.
|
|
14
14
|
*/
|
|
15
|
-
|
|
15
|
+
let promisePrefix;
|
|
16
16
|
let promiseCount = 0;
|
|
17
|
+
function nextPromiseName() {
|
|
18
|
+
promisePrefix ??= `${Array.from(crypto.getRandomValues(/* @__PURE__ */ new Uint32Array(2)), (part) => part.toString(36)).join("")}.`;
|
|
19
|
+
return promisePrefix + (promiseCount++).toString(36);
|
|
20
|
+
}
|
|
17
21
|
function isThenable(value) {
|
|
18
22
|
return value !== null && (typeof value === "object" || typeof value === "function") && typeof value.then === "function";
|
|
19
23
|
}
|
|
@@ -23,7 +27,7 @@ setIslandServerHook({
|
|
|
23
27
|
const addressed = addressValues(props, { encodeSpecial: (value) => {
|
|
24
28
|
if (value && typeof value === "object" && runtimeTagged.has(value)) return value;
|
|
25
29
|
if (isThenable(value)) {
|
|
26
|
-
const name =
|
|
30
|
+
const name = nextPromiseName();
|
|
27
31
|
promises.push({
|
|
28
32
|
name,
|
|
29
33
|
promise: value
|
package/dist/server.d.mts
CHANGED
|
@@ -71,15 +71,15 @@ declare class SealError extends Error {
|
|
|
71
71
|
interface ActionKeys {
|
|
72
72
|
/** The key new values are sealed with. */
|
|
73
73
|
current: CryptoKey;
|
|
74
|
-
/** Its id, written into each sealed value (
|
|
74
|
+
/** Its id, written into each sealed value (a 32-bit unsigned integer). */
|
|
75
75
|
currentId: number;
|
|
76
76
|
/**
|
|
77
|
-
* Every key that opens a value, by id: the current one and earlier ones still accepted.
|
|
78
|
-
*
|
|
77
|
+
* Every key that opens a value, by id: the current one and earlier ones still accepted. Two keys
|
|
78
|
+
* may still share an id; each is tried.
|
|
79
79
|
*/
|
|
80
80
|
byId: ReadonlyMap<number, readonly CryptoKey[]>;
|
|
81
81
|
}
|
|
82
|
-
/** A new random key, as base64url: what `
|
|
82
|
+
/** A new random key, as base64url: what `ISLX_ACTIONS_KEY` holds. */
|
|
83
83
|
declare function generateActionKey(): string;
|
|
84
84
|
/**
|
|
85
85
|
* The keys values are sealed and opened with. `current` seals; `previous` keys still open
|
package/dist/server.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { a as parseHeldValues, i as hashValue, n as addressValues, o as selectValuesToSend, r as escapeScriptJson, t as VALUE_MIN_LENGTH } from "./_chunks/values-37UIzxM0.mjs";
|
|
2
|
-
import { a as getActionReference, c as resolveAction, d as generateActionKey, f as sealBound, i as encodeActionReference, l as SealError, n as configureActionKeys, o as registerInlineAction, p as unsealBound, r as decodeActionReference, s as registerServerReference, t as bindInlineAction, u as createActionKeys } from "./_chunks/actions-
|
|
2
|
+
import { a as getActionReference, c as resolveAction, d as generateActionKey, f as sealBound, i as encodeActionReference, l as SealError, n as configureActionKeys, o as registerInlineAction, p as unsealBound, r as decodeActionReference, s as registerServerReference, t as bindInlineAction, u as createActionKeys } from "./_chunks/actions-CvQ0-zjg.mjs";
|
|
3
3
|
import { a as encode, r as decode } from "./_chunks/codec-C8to-RQW.mjs";
|
|
4
4
|
import { I as readPostedAction, L as stripRuntimeFields, N as attachmentField, n as ACTION_KEY_FIELD, o as BOUND_FIELD, r as ACTION_STALE_HEADER, s as CALL_FIELD, t as ACTION_HEADER } from "./_chunks/wire-QEQWBr5s.mjs";
|
|
5
5
|
import { n as setBuildDefaults, t as getBuildDefaults } from "./_chunks/build-COIcs7wm.mjs";
|
package/package.json
CHANGED