@runeditrun/rer 0.2.0 → 0.2.2
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 +15 -4
- package/cli/README.md +4 -2
- package/cli/auth-copy.mjs +25 -18
- package/cli/bounded.mjs +13 -4
- package/cli/contract.mjs +3 -3
- package/cli/deploy-inputs.mjs +33 -0
- package/cli/deploy-owner.mjs +61 -0
- package/cli/deploy.mjs +95 -9
- package/cli/detached-server.mjs +51 -0
- package/cli/door.mjs +1 -1
- package/cli/hosted-output.mjs +2 -2
- package/cli/hydration/check.mjs +2 -2
- package/cli/inspect.mjs +24 -32
- package/cli/lifecycle.mjs +14 -4
- package/cli/main.mjs +1 -0
- package/cli/managed-server.mjs +12 -5
- package/cli/parse.mjs +5 -2
- package/cli/pnpm-probe.mjs +14 -0
- package/cli/pnpm-runtime.mjs +22 -22
- package/cli/portability-policy.mjs +55 -0
- package/cli/portability-worker.mjs +8 -4
- package/cli/portability.mjs +20 -4
- package/cli/project-pnpm.mjs +67 -0
- package/cli/release.mjs +36 -0
- package/cli/server-registry.mjs +2 -2
- package/cli/site-bindings.mjs +291 -0
- package/cli/site-browser-objects.mjs +36 -0
- package/cli/site-check.mjs +55 -31
- package/cli/site-client.mjs +50 -0
- package/cli/site-data.mjs +55 -0
- package/cli/site-entities.json +1 -0
- package/cli/site-input-assets.mjs +58 -0
- package/cli/site-inputs.mjs +79 -0
- package/cli/site-markup.mjs +102 -0
- package/cli/site-private.mjs +36 -0
- package/cli/site-references.mjs +243 -0
- package/cli/site-render.mjs +7 -5
- package/cli/site-syntax.mjs +21 -0
- package/cli/site.mjs +32 -11
- package/global/CHANGELOG.md +39 -0
- package/global/README.md +15 -4
- package/global/cli/install.mjs +26 -9
- package/global/cli/path-link.mjs +30 -0
- package/global/cli/status.mjs +1 -1
- package/global/cli/uninstall.mjs +5 -1
- package/global/launch/install-pinned.mjs +4 -0
- package/global/main.mjs +30 -3
- package/global/output.mjs +12 -0
- package/global/registry.mjs +2 -2
- package/global/rer.mjs +3 -0
- package/modules/container/deploy-input.mjs +5 -9
- package/modules/dependencies/auth.mjs +2 -1
- package/modules/deploy-kit/README.md +25 -7
- package/modules/deploy-kit/d1-import.mjs +35 -0
- package/modules/deploy-kit/deploy.mjs +9 -5
- package/modules/deploy-kit/diagnostics.mjs +65 -0
- package/modules/deploy-kit/env-file.mjs +26 -0
- package/modules/deploy-kit/health-request.mjs +6 -1
- package/modules/deploy-kit/health.mjs +35 -15
- package/modules/deploy-kit/migration-ledger.mjs +48 -0
- package/modules/deploy-kit/migrations.mjs +11 -15
- package/modules/deploy-kit/owner-cli.mjs +29 -10
- package/modules/deploy-kit/owner-remove.mjs +105 -0
- package/modules/deploy-kit/owner-secret.mjs +49 -0
- package/modules/deploy-kit/owner.mjs +71 -10
- package/modules/deploy-kit/provider.mjs +59 -7
- package/modules/job-queue/config.mjs +4 -2
- package/modules/operator-auth/accounts/native.mjs +1 -1
- package/modules/operator-auth/dev/badge.mjs +4 -1
- package/modules/operator-auth/identity/oidc/README.md +18 -0
- package/modules/operator-auth/identity/oidc/config.mjs +4 -2
- package/modules/operator-auth/identity/oidc/index.d.mts +1 -1
- package/modules/operator-auth/identity/oidc/index.mjs +28 -9
- package/modules/operator-auth/identity/oidc/network.mjs +9 -4
- package/modules/operator-auth/identity/oidc/store.mjs +19 -3
- package/modules/operator-auth/index.d.mts +6 -9
- package/modules/operator-auth/page.mjs +1 -1
- package/modules/operator-auth/routes.mjs +1 -1
- package/modules/operator-auth/service.mjs +1 -0
- package/modules/operator-auth/signin/index.d.mts +11 -0
- package/modules/operator-auth/signin/index.mjs +10 -7
- package/modules/portability/README.md +28 -0
- package/modules/portability/bridge.mjs +10 -3
- package/modules/portability/import.mjs +4 -2
- package/modules/release/README.md +63 -0
- package/modules/release/keygen.mjs +18 -0
- package/modules/release/produce.mjs +9 -0
- package/modules/release/sign.mjs +44 -0
- package/modules/release/signature.mjs +336 -0
- package/modules/release/signing-cli.mjs +56 -0
- package/modules/release/signing-io.mjs +61 -0
- package/modules/release/signing-key.mjs +22 -0
- package/modules/release/statement.mjs +80 -0
- package/modules/release/vectors/generate.mjs +381 -0
- package/modules/release/vectors/release-signature-vectors.json +2305 -0
- package/modules/release/verify.mjs +27 -0
- package/onboarding/commands.mjs +3 -1
- package/onboarding/production-assets.mjs +23 -0
- package/onboarding/production-runtime.mjs +8 -4
- package/package.json +2 -1
- package/scripts/publish-check/README.md +27 -17
- package/scripts/publish-check/content_identities.py +14 -233
- package/scripts/publish-check/git_history.py +2 -2
- package/scripts/publish-check/gitleaks.toml +1 -1
- package/scripts/publish-check/history_scan.py +6 -4
- package/scripts/publish-check/hygiene_config.py +3 -1
- package/scripts/publish-check/public_hygiene.py +29 -5
- package/scripts/publish-check/publication-config.json +9 -1
- package/scripts/publish-check/source_metadata.py +62 -0
- package/scripts/publish-check/source_shapes.py +91 -0
- package/scripts/publish-check/tree_scan.py +2 -2
- package/scripts/publish-check/source_hashes.mjs +0 -6
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import { memberName, stringValue, walkSyntax } from './site-syntax.mjs';
|
|
2
|
+
import { localValues } from './site-bindings.mjs';
|
|
3
|
+
import { browserObjects } from './site-browser-objects.mjs';
|
|
4
|
+
import { markupReferences, cssReferences, markupStrings, cssStrings, htmlElements } from './site-markup.mjs';
|
|
5
|
+
|
|
6
|
+
// A firstChild expression is not a type proof: its parent may contain an
|
|
7
|
+
// object element. Admit CharacterData only from an unambiguous emitted text
|
|
8
|
+
// child, consistent across all pages, with no script that can replace it.
|
|
9
|
+
export function javascriptContext(documents, trees) {
|
|
10
|
+
const children = new Map();
|
|
11
|
+
// Recognize a balanced subset of ordinary HTML, rather than guessing at
|
|
12
|
+
// browser repair rules. Tables, foreign content, templates and formatting
|
|
13
|
+
// reconstruction leave every node unknown. Common page links, headings and
|
|
14
|
+
// buttons are safe when explicitly closed in their ordinary insertion mode.
|
|
15
|
+
const containers = new Set('p div span section article aside nav header footer main object a button h1 h2 h3 h4 h5 h6 ul ol li dl dt dd figure figcaption details summary'.split(' '));
|
|
16
|
+
const voids = new Set('br hr img input link meta wbr'.split(' '));
|
|
17
|
+
const raw = new Set(['script','style','title','textarea']);
|
|
18
|
+
const ordinary = new Set([...containers, ...voids, ...raw, 'html','head','body']);
|
|
19
|
+
const blocks = new Set('p div section article aside nav header footer main h1 h2 h3 h4 h5 h6 ul ol li dl dt dd figure figcaption details summary hr'.split(' '));
|
|
20
|
+
const unknown = () => ({textParents:new Set()});
|
|
21
|
+
for (const text of documents) {
|
|
22
|
+
const seen = new Set(), structure = new Set(), stack = [];
|
|
23
|
+
const tags = /<!--[\s\S]*?(?:-->|$)|<!doctype\s+html\s*>|<\/?([a-z][a-z0-9:-]*)\b((?:[^"'<>]|"[^"]*"|'[^']*')*)>|[^<]+|</gi;
|
|
24
|
+
for (let match; (match = tags.exec(text));) {
|
|
25
|
+
if (match[0] === '<') return unknown();
|
|
26
|
+
if (match[0].startsWith('<!--') && !/^<!--(?:[^<>-]|-(?!-))*-->$/.test(match[0])) return unknown();
|
|
27
|
+
if (!match[1]) continue;
|
|
28
|
+
const tag = match[1].toLowerCase();
|
|
29
|
+
if (!ordinary.has(tag)) return unknown();
|
|
30
|
+
if (match[0].startsWith('</')) {
|
|
31
|
+
if (match[2].trim() || stack.pop() !== tag) return unknown();
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
// Reject malformed attributes and non-void self-closing tags, which HTML
|
|
35
|
+
// parses differently from XML. Never infer text from the repaired tree.
|
|
36
|
+
if (!/^(?:\s+[^\s"'<>/=]+(?:\s*=\s*(?:"[^"]*"|'[^']*'|[^\s"'=<>`]+))?)*\s*\/?$/.test(match[2]) || (!voids.has(tag) && /\/\s*$/.test(match[2]))) return unknown();
|
|
37
|
+
if (stack.includes('head') && !['meta','link','title','style','script'].includes(tag)) return unknown();
|
|
38
|
+
if ((blocks.has(tag) && stack.includes('p')) || (['a','button','li'].includes(tag) && stack.includes(tag)) || (/^h[1-6]$/.test(tag) && stack.some(parent => /^h[1-6]$/.test(parent))) || (['dt','dd'].includes(tag) && stack.some(parent => ['dt','dd'].includes(parent)))) return unknown();
|
|
39
|
+
if (['html','head','body'].includes(tag)) {
|
|
40
|
+
if (structure.has(tag) || stack.some(parent => parent !== 'html') || (tag === 'html' && stack.length) || (tag === 'head' && structure.has('body'))) return unknown();
|
|
41
|
+
structure.add(tag);
|
|
42
|
+
}
|
|
43
|
+
const {attributes} = htmlElements(match[0])[0];
|
|
44
|
+
if ([...attributes.keys()].some(name => name.startsWith('on'))) return unknown();
|
|
45
|
+
const id = attributes.get('id');
|
|
46
|
+
if (id !== undefined) {
|
|
47
|
+
const firstText = /^[^<]*/.exec(text.slice(tags.lastIndex))[0].replace(/\0/g, '');
|
|
48
|
+
const textChild = containers.has(tag) && firstText.length > 0;
|
|
49
|
+
children.set(id, textChild && !seen.has(id) && children.get(id) !== false);
|
|
50
|
+
seen.add(id);
|
|
51
|
+
}
|
|
52
|
+
if (raw.has(tag)) {
|
|
53
|
+
if (tag === 'script' && !/^(?:|module|(?:text|application)\/(?:java|ecma)script|application\/(?:ld\+)?json)$/i.test(attributes.get('type') ?? '')) return unknown();
|
|
54
|
+
const closing = new RegExp('</' + tag + '(?=[\\s/>])[^>]*>', 'ig'); closing.lastIndex = tags.lastIndex;
|
|
55
|
+
const end = closing.exec(text);
|
|
56
|
+
if (!end || !new RegExp('^</' + tag + '\\s*>$', 'i').test(end[0])) return unknown();
|
|
57
|
+
if (tag === 'script' && /<!--|<script\b/i.test(text.slice(tags.lastIndex, end.index))) return unknown();
|
|
58
|
+
tags.lastIndex = closing.lastIndex;
|
|
59
|
+
} else if (!voids.has(tag)) stack.push(tag);
|
|
60
|
+
}
|
|
61
|
+
if (stack.length) return unknown();
|
|
62
|
+
}
|
|
63
|
+
// Include every emitted script: a different island can change the selected
|
|
64
|
+
// element, its id or its children before this assignment runs.
|
|
65
|
+
const nativeMethods = new Set(['querySelector','querySelectorAll','getElementById','getElementsByTagName','getElementsByClassName','getElementsByName','createElement','createElementNS','getAttribute','hasAttribute','addEventListener','removeEventListener','setAttribute','setAttributeNS','removeAttribute','removeAttributeNS']);
|
|
66
|
+
const reflectiveMethods = new Set(['assign','defineProperty','defineProperties','setPrototypeOf','set','deleteProperty']);
|
|
67
|
+
const writes = new Set(['id','innerHTML','outerHTML','textContent','innerText','outerText','srcdoc','document','head','firstChild','__proto__',...nativeMethods,...reflectiveMethods]);
|
|
68
|
+
const changes = new Set(['before','after','append','prepend','appendChild','removeChild','replaceChild','replaceChildren','insertBefore','insertAdjacentHTML','insertAdjacentElement','insertAdjacentText','replaceWith','remove','write','writeln','setHTML','setHTMLUnsafe','normalize','insertNode','surroundContents','extractContents','deleteContents','adoptNode','execCommand','setAttributeNode','setAttributeNodeNS','__defineGetter__','__defineSetter__']);
|
|
69
|
+
let stable = true;
|
|
70
|
+
for (const tree of trees) {
|
|
71
|
+
const values = localValues(tree), browser = browserObjects(values);
|
|
72
|
+
// A newly created metadata link appended to the head cannot replace a
|
|
73
|
+
// selected text child in the body. Its resource URLs still get checked.
|
|
74
|
+
// Requiring fresh native nodes also excludes moving an existing child out
|
|
75
|
+
// of a text parent. Any id write above still invalidates this proof.
|
|
76
|
+
const documentObject = node => {
|
|
77
|
+
const alternatives = values.origins(node);
|
|
78
|
+
return alternatives.length > 0 && alternatives.every(browser.document);
|
|
79
|
+
};
|
|
80
|
+
const plainObject = node => {
|
|
81
|
+
const receivers = values.origins(node);
|
|
82
|
+
return receivers.length > 0 && receivers.every(value => value.type === 'ObjectExpression' && value.properties.every(property => property.type === 'Property' && property.kind === 'init'));
|
|
83
|
+
};
|
|
84
|
+
// Selector, traversal and head-link proofs depend on native DOM methods.
|
|
85
|
+
// Writes to those members, including prototype mutation through reflection,
|
|
86
|
+
// remove the proof across all emitted scripts. A known fresh plain object
|
|
87
|
+
// can still receive ordinary metadata without changing the native APIs.
|
|
88
|
+
const reflectiveMutation = node => values.names(node).some(name => reflectiveMethods.has(name))
|
|
89
|
+
&& ['Object','Reflect'].some(name => values.global(node.object, name));
|
|
90
|
+
const writeTargets = node => {
|
|
91
|
+
if (!node) return [];
|
|
92
|
+
if (node.type === 'ObjectPattern') return node.properties.flatMap(property => writeTargets(property.value ?? property.argument));
|
|
93
|
+
if (node.type === 'ArrayPattern') return node.elements.flatMap(writeTargets);
|
|
94
|
+
if (node.type === 'AssignmentPattern') return writeTargets(node.left);
|
|
95
|
+
if (node.type === 'RestElement') return writeTargets(node.argument);
|
|
96
|
+
return [node];
|
|
97
|
+
};
|
|
98
|
+
const headLink = node => {
|
|
99
|
+
if (node?.type !== 'CallExpression' || memberName(node.callee) !== 'appendChild' || node.arguments.length !== 1) return false;
|
|
100
|
+
const receivers = values.origins(node.callee.object), arguments_ = values.origins(node.arguments[0]);
|
|
101
|
+
return receivers.length > 0 && receivers.every(receiver => memberName(receiver) === 'head' && documentObject(receiver.object))
|
|
102
|
+
&& arguments_.length > 0 && arguments_.every(value => value.type === 'CallExpression' && memberName(value.callee) === 'createElement' && value.arguments.length === 1
|
|
103
|
+
&& documentObject(value.callee.object) && values.resources(value.arguments[0]).every(tag => tag.complete && tag.value.toLowerCase() === 'link'));
|
|
104
|
+
};
|
|
105
|
+
walkSyntax(tree, (node, parents) => {
|
|
106
|
+
const target = node.type === 'AssignmentExpression' ? node.left : node.type === 'UpdateExpression' || (node.type === 'UnaryExpression' && node.operator === 'delete') ? node.argument : undefined;
|
|
107
|
+
for (const write of writeTargets(target)) {
|
|
108
|
+
if (write.type === 'Identifier' && values.global(write, 'document')) stable = false;
|
|
109
|
+
if (write.type !== 'MemberExpression') continue;
|
|
110
|
+
const fields = values.names(write);
|
|
111
|
+
if ((!fields.length && !plainObject(write.object)) || fields.some(name => writes.has(name))) stable = false;
|
|
112
|
+
}
|
|
113
|
+
if (node.type === 'Property' && values.names(node).some(name => writes.has(name) || changes.has(name))) stable = false;
|
|
114
|
+
if (node.type === 'MemberExpression' && values.names(node).some(name => name === 'attributes' || changes.has(name)) && !headLink(parents.at(-1))) stable = false;
|
|
115
|
+
if (node.type === 'MemberExpression' && reflectiveMutation(node)) {
|
|
116
|
+
const call = parents.at(-1);
|
|
117
|
+
if (call?.type !== 'CallExpression' || call.callee !== node || !plainObject(call.arguments[0])) stable = false;
|
|
118
|
+
}
|
|
119
|
+
if (!['CallExpression','NewExpression'].includes(node.type)) return;
|
|
120
|
+
if (values.global(node.callee, 'eval') || values.global(node.callee, 'Function')) stable = false;
|
|
121
|
+
const appendsHeadLink = headLink(node);
|
|
122
|
+
for (const callee of values.origins(node.callee)) {
|
|
123
|
+
const methods = values.names(callee);
|
|
124
|
+
if (reflectiveMutation(callee) && !plainObject(node.arguments[0])) stable = false;
|
|
125
|
+
if (methods.some(name => changes.has(name)) && !appendsHeadLink) stable = false;
|
|
126
|
+
if (methods.some(name => ['setAttribute','setAttributeNS','removeAttribute','removeAttributeNS'].includes(name))) {
|
|
127
|
+
const index = methods.some(name => name.endsWith('NS')) ? 1 : 0, attributes = values.resources(node.arguments[index]);
|
|
128
|
+
if (attributes.some(({value, complete}) => !complete || /^(?:id|on.*)$/i.test(value))) stable = false;
|
|
129
|
+
} else if (!appendsHeadLink && node.arguments.some(argument => browser.dom(argument))) stable = false;
|
|
130
|
+
if (!appendsHeadLink && callee.type === 'MemberExpression' && (!methods.length || (browser.dom(callee.object) && methods.some(name => !nativeMethods.has(name))))) stable = false;
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
return {textParents:new Set(stable ? [...children].filter(([,text]) => text).map(([id]) => id) : [])};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function javascriptContent(tree, {textParents = new Set()} = {}) {
|
|
138
|
+
const values = localValues(tree), {strings, names, resources, origins, global, localClass} = values;
|
|
139
|
+
const browser = browserObjects(values), found = [], literals = [], unresolved = [];
|
|
140
|
+
const properties = new Set(['src','href','srcset','action','formaction','poster','data']);
|
|
141
|
+
// Unknown DOM nodes retain the data URL check. A selector or traversal
|
|
142
|
+
// alone cannot distinguish HTMLObjectElement from CharacterData.
|
|
143
|
+
const characterData = node => {
|
|
144
|
+
const alternatives = origins(node);
|
|
145
|
+
return alternatives.length > 0 && alternatives.every(value => {
|
|
146
|
+
if (value.type !== 'MemberExpression' || memberName(value) !== 'firstChild') return false;
|
|
147
|
+
const parents = origins(value.object);
|
|
148
|
+
return parents.length > 0 && parents.every(parent => {
|
|
149
|
+
if (parent.type !== 'CallExpression') return false;
|
|
150
|
+
const receivers = origins(parent.callee.object);
|
|
151
|
+
if (!receivers.length || !receivers.every(browser.document)) return false;
|
|
152
|
+
const method = memberName(parent.callee), selectors = resources(parent.arguments[0]);
|
|
153
|
+
return ['querySelector','getElementById'].includes(method) && selectors.length > 0 && selectors.every(({value, complete}) => {
|
|
154
|
+
const id = method === 'getElementById' ? value : /^#([a-z_][\w-]*)$/i.exec(value)?.[1];
|
|
155
|
+
return complete && id !== undefined && textParents.has(id);
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
};
|
|
160
|
+
const elementFactory = callee => {
|
|
161
|
+
const methods = names(callee);
|
|
162
|
+
if (!methods.some(name => ['createElement','jsx','jsxs','jsxDEV'].includes(name)) || localClass(callee.object)) return false;
|
|
163
|
+
const receivers = origins(callee.object);
|
|
164
|
+
// Native document construction takes options, whereas framework factories
|
|
165
|
+
// render props. Mixed receiver alternatives must retain their framework use.
|
|
166
|
+
return !(methods.every(name => name === 'createElement') && receivers.length && receivers.every(browser.document));
|
|
167
|
+
};
|
|
168
|
+
const add = (node, kind, navigates = false, documentRelative = true, srcset = false) => {
|
|
169
|
+
const targets = resources(node);
|
|
170
|
+
if (targets.some(target => !target.complete)) unresolved.push({kind});
|
|
171
|
+
for (const {value, complete} of targets) for (const url of srcset ? value.split(',').map(part => part.trim().split(/\s+/)[0]) : [value]) found.push({url, navigates, documentRelative, dynamic:!complete});
|
|
172
|
+
};
|
|
173
|
+
const css = (node, emitted) => {for (const {value, complete} of resources(node)) {
|
|
174
|
+
literals.push(...cssStrings(value));
|
|
175
|
+
if (!emitted) continue;
|
|
176
|
+
const references = cssReferences(value);
|
|
177
|
+
found.push(...references.map(reference => ({...reference,documentRelative:true,dynamic:!complete})));
|
|
178
|
+
if (!complete && references.length) unresolved.push({kind:'CSS.url'});
|
|
179
|
+
}};
|
|
180
|
+
const markup = (node, emitted) => { for (const value of strings(node)) {
|
|
181
|
+
literals.push(...markupStrings(value));
|
|
182
|
+
if (emitted) found.push(...markupReferences(value, true).map(reference => ({...reference, documentRelative:true})));
|
|
183
|
+
}};
|
|
184
|
+
walkSyntax(tree, node => {
|
|
185
|
+
if (['ImportDeclaration','ExportNamedDeclaration','ExportAllDeclaration','ImportExpression'].includes(node.type) && node.source) add(node.source, node.type === 'ImportExpression' ? 'import()' : 'import', false, false);
|
|
186
|
+
if (node.type === 'AssignmentExpression') {
|
|
187
|
+
const object = node.left.object, fields = names(node.left);
|
|
188
|
+
if (browser.location(node.left)) add(node.right, 'location.href');
|
|
189
|
+
for (const property of fields) {
|
|
190
|
+
const name = property.toLowerCase();
|
|
191
|
+
if (properties.has(name) && browser.dom(object) && (name !== 'data' || !characterData(object))) add(node.right, `DOM.${property}`, name === 'href' && browser.anchor(object), true, name === 'srcset');
|
|
192
|
+
if (property === 'href' && browser.location(object)) add(node.right, 'location.href');
|
|
193
|
+
if (['innerHTML','outerHTML','srcdoc'].includes(property)) markup(node.right, browser.dom(object));
|
|
194
|
+
}
|
|
195
|
+
// String extraction retains its privacy scope even for inert object fields.
|
|
196
|
+
if (fields.some(property => ['style','cssText'].includes(property)) || origins(object).some(value => memberName(value) === 'style')) css(node.right, browser.style(object) || (fields.includes('style') && browser.dom(object)));
|
|
197
|
+
}
|
|
198
|
+
if (node.type === 'Property') {
|
|
199
|
+
const property = node.computed ? stringValue(node.key) : node.key.name ?? node.key.value;
|
|
200
|
+
// Keep extracting private text from stored markup; resource use is checked
|
|
201
|
+
// where the value reaches a DOM write or a recognized element factory.
|
|
202
|
+
if (property === '__html') markup(node.value, false);
|
|
203
|
+
}
|
|
204
|
+
if (!['CallExpression','NewExpression'].includes(node.type)) return;
|
|
205
|
+
const args = node.arguments;
|
|
206
|
+
const createsElement = node.type === 'CallExpression' && origins(node.callee).some(elementFactory);
|
|
207
|
+
const tags = createsElement ? strings(args[0]) : [];
|
|
208
|
+
if (tags.length) for (const props of origins(args[1])) {
|
|
209
|
+
if (props.type !== 'ObjectExpression') continue;
|
|
210
|
+
for (const field of props.properties) for (const property of names(field)) {
|
|
211
|
+
if (properties.has(property.toLowerCase())) add(field.value, `DOM.${property}`, property === 'href' && tags.some(tag => ['a','area'].includes(tag)), true, property.toLowerCase() === 'srcset');
|
|
212
|
+
if (property === 'dangerouslySetInnerHTML') for (const html of origins(field.value)) {
|
|
213
|
+
if (html.type === 'ObjectExpression') for (const content of html.properties) if (names(content).includes('__html')) markup(content.value, true);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
for (const name of ['fetch','Worker','SharedWorker','WebSocket','EventSource']) if (global(node.callee, name)) add(args[0], name);
|
|
218
|
+
for (const callee of origins(node.callee)) for (const method of names(callee)) {
|
|
219
|
+
const object = callee.object;
|
|
220
|
+
if (method === 'sendBeacon' && global(object, 'navigator')) add(args[0], 'navigator.sendBeacon');
|
|
221
|
+
// URL and Request construct data. Their arguments are resolved only when
|
|
222
|
+
// the resulting value reaches a resource operation such as fetch.
|
|
223
|
+
if (method === 'setProperty' && origins(object).some(value => memberName(value) === 'style')) css(args[1], browser.style(object));
|
|
224
|
+
if (method === 'open') {
|
|
225
|
+
if (browser.window(object)) add(args[0], 'window.open');
|
|
226
|
+
else if (browser.constructed(object, 'XMLHttpRequest')) add(args[1], 'XMLHttpRequest.open');
|
|
227
|
+
}
|
|
228
|
+
if (['assign','replace'].includes(method) && browser.location(object)) add(args[0], `location.${method}`);
|
|
229
|
+
if (['setAttribute','setAttributeNS'].includes(method)) {
|
|
230
|
+
const index = method === 'setAttributeNS' ? 1 : 0;
|
|
231
|
+
for (const attribute of strings(args[index])) {
|
|
232
|
+
const property = attribute.toLowerCase();
|
|
233
|
+
if (properties.has(property) && browser.dom(object)) add(args[index + 1], `DOM.${method}(${property})`, property === 'href' && browser.anchor(object), true, property === 'srcset');
|
|
234
|
+
if (property === 'srcdoc') markup(args[index + 1], browser.dom(object));
|
|
235
|
+
if (property === 'style') css(args[index + 1], browser.dom(object));
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
if (method === 'insertAdjacentHTML') markup(args[1], browser.dom(object));
|
|
239
|
+
if (method === 'write' || method === 'writeln') for (const argument of args) markup(argument, browser.document(object));
|
|
240
|
+
}
|
|
241
|
+
});
|
|
242
|
+
return {references:found, unresolved, literals};
|
|
243
|
+
}
|
package/cli/site-render.mjs
CHANGED
|
@@ -9,12 +9,13 @@
|
|
|
9
9
|
// toolchain); the app's own Vite config is never loaded, because its Worker
|
|
10
10
|
// plugins do not run in Node. It reads no installation data.
|
|
11
11
|
// Usage: node site-render.mjs ROOT ENTRY OUTPUT [CONFIG]
|
|
12
|
-
import { cp, lstat, mkdir, rm, writeFile } from 'node:fs/promises';
|
|
12
|
+
import { cp, lstat, mkdir, readFile, rm, writeFile } from 'node:fs/promises';
|
|
13
13
|
import { createRequire } from 'node:module';
|
|
14
14
|
import { dirname, join, resolve, sep } from 'node:path';
|
|
15
15
|
import { pathToFileURL } from 'node:url';
|
|
16
|
+
import { buildClients, writeSiteAsset } from './site-client.mjs';
|
|
16
17
|
|
|
17
|
-
const [root, entry, output, config] = process.argv.slice(2);
|
|
18
|
+
const [root, entry, output, config, clientJSON] = process.argv.slice(2);
|
|
18
19
|
const fail = message => { process.stderr.write(`${message}\n`); process.exit(2); };
|
|
19
20
|
if (!root || !entry || !output) fail('Usage: site-render.mjs ROOT ENTRY OUTPUT [CONFIG]');
|
|
20
21
|
const inside = (base, path) => { const target = resolve(base, path); return target === base || target.startsWith(base + sep) ? target : null; };
|
|
@@ -46,7 +47,9 @@ try {
|
|
|
46
47
|
await writeFile(join(server, 'package.json'), '{"type":"module"}\n');
|
|
47
48
|
module = await import(pathToFileURL(join(server, chunk.fileName)).href);
|
|
48
49
|
if (typeof module.sitePages !== 'function') fail(`${entry} must export sitePages(), returning { pages: [{ path, html }], assets: [public paths] }.`);
|
|
49
|
-
const
|
|
50
|
+
const clientBuild = await buildClients(vite, root, output, clientJSON ? JSON.parse(clientJSON) : {}), clients = clientBuild.clients;
|
|
51
|
+
styles = [...new Set([...styles,...clientBuild.styles])];
|
|
52
|
+
const build = await module.sitePages({ styles, clients });
|
|
50
53
|
if (!Array.isArray(build?.pages) || !Array.isArray(build.assets)) fail('sitePages() returns { pages, assets }.');
|
|
51
54
|
|
|
52
55
|
const written = new Set();
|
|
@@ -68,8 +71,7 @@ try {
|
|
|
68
71
|
}
|
|
69
72
|
// Only the emitted files: the Node chunks share the folder and never ship.
|
|
70
73
|
for (const file of files) {
|
|
71
|
-
await
|
|
72
|
-
await cp(join(server, file), join(output, file), { errorOnExist: true, force: false });
|
|
74
|
+
await writeSiteAsset(output,file,await readFile(join(server,file)));
|
|
73
75
|
}
|
|
74
76
|
process.stdout.write(`${JSON.stringify({ pages: build.pages.length, assets: build.assets.length, styles: styles.length })}\n`);
|
|
75
77
|
} finally { await rm(server, { recursive: true, force: true }); }
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// Shared ESTree helpers. Walk actual syntax so comments, string delimiters and
|
|
2
|
+
// formatting do not change which values the site checks.
|
|
3
|
+
export function walkSyntax(node, visit, parents = []) {
|
|
4
|
+
if (!node || typeof node !== 'object' || !node.type) return;
|
|
5
|
+
visit(node, parents);
|
|
6
|
+
for (const value of Object.values(node)) {
|
|
7
|
+
if (Array.isArray(value)) for (const child of value) walkSyntax(child, visit, [...parents, node]);
|
|
8
|
+
else if (value?.type) walkSyntax(value, visit, [...parents, node]);
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
export const memberName = node => node?.type === 'MemberExpression' ? node.computed ? stringValue(node.property) : node.property.name : undefined;
|
|
12
|
+
export const stringValue = node => node?.type === 'Literal' && typeof node.value === 'string' ? node.value : node?.type === 'TemplateLiteral' && !node.expressions.length ? node.quasis[0].value.cooked : undefined;
|
|
13
|
+
export function javascriptStrings(tree) {
|
|
14
|
+
const values = [];
|
|
15
|
+
walkSyntax(tree, node => {
|
|
16
|
+
const value = stringValue(node);
|
|
17
|
+
if (value !== undefined) values.push(value);
|
|
18
|
+
if (node.type === 'TemplateElement' && node.value.cooked !== null) values.push(node.value.cooked);
|
|
19
|
+
});
|
|
20
|
+
return values;
|
|
21
|
+
}
|
package/cli/site.mjs
CHANGED
|
@@ -6,6 +6,8 @@ import { invalid, missing, refuse, RerError } from './errors.mjs';
|
|
|
6
6
|
import { checkSite } from './site-check.mjs';
|
|
7
7
|
import { readDevKey } from './dev-identity.mjs';
|
|
8
8
|
import { shared } from './shared.mjs';
|
|
9
|
+
import { unpublishedIdentities } from './site-private.mjs';
|
|
10
|
+
import { privateSitePaths } from './site-inputs.mjs';
|
|
9
11
|
|
|
10
12
|
// The run site: a seed's static marketing pages, /run and docs, built from
|
|
11
13
|
// edition identity with no installation data. A seed opts in with
|
|
@@ -18,21 +20,28 @@ const relativePath = value => typeof value === 'string' && /^[A-Za-z0-9._/-]+$/.
|
|
|
18
20
|
export function declaredSite(project) {
|
|
19
21
|
const site = project.adapter?.site;
|
|
20
22
|
if (!site) throw refuse('This checkout declares no run site. A seed opts in with site: { entry, output, declaration } in its command adapter; see SEED-COMMAND-CONTRACT.md.');
|
|
21
|
-
if (!relativePath(site.entry) || !relativePath(site.output) || !relativePath(site.declaration) || (site.config !== undefined && !relativePath(site.config)) || ['src', 'public', 'node_modules', 'migrations'].some(top => site.output === top || site.output.startsWith(top + '/'))) throw invalid('The declared run site needs
|
|
23
|
+
if (!relativePath(site.entry) || !relativePath(site.output) || !relativePath(site.declaration) || (site.config !== undefined && !relativePath(site.config)) || (site.clientConfig !== undefined && !relativePath(site.clientConfig)) || ['src', 'public', 'node_modules', 'migrations'].some(top => site.output === top || site.output.startsWith(top + '/'))) throw invalid('The declared run site needs relative entries/configs, an output folder outside src, public and node_modules, and a deploy declaration.');
|
|
24
|
+
if (site.clients !== undefined && (!site.clients || Array.isArray(site.clients) || typeof site.clients !== 'object' || Object.entries(site.clients).some(([name, entry]) => !/^[a-z][a-z0-9-]*$/i.test(name) || !relativePath(entry)))) throw invalid('Run site clients is a map of island names to relative client entry paths.');
|
|
22
25
|
return site;
|
|
23
26
|
}
|
|
24
27
|
|
|
25
28
|
// Owner identity, secrets and the dev key: none may appear in the run site.
|
|
26
29
|
async function privateValues(project) {
|
|
27
|
-
const
|
|
30
|
+
const privateValues = [], privateIdentities = [], names = project.adapter.input?.names ?? {};
|
|
28
31
|
const { localVariables } = await shared('local/environment.mjs');
|
|
32
|
+
const manifest = await readFile(join(project.root, project.adapter.setup?.manifestFile ?? 'rer-setup.json'), 'utf8').then(JSON.parse, () => ({}));
|
|
33
|
+
const secretNames = new Set((manifest.fields ?? []).filter(field => field.kind === 'secret').map(field => field.name));
|
|
29
34
|
const variables = await readFile(join(project.root, project.adapter.hooks?.secrets?.file ?? '.dev.vars'), 'utf8').then(localVariables, () => ({}));
|
|
30
|
-
for (const [key, value] of Object.entries(variables))
|
|
35
|
+
for (const [key, value] of Object.entries(variables)) {
|
|
36
|
+
if ([names.email, names.name].includes(key)) privateIdentities.push(value);
|
|
37
|
+
else if (secretNames.has(key) || /SECRET|TOKEN|PASSWORD|_KEY$/.test(key)) privateValues.push(value);
|
|
38
|
+
}
|
|
31
39
|
const answers = await readFile(join(project.root, '.seed/installer/setup-answers.json'), 'utf8').then(JSON.parse, () => ({}));
|
|
32
|
-
for (const key of
|
|
40
|
+
for (const [key, value] of Object.entries(answers)) if (secretNames.has(key) || /SECRET|TOKEN|PASSWORD|_KEY$/.test(key)) privateValues.push(value);
|
|
41
|
+
for (const key of [names.email, names.name]) if (typeof answers?.[key] === 'string') privateIdentities.push(answers[key]);
|
|
33
42
|
const key = await readDevKey(project.root).catch(() => null);
|
|
34
|
-
if (key)
|
|
35
|
-
return
|
|
43
|
+
if (key) privateValues.push(key);
|
|
44
|
+
return { privateValues, privateIdentities: await unpublishedIdentities(project.root, privateIdentities, privateSitePaths(project.adapter)) };
|
|
36
45
|
}
|
|
37
46
|
|
|
38
47
|
/** Render the run site into its declared folder, check it and replace the previous build. */
|
|
@@ -40,18 +49,26 @@ export async function buildSite(project, store, signal) {
|
|
|
40
49
|
const site = declaredSite(project);
|
|
41
50
|
await contained(project.root, site.entry);
|
|
42
51
|
if (site.config) await contained(project.root, site.config);
|
|
52
|
+
if (site.clientConfig) await contained(project.root, site.clientConfig);
|
|
53
|
+
for (const entry of Object.values(site.clients ?? {})) await contained(project.root, entry);
|
|
43
54
|
const folder = await contained(project.root, site.output, { optional: true });
|
|
44
55
|
await mkdir(dirname(folder), { recursive: true });
|
|
45
56
|
const staging = `${folder}.building-${process.pid}`;
|
|
46
57
|
await rm(staging, { recursive: true, force: true });
|
|
47
58
|
try {
|
|
48
59
|
await mkdir(staging);
|
|
49
|
-
await store.run('Building the run site', process.execPath, [fileURLToPath(new URL('./site-render.mjs', import.meta.url)), project.root, site.entry, staging,
|
|
50
|
-
|
|
60
|
+
try { await store.run('Building the run site', process.execPath, [fileURLToPath(new URL('./site-render.mjs', import.meta.url)), project.root, site.entry, staging, site.config ?? '', JSON.stringify({entries:site.clients ?? {}, config:site.clientConfig, privatePaths:privateSitePaths(project.adapter)})], { cwd: project.root, signal, heavy: false }); }
|
|
61
|
+
catch (error) {
|
|
62
|
+
const code = await readFile(`${staging}.refusal`, 'utf8').catch(() => '');
|
|
63
|
+
if (code === 'site_private_input') throw new RerError(code, 'Run site client imports a private installation file or a file outside the seed and its declared dependencies.', 4);
|
|
64
|
+
if (code === 'site_client_server_import') throw new RerError(code, 'Run site client imports a server-only module. Use browser dependencies in every reachable island module.', 4);
|
|
65
|
+
throw error;
|
|
66
|
+
}
|
|
67
|
+
const checked = await checkSite(staging, {root:project.root, ...await privateValues(project)});
|
|
51
68
|
await rm(folder, { recursive: true, force: true });
|
|
52
69
|
await rename(staging, folder);
|
|
53
70
|
return { folder, ...checked };
|
|
54
|
-
} finally { await Promise.all([staging, `${staging}.render`].map(path => rm(path, { recursive: true, force: true }))); }
|
|
71
|
+
} finally { await Promise.all([staging, `${staging}.render`, `${staging}.refusal`].map(path => rm(path, { recursive: true, force: true }))); }
|
|
55
72
|
}
|
|
56
73
|
|
|
57
74
|
async function siteDeployment(project, options, folder) {
|
|
@@ -66,17 +83,21 @@ async function siteDeployment(project, options, folder) {
|
|
|
66
83
|
/** `rer build --site` and `rer deploy --site [--dry-run]`. */
|
|
67
84
|
export async function siteCommand(project, options, store, signal, out) {
|
|
68
85
|
const built = await buildSite(project, store, signal);
|
|
86
|
+
if (!options.json && built.references.unresolved.length) {
|
|
87
|
+
out.human(`Run site references: ${built.references.unresolved.length} runtime resource target(s) could not be resolved statically:`);
|
|
88
|
+
for (const {asset, kind} of built.references.unresolved) out.human(` ${asset}: ${kind}`);
|
|
89
|
+
}
|
|
69
90
|
if (options.verb === 'build') {
|
|
70
91
|
// Output redaction masks an inherited PWD (a credential-shaped name), so the
|
|
71
92
|
// checkout-relative folder is named too.
|
|
72
93
|
const path = relative(project.root, built.folder);
|
|
73
94
|
if (!options.json) out.human(`Run site written to ${path} (${built.pages} pages, ${built.files} files): ${built.folder}`);
|
|
74
|
-
return { site: { folder: built.folder, path, pages: built.pages, files: built.files } };
|
|
95
|
+
return { site: { folder: built.folder, path, pages: built.pages, files: built.files, references:built.references } };
|
|
75
96
|
}
|
|
76
97
|
const plan = await siteDeployment(project, options, built.folder);
|
|
77
98
|
const human = [`Run site plan: site Worker ${plan.worker}, Base-generated, its only binding the assets (the app's Worker ${plan.appWorker} is not touched)`, ` assets: ${plan.assets} (${built.files} files; HTML ${plan.assetsConfig.html_handling}, not found ${plan.assetsConfig.not_found_handling}; the Worker runs first)`,
|
|
78
99
|
` custom domains: ${plan.hostnames.join(', ')}; workers.dev off`, ...plan.redirects.map(rule => ` redirect in the Worker: ${rule.from} -> ${rule.to} (${rule.status}, keeps ${rule.keeps.join(' and ')})`)];
|
|
79
|
-
const summary = { worker: plan.worker, appWorker: plan.appWorker, module: plan.module, bindings: plan.bindings, assets: plan.assets, files: built.files, domain: plan.domain, hostnames: plan.hostnames, workersDev: false, runWorkerFirst: true, assetsConfig: plan.assetsConfig, redirects: plan.redirects.map(({ from, to, status }) => ({ from, to, status })) };
|
|
100
|
+
const summary = { worker: plan.worker, appWorker: plan.appWorker, module: plan.module, bindings: plan.bindings, assets: plan.assets, files: built.files, references:built.references, domain: plan.domain, hostnames: plan.hostnames, workersDev: false, runWorkerFirst: true, assetsConfig: plan.assetsConfig, redirects: plan.redirects.map(({ from, to, status }) => ({ from, to, status })) };
|
|
80
101
|
if (options.dryRun) {
|
|
81
102
|
if (options.json) out.event('site', summary); else for (const line of human) out.human(line);
|
|
82
103
|
store.report('deploy --site: dry run complete; no deployment occurred and no provider request was made.');
|
package/global/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,44 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.2
|
|
4
|
+
|
|
5
|
+
- `rer deploy` claims the hosted owner, exits cleanly when it finishes, changes and removes secrets safely, and removes the resources it created when you ask it to.
|
|
6
|
+
- Local restore runs the seed's own restore-validation policies, so a seed no longer needs its own patch or adapter for them.
|
|
7
|
+
- Hosted exports stay portable: the deploy kit's migration ledger is kept out of seed archives, compound D1 migrations such as triggers apply, and an archive or import target with uncertain migration history is refused.
|
|
8
|
+
- Package-mode guidance in the exported docs names the commands a package-mode seed actually runs, and the local-run docs describe the sign-in outbox.
|
|
9
|
+
- `rer release --help` and the job-queue help print their usage.
|
|
10
|
+
- `rer describe` reports the effects a seed declares for each command instead of generic account effects.
|
|
11
|
+
- Linked OIDC providers report health from the token exchange, stay healthy when one authorization code is rejected, and send failures to the configured sign-in path.
|
|
12
|
+
- The run-site privacy check catches registered secrets inside prefixed base64 and accepts constant text that a client island imports.
|
|
13
|
+
- Fresh rer launches provision pnpm, prefer the provisioned pnpm, keep state under your HOME, and clean up their probe processes.
|
|
14
|
+
- Base can export a seed Verify workflow with source freshness checks for manual adoption.
|
|
15
|
+
- Base's default seed and source import kit erase a person in bounded steps after retiring their imported custody; this kit is outside the rer npm package.
|
|
16
|
+
|
|
17
|
+
## 0.2.1
|
|
18
|
+
|
|
19
|
+
- `rer release keygen` prepares publisher keys and anchor entries; `rer release sign` writes a detached signature for a completed tagged release.
|
|
20
|
+
- `rer release receipt` verifies signed artifacts against local anchors offline, using the shared verifier and vectors exported for reuse.
|
|
21
|
+
- Tagged releases record the annotated tag object, public commit and public tree needed to sign their artifacts.
|
|
22
|
+
- Package-mode conversion removes obsolete vendored-copy checks, component selections and packaging globs, and rewrites test launches to the installed rer entry with foreground development.
|
|
23
|
+
- Hosted-output and hydration checks run when invoked through pnpm's package symlinks.
|
|
24
|
+
- Run sites support seed-declared browser client islands, with bundled scripts and styles checked during the build.
|
|
25
|
+
- Run-site privacy checks match complete owner identifiers and accept examples already committed in source; `rer dev` warns on a run-site privacy refusal while `rer build --site` still refuses.
|
|
26
|
+
- `rer preview --detach` starts a background preview, refreshes stale build output after source changes and serves scheduled trigger routes.
|
|
27
|
+
- `rer dev --foreground --help` shows the command's help.
|
|
28
|
+
- Deploy failures name the failed step and error code, show redacted provider output and list resources created during that attempt.
|
|
29
|
+
- Sign-in pages hide the email form when mail is unavailable and expose typed handler options.
|
|
30
|
+
- Dev badge and sign-in outbox controls have larger click targets.
|
|
31
|
+
- The Lobby stays script-free; `RER_OPEN` exchanges the development key on `/__dev/key` under its own content security policy.
|
|
32
|
+
- `rer --version` handles a closed output pipe without an EPIPE stack.
|
|
33
|
+
- The publication scan accepts recognised vendor no-reply co-author trailers while continuing to flag personal email addresses.
|
|
34
|
+
- Local-run instructions give the stop, export, reset, import and setup order.
|
|
35
|
+
- Base provides an optional run-site workflow template for manual adoption.
|
|
36
|
+
- Base's source and reference import kit can erase selected personal fields from retained shared records and undo images; this kit is outside the rer npm package.
|
|
37
|
+
|
|
38
|
+
### Known limitations
|
|
39
|
+
|
|
40
|
+
the run-site privacy check does not detect deliberately obfuscated secrets, such as base64 concatenated to other text, in client islands
|
|
41
|
+
|
|
3
42
|
## 0.2.0
|
|
4
43
|
|
|
5
44
|
What changes for a user since 0.1.1.
|
package/global/README.md
CHANGED
|
@@ -22,8 +22,11 @@ refuses archives with absolute paths, `..` entries, links or devices, then runs
|
|
|
22
22
|
the package's own `rer cli install`. A failed or refused download leaves the
|
|
23
23
|
version you had selected.
|
|
24
24
|
|
|
25
|
-
No installer edits shell profiles.
|
|
26
|
-
|
|
25
|
+
No installer edits shell profiles. The canonical launcher is `~/.local/bin/rer`.
|
|
26
|
+
When that directory is absent from PATH, installation also links it into an
|
|
27
|
+
existing writable, user-owned PATH directory if available, preserving foreign
|
|
28
|
+
commands. Otherwise it prints one activation line to run in the current shell:
|
|
29
|
+
`export PATH="$HOME/.local/bin:$PATH"` (with fish syntax for fish). The launcher runs `node` from
|
|
27
30
|
PATH, so a Node upgrade does not break it. A re-run with the same build changes
|
|
28
31
|
nothing; a different build is installed beside the current one, the previous one
|
|
29
32
|
is kept and older ones are removed. It refuses to replace a `~/.local/bin/rer`
|
|
@@ -247,7 +250,7 @@ line that still names it. A later refresh is a version bump:
|
|
|
247
250
|
| `@runeditrun/rer/server-resources` | managed server resource readings (runtime journeys) | `scripts/base-cli/cli/server-resources.mjs` |
|
|
248
251
|
| `@runeditrun/rer/operations` | OPERATIONS describe and check | `scripts/base-cli/operations/operations.mjs` |
|
|
249
252
|
| `@runeditrun/rer/modules/portability`, `/catalogue`, `/cli`, `/cli-options`, `/files`, `/format`, `/http`, `/import`, `/journal`, `/legacy-safety`, `/migration-history`, `/storage` | portability module | `scripts/base-cli/modules/portability/*.mjs` |
|
|
250
|
-
| `@runeditrun/rer/modules/release/export`, `/publication-scan`, `/source` | release export
|
|
253
|
+
| `@runeditrun/rer/modules/release/export`, `/publication-scan`, `/source`, `/signature` | release export, publication scan and shared signature verifier | `scripts/base-cli/modules/release/*.mjs` |
|
|
251
254
|
| `@runeditrun/rer/modules/deploy-kit/owner`, `/owner-cli`, `/prepare`, `/provider` | deploy kit | `scripts/base-cli/modules/deploy-kit/*.mjs` |
|
|
252
255
|
| `@runeditrun/rer/modules/operator-auth`, `/dev`, `/dev/badge`, `/dev-admission`, `/accounts/native`, `/identity/email-link` | operator auth | `scripts/base-cli/modules/operator-auth/index.mjs`, `dev/index.mjs`, `dev/badge.mjs`, `dev-admission.mjs`, `accounts/native.mjs`, `identity/email-link/index.mjs` |
|
|
253
256
|
| `@runeditrun/rer/modules/erasure/fence` | erasure fence statements | `scripts/base-cli/modules/erasure/fence.mjs` |
|
|
@@ -260,9 +263,17 @@ In a checkout, every verb runs that checkout's own pinned `rer`; `rer VERB --hel
|
|
|
260
263
|
there lists its flags:
|
|
261
264
|
|
|
262
265
|
`install`, `setup`, `dev`, `preview`, `build`, `verify`, `reset --yes`, `deploy`,
|
|
263
|
-
`export`, `import`, `outbox`, `doctor`, `describe`, `check`, `release
|
|
266
|
+
`export`, `import`, `outbox`, `doctor`, `describe`, `check`, `release`,
|
|
267
|
+
`release sign`, `release receipt` and `release keygen`.
|
|
264
268
|
|
|
265
269
|
Anywhere: `rer list [DIR]`, `rer cli status|install|uninstall`, `rer update`,
|
|
266
270
|
`rer queue NAME -- COMMAND`, `rer --version` and `rer --help`.
|
|
267
271
|
|
|
268
272
|
Licence: MIT.
|
|
273
|
+
|
|
274
|
+
First-party commands acquire the exact `packageManager` pnpm pin when PATH lacks
|
|
275
|
+
it, verify its registry sha512 integrity and cache it under `$HOME/.rer/pnpm`.
|
|
276
|
+
No global package manager installation or sudo is needed. Independent projects
|
|
277
|
+
continue to run their declared commands with their own tools. Server and job
|
|
278
|
+
state default to `$HOME/.rer/servers` and `$HOME/.rer/jobs`; supplied
|
|
279
|
+
`RER_SERVERS_DIR` and `RER_JOBS_DIR` take precedence and are passed to older pins.
|
package/global/cli/install.mjs
CHANGED
|
@@ -2,10 +2,12 @@
|
|
|
2
2
|
// version directory, switch `current` atomically, keep the previous version,
|
|
3
3
|
// prune older ones and write the launcher. Never edits shell profiles.
|
|
4
4
|
import { randomUUID } from 'node:crypto';
|
|
5
|
-
import { chmod, cp, mkdir, readdir, readlink, realpath, rename, rm, symlink, writeFile } from 'node:fs/promises';
|
|
5
|
+
import { access, constants, chmod, cp, mkdir, readdir, readlink, realpath, rename, rm, symlink, writeFile } from 'node:fs/promises';
|
|
6
6
|
import { basename, join, resolve } from 'node:path';
|
|
7
7
|
import { ownManifest, ownRoot, packageDigest, packageFileList } from '../package-info.mjs';
|
|
8
8
|
import { installRecord, launcherOwner, launcherScript, layout } from './layout.mjs';
|
|
9
|
+
import { exposeLauncher } from './path-link.mjs';
|
|
10
|
+
import { shellQuote } from '../../cli/node-runtime-cache.mjs';
|
|
9
11
|
import { runningDoor } from '../../cli/door.mjs';
|
|
10
12
|
|
|
11
13
|
async function readLink(path) { try { return await readlink(path); } catch (error) { if (['ENOENT', 'EINVAL'].includes(error.code)) return undefined; throw error; } }
|
|
@@ -38,13 +40,20 @@ async function writeLauncher(paths) {
|
|
|
38
40
|
await rename(temporary, paths.launcher);
|
|
39
41
|
}
|
|
40
42
|
|
|
41
|
-
export function pathAdvice(paths, env = process.env) {
|
|
43
|
+
export async function pathAdvice(paths, env = process.env) {
|
|
42
44
|
const entries = (env.PATH ?? '').split(':').filter(Boolean).map(entry => resolve(entry));
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
45
|
+
for (const directory of entries) {
|
|
46
|
+
if (directory === resolve(paths.bin)) return undefined;
|
|
47
|
+
const candidate = join(directory, 'rer');
|
|
48
|
+
try {
|
|
49
|
+
await access(candidate, constants.X_OK);
|
|
50
|
+
const found = await launcherOwner(await realpath(candidate));
|
|
51
|
+
if (found.state === 'ours' && found.root === paths.data) return undefined;
|
|
52
|
+
break; // This command shadows the canonical launcher, even if it is later on PATH.
|
|
53
|
+
} catch (error) { if (!['ENOENT', 'EACCES', 'ENOTDIR'].includes(error.code)) throw error; }
|
|
54
|
+
}
|
|
55
|
+
if (basename(env.SHELL ?? '') === 'fish') return `Activate rer in this shell:\n set -gx PATH ${shellQuote(paths.bin)} $PATH`;
|
|
56
|
+
return `Activate rer in this shell:\n export PATH=${shellQuote(paths.bin)}:"$PATH"`;
|
|
48
57
|
}
|
|
49
58
|
|
|
50
59
|
export async function install() {
|
|
@@ -59,8 +68,8 @@ export async function install() {
|
|
|
59
68
|
const script = launcherScript(paths.data);
|
|
60
69
|
let present = false;
|
|
61
70
|
try { present = await packageDigest(join(paths.versions, name)) === digest; } catch (error) { if (error.code !== 'ENOENT') throw error; }
|
|
62
|
-
const advice = pathAdvice(paths);
|
|
63
71
|
if (present && current === join('versions', name) && launcher.text === script && record?.channel === channel) {
|
|
72
|
+
const advice = await activation(paths, record);
|
|
64
73
|
process.stdout.write(`rer ${manifest.version} is already current (${join(paths.versions, name)}). Nothing changed.\n${advice ? advice + '\n' : ''}`);
|
|
65
74
|
return 0;
|
|
66
75
|
}
|
|
@@ -72,8 +81,16 @@ export async function install() {
|
|
|
72
81
|
const doorVersion = door?.versionDir && resolve(door.versionDir, '..') === await realpath(paths.versions).catch(() => resolve(paths.versions)) ? basename(door.versionDir) : undefined;
|
|
73
82
|
const keep = new Set([name, previousName, doorVersion].filter(Boolean));
|
|
74
83
|
for (const entry of await readdir(paths.versions)) if (!keep.has(entry) && !entry.startsWith('.staging-')) await rm(join(paths.versions, entry), { recursive: true, force: true });
|
|
75
|
-
await writeFile(paths.record, JSON.stringify({ channel, previous: previousName && keep.has(previousName) ? previousName : null }, null, 2) + '\n', { mode: 0o644 });
|
|
84
|
+
await writeFile(paths.record, JSON.stringify({ channel, links: record?.links ?? [], previous: previousName && keep.has(previousName) ? previousName : null }, null, 2) + '\n', { mode: 0o644 });
|
|
76
85
|
if (launcher.text !== script) await writeLauncher(paths);
|
|
86
|
+
const advice = await activation(paths, await installRecord(paths));
|
|
77
87
|
process.stdout.write(`Installed rer ${manifest.version} in ${join(paths.versions, name)}.\n${paths.launcher} runs it.${previousName ? ` The previous version (${previousName}) is kept.` : ''}\n${advice ? advice + '\n' : ''}`);
|
|
78
88
|
return 0;
|
|
79
89
|
}
|
|
90
|
+
|
|
91
|
+
async function activation(paths, record) {
|
|
92
|
+
const link = await exposeLauncher(paths);
|
|
93
|
+
const links = [...new Set([...(record?.links ?? []), ...(link ? [link] : [])])];
|
|
94
|
+
if (JSON.stringify(record?.links ?? []) !== JSON.stringify(links)) await writeFile(paths.record, JSON.stringify({ ...record, links }, null, 2) + '\n', { mode: 0o644 });
|
|
95
|
+
return link ? undefined : await pathAdvice(paths);
|
|
96
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// Offer the canonical launcher through an existing user-owned PATH directory.
|
|
2
|
+
// Foreign commands and links are never replaced, including during uninstall.
|
|
3
|
+
import { access, constants, lstat, readlink, symlink } from 'node:fs/promises';
|
|
4
|
+
import { delimiter, isAbsolute, join, resolve } from 'node:path';
|
|
5
|
+
|
|
6
|
+
export async function ownedPathLink(path, launcher) {
|
|
7
|
+
try { return (await lstat(path)).isSymbolicLink() && (await readlink(path)) === resolve(launcher); }
|
|
8
|
+
catch (error) { if (error.code === 'ENOENT' || error.code === 'EINVAL') return false; throw error; }
|
|
9
|
+
}
|
|
10
|
+
export async function exposeLauncher(paths, env = process.env) {
|
|
11
|
+
for (const entry of (env.PATH ?? '').split(delimiter).filter(Boolean)) {
|
|
12
|
+
const directory = resolve(entry), candidate = join(directory, 'rer');
|
|
13
|
+
if (directory === resolve(paths.bin)) return undefined;
|
|
14
|
+
try {
|
|
15
|
+
try {
|
|
16
|
+
await lstat(candidate);
|
|
17
|
+
if (await ownedPathLink(candidate, paths.launcher)) return candidate;
|
|
18
|
+
await access(candidate, constants.X_OK);
|
|
19
|
+
return undefined;
|
|
20
|
+
} catch (error) { if (error.code !== 'ENOENT') continue; }
|
|
21
|
+
// npm/npx add transient command directories; never install into them.
|
|
22
|
+
if (!isAbsolute(entry) || directory.split('/').includes('node_modules') || directory.split('/').includes('_npx')) continue;
|
|
23
|
+
const info = await lstat(directory);
|
|
24
|
+
if (!info.isDirectory() || !process.getuid || info.uid !== process.getuid() || (!(info.mode & 0o200) || (info.mode & 0o022))) continue;
|
|
25
|
+
await access(directory, constants.W_OK | constants.X_OK);
|
|
26
|
+
try { await symlink(paths.launcher, candidate); return candidate; }
|
|
27
|
+
catch (error) { if (error.code !== 'EEXIST' && error.code !== 'EACCES' && error.code !== 'EPERM') throw error; }
|
|
28
|
+
} catch (error) { if (!['ENOENT', 'EACCES', 'ENOTDIR'].includes(error.code)) throw error; }
|
|
29
|
+
}
|
|
30
|
+
}
|
package/global/cli/status.mjs
CHANGED
|
@@ -24,7 +24,7 @@ export async function pathCommands(installRoot, env = process.env) {
|
|
|
24
24
|
seen.add(candidate);
|
|
25
25
|
try { await access(candidate, constants.X_OK); } catch { continue; }
|
|
26
26
|
const target = await real(candidate);
|
|
27
|
-
const launcher = await launcherOwner(candidate);
|
|
27
|
+
const launcher = await launcherOwner(target ?? candidate);
|
|
28
28
|
if (target && inside(target, ownRoot) || launcher.state === 'ours' && launcher.root && await real(launcher.root) === installRoot) self.push(candidate);
|
|
29
29
|
else others.push(candidate);
|
|
30
30
|
}
|