@runeditrun/rer 0.1.1 → 0.2.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/README.md +111 -1
- package/cli/README.md +28 -3
- package/cli/contract.mjs +4 -4
- package/cli/dev-identity.mjs +2 -1
- package/cli/door-mode.mjs +134 -0
- package/cli/door.mjs +263 -0
- package/cli/hosted-output.mjs +15 -5
- package/cli/hydration/browser.mjs +91 -0
- package/cli/hydration/cases.mjs +106 -0
- package/cli/hydration/check.mjs +105 -0
- package/cli/hydration/probe.mjs +164 -0
- package/cli/hydration/scripts.mjs +39 -0
- package/cli/hydration/verdict.mjs +85 -0
- package/cli/inspect.mjs +56 -9
- package/cli/lifecycle.mjs +36 -12
- package/cli/local.mjs +9 -3
- package/cli/main.mjs +2 -0
- package/cli/managed-server.mjs +52 -18
- package/cli/package-calls.mjs +81 -0
- package/cli/package-mode.mjs +46 -0
- package/cli/parse.mjs +14 -4
- package/cli/pnpm-runtime.mjs +96 -0
- package/cli/portability-worker.mjs +2 -2
- package/cli/runtime-front-door.mjs +4 -2
- package/cli/server-proxy.mjs +69 -5
- package/cli/server-registry.mjs +10 -7
- package/cli/settings.mjs +14 -0
- package/cli/site-check.mjs +96 -0
- package/cli/site-preview.mjs +53 -0
- package/cli/site-render.mjs +77 -0
- package/cli/site.mjs +109 -0
- package/global/CHANGELOG.md +63 -0
- package/global/README.md +111 -1
- package/global/checkout.mjs +24 -10
- package/global/cli/install.mjs +6 -2
- package/global/delegate.mjs +9 -7
- package/global/door/command.mjs +254 -0
- package/global/door/control.mjs +65 -0
- package/global/door/cookies.mjs +41 -0
- package/global/door/pages.mjs +49 -0
- package/global/door/policy.mjs +49 -0
- package/global/door/routes.mjs +45 -0
- package/global/door/server.mjs +255 -0
- package/global/door/sites.mjs +192 -0
- package/global/launch/dev.mjs +21 -7
- package/global/launch/door.mjs +36 -0
- package/global/launch/install-pinned.mjs +22 -0
- package/global/launch/prepare.mjs +38 -5
- package/global/launch/render.mjs +6 -1
- package/global/launch/steps.mjs +12 -7
- package/global/launch/worker.mjs +3 -3
- package/global/list.mjs +6 -3
- package/global/main.mjs +37 -3
- package/global/version.mjs +8 -8
- package/modules/dependencies/protocol.mjs +2 -1
- package/modules/deploy-kit/README.md +34 -1
- package/modules/deploy-kit/artifact.mjs +10 -0
- package/modules/deploy-kit/owner.mjs +5 -4
- package/modules/deploy-kit/prepare.mjs +3 -2
- package/modules/deploy-kit/provider.mjs +6 -3
- package/modules/deploy-kit/site.mjs +135 -0
- package/modules/erasure/CONTRACT.md +6 -3
- package/modules/erasure/README.md +8 -1
- package/modules/erasure/audit.mjs +2 -2
- package/modules/erasure/cascade.mjs +52 -11
- package/modules/erasure/order.mjs +29 -22
- package/modules/erasure/plan.mjs +4 -2
- package/modules/erasure/queue.mjs +41 -0
- package/modules/erasure/scope.mjs +3 -3
- package/modules/erasure/verification.mjs +4 -2
- package/modules/mail/README.md +1 -1
- package/modules/mail/index.mjs +1 -1
- package/modules/mail/src/core/errors.mjs +9 -0
- package/modules/mail/src/development/index.d.mts +6 -0
- package/modules/mail/src/development/index.mjs +6 -3
- package/modules/mail/src/development/viewer-html.mjs +3 -3
- package/modules/mail/src/development/viewer.mjs +8 -1
- package/modules/operator-auth/dev/index.mjs +15 -5
- package/modules/operator-auth/http.mjs +2 -3
- package/modules/operator-auth/loopback.mjs +17 -0
- package/modules/owner-claim/verify.mjs +2 -1
- package/modules/portability/README.md +8 -0
- package/modules/portability/hash.mjs +6 -2
- package/modules/portability/import.mjs +1 -1
- package/modules/portability/legacy-files.mjs +4 -4
- package/modules/portability/legacy-safety.mjs +51 -10
- package/modules/portability/migration-history.mjs +15 -0
- package/modules/release/README.md +5 -3
- package/modules/release/produce.mjs +5 -2
- package/modules/release/source.mjs +15 -4
- package/onboarding/adapter-auth.mjs +4 -3
- package/onboarding/adapter-settings.mjs +21 -5
- package/onboarding/adapter.mjs +2 -1
- package/onboarding/app-fetch.mjs +79 -0
- package/onboarding/archive-output.d.mts +4 -2
- package/onboarding/archive-output.mjs +22 -10
- package/onboarding/dependency-approval.mjs +4 -3
- package/onboarding/local/environment.mjs +7 -2
- package/onboarding/operator-install.mjs +8 -2
- package/onboarding/operator-prerequisites.mjs +5 -2
- package/onboarding/queued-entry.mjs +9 -5
- package/operations/operations.mjs +11 -2
- package/package.json +75 -1
- package/scripts/publish-check/github_refs.py +28 -2
- package/scripts/publish-check/public_hygiene.py +35 -4
- package/scripts/publish-check/publication-config.json +14 -1
|
@@ -5,41 +5,73 @@
|
|
|
5
5
|
// settings are never changed. An install or setup this launcher started and
|
|
6
6
|
// that did not finish runs again on the next launch.
|
|
7
7
|
import { realpath } from 'node:fs/promises';
|
|
8
|
-
import { dispatcher } from '../checkout.mjs';
|
|
8
|
+
import { dispatcher, pinnedPackage } from '../checkout.mjs';
|
|
9
9
|
import { liveServers } from '../registry.mjs';
|
|
10
10
|
import { checkCapacity, personEnvironment, pinPolicy } from './capacity.mjs';
|
|
11
|
+
import { bringBackDoor, doorPlan } from './door.mjs';
|
|
12
|
+
import { LaunchFailure } from './failure.mjs';
|
|
11
13
|
import { describeCheckout, freePort, localOwner, needsInstall, setupNeeded } from './facts.mjs';
|
|
12
14
|
import { claimPort, installState, setupState } from './state.mjs';
|
|
13
15
|
import { runVerb } from './steps.mjs';
|
|
14
16
|
import { localDefaults } from './local-defaults.mjs';
|
|
17
|
+
import { installPinned } from './install-pinned.mjs';
|
|
15
18
|
|
|
16
19
|
// The value given for a flag in the user's dev arguments.
|
|
17
20
|
export const flagValue = (args, flag) => { const index = args.indexOf(flag); return index < 0 ? undefined : args[index + 1]; };
|
|
18
21
|
|
|
19
22
|
// A worker passes the signal a stop aborts: no step starts after it, and the
|
|
20
|
-
// step running then is signalled through `children`.
|
|
21
|
-
|
|
23
|
+
// step running then is signalled through `children`. `doorStart`: the front
|
|
24
|
+
// door is starting this checkout on a visit, which never installs or sets up;
|
|
25
|
+
// an unprepared checkout is refused with the reason.
|
|
26
|
+
export async function prepare({ root, args, env, report, directory, log, children, signal, doorStart = false }) {
|
|
27
|
+
// A checkout pinning the rer package whose node_modules lacks that version
|
|
28
|
+
// installs it first (after the same quick server-limit check: a pin with
|
|
29
|
+
// package mode has the person defaults); every step after this runs the
|
|
30
|
+
// pinned package's verbs.
|
|
31
|
+
let installed = false;
|
|
32
|
+
if (pinnedPackage(root)?.ready === false) {
|
|
33
|
+
if (doorStart) throw new LaunchFailure(3, `${root} is not installed, and the front door starts only prepared checkouts. Run rer dev in it once.`);
|
|
34
|
+
const current = await pinPolicy(root, 'package');
|
|
35
|
+
report({ phase: 'check', message: 'checking the server limit' });
|
|
36
|
+
const capacity = await checkCapacity(await realpath(root), { ...env, ...personEnvironment(env, args, current) }, current);
|
|
37
|
+
if (capacity.own) throw new LaunchFailure(3, `A server of ${root} is running, but its node_modules lacks the rer version it pins. Stop it with rer dev --stop, then run rer dev again.`);
|
|
38
|
+
report({ phase: 'install', message: 'installing dependencies' });
|
|
39
|
+
await recorded(directory, installState, () => installPinned(root, { env, output: text => log(text.trimEnd()), children, signal }));
|
|
40
|
+
installed = true;
|
|
41
|
+
}
|
|
22
42
|
const checkout = await realpath(root), { mode, entry } = dispatcher(root);
|
|
23
43
|
const described = await describeCheckout(root, entry, env, signal);
|
|
24
44
|
const pin = await pinPolicy(root, mode, described);
|
|
25
45
|
const added = personEnvironment(env, args, pin), childEnv = { ...env, ...added };
|
|
46
|
+
const plan = await doorPlan(described, args, env);
|
|
47
|
+
if (plan.note) report({ phase: 'note', message: plan.note });
|
|
26
48
|
report({ phase: 'check', message: 'checking the server limit' });
|
|
27
49
|
const capacity = await checkCapacity(checkout, childEnv, pin);
|
|
28
|
-
if (capacity.own) return { entry, added, own: capacity.own };
|
|
50
|
+
if (capacity.own) { await bringBackDoor(plan, env, report); return { entry, added, own: capacity.own }; }
|
|
29
51
|
const note = message => report({ phase: 'note', message });
|
|
30
52
|
const step = (record, args, fields, env = childEnv) => recorded(directory, record, () => runVerb(entry, args, { root, env, note, log, children, signal }), fields);
|
|
31
53
|
// Only this setup fills the seed's declared local defaults.
|
|
32
54
|
const setupEnv = { ...childEnv, [localDefaults]: '1' };
|
|
33
|
-
|
|
55
|
+
const unprepared = what => new LaunchFailure(3, `${checkout} is not ${what}, and the front door starts only prepared checkouts. Run rer dev in it once.`);
|
|
56
|
+
if (!installed && await needsInstall(root, await installState.read(directory))) {
|
|
57
|
+
if (doorStart) throw unprepared('installed');
|
|
34
58
|
report({ phase: 'install', message: 'installing dependencies' });
|
|
35
59
|
await step(installState, ['install']);
|
|
36
60
|
}
|
|
37
61
|
const last = await setupState.read(directory), setup = await setupNeeded(root, described, last);
|
|
62
|
+
if (setup && doorStart) throw unprepared('set up');
|
|
38
63
|
const host = flagValue(args, '--host'), hostArgs = host ? ['--host', host] : [];
|
|
39
64
|
if (setup === 'resume') {
|
|
40
65
|
// Saved settings hold an owner and origin; setup keeps them.
|
|
41
66
|
report({ phase: 'setup', message: 'finishing this checkout\'s setup, keeping its saved owner and port' });
|
|
42
67
|
await step(setupState, ['setup', ...hostArgs], {}, setupEnv);
|
|
68
|
+
} else if (setup === 'fresh' && plan.freshAtDoor) {
|
|
69
|
+
// With the door enabled, a fresh checkout is set up at its door origin.
|
|
70
|
+
const owner = localOwner(checkout);
|
|
71
|
+
report({ phase: 'setup', message: `setting up a local owner (${owner}) at its front-door address` });
|
|
72
|
+
await step(setupState, ['setup', '--owner-email', owner, '--door'], { owner, door: true }, setupEnv);
|
|
73
|
+
await bringBackDoor({ ...plan, atDoor: true }, env, report);
|
|
74
|
+
return { entry, added };
|
|
43
75
|
} else if (setup === 'fresh') {
|
|
44
76
|
// An interrupted setup resumes on the port it chose while that port is free.
|
|
45
77
|
const owner = localOwner(checkout), explicit = flagValue(args, '--port');
|
|
@@ -50,6 +82,7 @@ export async function prepare({ root, args, env, report, directory, log, childre
|
|
|
50
82
|
// The claim lasts until the server registers the port: the caller releases it.
|
|
51
83
|
return { entry, added, release };
|
|
52
84
|
}
|
|
85
|
+
await bringBackDoor(plan, env, report);
|
|
53
86
|
return { entry, added };
|
|
54
87
|
}
|
|
55
88
|
|
package/global/launch/render.mjs
CHANGED
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
const elapsed = ms => { const s = Math.round(ms / 1000); return s < 60 ? `${s}s` : `${Math.floor(s / 60)}m ${s % 60}s`; };
|
|
4
4
|
const quiet = 20000;
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
// `keyless` (a start by the front door, whose log this is): the ready lines
|
|
7
|
+
// leave out the dev-key link, so the door's files never hold a key.
|
|
8
|
+
export function printer({ json, login, stopHint, keyless = false }) {
|
|
7
9
|
const started = Date.now();
|
|
8
10
|
let current = 'preparing', last = Date.now();
|
|
9
11
|
const event = (name, fields) => process.stdout.write(JSON.stringify({ version: 1, verb: 'dev', event: name, ...fields }) + '\n');
|
|
@@ -19,6 +21,7 @@ export function printer({ json, login, stopHint }) {
|
|
|
19
21
|
heartbeat() { if (Date.now() - last >= quiet) progress('waiting', `still ${current} (${elapsed(Date.now() - started)}). Allow up to 10 minutes; if this command is stopped, run rer dev again to reattach.`); },
|
|
20
22
|
// The dispatcher's ready event, as its own human ready lines.
|
|
21
23
|
ready(ready, { log, pid }) {
|
|
24
|
+
if (keyless) { const { openUrl, ...rest } = ready; ready = rest; }
|
|
22
25
|
if (json) { event('server', { pid, log }); process.stdout.write(JSON.stringify(ready) + '\n'); return; }
|
|
23
26
|
const out = [`RER_READY ${ready.url}`];
|
|
24
27
|
if (ready.openUrl && !login) {
|
|
@@ -29,6 +32,8 @@ export function printer({ json, login, stopHint }) {
|
|
|
29
32
|
if (ready.openUrl) out.push(`RER_OPEN ${ready.openUrl}`);
|
|
30
33
|
out.push(`Open ${ready.openUrl ? 'the last link on this computer (it carries the dev key; never share it)' : 'the outbox'}, choose the newest sign-in message and follow its link. No email was sent.`);
|
|
31
34
|
}
|
|
35
|
+
// A seed with a run site: its static homepage, /run and docs, served locally.
|
|
36
|
+
if (ready.siteUrl) out.push(`RER_SITE ${ready.siteUrl}`);
|
|
32
37
|
out.push(`The server keeps running in the background${pid ? ` (PID ${pid})` : ''}. Stop it with: ${stopHint}`);
|
|
33
38
|
if (log) out.push(`Server log (private): ${log}`);
|
|
34
39
|
process.stdout.write(out.join('\n') + '\n');
|
package/global/launch/steps.mjs
CHANGED
|
@@ -63,19 +63,24 @@ async function follow(path, from, onLine, stopped) {
|
|
|
63
63
|
// Start `dev` detached into its own process group with its output in a private
|
|
64
64
|
// log, and return its ready event once it prints one. The server outlives this
|
|
65
65
|
// process and stops on rer dev --stop or its idle timeout.
|
|
66
|
-
export async function startServer(entry, args, { root, env, logPath, children, signal }) {
|
|
66
|
+
export async function startServer(entry, args, { root, env, logPath, children, signal, note = () => {} }) {
|
|
67
67
|
signal?.throwIfAborted();
|
|
68
68
|
const handle = await open(logPath, 'a', 0o600), from = (await handle.stat()).size;
|
|
69
|
-
let child;
|
|
70
|
-
|
|
71
|
-
|
|
69
|
+
let child, exit, spawnError;
|
|
70
|
+
// Listening before anything is awaited: a spawn that fails reports it on the next tick.
|
|
71
|
+
try {
|
|
72
|
+
child = spawn(process.execPath, [entry, 'dev', ...args, '--json'], { cwd: root, env, detached: true, stdio: ['ignore', handle.fd, handle.fd] });
|
|
73
|
+
child.once('error', failure => { spawnError = failure; exit ??= 1; });
|
|
74
|
+
child.once('exit', (code, name) => { exit = code ?? (name ? 128 : 1); });
|
|
75
|
+
} finally { await handle.close(); }
|
|
72
76
|
own(children, child, signal);
|
|
73
|
-
let
|
|
74
|
-
child.once('exit', (code, name) => { exit = code ?? (name ? 128 : 1); });
|
|
77
|
+
let ready, error, output = '';
|
|
75
78
|
await follow(logPath, from, line => {
|
|
76
79
|
const event = events(line);
|
|
77
80
|
if (event?.event === 'ready') { ready = event; return true; }
|
|
78
81
|
if (event?.event === 'error') error = event;
|
|
82
|
+
// rer dev --door / --direct: where the checkout moved.
|
|
83
|
+
if (event?.event === 'moved') note(`moved this checkout from ${event.from} to ${event.to}; its data is kept`);
|
|
79
84
|
if (!event) output = (output + line + '\n').slice(-8192);
|
|
80
85
|
return false;
|
|
81
86
|
}, () => exit !== undefined);
|
|
@@ -84,7 +89,7 @@ export async function startServer(entry, args, { root, env, logPath, children, s
|
|
|
84
89
|
for await (const line of createInterface({ input: createReadStream(logPath, { start: from }) })) { const event = events(line); if (event?.event === 'ready') ready = event; if (event?.event === 'error') error = event; }
|
|
85
90
|
}
|
|
86
91
|
children.delete(child);
|
|
87
|
-
if (!ready) throw failureFrom(exit, error, output);
|
|
92
|
+
if (!ready) throw failureFrom(exit, error, spawnError ? `rer could not run in ${root}: ${spawnError.code ?? spawnError.message}.` : output);
|
|
88
93
|
child.unref();
|
|
89
94
|
return { ready, pid: child.pid, reused: exit === 0 };
|
|
90
95
|
}
|
package/global/launch/worker.mjs
CHANGED
|
@@ -16,7 +16,7 @@ import { prepare } from './prepare.mjs';
|
|
|
16
16
|
import { claimLaunch, eventLog, launchDirectory, liveWorker, releaseLaunch, serverRecord, stopRequest } from './state.mjs';
|
|
17
17
|
import { startServer } from './steps.mjs';
|
|
18
18
|
|
|
19
|
-
const [root, token, argsJSON, requestedAtText] = process.argv.slice(2), args = JSON.parse(argsJSON), requestedAt = Number(requestedAtText);
|
|
19
|
+
const [root, token, argsJSON, requestedAtText, mode] = process.argv.slice(2), args = JSON.parse(argsJSON), requestedAt = Number(requestedAtText), doorStart = mode === 'door-start';
|
|
20
20
|
const checkout = await realpath(root), directory = await launchDirectory(checkout), events = eventLog(directory, token);
|
|
21
21
|
// Listening before the claim: a stop may signal this worker as soon as it is the holder.
|
|
22
22
|
const children = new Set(), stopping = new AbortController();
|
|
@@ -32,13 +32,13 @@ if (!await claimLaunch(directory, token)) {
|
|
|
32
32
|
try {
|
|
33
33
|
if (await stopRequest.covers(directory, requestedAt)) stop();
|
|
34
34
|
stopping.signal.throwIfAborted();
|
|
35
|
-
const prepared = await prepare({ root, args, env: process.env, report: fields => events.write({ event: 'phase', ...fields }), directory, log, children, signal: stopping.signal });
|
|
35
|
+
const prepared = await prepare({ root, args, env: process.env, report: fields => events.write({ event: 'phase', ...fields }), directory, log, children, signal: stopping.signal, doorStart });
|
|
36
36
|
await events.write({ event: 'phase', phase: 'start', message: prepared.own ? 'the app is already running; checking it' : 'starting the app' });
|
|
37
37
|
// A server rer started logs to server.log; asking a running one for its
|
|
38
38
|
// ready lines goes to reuse.log, and names server.log only when rer started it.
|
|
39
39
|
const serverLog = join(directory, 'server.log');
|
|
40
40
|
let started;
|
|
41
|
-
try { started = await startServer(prepared.entry, args, { root, env: { ...process.env, ...prepared.added }, logPath: prepared.own ? join(directory, 'reuse.log') : serverLog, children, signal: stopping.signal }); }
|
|
41
|
+
try { started = await startServer(prepared.entry, args, { root, env: { ...process.env, ...prepared.added }, logPath: prepared.own ? join(directory, 'reuse.log') : serverLog, children, signal: stopping.signal, note: message => events.write({ event: 'phase', phase: 'note', message }) }); }
|
|
42
42
|
finally { await prepared.release?.(); }
|
|
43
43
|
const record = (await liveServers(process.env)).find(item => item.checkout === checkout);
|
|
44
44
|
const pid = record?.owner?.pid ?? started.pid;
|
package/global/list.mjs
CHANGED
|
@@ -4,7 +4,7 @@ import { execFile } from 'node:child_process';
|
|
|
4
4
|
import { readdir, realpath, stat } from 'node:fs/promises';
|
|
5
5
|
import { join, resolve } from 'node:path';
|
|
6
6
|
import { promisify } from 'node:util';
|
|
7
|
-
import { descriptorProfile, dispatcher } from './checkout.mjs';
|
|
7
|
+
import { descriptorProfile, dispatcher, pinnedPackage } from './checkout.mjs';
|
|
8
8
|
import { liveServers } from './registry.mjs';
|
|
9
9
|
import { describeCheckout, setupNeeded } from './launch/facts.mjs';
|
|
10
10
|
import { launchPath, setupState } from './launch/state.mjs';
|
|
@@ -27,8 +27,11 @@ async function entry(path, running) {
|
|
|
27
27
|
if (!result.launchable) result.reason = reasons[facts.profile] ?? `OPERATIONS.json is version ${facts.version ?? '(none)'}, which rer does not run`;
|
|
28
28
|
result.prepared = null;
|
|
29
29
|
if (profile === 'first-party') {
|
|
30
|
-
// Set up: saved settings, and no setup a launch started left unfinished.
|
|
31
|
-
|
|
30
|
+
// Set up: saved settings, and no setup a launch started left unfinished. A
|
|
31
|
+
// checkout whose pinned rer package is not installed yet has never been
|
|
32
|
+
// launched: rer dev installs it first.
|
|
33
|
+
if (pinnedPackage(path)?.ready === false) result.prepared = false;
|
|
34
|
+
else try { result.prepared = !await setupNeeded(path, await describeCheckout(path, dispatcher(path).entry, process.env), await setupState.read(launchPath(path))); }
|
|
32
35
|
catch (failure) { result.launchable = false; result.reason = `rer describe fails here: ${failure.message.split('\n')[0]}`; }
|
|
33
36
|
}
|
|
34
37
|
result.url = running.get(path) ?? null;
|
package/global/main.mjs
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
|
-
import { dispatcher, locate, unsupportedMessage } from './checkout.mjs';
|
|
1
|
+
import { dispatcher, locate, pinnedPackage, unsupportedMessage } from './checkout.mjs';
|
|
2
2
|
import { delegate } from './delegate.mjs';
|
|
3
3
|
import { localDefaults } from './launch/local-defaults.mjs';
|
|
4
|
+
import { packageName as pinnedName } from './package-info.mjs';
|
|
4
5
|
|
|
5
6
|
const help = `Usage: rer VERB [FLAGS]
|
|
6
7
|
|
|
7
8
|
Inside a project (the nearest OPERATIONS.json at or above this folder, or --cwd DIR),
|
|
8
|
-
every verb runs that checkout's own pinned rer
|
|
9
|
+
every verb runs that checkout's own pinned rer: the @runeditrun/rer version its package.json
|
|
10
|
+
pins (installed first by rer install or rer dev), else its vendored scripts/rer.mjs.
|
|
9
11
|
|
|
10
12
|
rer dev Launch a first-party checkout with no setup steps: installs dependencies,
|
|
11
13
|
sets up a local owner and a free port when needed, starts the app and prints
|
|
@@ -14,6 +16,8 @@ every verb runs that checkout's own pinned rer, exactly as node scripts/rer.mjs
|
|
|
14
16
|
to 10 minutes on a first launch, or rerun it to reattach. rer dev --stop stops it.
|
|
15
17
|
rer list [DIR] The checkouts in DIR (default: here), newest first, with whether rer dev can
|
|
16
18
|
launch them, whether they are set up, and their running URL. --json for data.
|
|
19
|
+
rer door ... The opt-in front door: every checkout at http://NAME.localhost:PORT, started
|
|
20
|
+
on its first visit and stopped when idle. rer door --help for its verbs.
|
|
17
21
|
rer cli status This rer: version, contents digest, install channel, launcher and other rer
|
|
18
22
|
commands on PATH. --json for data.
|
|
19
23
|
rer cli install Install this package for the current user (~/.local/bin/rer).
|
|
@@ -21,11 +25,17 @@ every verb runs that checkout's own pinned rer, exactly as node scripts/rer.mjs
|
|
|
21
25
|
rer update Move to the latest published rer (--version X for an exact one), the way it
|
|
22
26
|
was installed: npx and install.sh installs switch atomically, npm installs run npm.
|
|
23
27
|
rer --version Print this rer's version; inside a checkout, also the version the checkout pins.
|
|
28
|
+
rer queue NAME -- COMMAND [ARGS]
|
|
29
|
+
Run COMMAND once this computer's job queue admits it (heavy package scripts
|
|
30
|
+
of a checkout that pins the rer package use it).
|
|
24
31
|
|
|
25
32
|
Checkout verbs: install, setup, dev, preview, build, verify, reset, deploy, export, import,
|
|
26
33
|
outbox, doctor, describe, check, release. Run rer VERB --help in a checkout for its flags.
|
|
27
34
|
`;
|
|
28
35
|
|
|
36
|
+
// The verbs that install an uninstalled pinned package before they run.
|
|
37
|
+
const installsFirst = new Set(['install', 'check', 'describe']);
|
|
38
|
+
|
|
29
39
|
export async function main(argv) {
|
|
30
40
|
const [verb] = argv;
|
|
31
41
|
if (verb === '--version' || verb === '-v') return (await import('./version.mjs')).versionCommand(argv);
|
|
@@ -38,10 +48,34 @@ export async function main(argv) {
|
|
|
38
48
|
if (!verb || verb === '--help' || verb === '-h' || verb === 'help') { process.stdout.write(help); return 0; }
|
|
39
49
|
if (verb === 'cli') return (await import('./cli/command.mjs')).cliCommand(argv.slice(1));
|
|
40
50
|
if (verb === 'list') return (await import('./list.mjs')).listCommand(argv.slice(1));
|
|
51
|
+
if (verb === 'door') return (await import('./door/command.mjs')).doorCommand(argv.slice(1));
|
|
41
52
|
const target = locate(argv);
|
|
42
53
|
if (target.profile === 'unsupported') { process.stderr.write(unsupportedMessage(target.root, target.version) + '\n'); return 2; }
|
|
54
|
+
const first = target.profile === 'first-party', mode = first ? dispatcher(target.root).mode : undefined;
|
|
55
|
+
if (verb === 'queue') {
|
|
56
|
+
const [, name, separator, command] = argv;
|
|
57
|
+
if (!name || separator !== '--' || !command) { process.stderr.write('Usage: rer queue NAME -- COMMAND [ARGS]\n'); return 2; }
|
|
58
|
+
if (mode === 'package') return delegate(target, target.args);
|
|
59
|
+
return (await import('../onboarding/queued-entry.mjs')).queuedCommandLine(argv.slice(1));
|
|
60
|
+
}
|
|
43
61
|
// A checkout that pins its own rer package runs all of it, launching included.
|
|
44
|
-
|
|
62
|
+
// Until node_modules holds the pinned version, install, check and describe
|
|
63
|
+
// install it with the frozen lockfile (then run the pinned verb, so a fresh
|
|
64
|
+
// clone checks with the pinned bytes), the zero-setup launch installs it
|
|
65
|
+
// first, and every other verb says to install.
|
|
66
|
+
const pinned = first && mode !== 'package' ? pinnedPackage(target.root) : undefined;
|
|
67
|
+
if (pinned && !pinned.ready && !argv.includes('--help')) {
|
|
68
|
+
if (installsFirst.has(verb)) {
|
|
69
|
+
const { installPinned } = await import('./launch/install-pinned.mjs'), { LaunchFailure } = await import('./launch/failure.mjs');
|
|
70
|
+
try { await installPinned(target.root); }
|
|
71
|
+
catch (failure) { if (!(failure instanceof LaunchFailure)) throw failure; process.stderr.write(failure.message + '\n'); return failure.exitCode; }
|
|
72
|
+
return delegate(target, target.args);
|
|
73
|
+
}
|
|
74
|
+
if (verb !== 'dev') {
|
|
75
|
+
process.stderr.write(`${target.root} pins ${pinnedName} ${pinned.declared}, ${pinned.installed ? `but node_modules holds ${pinned.installed}` : 'which is not installed yet'}. Run rer install, then rer ${verb} again.\n`);
|
|
76
|
+
return 3;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
45
79
|
if (verb === 'dev' && mode && mode !== 'package' && !argv.includes('--help')) return (await import('./launch/dev.mjs')).launchDev(target);
|
|
46
80
|
// Every other server start or queued job on a vendored pin gets the person
|
|
47
81
|
// defaults too (a pinned package applies its own).
|
package/global/version.mjs
CHANGED
|
@@ -2,9 +2,7 @@
|
|
|
2
2
|
// checkout (or with --cwd DIR) a second line names the version that checkout
|
|
3
3
|
// pins: its package's, or the one its vendored copy recorded, which
|
|
4
4
|
// `rer describe` reports as cliVersion.
|
|
5
|
-
import {
|
|
6
|
-
import { dirname, join } from 'node:path';
|
|
7
|
-
import { dispatcher, locate } from './checkout.mjs';
|
|
5
|
+
import { dispatcher, locate, pinnedPackage } from './checkout.mjs';
|
|
8
6
|
import { describeCheckout } from './launch/facts.mjs';
|
|
9
7
|
import { ownManifest } from './package-info.mjs';
|
|
10
8
|
|
|
@@ -12,13 +10,15 @@ export async function versionCommand(argv) {
|
|
|
12
10
|
process.stdout.write(ownManifest().version + '\n');
|
|
13
11
|
const target = locate(argv);
|
|
14
12
|
if (target.profile !== 'first-party') return 0;
|
|
15
|
-
const
|
|
16
|
-
if (
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
13
|
+
const pinned = pinnedPackage(target.root);
|
|
14
|
+
if (pinned) {
|
|
15
|
+
process.stdout.write(pinned.ready
|
|
16
|
+
? `This checkout (${target.root}) pins rer ${pinned.installed} (package).\n`
|
|
17
|
+
: `This checkout (${target.root}) pins rer ${pinned.declared} (package), ${pinned.installed ? `but node_modules holds ${pinned.installed}` : 'not installed yet'}: run rer install.\n`);
|
|
20
18
|
return 0;
|
|
21
19
|
}
|
|
20
|
+
const { mode, entry } = dispatcher(target.root);
|
|
21
|
+
if (mode === 'bundled') return 0;
|
|
22
22
|
let described;
|
|
23
23
|
try { described = await describeCheckout(target.root, entry, process.env); }
|
|
24
24
|
catch (failure) { process.stdout.write(`This checkout (${target.root}): its pinned rer version could not be read: ${failure.message.split('\n')[0]}\n`); return 0; }
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { readJson, discover } from '../operator-auth/identity/oidc/network.mjs';
|
|
2
2
|
import { PRESETS, MINIMUM_PROVIDER_VERSION } from '../operator-auth/identity/oidc/presets.mjs';
|
|
3
|
+
import { isLoopbackHost } from '../operator-auth/loopback.mjs';
|
|
3
4
|
export const INTERFACE = 'oidc-provider@1';
|
|
4
5
|
export const CALLBACK = '/.well-known/rer-link/callback';
|
|
5
6
|
export const PATHS = ['/auth/oidc/auth/callback','/auth/oidc/auth/logout'];
|
|
@@ -7,7 +8,7 @@ export const NETWORK = { timeoutMs:5000 };
|
|
|
7
8
|
export function insist(value, code = 'invalid_link', status = 400) { if (!value) throw Object.assign(new Error(code),{code,status}); }
|
|
8
9
|
export function originOf(value, local = false) {
|
|
9
10
|
let u;try { u=new URL(value); } catch { insist(false); }
|
|
10
|
-
insist(u.origin===value && !u.username && !u.password && (u.protocol==='https:' || local && u.protocol==='http:' &&
|
|
11
|
+
insist(u.origin===value && !u.username && !u.password && (u.protocol==='https:' || local && u.protocol==='http:' && isLoopbackHost(u.hostname)));
|
|
11
12
|
return u.origin;
|
|
12
13
|
}
|
|
13
14
|
export async function providerMetadata(issuer, fetcher, contract = INTERFACE) {
|
|
@@ -140,12 +140,45 @@ When a token lacks a permission, the CLI prints which one instead of only a stat
|
|
|
140
140
|
A seed carries a copy of `modules/deploy-kit/` from a Base pin. Refresh the copy, write a `deploy.json` from the plan the seed already builds, and point `deploy`, and the hosting step of `setup`, at `owner-cli.mjs`. Keep any throwaway controller only for proofs. In `deploy.json` declare:
|
|
141
141
|
|
|
142
142
|
- the bindings the app uses (`DB`, `FILES`, `JOBS` and so on), with each queue's consumer and dead-letter queue and each SQLite Durable Object class;
|
|
143
|
-
- the secrets the app reads, such as its session secret and the mail provider's key;
|
|
143
|
+
- the secrets the app reads, such as its session secret and the mail provider's key. A secret item may add `verifyStandIn`, the hosted-valid value `rer verify`'s hosted check binds in its place (`{"binding": "MAIL_PROVIDER", "environment": "MAIL_PROVIDER", "verifyStandIn": "resend"}`); the kit validates it (a non-empty string), leaves it out of the plan and its fingerprint, and never sends it or makes it a secret value;
|
|
144
144
|
- any Cron schedule and the public setup fields the app takes in `vars`;
|
|
145
145
|
- `originVar`, the variable that holds the app's public origin (for example `APP_ORIGIN`).
|
|
146
146
|
|
|
147
147
|
A seed whose setup provisions nothing declares the same bindings and deploys through its own `deploy` verb. Resource names change from `rer-seedproof-*` or a chosen prefix to `<project>-<environment>-*`. The kit refuses rather than adopts an earlier installation's resources.
|
|
148
148
|
|
|
149
|
+
### Run site (`rer deploy --site`)
|
|
150
|
+
|
|
151
|
+
A seed's run site (`run-<seed>.com`: its marketing homepage, `/run` and docs, written by `rer build --site`) deploys to the owner's account as a **Base-generated site Worker** (`site.mjs`). The seed declares only the apex domain, beside `deploy.json`, in the file its `commandAdapter` `site.declaration` names (Base's seeds: `deploy-site.json`):
|
|
152
|
+
|
|
153
|
+
```json
|
|
154
|
+
{ "version": 1, "domain": "run-cradle.com" }
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
The domain is an apex hostname: no scheme, path, port, upper case or `www.`. The plan (`sitePlan`) names:
|
|
158
|
+
|
|
159
|
+
- the Worker `<project>-<environment>--site`. Owner Worker names never contain `--`, so it can never be the installed app's Worker (`<project>-<environment>`), and a site deploy never reads or writes the app Worker or any of its resources;
|
|
160
|
+
- its one module, `site-worker.mjs` (`SITE_WORKER`): fixed content Base generates, with no seed code. Its only binding is the assets (`ASSETS`), it runs first on every request (`run_worker_first`), and it has no secrets, no schedules and workers.dev off. A request to `www.<domain>` gets a 301 to `https://<domain>` with path and query kept; every other request is answered from the assets exactly as an assets-only Worker would answer it: the `rer build --site` folder with `html_handling: auto-trailing-slash` (`/run` serves `run.html`) and `not_found_handling: 404-page`;
|
|
161
|
+
- the apex as a Workers Custom Domain, and `www.<domain>` as a second Custom Domain on the same Worker, so the www host has DNS and a certificate and its requests reach the Worker's redirect.
|
|
162
|
+
|
|
163
|
+
No zone ruleset is read or written. Every request now invokes the Worker: one page view costs one Worker request for the page and one per same-origin file it loads (the default seed's run site: at most 7 per uncached page view).
|
|
164
|
+
|
|
165
|
+
`rer deploy --site --dry-run` builds the site and prints that plan (`--json`: a `site` event and a result with `dryRun: true`, `deployed: false` and the same `site` object) with no provider request and nothing written, even with `CLOUDFLARE_ACCOUNT_ID` and `CLOUDFLARE_API_TOKEN` set. Without `--dry-run` it needs both inputs. Ownership is read from the provider alone, so a deploy from a fresh checkout (a CI runner with no `.rer/`) recognises the site it deployed before:
|
|
166
|
+
|
|
167
|
+
- the Worker is the kit's when its latest deployment serves, at 100%, the Worker's newest version, and that version's message annotation is the kit's `rer-site:<fingerprint>` (every site upload sets it);
|
|
168
|
+
- a Custom Domain is the kit's when its service is the kit's Worker.
|
|
169
|
+
|
|
170
|
+
A read-only preflight reads the zone (it must be in this account, else `site-zone-missing`), the Worker and the zone's Custom Domains, and refuses before any write: `resource-name-collision` for a Worker of the name whose live version the kit did not upload, or for a hostname attached to another service (or to a Worker that is not the kit's); `owned-resource-replaced` for a kit Worker whose latest deployment is not the kit's upload (a dashboard rollback or a split deployment). It then uploads the assets and the site Worker, checks that the new version is live, turns workers.dev off, attaches each missing hostname, and reads the Worker and both domains back. A repeat attaches nothing new. `.rer/deploy/site-<environment>.json` receives a receipt for the person; it is never read. The real path is tested only against the fake Cloudflare API in `tests/site-provider.mjs`, where every zone ruleset path answers 403; it has not run against a live account.
|
|
171
|
+
|
|
172
|
+
The token needs, beyond the owner-account permissions above:
|
|
173
|
+
|
|
174
|
+
| Permission | Scope | Used for |
|
|
175
|
+
| --- | --- | --- |
|
|
176
|
+
| Workers Scripts: Edit | the account | the site Worker upload, its assets, versions and deployments, workers.dev, and Workers Custom Domains (`/workers/domains`) |
|
|
177
|
+
| Zone: Read | the run site zones | finding the zone by name |
|
|
178
|
+
| Workers Routes: Edit | the run site zones | Workers Custom Domains, which also create their DNS records and certificates |
|
|
179
|
+
|
|
180
|
+
No Single Redirect (dynamic redirect ruleset) permission is needed. Base's references declare reserved `.example` domains (`run-default-seed.example`, `run-<edition>.example` for a new foundation), so a real deploy of either stops at `site-zone-missing`.
|
|
181
|
+
|
|
149
182
|
## Migration safety
|
|
150
183
|
|
|
151
184
|
SQL files have ordered unique names and SHA-256 identities. `_rer_deploy_migrations` is permanent controller-owned history. Applied entries must be an unchanged prefix of the desired set. A new migration gets an `applying` claim before its SQL and completion marker are submitted together to the D1 batch query API. Any interrupted, failed or uncertain outcome remains blocked and must be inspected explicitly; the kit never automatically retries, rolls back user data, or marks an unknown outcome successful. Readback checks every final identity.
|
|
@@ -40,6 +40,16 @@ export async function collectFiles(root, directory = '', budget = {bytes:0,count
|
|
|
40
40
|
}
|
|
41
41
|
return files;
|
|
42
42
|
}
|
|
43
|
+
// Wrangler's .assetsignore in an assets folder: gitignore-style lines naming
|
|
44
|
+
// files that are never assets. The file itself is never one either.
|
|
45
|
+
export function assetsIgnore(text = '') {
|
|
46
|
+
const rules = String(text).split(/\r?\n/).map(line => line.trim()).filter(line => line && !line.startsWith('#')).map(line => {
|
|
47
|
+
const negate = line.startsWith('!'), body = negate ? line.slice(1) : line, directory = body.endsWith('/'), pattern = body.replace(/\/+$/, '');
|
|
48
|
+
const glob = pattern.replace(/^\//, '').split('/').map(part => part === '**' ? '.*' : part.replace(/[.+^${}()|[\]\\]/g, '\\$&').replaceAll('*', '[^/]*').replaceAll('?', '[^/]')).join('/');
|
|
49
|
+
return { negate, test: new RegExp(`^${pattern.includes('/') ? '' : '(?:.*/)?'}${glob}${directory ? '/.*' : '(?:/.*)?'}$`) };
|
|
50
|
+
});
|
|
51
|
+
return path => path === '.assetsignore' || rules.reduce((ignored, rule) => rule.test.test(path) ? !rule.negate : ignored, false);
|
|
52
|
+
}
|
|
43
53
|
/** Built files a deployed Worker must serve: everything for the browser except documents
|
|
44
54
|
* (the app answers those; asset html_handling redirects them) and dot paths (never public). */
|
|
45
55
|
export const publicAssetPaths = paths => paths.filter(path => !path.split('/').some(part => part.startsWith('.')) && extname(path) !== '.html');
|
|
@@ -18,7 +18,7 @@ const segment = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
|
|
|
18
18
|
const STATE_KIND = 'rer-deploy-owner-state';
|
|
19
19
|
const KINDS = ['d1','r2','kv','queues'];
|
|
20
20
|
const DECLARATION_KEYS = ['version','project','compatibilityDate','compatibilityFlags','d1','r2','kv','queues','durableObjects','secrets','crons','vars','originVar','services','workersDev','platforms','container'];
|
|
21
|
-
const ENTRY_KEYS = {d1:['binding'],r2:['binding','locationHint'],kv:['binding'],queues:['binding','consumer','deadLetterQueue'],durableObjects:['binding','className','storage'],secrets:['binding','environment'],services:['binding','project','entrypoint','allowedHosts','grant']};
|
|
21
|
+
const ENTRY_KEYS = {d1:['binding'],r2:['binding','locationHint'],kv:['binding'],queues:['binding','consumer','deadLetterQueue'],durableObjects:['binding','className','storage'],secrets:['binding','environment','verifyStandIn'],services:['binding','project','entrypoint','allowedHosts','grant']};
|
|
22
22
|
|
|
23
23
|
function refuse(code, kind, name) { const error = new Error(code); error.resource = `${kind}:${name}`; return error; }
|
|
24
24
|
|
|
@@ -110,7 +110,8 @@ export function ownerPlan(target, declaration, artifact, { localOnlyKeys = [] }
|
|
|
110
110
|
...(kv.length ? {kv} : {}),
|
|
111
111
|
queues:queues.map(item => ({...item,name:name(item.binding),...(item.deadLetterQueue === undefined ? {} : {deadLetterQueue:name(item.deadLetterQueue)})})),
|
|
112
112
|
durableObjects:entries(declaration,'durableObjects').map(item => ({...item})),
|
|
113
|
-
|
|
113
|
+
// verifyStandIn is only `rer verify`'s hosted-check value: never planned, sent or stored.
|
|
114
|
+
secrets:entries(declaration,'secrets').map(({verifyStandIn,...item}) => { assert(verifyStandIn === undefined || typeof verifyStandIn === 'string' && verifyStandIn.length > 0 && verifyStandIn.length <= 1024, 'invalid-verify-stand-in'); return item; }),
|
|
114
115
|
crons:[...(declaration.crons ?? [])],vars,
|
|
115
116
|
// Only declarations using these add the keys, so earlier plans and fingerprints are unchanged.
|
|
116
117
|
...(services.length ? {services} : {}),...(declaration.workersDev === false ? {workersDev:false} : {})});
|
|
@@ -182,7 +183,7 @@ async function checkSavedResourceIdentities(api, state) {
|
|
|
182
183
|
async function checkServiceWorkers(api, plan) {
|
|
183
184
|
for (const {service} of plan.services ?? []) if (await workerSettings(api, service) === null) throw refuse('service-worker-missing','service',service);
|
|
184
185
|
}
|
|
185
|
-
async function workerSettings(api, worker) {
|
|
186
|
+
export async function workerSettings(api, worker) {
|
|
186
187
|
try { return await api.request(`/workers/scripts/${worker}/settings`); }
|
|
187
188
|
catch (error) { if (error.status === 404) return null; throw error; }
|
|
188
189
|
}
|
|
@@ -220,7 +221,7 @@ async function checkOwnedWorker(api, plan, artifact, state) {
|
|
|
220
221
|
}
|
|
221
222
|
|
|
222
223
|
/** One deploy at a time per state file: an exclusive lock file for the whole run. */
|
|
223
|
-
async function withStateLock(path, run) {
|
|
224
|
+
export async function withStateLock(path, run) {
|
|
224
225
|
await mkdir(dirname(path),{recursive:true,mode:0o700});
|
|
225
226
|
const lock = `${path}.lock`;
|
|
226
227
|
let handle;
|
|
@@ -3,7 +3,7 @@ import { mkdir, writeFile, readdir, rm } from 'node:fs/promises';
|
|
|
3
3
|
import { resolve, join, relative } from 'node:path';
|
|
4
4
|
import { pathToFileURL } from 'node:url';
|
|
5
5
|
import { assert, hash, localPath } from './validation.mjs';
|
|
6
|
-
import { collectFiles, regularBytes, validateArtifact, MAX_ARTIFACT } from './artifact.mjs';
|
|
6
|
+
import { assetsIgnore, collectFiles, regularBytes, validateArtifact, MAX_ARTIFACT } from './artifact.mjs';
|
|
7
7
|
import { collectBuildModules } from './build-modules.mjs';
|
|
8
8
|
import { refuseLocalModels } from './local-only.mjs';
|
|
9
9
|
|
|
@@ -24,7 +24,8 @@ export async function packageBuild({root,source,runIdentity,productDirectory = '
|
|
|
24
24
|
const config = JSON.parse(await regularBytes(join(build,'wrangler.json'),1024*1024));
|
|
25
25
|
const main = localPath(relative(build,resolve(build,config.main)));
|
|
26
26
|
const modules = await collectBuildModules(build,config,main);
|
|
27
|
-
const
|
|
27
|
+
const ignored = assetsIgnore(await regularBytes(join(product,'dist/client/.assetsignore'),65536).then(String,error => { if (error.code === 'ENOENT') return ''; throw error; }));
|
|
28
|
+
const assets = (await collectFiles(join(product,'dist/client'))).filter(file => !ignored(file.path));
|
|
28
29
|
const migrations = [];
|
|
29
30
|
for (const name of (await readdir(join(product,'migrations/core'))).sort()) {
|
|
30
31
|
assert(/^[0-9]+_[a-zA-Z0-9_-]+\.sql$/.test(name),'unexpected-migration-file');
|
|
@@ -12,13 +12,15 @@ export function cloudflare({ accountId, token, fetch: request = globalThis.fetch
|
|
|
12
12
|
assert(hexId(accountId), 'invalid-cloudflare-account-id');
|
|
13
13
|
assert(typeof token === 'string' && token.trim() && !/[\r\n]/.test(token), 'invalid-cloudflare-api-token');
|
|
14
14
|
let requests = 0;
|
|
15
|
-
|
|
15
|
+
// `zone` scope: the zone-level API (/zones/...), for a run site's zone lookup and redirect rule.
|
|
16
|
+
async function envelope(path, { method = 'GET', body, assetToken, zone = false } = {}) {
|
|
16
17
|
assert(typeof path === 'string' && /^\/[a-z0-9/_?=&.,%-]+$/i.test(path) && !path.includes('..') && !path.split('?')[0].includes('%'), 'invalid-provider-path');
|
|
18
|
+
assert(!zone || /^\/zones(?:[/?]|$)/.test(path), 'invalid-provider-path');
|
|
17
19
|
assert(++requests <= maxRequests, 'provider-request-budget');
|
|
18
20
|
assert(!assetToken || path === '/workers/assets/upload?base64=true', 'invalid-asset-auth-target');
|
|
19
21
|
const signal = AbortSignal.timeout(timeoutMs);
|
|
20
22
|
let response;
|
|
21
|
-
try { response = await request(`https://api.cloudflare.com/client/v4/accounts/${accountId}${path}`, {
|
|
23
|
+
try { response = await request(zone ? `https://api.cloudflare.com/client/v4${path}` : `https://api.cloudflare.com/client/v4/accounts/${accountId}${path}`, {
|
|
22
24
|
method, redirect: 'error', signal, headers: { Authorization: `Bearer ${assetToken ?? token}`, ...(body && !(body instanceof FormData) ? { 'Content-Type':'application/json' } : {}) },
|
|
23
25
|
...(body === undefined ? {} : { body: body instanceof FormData ? body : JSON.stringify(body) }),
|
|
24
26
|
}); } catch { throw new Error('provider-transport-failed'); }
|
|
@@ -95,5 +97,6 @@ export function cloudflare({ accountId, token, fetch: request = globalThis.fetch
|
|
|
95
97
|
}
|
|
96
98
|
throw new Error('provider-pagination-limit');
|
|
97
99
|
}
|
|
98
|
-
return { accountId, envelope, list, async request(path, options) { return (await envelope(path, options)).result; }
|
|
100
|
+
return { accountId, envelope, list, async request(path, options) { return (await envelope(path, options)).result; },
|
|
101
|
+
async zoneRequest(path, options = {}) { return (await envelope(path, { ...options, zone: true })).result; } };
|
|
99
102
|
}
|