@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.
Files changed (106) hide show
  1. package/README.md +111 -1
  2. package/cli/README.md +28 -3
  3. package/cli/contract.mjs +4 -4
  4. package/cli/dev-identity.mjs +2 -1
  5. package/cli/door-mode.mjs +134 -0
  6. package/cli/door.mjs +263 -0
  7. package/cli/hosted-output.mjs +15 -5
  8. package/cli/hydration/browser.mjs +91 -0
  9. package/cli/hydration/cases.mjs +106 -0
  10. package/cli/hydration/check.mjs +105 -0
  11. package/cli/hydration/probe.mjs +164 -0
  12. package/cli/hydration/scripts.mjs +39 -0
  13. package/cli/hydration/verdict.mjs +85 -0
  14. package/cli/inspect.mjs +56 -9
  15. package/cli/lifecycle.mjs +36 -12
  16. package/cli/local.mjs +9 -3
  17. package/cli/main.mjs +2 -0
  18. package/cli/managed-server.mjs +52 -18
  19. package/cli/package-calls.mjs +81 -0
  20. package/cli/package-mode.mjs +46 -0
  21. package/cli/parse.mjs +14 -4
  22. package/cli/pnpm-runtime.mjs +96 -0
  23. package/cli/portability-worker.mjs +2 -2
  24. package/cli/runtime-front-door.mjs +4 -2
  25. package/cli/server-proxy.mjs +69 -5
  26. package/cli/server-registry.mjs +10 -7
  27. package/cli/settings.mjs +14 -0
  28. package/cli/site-check.mjs +96 -0
  29. package/cli/site-preview.mjs +53 -0
  30. package/cli/site-render.mjs +77 -0
  31. package/cli/site.mjs +109 -0
  32. package/global/CHANGELOG.md +63 -0
  33. package/global/README.md +111 -1
  34. package/global/checkout.mjs +24 -10
  35. package/global/cli/install.mjs +6 -2
  36. package/global/delegate.mjs +9 -7
  37. package/global/door/command.mjs +254 -0
  38. package/global/door/control.mjs +65 -0
  39. package/global/door/cookies.mjs +41 -0
  40. package/global/door/pages.mjs +49 -0
  41. package/global/door/policy.mjs +49 -0
  42. package/global/door/routes.mjs +45 -0
  43. package/global/door/server.mjs +255 -0
  44. package/global/door/sites.mjs +192 -0
  45. package/global/launch/dev.mjs +21 -7
  46. package/global/launch/door.mjs +36 -0
  47. package/global/launch/install-pinned.mjs +22 -0
  48. package/global/launch/prepare.mjs +38 -5
  49. package/global/launch/render.mjs +6 -1
  50. package/global/launch/steps.mjs +12 -7
  51. package/global/launch/worker.mjs +3 -3
  52. package/global/list.mjs +6 -3
  53. package/global/main.mjs +37 -3
  54. package/global/version.mjs +8 -8
  55. package/modules/dependencies/protocol.mjs +2 -1
  56. package/modules/deploy-kit/README.md +34 -1
  57. package/modules/deploy-kit/artifact.mjs +10 -0
  58. package/modules/deploy-kit/owner.mjs +5 -4
  59. package/modules/deploy-kit/prepare.mjs +3 -2
  60. package/modules/deploy-kit/provider.mjs +6 -3
  61. package/modules/deploy-kit/site.mjs +135 -0
  62. package/modules/erasure/CONTRACT.md +6 -3
  63. package/modules/erasure/README.md +8 -1
  64. package/modules/erasure/audit.mjs +2 -2
  65. package/modules/erasure/cascade.mjs +52 -11
  66. package/modules/erasure/order.mjs +29 -22
  67. package/modules/erasure/plan.mjs +4 -2
  68. package/modules/erasure/queue.mjs +41 -0
  69. package/modules/erasure/scope.mjs +3 -3
  70. package/modules/erasure/verification.mjs +4 -2
  71. package/modules/mail/README.md +1 -1
  72. package/modules/mail/index.mjs +1 -1
  73. package/modules/mail/src/core/errors.mjs +9 -0
  74. package/modules/mail/src/development/index.d.mts +6 -0
  75. package/modules/mail/src/development/index.mjs +6 -3
  76. package/modules/mail/src/development/viewer-html.mjs +3 -3
  77. package/modules/mail/src/development/viewer.mjs +8 -1
  78. package/modules/operator-auth/dev/index.mjs +15 -5
  79. package/modules/operator-auth/http.mjs +2 -3
  80. package/modules/operator-auth/loopback.mjs +17 -0
  81. package/modules/owner-claim/verify.mjs +2 -1
  82. package/modules/portability/README.md +8 -0
  83. package/modules/portability/hash.mjs +6 -2
  84. package/modules/portability/import.mjs +1 -1
  85. package/modules/portability/legacy-files.mjs +4 -4
  86. package/modules/portability/legacy-safety.mjs +51 -10
  87. package/modules/portability/migration-history.mjs +15 -0
  88. package/modules/release/README.md +5 -3
  89. package/modules/release/produce.mjs +5 -2
  90. package/modules/release/source.mjs +15 -4
  91. package/onboarding/adapter-auth.mjs +4 -3
  92. package/onboarding/adapter-settings.mjs +21 -5
  93. package/onboarding/adapter.mjs +2 -1
  94. package/onboarding/app-fetch.mjs +79 -0
  95. package/onboarding/archive-output.d.mts +4 -2
  96. package/onboarding/archive-output.mjs +22 -10
  97. package/onboarding/dependency-approval.mjs +4 -3
  98. package/onboarding/local/environment.mjs +7 -2
  99. package/onboarding/operator-install.mjs +8 -2
  100. package/onboarding/operator-prerequisites.mjs +5 -2
  101. package/onboarding/queued-entry.mjs +9 -5
  102. package/operations/operations.mjs +11 -2
  103. package/package.json +75 -1
  104. package/scripts/publish-check/github_refs.py +28 -2
  105. package/scripts/publish-check/public_hygiene.py +35 -4
  106. 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
- async function hasManifest(file) {
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
- if (header.length > 24 * 1024 * 1024) { reading = false; header = ''; break; }
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
- export function archiveOutputGuard() {
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 fetch(new URL('/auth/dependencies/status',context.origin),{signal:context.signal}).then(response=>response.ok?response.json():null);
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 fetch(new URL(path+'?message='+encodeURIComponent(id),context.origin),{signal:context.signal,headers:context.devKey?{'x-rer-dev-key':context.devKey}:{}});
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 fetch(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
+ 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,['scripts/rer.mjs',...args,'--non-interactive','--json'],{cwd:root,signal,env,heavy:false});
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,['scripts/rer.mjs','preview','--non-interactive','--json'],{cwd:root,signal,env});onApp(app);
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
- const owner=db.prepare("SELECT COUNT(*) AS count FROM oa_operators WHERE email=? AND role='owner' AND status='active'").get(email).count;
22
- if(claimed===1&&owner===1&&migrations.every(name=>applied.has(name)))return true;
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
- if(process.argv[1] && pathToFileURL(resolve(process.argv[1])).href===import.meta.url){
27
- const [verb,separator,command,...args]=process.argv.slice(2);
28
- if(separator!=='--'||!command)throw Error('Usage: queued-entry VERB -- COMMAND ARGS');
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){process.exitCode=error.childExitCode??1;}
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 = resolve(root, 'scripts/rer.mjs');
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.1.1",
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 not suffix or REPOSITORY_ROUTE.match(suffix) or labeled:
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, references
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
- version = r'(?:v?[0-9]+(?:\.[0-9]+)*(?:-[A-Za-z0-9-]+)?|[a-f0-9]{40})(?![A-Za-z0-9_.-])'
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
- rules.add('hygiene.email')
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
- for repository in references(ref_text, path, config.get('localPaths', frozenset()), config.get('projectIdentity')):
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
  }