@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
|
@@ -4,9 +4,13 @@ import { join, resolve } from 'node:path';
|
|
|
4
4
|
|
|
5
5
|
// Stream output, including copied public assets: a renamed archive is still
|
|
6
6
|
// private. Never include its contents in an error or read it into one buffer.
|
|
7
|
-
|
|
7
|
+
// A seed whose own export writes another format declares that format's
|
|
8
|
+
// `payload.format` strings; a file whose leading JSON object carries one of them
|
|
9
|
+
// is an archive too.
|
|
10
|
+
async function hasManifest(file, formats) {
|
|
8
11
|
let tail = '', kind = false, format = false, header = '', depth = 0, quoted = false, escaped = false, reading = true, started = false;
|
|
9
|
-
const manifest = text => { try { const value = JSON.parse(text); return value?.kind === 'manifest' && value.format === 'runeditrun-portability'; } catch { return false; } };
|
|
12
|
+
const manifest = text => { try { const value = JSON.parse(text); return value?.kind === 'manifest' && value.format === 'runeditrun-portability' || formats.includes(value?.payload?.format); } catch { return false; } };
|
|
13
|
+
const declared = formats.map(name => new RegExp(`"format"\\s*:\\s*${JSON.stringify(name).replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`));
|
|
10
14
|
for await (const chunk of createReadStream(file, { encoding: 'utf8' })) {
|
|
11
15
|
// Parse the leading JSON object too, so escaped property names and pretty
|
|
12
16
|
// printing cannot hide an otherwise valid portability manifest. The format
|
|
@@ -14,7 +18,8 @@ async function hasManifest(file) {
|
|
|
14
18
|
if (reading) for (const character of chunk) {
|
|
15
19
|
if (!started) { if (/\s/.test(character)) continue; if (character !== '{') { reading = false; break; } started = true; }
|
|
16
20
|
header += character;
|
|
17
|
-
|
|
21
|
+
// A leading object past the limit is an archive when it names a declared format.
|
|
22
|
+
if (header.length > 24 * 1024 * 1024) { if (declared.some(pattern => pattern.test(header))) return true; reading = false; header = ''; break; }
|
|
18
23
|
if (quoted) { if (escaped) escaped = false; else if (character === '\\') escaped = true; else if (character === '"') quoted = false; }
|
|
19
24
|
else if (character === '"') quoted = true;
|
|
20
25
|
else if (character === '{') depth++;
|
|
@@ -28,27 +33,34 @@ async function hasManifest(file) {
|
|
|
28
33
|
}
|
|
29
34
|
return false;
|
|
30
35
|
}
|
|
31
|
-
export async function assertArchiveFree(directory) {
|
|
36
|
+
export async function assertArchiveFree(directory, { formats = [] } = {}) {
|
|
37
|
+
assert(formats);
|
|
32
38
|
const status = await lstat(directory).catch(error => { if (error.code === 'ENOENT') return null; throw error; });
|
|
33
39
|
if (!status) return;
|
|
34
40
|
if (status.isSymbolicLink()) throw new Error('Build output must not contain symbolic links.');
|
|
35
41
|
if (status.isDirectory()) {
|
|
36
|
-
for (const name of await readdir(directory)) await assertArchiveFree(join(directory, name));
|
|
37
|
-
} else if (status.isFile() && (/\.rer(?:\.[\w-]+\.partial)?$/i.test(directory) || await hasManifest(directory))) {
|
|
42
|
+
for (const name of await readdir(directory)) await assertArchiveFree(join(directory, name), { formats });
|
|
43
|
+
} else if (status.isFile() && (/\.rer(?:\.[\w-]+\.partial)?$/i.test(directory) || await hasManifest(directory, formats))) {
|
|
38
44
|
throw new Error('Build output contains an installation archive. Move archives to exports/ and build again.');
|
|
39
45
|
}
|
|
40
46
|
}
|
|
41
47
|
|
|
48
|
+
function assert(formats) {
|
|
49
|
+
if (!Array.isArray(formats) || formats.some(name => typeof name !== 'string' || !name.trim())) throw new Error('Archive formats must be a list of non-empty payload.format strings.');
|
|
50
|
+
}
|
|
51
|
+
|
|
42
52
|
// Runs after Vite copies publicDir and writes each client/Worker bundle, also
|
|
43
|
-
// for direct vite build and custom output directories.
|
|
44
|
-
|
|
53
|
+
// for direct vite build and custom output directories. `formats` lists the
|
|
54
|
+
// seed's own archive formats (`archiveOutputGuard({ formats: ['acme/local-data.v1'] })`).
|
|
55
|
+
export function archiveOutputGuard({ formats = [] } = {}) {
|
|
56
|
+
assert(formats);
|
|
45
57
|
let root;
|
|
46
58
|
return {
|
|
47
59
|
name: 'rer:private-archives', apply: 'build', enforce: 'post',
|
|
48
60
|
configResolved(config) { root = config.root; },
|
|
49
61
|
writeBundle: { order: 'post', async handler(options) {
|
|
50
|
-
if (options.dir) await assertArchiveFree(resolve(root, options.dir));
|
|
51
|
-
else if (options.file) await assertArchiveFree(resolve(root, options.file));
|
|
62
|
+
if (options.dir) await assertArchiveFree(resolve(root, options.dir), { formats });
|
|
63
|
+
else if (options.file) await assertArchiveFree(resolve(root, options.file), { formats });
|
|
52
64
|
} },
|
|
53
65
|
};
|
|
54
66
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { appFetch } from './app-fetch.mjs';
|
|
1
2
|
// Setup opens app-local approvals before native bootstrap. Existing owners must
|
|
2
3
|
// prove their email afresh through the local outbox, just like owner controls.
|
|
3
4
|
export async function approveSetupDependencies(context, post, messages) {
|
|
@@ -5,7 +6,7 @@ export async function approveSetupDependencies(context, post, messages) {
|
|
|
5
6
|
if(!selected.length)return;
|
|
6
7
|
const failure=()=>Error('Application dependency approval failed. Complete fresh app-owner approval before linking.');
|
|
7
8
|
let cookie;
|
|
8
|
-
const current=await
|
|
9
|
+
const current=await appFetch(new URL('/auth/dependencies/status',context.origin),{signal:context.signal}).then(response=>response.ok?response.json():null);
|
|
9
10
|
const files=process.env.RER_LINK_TOKEN_FILES?JSON.parse(process.env.RER_LINK_TOKEN_FILES):{};
|
|
10
11
|
for(const dependency of selected) {
|
|
11
12
|
const body={interface:dependency.interface,issuer:context.input[dependency.address]};
|
|
@@ -21,7 +22,7 @@ export async function approveSetupDependencies(context, post, messages) {
|
|
|
21
22
|
let id;
|
|
22
23
|
for(let attempt=0;attempt<50 && !id;attempt++) {id=(await messages(context,path,email)).find(value=>!before.has(value));if(!id)await new Promise(resolve=>setTimeout(resolve,200));}
|
|
23
24
|
if(!id)throw failure();
|
|
24
|
-
const response=await
|
|
25
|
+
const response=await appFetch(new URL(path+'?message='+encodeURIComponent(id),context.origin),{signal:context.signal,headers:context.devKey?{'x-rer-dev-key':context.devKey}:{}});
|
|
25
26
|
if(!response.ok)throw failure();
|
|
26
27
|
const html=await response.text(),href=/<a\b[^>]*class="open"[^>]*href="([^"]+)"/.exec(html)?.[1];
|
|
27
28
|
if(!href)throw failure();
|
|
@@ -30,7 +31,7 @@ export async function approveSetupDependencies(context, post, messages) {
|
|
|
30
31
|
if(confirmed.status!==303)throw failure();cookie=confirmed.headers.get('set-cookie')?.split(';')[0];await confirmed.body?.cancel();
|
|
31
32
|
if(!cookie)throw failure();context.store.addSecrets?.([cookie]);
|
|
32
33
|
}
|
|
33
|
-
const approved=await
|
|
34
|
+
const approved=await appFetch(new URL('/auth/dependencies/relink',context.origin),{method:'POST',redirect:'manual',signal:context.signal,headers:{Origin:context.origin,'Content-Type':'application/json',Cookie:cookie},body:JSON.stringify(body)});
|
|
34
35
|
if(!approved.ok)throw failure();await approved.body?.cancel();
|
|
35
36
|
}
|
|
36
37
|
}
|
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
import { parseEnv } from 'node:util'
|
|
2
|
+
import { basename, dirname } from 'node:path'
|
|
3
|
+
import { fileURLToPath } from 'node:url'
|
|
4
|
+
// The shared loopback predicate, beside this copy in Base or in a seed's scripts/.
|
|
5
|
+
const portable = basename(dirname(dirname(fileURLToPath(import.meta.url)))) === 'base-onboarding'
|
|
6
|
+
const { isLocalhostName } = await import(new URL(portable ? '../../base-cli/modules/operator-auth/loopback.mjs' : '../../modules/operator-auth/loopback.mjs', import.meta.url))
|
|
2
7
|
|
|
3
8
|
export function localVariables(text) {
|
|
4
9
|
// Node's dotenv parser can consume the next assignment after an unquoted
|
|
@@ -23,8 +28,8 @@ export const secretValues = (vars) => Object.entries(vars).filter(([key]) => /SE
|
|
|
23
28
|
export function localOrigin(vars) {
|
|
24
29
|
let url
|
|
25
30
|
try { url = new URL(vars.AUTH_ORIGIN) } catch { /* Use a fixed, non-secret diagnostic. */ }
|
|
26
|
-
if (vars.AUTH_LOCAL !== 'true' || !url || url.protocol !== 'http:' || url.hostname !== '127.0.0.1' || url.origin !== vars.AUTH_ORIGIN || url.username || url.password) {
|
|
27
|
-
throw new Error('Invalid local origin in .dev.vars: AUTH_ORIGIN must be http://127.0.0.1:PORT with AUTH_LOCAL=true. Run rer setup to save a valid one. If you changed AUTH_ORIGIN yourself, put back the http://127.0.0.1:PORT origin setup saved, or pick a port with rer setup --port PORT.')
|
|
31
|
+
if (vars.AUTH_LOCAL !== 'true' || !url || url.protocol !== 'http:' || (url.hostname !== '127.0.0.1' && !isLocalhostName(url.hostname)) || url.origin !== vars.AUTH_ORIGIN || url.username || url.password) {
|
|
32
|
+
throw new Error('Invalid local origin in .dev.vars: AUTH_ORIGIN must be http://127.0.0.1:PORT (or a front-door http://NAME.localhost:PORT) with AUTH_LOCAL=true. Run rer setup to save a valid one. If you changed AUTH_ORIGIN yourself, put back the http://127.0.0.1:PORT origin setup saved, or pick a port with rer setup --port PORT.')
|
|
28
33
|
}
|
|
29
34
|
return url
|
|
30
35
|
}
|
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
2
3
|
import { readFile, unlink } from 'node:fs/promises';
|
|
3
4
|
import { join, relative } from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
4
6
|
import { chooseOrigin } from './adapter-settings.mjs';
|
|
5
7
|
import { privateDirectory, writePrivateFile } from './private-files.mjs';
|
|
6
8
|
import { installationFailure, installationFailureMessage } from './failures.mjs';
|
|
7
9
|
import { labels } from './adapter-labels.mjs';
|
|
8
10
|
|
|
11
|
+
// The rer that nested commands run: a vendored checkout's own scripts/rer.mjs,
|
|
12
|
+
// else this copy's own (a checkout that pins the @runeditrun/rer package).
|
|
13
|
+
const rerEntry=root=>existsSync(join(root,'scripts/rer.mjs'))?'scripts/rer.mjs':fileURLToPath(new URL('../bin/rer.mjs',import.meta.url));
|
|
14
|
+
|
|
9
15
|
// The browser and terminal use the same bounded setup journey. Its temporary
|
|
10
16
|
// runtime is gone before that lease releases. The foreground preview belongs
|
|
11
17
|
// to the installer process, outside the bounded job's descendant tree.
|
|
@@ -20,14 +26,14 @@ export async function installOperator(root,input,store,onApp,signal,hooks){
|
|
|
20
26
|
// The browser owns no local operation. A verifier may have launched it while
|
|
21
27
|
// holding a different checkout's token; each public command takes its own.
|
|
22
28
|
const env={...process.env};delete env.RER_LOCAL_OPERATION;
|
|
23
|
-
const run=(label,args)=>store.run(label,process.execPath,[
|
|
29
|
+
const run=(label,args)=>store.run(label,process.execPath,[rerEntry(root),...args,'--non-interactive','--json'],{cwd:root,signal,env,heavy:false});
|
|
24
30
|
step='dependencies';await store.step(labels.dependencies);await run(labels.dependencies,['install']);
|
|
25
31
|
step='setup';await store.step(labels.build);await run(labels.build,['setup','--answers',relative(root,answer),'--host',address.hostname.replace(/^\[|\]$/g,''),'--port',address.port]);
|
|
26
32
|
await unlink(answer);answer=undefined;
|
|
27
33
|
// Preview's ready event carries RER_OPEN; keep the checkout's dev key out of this log.
|
|
28
34
|
const devKey=/^RER_DEV_KEY=(\S+)$/m.exec(await readFile(join(root,'.rer/local/dev-key.env'),'utf8').catch(()=>''))?.[1];if(devKey)store.addSecrets?.([devKey]);
|
|
29
35
|
step='start';await store.step(labels.start);
|
|
30
|
-
app=store.spawn(labels.start,process.execPath,[
|
|
36
|
+
app=store.spawn(labels.start,process.execPath,[rerEntry(root),'preview','--non-interactive','--json'],{cwd:root,signal,env});onApp(app);
|
|
31
37
|
let buffer='',ready=false,openUrl;
|
|
32
38
|
app.stdout.on('data',chunk=>{buffer+=chunk;let end;while((end=buffer.indexOf('\n'))>=0){const line=buffer.slice(0,end);buffer=buffer.slice(end+1);try{const event=JSON.parse(line);if(event.version===1&&event.verb==='preview'&&event.event==='ready'&&event.url===origin){ready=true;openUrl=event.openUrl;}}catch{}}});
|
|
33
39
|
await store.waitForReady(labels.start,app,async probeSignal=>{
|
|
@@ -18,8 +18,11 @@ export async function operatorPrerequisites(root,email){
|
|
|
18
18
|
db=new DatabaseSync(join(directory,file.name),{readOnly:true});
|
|
19
19
|
const applied=new Set(db.prepare('SELECT name FROM d1_migrations').all().map(row=>row.name));
|
|
20
20
|
const claimed=db.prepare('SELECT COUNT(*) AS count FROM oa_bootstrap').get().count;
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
// Setup claimed this saved owner, and an active owner remains: after that
|
|
22
|
+
// owner has left, dev signs in as the remaining owner (dev identity's owner persona).
|
|
23
|
+
const saved=db.prepare('SELECT COUNT(*) AS count FROM oa_operators WHERE email=?').get(email).count;
|
|
24
|
+
const owners=db.prepare("SELECT COUNT(*) AS count FROM oa_operators WHERE role='owner' AND status='active'").get().count;
|
|
25
|
+
if(claimed===1&&saved===1&&owners>0&&migrations.every(name=>applied.has(name)))return true;
|
|
23
26
|
}catch{/* A missing/corrupt/unrelated database cannot prove prior setup. */}
|
|
24
27
|
finally{db?.close();}
|
|
25
28
|
}
|
|
@@ -23,12 +23,16 @@ export async function queuedEntrypoint(url,verb='verify') {
|
|
|
23
23
|
}finally{await store?.closeCommands();await rm(directory,{recursive:true,force:true});}
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
26
|
+
// `node queued-entry.mjs VERB -- COMMAND ARGS` (a vendored copy) and
|
|
27
|
+
// `rer queue VERB -- COMMAND ARGS` (a pinned package): run COMMAND once the
|
|
28
|
+
// machine job queue admits it, and return its exit status.
|
|
29
|
+
export async function queuedCommandLine([verb,separator,command,...args]){
|
|
30
|
+
if(!verb||separator!=='--'||!command)throw Error('Usage: queued-entry VERB -- COMMAND ARGS');
|
|
29
31
|
const directory=await mkdtemp(join(tmpdir(),'rer-heavy-'));
|
|
30
32
|
const store=await createCommands(directory,{logs:[]},{terminal:text=>process.stdout.write(text),queue:{verb}});
|
|
31
|
-
try{await store.run(verb,command,args,{cwd:process.cwd()});}
|
|
32
|
-
catch(error){
|
|
33
|
+
try{await store.run(verb,command,args,{cwd:process.cwd()});return 0;}
|
|
34
|
+
catch(error){return error.childExitCode??1;}
|
|
33
35
|
finally{await store.closeCommands();await rm(directory,{recursive:true,force:true});}
|
|
34
36
|
}
|
|
37
|
+
|
|
38
|
+
if(process.argv[1] && pathToFileURL(resolve(process.argv[1])).href===import.meta.url)process.exitCode=await queuedCommandLine(process.argv.slice(2));
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { readFile, access } from 'node:fs/promises';
|
|
2
|
-
import { existsSync } from 'node:fs';
|
|
2
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
3
3
|
import { relative, resolve } from 'node:path';
|
|
4
4
|
import { pathToFileURL } from 'node:url';
|
|
5
5
|
import { spawnSync } from 'node:child_process';
|
|
@@ -136,8 +136,17 @@ async function checkSummary(result, descriptor, root) {
|
|
|
136
136
|
return lines.join('\n');
|
|
137
137
|
}
|
|
138
138
|
|
|
139
|
+
// A first-party checkout's rer: its vendored scripts/rer.mjs, or the
|
|
140
|
+
// @runeditrun/rer package it pins.
|
|
141
|
+
function checkoutRer(root) {
|
|
142
|
+
const vendored = resolve(root, 'scripts/rer.mjs'), pinned = resolve(root, 'node_modules/@runeditrun/rer/bin/rer.mjs');
|
|
143
|
+
if (existsSync(vendored)) return vendored;
|
|
144
|
+
try { const pkg = JSON.parse(readFileSync(resolve(root, 'package.json'), 'utf8')); if ((pkg.devDependencies?.['@runeditrun/rer'] ?? pkg.dependencies?.['@runeditrun/rer']) && existsSync(pinned)) return pinned; } catch {}
|
|
145
|
+
return vendored;
|
|
146
|
+
}
|
|
147
|
+
|
|
139
148
|
export async function main(args = process.argv.slice(2), root = process.cwd()) {
|
|
140
|
-
const rer =
|
|
149
|
+
const rer = checkoutRer(root);
|
|
141
150
|
if (!['describe', 'check'].includes(args[0]) || (args.length !== 1 && !existsSync(rer))) throw new Error('Unsupported operations command.');
|
|
142
151
|
// First-party projects describe and check through their RER command.
|
|
143
152
|
if (existsSync(rer)) {
|
package/package.json
CHANGED
|
@@ -1,12 +1,86 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@runeditrun/rer",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "The rer command: install, set up, run and launch Run Edit Run checkouts.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"bin": {
|
|
8
8
|
"rer": "global/rer.mjs"
|
|
9
9
|
},
|
|
10
|
+
"exports": {
|
|
11
|
+
"./adapter": "./cli/adapter.mjs",
|
|
12
|
+
"./deploy": "./cli/deploy.mjs",
|
|
13
|
+
"./portability": "./cli/portability.mjs",
|
|
14
|
+
"./errors": "./cli/errors.mjs",
|
|
15
|
+
"./jsonc": "./cli/jsonc.mjs",
|
|
16
|
+
"./parse": "./cli/parse.mjs",
|
|
17
|
+
"./scheduled": "./cli/scheduled.mjs",
|
|
18
|
+
"./dev-identity": "./cli/dev-identity.mjs",
|
|
19
|
+
"./local-installer": "./onboarding/adapter.mjs",
|
|
20
|
+
"./state": "./onboarding/state.mjs",
|
|
21
|
+
"./commands": "./onboarding/commands.mjs",
|
|
22
|
+
"./operator-installer": "./onboarding/operator-installer.mjs",
|
|
23
|
+
"./operator-install": "./onboarding/operator-install.mjs",
|
|
24
|
+
"./operator-prerequisites": "./onboarding/operator-prerequisites.mjs",
|
|
25
|
+
"./adapter-auth": "./onboarding/adapter-auth.mjs",
|
|
26
|
+
"./adapter-hosting": "./onboarding/adapter-hosting.mjs",
|
|
27
|
+
"./adapter-settings": "./onboarding/adapter-settings.mjs",
|
|
28
|
+
"./failures": "./onboarding/failures.mjs",
|
|
29
|
+
"./private-files": "./onboarding/private-files.mjs",
|
|
30
|
+
"./process-diagnostics": "./onboarding/process-diagnostics.mjs",
|
|
31
|
+
"./process-cleanup": "./onboarding/process-cleanup.mjs",
|
|
32
|
+
"./process-scope": "./onboarding/process-scope.mjs",
|
|
33
|
+
"./redaction": "./onboarding/redaction.mjs",
|
|
34
|
+
"./setup-manifest": "./onboarding/setup-manifest.mjs",
|
|
35
|
+
"./setup-schema": "./onboarding/schema/SETUP.schema.json",
|
|
36
|
+
"./local/environment": "./onboarding/local/environment.mjs",
|
|
37
|
+
"./local/files": "./onboarding/local/files.mjs",
|
|
38
|
+
"./local/build": "./onboarding/local/build.mjs",
|
|
39
|
+
"./local/operation": "./onboarding/local/operation.mjs",
|
|
40
|
+
"./local/recovery": "./onboarding/local/recovery.mjs",
|
|
41
|
+
"./local/state": "./onboarding/local/state.mjs",
|
|
42
|
+
"./production-runtime": "./onboarding/production-runtime.mjs",
|
|
43
|
+
"./production-modules": "./onboarding/production-modules.mjs",
|
|
44
|
+
"./queued-entry": "./onboarding/queued-entry.mjs",
|
|
45
|
+
"./archive-output": {
|
|
46
|
+
"types": "./onboarding/archive-output.d.mts",
|
|
47
|
+
"default": "./onboarding/archive-output.mjs"
|
|
48
|
+
},
|
|
49
|
+
"./launcher": "./onboarding/launcher.mjs",
|
|
50
|
+
"./installer-server": "./onboarding/server.mjs",
|
|
51
|
+
"./server-resources": "./cli/server-resources.mjs",
|
|
52
|
+
"./operations": "./operations/operations.mjs",
|
|
53
|
+
"./modules/portability": "./modules/portability/index.mjs",
|
|
54
|
+
"./modules/portability/catalogue": "./modules/portability/catalogue.mjs",
|
|
55
|
+
"./modules/portability/cli": "./modules/portability/cli.mjs",
|
|
56
|
+
"./modules/portability/cli-options": "./modules/portability/cli-options.mjs",
|
|
57
|
+
"./modules/portability/files": "./modules/portability/files.mjs",
|
|
58
|
+
"./modules/portability/format": "./modules/portability/format.mjs",
|
|
59
|
+
"./modules/portability/http": "./modules/portability/http.mjs",
|
|
60
|
+
"./modules/portability/import": "./modules/portability/import.mjs",
|
|
61
|
+
"./modules/portability/journal": "./modules/portability/journal.mjs",
|
|
62
|
+
"./modules/portability/legacy-safety": "./modules/portability/legacy-safety.mjs",
|
|
63
|
+
"./modules/portability/migration-history": "./modules/portability/migration-history.mjs",
|
|
64
|
+
"./modules/portability/storage": "./modules/portability/storage.mjs",
|
|
65
|
+
"./modules/erasure/fence": "./modules/erasure/fence.mjs",
|
|
66
|
+
"./modules/deploy-kit/owner": "./modules/deploy-kit/owner.mjs",
|
|
67
|
+
"./modules/deploy-kit/owner-cli": "./modules/deploy-kit/owner-cli.mjs",
|
|
68
|
+
"./modules/deploy-kit/prepare": "./modules/deploy-kit/prepare.mjs",
|
|
69
|
+
"./modules/deploy-kit/provider": "./modules/deploy-kit/provider.mjs",
|
|
70
|
+
"./modules/release/export": "./modules/release/export.mjs",
|
|
71
|
+
"./modules/release/publication-scan": "./modules/release/publication-scan.mjs",
|
|
72
|
+
"./modules/release/source": "./modules/release/source.mjs",
|
|
73
|
+
"./modules/operator-auth": "./modules/operator-auth/index.mjs",
|
|
74
|
+
"./modules/operator-auth/accounts/native": "./modules/operator-auth/accounts/native.mjs",
|
|
75
|
+
"./modules/operator-auth/dev": "./modules/operator-auth/dev/index.mjs",
|
|
76
|
+
"./modules/operator-auth/dev-admission": "./modules/operator-auth/dev-admission.mjs",
|
|
77
|
+
"./modules/operator-auth/dev/badge": "./modules/operator-auth/dev/badge.mjs",
|
|
78
|
+
"./modules/operator-auth/identity/email-link": "./modules/operator-auth/identity/email-link/index.mjs",
|
|
79
|
+
"./modules/owner-claim": "./modules/owner-claim/index.mjs",
|
|
80
|
+
"./modules/dependencies": "./modules/dependencies/index.mjs",
|
|
81
|
+
"./internal/*": "./*",
|
|
82
|
+
"./package.json": "./package.json"
|
|
83
|
+
},
|
|
10
84
|
"engines": {
|
|
11
85
|
"node": ">=22"
|
|
12
86
|
}
|
|
@@ -51,6 +51,10 @@ CONTEXT_URL = re.compile(r'(?i)(?<![A-Za-z])(?:' + REPOSITORY_CONTEXT + r')'
|
|
|
51
51
|
+ r'''[`*_~\[\]\s"']*(?:''' + URL.pattern.replace('(?i)', '') + ')')
|
|
52
52
|
LINK_URL = re.compile(r'(?i)\[(?P<label>[^]\n]+)\]\(\s*(?P<url>' + URL.pattern.replace('(?i)', '') + ')')
|
|
53
53
|
REPOSITORY_LABEL = re.compile(r'(?i)\b(?:' + REPOSITORY_CONTEXT + r')\b')
|
|
54
|
+
# Words that make a nearby `owner/name` a repository rather than an identifier.
|
|
55
|
+
# A bare "source" is not one: "source commit/tree" is ordinary prose.
|
|
56
|
+
NEARBY_CONTEXT = re.compile(r'(?i)\b(?:github|repositor(?:y|ies)|repos?|forks?|forked|clones?|cloned|upstream|downstream|origin|submodules?)\b')
|
|
57
|
+
CONTEXT_WINDOW = 60
|
|
54
58
|
|
|
55
59
|
|
|
56
60
|
def explicit_references(text, path):
|
|
@@ -76,6 +80,20 @@ class RepositoryObject(dict):
|
|
|
76
80
|
|
|
77
81
|
|
|
78
82
|
def references(text, path, local_paths=frozenset(), project_identity=None):
|
|
83
|
+
stated, ambiguous = reference_sets(text, path, local_paths, project_identity)
|
|
84
|
+
return stated | ambiguous
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def reference_sets(text, path, local_paths=frozenset(), project_identity=None):
|
|
88
|
+
"""Split `owner/name` tokens into stated repositories and ambiguous ones.
|
|
89
|
+
|
|
90
|
+
A stated reference has GitHub context: a URL, a repository field, a labelled
|
|
91
|
+
or commanded mention, a repository route. An ambiguous token is a bare
|
|
92
|
+
word/word in prose (`commit/tree`, `rer-delegation/1`, `dmsans/v17`): it is
|
|
93
|
+
a repository only if one answers to that name privately, never because none
|
|
94
|
+
does.
|
|
95
|
+
"""
|
|
96
|
+
ambiguous = set()
|
|
79
97
|
repository_text = SERVICE_URL.sub('', text)
|
|
80
98
|
found = {normalize(m.group(1)) for m in URL.finditer(repository_text)
|
|
81
99
|
if not SPONSORS_URL.match(repository_text, m.start())}
|
|
@@ -106,8 +124,16 @@ def references(text, path, local_paths=frozenset(), project_identity=None):
|
|
|
106
124
|
# A plain multi-segment path is ambiguous; GitHub's repository
|
|
107
125
|
# routes, a labeled field or a GitHub command disambiguate it.
|
|
108
126
|
labeled = explicit and (re.search(r'[:=|]', match['separator']) or re.match(r'(?i)github|gh\s', match['label']))
|
|
109
|
-
if
|
|
127
|
+
if REPOSITORY_ROUTE.match(suffix) or labeled:
|
|
110
128
|
found.add(normalize(repository))
|
|
129
|
+
elif not suffix:
|
|
130
|
+
if explicit:
|
|
131
|
+
weak = re.fullmatch(r'(?i)source', match['label']) and not re.search(r'(?i)repository|repo', markdown[match.start():match.start('reference')])
|
|
132
|
+
else:
|
|
133
|
+
line_start = markdown.rfind('\n', 0, match.start()) + 1
|
|
134
|
+
around = markdown[max(line_start, match.start() - CONTEXT_WINDOW):match.end() + CONTEXT_WINDOW].split('\n', 1)[0]
|
|
135
|
+
weak = not NEARBY_CONTEXT.search(around)
|
|
136
|
+
(ambiguous if weak else found).add(normalize(repository))
|
|
111
137
|
elif METADATA.search(path) or SOURCE.search(path):
|
|
112
138
|
if path.lower().endswith('.json'):
|
|
113
139
|
try:
|
|
@@ -146,7 +172,7 @@ def references(text, path, local_paths=frozenset(), project_identity=None):
|
|
|
146
172
|
found.update(normalize(m.group(1)) for m in TYPED_REPOSITORY.finditer(text))
|
|
147
173
|
for declaration in declarations(text):
|
|
148
174
|
found.update(normalize(m.group(1)) for m in DECLARED_SHORTHAND.finditer(declaration))
|
|
149
|
-
return found
|
|
175
|
+
return found, ambiguous - found
|
|
150
176
|
|
|
151
177
|
|
|
152
178
|
def local_dependencies(data, path):
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""Public-tree and message hygiene; findings never include matched values."""
|
|
2
2
|
from collections import Counter
|
|
3
3
|
import re
|
|
4
|
-
from github_refs import REPOSITORY_HOSTS,
|
|
4
|
+
from github_refs import REPOSITORY_HOSTS, reference_sets
|
|
5
5
|
from destination import RULE as PENDING_DESTINATION
|
|
6
6
|
from hygiene_text import decoded_text
|
|
7
7
|
from provenance import provenance_text
|
|
@@ -25,12 +25,35 @@ def private_emails(text, config):
|
|
|
25
25
|
# URL paths, queries and fragments remain visible to email inspection.
|
|
26
26
|
email_text = URI_AUTHORITY.sub(email_userinfo, without_required_notices(text))
|
|
27
27
|
email_text = re.sub(r'(?i)\b(?a:git@github\.com:)(?=[A-Za-z0-9])', '', email_text)
|
|
28
|
-
|
|
28
|
+
# A package manager's patch file is named name@version.patch.
|
|
29
|
+
version = r'(?:v?[0-9]+(?:\.[0-9]+)*(?:-[A-Za-z0-9-]+)?|[a-f0-9]{40})(?:\.patch)?(?![A-Za-z0-9_.-])'
|
|
29
30
|
email_text = re.sub(r'(?m)^\s*-?\s*uses:\s*[\"\']?[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+@[^\s\"\']+', '', email_text)
|
|
30
31
|
email_text = re.sub(r'(?<![A-Za-z0-9])(?:@[A-Za-z0-9_.-]+/)?[A-Za-z0-9_.-]+(?:/[A-Za-z0-9_.-]+)?@' + version, '', email_text)
|
|
31
32
|
return [match.group() for match in EMAIL.finditer(email_text) if not public_email(match.group(), config)]
|
|
32
33
|
|
|
33
34
|
|
|
35
|
+
# A stored host name is lower case, upper case or capitalised, label by label.
|
|
36
|
+
# Bytes that merely decode to text, such as compressed pixels, rarely are.
|
|
37
|
+
HOST_LABEL = re.compile(r'(?:[a-z0-9-]+|[A-Z0-9-]+|[A-Z][a-z0-9-]*)')
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
# Control bytes that no text encoding uses (tab, newline, form feed and carriage
|
|
41
|
+
# return are text). Legacy 8-bit text such as latin-1 or Windows-1252 has none,
|
|
42
|
+
# while compressed or packed data is about one byte in nine.
|
|
43
|
+
NON_TEXT = re.compile(rb'[\x01-\x08\x0b\x0e-\x1f\x7f]')
|
|
44
|
+
BINARY_SHARE = 0.05
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def binary(data):
|
|
48
|
+
# NULs are not counted: UTF-16 and UTF-32 text carries them between letters.
|
|
49
|
+
text = data.replace(b'\0', b'')
|
|
50
|
+
return bool(text) and len(NON_TEXT.findall(text)) / len(text) > BINARY_SHARE
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def stored_host(address):
|
|
54
|
+
return all(HOST_LABEL.fullmatch(label) for label in address.rsplit('@', 1)[1].split('.'))
|
|
55
|
+
|
|
56
|
+
|
|
34
57
|
def certificate_metadata(data, bundle, emails, config):
|
|
35
58
|
# Every match lies inside a verified trust-anchor subject value: none
|
|
36
59
|
# remains once those values are blanked, and each is one of theirs.
|
|
@@ -62,15 +85,23 @@ def inspect_text(data, path, config, resolver):
|
|
|
62
85
|
emails = private_emails(text, config)
|
|
63
86
|
bundle = config.get('trustBundles', {}).get(path)
|
|
64
87
|
if emails and not (bundle and certificate_metadata(data, bundle, emails, config)):
|
|
65
|
-
|
|
88
|
+
# Inside binary data a mailbox is judged by its own shape: a host
|
|
89
|
+
# name with mixed-case letters inside a label is not one.
|
|
90
|
+
if not binary(data) or any(stored_host(email) for email in emails):
|
|
91
|
+
rules.add('hygiene.email')
|
|
66
92
|
ref_text, provenance = provenance_text(text, path, config.get('declaredSources', frozenset()))
|
|
67
|
-
|
|
93
|
+
stated, ambiguous = reference_sets(ref_text, path, config.get('localPaths', frozenset()), config.get('projectIdentity'))
|
|
94
|
+
for repository in sorted(stated | ambiguous):
|
|
68
95
|
# A future source identity does not assert current GitHub visibility.
|
|
69
96
|
# Explicit release destinations still take the strict visibility path.
|
|
70
97
|
if (repository in config.get('intendedPublicRepositories', frozenset()) and
|
|
71
98
|
repository != config.get('pendingDestination')):
|
|
72
99
|
continue
|
|
73
100
|
state = resolver.resolve(repository)
|
|
101
|
+
# A bare word/word with no GitHub context names a repository only when
|
|
102
|
+
# one exists privately; that no repository answers to it proves nothing.
|
|
103
|
+
if state == 'missing' and repository in ambiguous and repository != config.get('pendingDestination'):
|
|
104
|
+
continue
|
|
74
105
|
if state == 'missing' and repository == config.get('pendingDestination'):
|
|
75
106
|
rules.add(PENDING_DESTINATION)
|
|
76
107
|
elif state in ('private', 'missing'):
|
|
@@ -7,6 +7,19 @@
|
|
|
7
7
|
{ "repository": "just-every/lamp", "reason": "Owner-declared public identity; created by clean-history export at the first public tag." },
|
|
8
8
|
{ "repository": "just-every/dial", "reason": "Owner-declared public identity; created by clean-history export at the first public tag." },
|
|
9
9
|
{ "repository": "just-every/orbit", "reason": "Owner-declared public identity; created by clean-history export at the first public tag." },
|
|
10
|
-
{ "repository": "just-every/uptime-monitor", "reason": "Owner-declared public identity; created by clean-history export at the first public tag." }
|
|
10
|
+
{ "repository": "just-every/uptime-monitor", "reason": "Owner-declared public identity; created by clean-history export at the first public tag." },
|
|
11
|
+
{ "repository": "just-every/run-slate", "reason": "Owner-proposed public name (13 Oct 2026), accepted ahead of the rename; created by clean-history export at the first public tag." },
|
|
12
|
+
{ "repository": "just-every/run-pencil", "reason": "Owner-proposed public name (13 Oct 2026), accepted ahead of the rename; created by clean-history export at the first public tag." },
|
|
13
|
+
{ "repository": "just-every/run-lamp", "reason": "Owner-proposed public name (13 Oct 2026), accepted ahead of the rename; created by clean-history export at the first public tag." },
|
|
14
|
+
{ "repository": "just-every/run-uptime", "reason": "Owner-proposed public name (13 Oct 2026), accepted ahead of the rename; created by clean-history export at the first public tag." },
|
|
15
|
+
{ "repository": "just-every/run-cradle", "reason": "Owner-proposed public name (13 Oct 2026), accepted ahead of the rename; created by clean-history export at the first public tag." },
|
|
16
|
+
{ "repository": "just-every/run-vector", "reason": "Owner-proposed public name (13 Oct 2026), accepted ahead of the rename; created by clean-history export at the first public tag." },
|
|
17
|
+
{ "repository": "just-every/run-mango", "reason": "Owner-proposed public name (13 Oct 2026), accepted ahead of the rename; created by clean-history export at the first public tag." },
|
|
18
|
+
{ "repository": "just-every/run-dial", "reason": "Owner-proposed public name (13 Oct 2026), accepted ahead of the rename; created by clean-history export at the first public tag." },
|
|
19
|
+
{ "repository": "just-every/run-penny", "reason": "Owner-proposed public name (13 Oct 2026), accepted ahead of the rename; created by clean-history export at the first public tag." },
|
|
20
|
+
{ "repository": "just-every/run-nook", "reason": "Owner-proposed public name (13 Oct 2026), accepted ahead of the rename; created by clean-history export at the first public tag." },
|
|
21
|
+
{ "repository": "just-every/run-orbit", "reason": "Owner-proposed public name (13 Oct 2026), accepted ahead of the rename; created by clean-history export at the first public tag." },
|
|
22
|
+
{ "repository": "just-every/run-oncall", "reason": "Owner-proposed public name (13 Oct 2026), accepted ahead of the rename; created by clean-history export at the first public tag." },
|
|
23
|
+
{ "repository": "just-every/run-spool", "reason": "Owner-proposed public name (13 Oct 2026), accepted ahead of the rename; created by clean-history export at the first public tag." }
|
|
11
24
|
]
|
|
12
25
|
}
|