@haposoft/cafekit 0.16.0-rc.2 → 0.16.0-rc.4
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/bin/__tests__/codex-hooks.test.js +794 -21
- package/bin/__tests__/codex-native.test.js +228 -7
- package/bin/__tests__/develop-contract.test.js +1838 -112
- package/bin/__tests__/installer-safety.test.js +82 -29
- package/bin/__tests__/opencode-spec-gate.test.js +799 -0
- package/bin/__tests__/package-inventory.test.js +841 -10
- package/bin/__tests__/specs-v2-execution-closeout.test.js +421 -0
- package/bin/__tests__/specs-v2-policy-and-scaffold.test.js +601 -0
- package/bin/__tests__/specs-v2-validator-grounder-contract.test.js +952 -0
- package/bin/__tests__/test-runtime-dependency-closure.cjs +182 -0
- package/bin/__tests__/validator-grounding.test.js +2469 -71
- package/bin/lib/managed-writer.js +12 -3
- package/bin/phases/codex-runtime.js +17 -1
- package/package.json +1 -1
- package/src/claude/CLAUDE.md +6 -0
- package/src/claude/agents/code-auditor.md +20 -0
- package/src/claude/agents/spec-maker.md +153 -202
- package/src/claude/hooks/__tests__/completion-authority.test.js +953 -0
- package/src/claude/hooks/__tests__/privacy-block.test.js +172 -0
- package/src/claude/hooks/__tests__/semantic-review-authority.test.js +257 -0
- package/src/claude/hooks/__tests__/spec-gate.test.js +604 -56
- package/src/claude/hooks/completion-authority-check.cjs +56 -0
- package/src/claude/hooks/completion-authority-state.cjs +527 -0
- package/src/claude/hooks/completion-authority.cjs +154 -0
- package/src/claude/hooks/docs-sync.cjs +5 -2
- package/src/claude/hooks/lib/privacy-command-analysis.cjs +393 -0
- package/src/claude/hooks/lib/runtime-path-safety.cjs +119 -0
- package/src/claude/hooks/privacy-block.cjs +77 -30
- package/src/claude/hooks/semantic-review-authority.cjs +152 -0
- package/src/claude/hooks/session.cjs +33 -19
- package/src/claude/hooks/spec-gate.cjs +181 -139
- package/src/claude/hooks/spec-state.cjs +112 -47
- package/src/claude/migration-manifest.json +15 -2
- package/src/claude/rules/state-sync.md +52 -21
- package/src/claude/rules/workflow.md +17 -9
- package/src/claude/scripts/provenance.cjs +315 -0
- package/src/claude/scripts/spec-final-state.cjs +244 -0
- package/src/claude/scripts/spec-ground.cjs +535 -173
- package/src/claude/scripts/spec-readiness.cjs +118 -0
- package/src/claude/scripts/spec-receipt.cjs +162 -0
- package/src/claude/scripts/spec-resolver.cjs +501 -0
- package/src/claude/scripts/spec-scaffold.cjs +1945 -55
- package/src/claude/scripts/spec-semantic-model.cjs +396 -0
- package/src/claude/scripts/validate-spec-output.cjs +3204 -262
- package/src/claude/scripts/workflow-policy.cjs +1888 -203
- package/src/claude/settings/settings.json +12 -0
- package/src/claude/skills/code-review/SKILL.md +94 -66
- package/src/claude/skills/code-review/references/spec-compliance-review.md +10 -2
- package/src/claude/skills/code-review/references/verification-gate.md +13 -0
- package/src/claude/skills/develop/SKILL.md +138 -283
- package/src/claude/skills/develop/references/parallel-waves.md +9 -6
- package/src/claude/skills/develop/references/quality-gate.md +123 -148
- package/src/claude/skills/develop/references/subagent-patterns.md +42 -37
- package/src/claude/skills/specs/SKILL.md +192 -397
- package/src/claude/skills/specs/references/ask-user-question-gates.md +27 -18
- package/src/claude/skills/specs/references/cross-spec-dependency.md +8 -4
- package/src/claude/skills/specs/references/research-strategy.md +4 -1
- package/src/claude/skills/specs/references/review.md +200 -260
- package/src/claude/skills/specs/references/scope-inquiry.md +18 -5
- package/src/claude/skills/specs/rules/design-discovery-full.md +33 -81
- package/src/claude/skills/specs/rules/design-discovery-light.md +21 -42
- package/src/claude/skills/specs/rules/design-principles.md +136 -180
- package/src/claude/skills/specs/rules/design-review.md +42 -97
- package/src/claude/skills/specs/rules/ears-format.md +42 -50
- package/src/claude/skills/specs/rules/phase-decision-matrix.md +42 -41
- package/src/claude/skills/specs/rules/task-scoring-rubric.md +46 -46
- package/src/claude/skills/specs/rules/tasks-generation.md +189 -271
- package/src/claude/skills/specs/templates/design.md +49 -263
- package/src/claude/skills/specs/templates/requirements.md +26 -36
- package/src/claude/skills/specs/templates/research.md +9 -98
- package/src/claude/skills/specs/templates/spec-state.json +26 -67
- package/src/claude/skills/specs/templates/task.md +30 -82
- package/src/claude/skills/sync/SKILL.md +45 -15
- package/src/claude/skills/sync/references/sync-protocols.md +34 -16
- package/src/claude/skills/test/SKILL.md +151 -167
- package/src/codex/AGENTS.md +6 -0
- package/src/codex/hooks/completion-authority-check.cjs +63 -0
- package/src/codex/hooks/completion-authority-state.cjs +527 -0
- package/src/codex/hooks/completion-authority.cjs +106 -0
- package/src/codex/hooks/lib/hook-context.cjs +58 -4
- package/src/codex/hooks/lib/privacy-command-analysis.cjs +3 -0
- package/src/codex/hooks/lib/privacy-state.cjs +38 -16
- package/src/codex/hooks/lib/runtime-path-safety.cjs +3 -0
- package/src/codex/hooks/lib/spec-receipt.cjs +86 -115
- package/src/codex/hooks/lib/spec-utils.cjs +104 -27
- package/src/codex/hooks/privacy-approval.cjs +1 -1
- package/src/codex/hooks/privacy-block.cjs +67 -11
- package/src/codex/hooks/semantic-review-authority.cjs +130 -0
- package/src/codex/hooks/session.cjs +2 -0
- package/src/codex/hooks/spec-gate.cjs +151 -24
- package/src/codex/hooks/spec-state.cjs +65 -12
- package/src/codex/hooks/state.cjs +2 -0
- package/src/codex/hooks.json +16 -1
- package/src/codex/rules/state-sync.md +47 -27
- package/src/common/AGENTS.md +1 -1
- package/src/opencode/AGENTS.md +4 -2
- package/src/opencode/plugins/spec-gate.ts +296 -122
- package/src/opencode/plugins/spec-state.ts +183 -116
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
let STATE;
|
|
7
|
+
let evaluateCloseout;
|
|
8
|
+
|
|
9
|
+
function emitBlock(reason) {
|
|
10
|
+
process.stdout.write(`${JSON.stringify({ decision: 'block', reason })}\n`);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function readPayload() {
|
|
14
|
+
const raw = fs.readFileSync(0, 'utf8').trim();
|
|
15
|
+
if (!raw) throw new Error('hook payload is empty');
|
|
16
|
+
const payload = JSON.parse(raw);
|
|
17
|
+
if (!payload || typeof payload !== 'object' || Array.isArray(payload)) throw new Error('hook payload must be a JSON object');
|
|
18
|
+
return payload;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function projectRoot(payload = {}) {
|
|
22
|
+
const configured = typeof process.env.CLAUDE_PROJECT_DIR === 'string'
|
|
23
|
+
? process.env.CLAUDE_PROJECT_DIR.trim()
|
|
24
|
+
: '';
|
|
25
|
+
if (configured) {
|
|
26
|
+
try { return fs.realpathSync(path.resolve(configured)); } catch { /* continue */ }
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const installedRoot = path.resolve(__dirname, '..', '..');
|
|
30
|
+
const installedHook = path.join(installedRoot, '.claude', 'hooks', path.basename(__filename));
|
|
31
|
+
if (fs.existsSync(installedHook)) return installedRoot;
|
|
32
|
+
|
|
33
|
+
const sourceFixture = typeof process.env.PROJECT_ROOT === 'string'
|
|
34
|
+
? process.env.PROJECT_ROOT.trim()
|
|
35
|
+
: '';
|
|
36
|
+
if (sourceFixture) {
|
|
37
|
+
try { return fs.realpathSync(path.resolve(sourceFixture)); } catch { /* continue */ }
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const legacy = typeof payload.cwd === 'string' ? payload.cwd.trim() : '';
|
|
41
|
+
if (legacy) {
|
|
42
|
+
try { return fs.realpathSync(path.resolve(legacy)); } catch { /* continue */ }
|
|
43
|
+
}
|
|
44
|
+
try { return fs.realpathSync(process.cwd()); } catch { return path.resolve(process.cwd()); }
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function sessionId(payload) {
|
|
48
|
+
return typeof payload.session_id === 'string' ? payload.session_id : null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function readRuntime(root) {
|
|
52
|
+
try {
|
|
53
|
+
const file = path.join(root, '.claude', 'runtime.json');
|
|
54
|
+
return fs.existsSync(file) ? JSON.parse(fs.readFileSync(file, 'utf8')) : {};
|
|
55
|
+
} catch { return {}; }
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function rejectApproval(reason) {
|
|
59
|
+
process.stdout.write(`CafeKit completion approval rejected: ${reason}.\n`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function loadAuthority() {
|
|
63
|
+
try {
|
|
64
|
+
STATE = require('./completion-authority-state.cjs');
|
|
65
|
+
({ evaluateCloseout } = require('./completion-authority-check.cjs'));
|
|
66
|
+
const requiredState = ['createPending', 'approvePending', 'consumeGrant', 'clearState', 'hasState', 'ensureKey', 'readBaseline', 'writeBaseline', 'readPolicyFloor', 'writePolicyFloor'];
|
|
67
|
+
if (requiredState.some((name) => typeof STATE[name] !== 'function') || typeof evaluateCloseout !== 'function') {
|
|
68
|
+
throw new Error('completion authority dependency exports are malformed');
|
|
69
|
+
}
|
|
70
|
+
return null;
|
|
71
|
+
} catch (error) {
|
|
72
|
+
return error;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function approve(payload) {
|
|
77
|
+
if (payload.hook_event_name !== 'UserPromptSubmit') return;
|
|
78
|
+
const root = projectRoot(payload);
|
|
79
|
+
const session = sessionId(payload);
|
|
80
|
+
const prompt = typeof payload.prompt === 'string' ? payload.prompt : null;
|
|
81
|
+
if (!session) return rejectApproval('host session ID is missing');
|
|
82
|
+
if (!prompt || prompt !== prompt.trim() || !prompt.startsWith(STATE.PREFIX)) return;
|
|
83
|
+
const nonce = prompt.slice(STATE.PREFIX.length);
|
|
84
|
+
if (!STATE.NONCE_RE.test(nonce) || `${STATE.PREFIX}${nonce}` !== prompt) return rejectApproval('the prompt did not exactly match a pending request');
|
|
85
|
+
const result = STATE.approvePending(root, session, nonce);
|
|
86
|
+
if (!result.ok) return rejectApproval('request missing, expired, consumed, or bound to another session');
|
|
87
|
+
process.stdout.write('CafeKit completion approval accepted for one bound closeout retry.\n');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function stop(payload) {
|
|
91
|
+
if (payload.stop_hook_active === true) return;
|
|
92
|
+
const root = projectRoot(payload);
|
|
93
|
+
const runtime = readRuntime(root);
|
|
94
|
+
const resolverPath = path.join(__dirname, '..', 'scripts', 'spec-resolver.cjs');
|
|
95
|
+
const policyPath = path.join(__dirname, '..', 'scripts', 'workflow-policy.cjs');
|
|
96
|
+
let resolver;
|
|
97
|
+
let policy;
|
|
98
|
+
try {
|
|
99
|
+
resolver = require(resolverPath);
|
|
100
|
+
policy = require(policyPath);
|
|
101
|
+
if (typeof resolver.findAllSpecCandidates !== 'function' || typeof resolver.resolveActiveSpec !== 'function') throw new Error('shared spec resolver lacks candidate scan functions');
|
|
102
|
+
if (typeof policy.completionDecisionForSpec !== 'function' || typeof policy.deriveRuntimeContext !== 'function' || typeof policy.validateWorkflowPolicySnapshot !== 'function') throw new Error('shared workflow policy lacks completion authority functions');
|
|
103
|
+
} catch (error) {
|
|
104
|
+
STATE.clearState(root);
|
|
105
|
+
emitBlock(`Completion authority unavailable: ${error.message}. Repair ${policyPath} and ${resolverPath} before completing tasks`);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
let result;
|
|
110
|
+
try {
|
|
111
|
+
result = evaluateCloseout({ resolver, policy, projectRoot: root, runtime, payload });
|
|
112
|
+
} catch (error) {
|
|
113
|
+
STATE.clearState(root);
|
|
114
|
+
emitBlock(`Completion authority controlled failure: ${error.message}. Completion is blocked until the authority is repaired`);
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
if (result.candidate === null) {
|
|
118
|
+
if (STATE.hasState(root)) {
|
|
119
|
+
STATE.clearState(root);
|
|
120
|
+
emitBlock('Completion authority: an observed persisted spec disappeared; fresh technical closeout and approval are required');
|
|
121
|
+
}
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
if (result.active === false) return;
|
|
125
|
+
if (!result.ok) {
|
|
126
|
+
STATE.clearState(root);
|
|
127
|
+
emitBlock(`Completion authority: ${result.reason}`);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
const consumed = STATE.consumeGrant(root, result.binding);
|
|
131
|
+
if (consumed.ok) return;
|
|
132
|
+
const pending = STATE.createPending(root, result.binding);
|
|
133
|
+
emitBlock(`Completion authority: technical proofs pass, but user closeout approval is required. Enter exactly "${STATE.PREFIX}${pending.nonce}" as your next user prompt. This approval is one-time and expires in five minutes.`);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
let mode = process.argv[2] || null;
|
|
137
|
+
try {
|
|
138
|
+
const payload = readPayload();
|
|
139
|
+
mode = mode || (payload.hook_event_name === 'UserPromptSubmit' ? '--approve' : '--stop');
|
|
140
|
+
if (mode === '--stop' && payload.stop_hook_active === true) {
|
|
141
|
+
// Host loop prevention is always silent, including when dependencies are unavailable.
|
|
142
|
+
} else {
|
|
143
|
+
const dependencyError = loadAuthority();
|
|
144
|
+
if (dependencyError) {
|
|
145
|
+
if (mode === '--approve') rejectApproval(`authority dependency unavailable (${dependencyError.message})`);
|
|
146
|
+
else emitBlock(`Completion authority unavailable: ${dependencyError.message}. Repair the installed authority before completing tasks`);
|
|
147
|
+
} else if (mode === '--approve') approve(payload);
|
|
148
|
+
else if (mode === '--stop') stop(payload);
|
|
149
|
+
else throw new Error('completion authority mode is required');
|
|
150
|
+
}
|
|
151
|
+
} catch (error) {
|
|
152
|
+
if (mode === '--approve') rejectApproval(`authority failed safely (${error.message})`);
|
|
153
|
+
else emitBlock(`Completion authority controlled failure: ${error.message}. Completion is blocked until the authority is repaired`);
|
|
154
|
+
}
|
|
@@ -87,8 +87,11 @@ try {
|
|
|
87
87
|
} catch (e) {
|
|
88
88
|
// Ghi log lỗi ẩn danh nếu sập hook
|
|
89
89
|
try {
|
|
90
|
-
const fs = require('fs'), p = require('path');
|
|
91
|
-
const
|
|
90
|
+
const fs = require('fs'), p = require('path'), os = require('os');
|
|
91
|
+
const isSourceTree = __dirname.includes(`${p.sep}packages${p.sep}spec${p.sep}src`);
|
|
92
|
+
const d = isSourceTree
|
|
93
|
+
? p.join(os.tmpdir(), 'cafekit-hook-logs', 'claude-docs-sync')
|
|
94
|
+
: p.join(__dirname, '.logs');
|
|
92
95
|
if (!fs.existsSync(d)) fs.mkdirSync(d, { recursive: true });
|
|
93
96
|
fs.appendFileSync(p.join(d, 'hook-log.jsonl'),
|
|
94
97
|
JSON.stringify({ ts: new Date().toISOString(), hook: 'docs-sync', status: 'crash', error: e.message }) + '\n');
|
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const DYNAMIC_READ_PATH = '<dynamic-read-path>';
|
|
4
|
+
const MAX_DEPTH = 8;
|
|
5
|
+
|
|
6
|
+
const SIMPLE_READERS = new Set([
|
|
7
|
+
'cat', 'less', 'more', 'head', 'tail', 'nl', 'strings', 'tac', 'bat',
|
|
8
|
+
'od', 'hexdump', 'xxd', 'base64', 'wc', 'cut', 'sort', 'uniq', 'tr',
|
|
9
|
+
'fold', 'fmt', 'diff', 'cmp', 'comm', 'file', 'stat', 'du', 'md5',
|
|
10
|
+
'md5sum', 'shasum', 'sha1sum', 'sha256sum', 'open',
|
|
11
|
+
]);
|
|
12
|
+
const GREP_READERS = new Set(['grep', 'egrep', 'fgrep', 'rg', 'ag']);
|
|
13
|
+
const AWK_READERS = new Set(['awk', 'gawk', 'mawk']);
|
|
14
|
+
const SHELLS = new Set(['bash', 'sh', 'zsh', 'dash', 'ksh', 'fish']);
|
|
15
|
+
const INLINE_RUNTIMES = new Set(['python', 'python3', 'node', 'perl', 'ruby', 'php']);
|
|
16
|
+
const WRAPPERS = new Set(['command', 'builtin', 'exec', 'env', 'nice', 'nohup', 'sudo', 'doas', 'timeout']);
|
|
17
|
+
const WRAPPER_OPTION_VALUE = new Set([
|
|
18
|
+
'-n', '--lines', '-c', '--bytes', '-m', '--max-count', '-A', '-B', '-C',
|
|
19
|
+
'--after-context', '--before-context', '--context', '-d', '--delimiter',
|
|
20
|
+
'-f', '--fields', '-k', '--key', '-t', '--field-separator', '-e',
|
|
21
|
+
'--regexp', '-I', '--include', '--exclude', '--exclude-dir', '-s',
|
|
22
|
+
'--signal', '--kill-after', '-u', '--user', '-g', '--group', '-C',
|
|
23
|
+
'--chdir', '-p', '--priority', '--adjustment',
|
|
24
|
+
]);
|
|
25
|
+
const SIMPLE_OPTION_VALUES = new Map([
|
|
26
|
+
['head', new Set(['-n', '--lines', '-c', '--bytes'])],
|
|
27
|
+
['tail', new Set(['-n', '--lines', '-c', '--bytes', '-s', '--sleep-interval'])],
|
|
28
|
+
['cut', new Set(['-b', '--bytes', '-c', '--characters', '-d', '--delimiter', '-f', '--fields'])],
|
|
29
|
+
['sort', new Set(['-k', '--key', '-t', '--field-separator', '-S', '--buffer-size', '-T', '--temporary-directory'])],
|
|
30
|
+
['tr', new Set()],
|
|
31
|
+
['fold', new Set(['-w', '--width'])],
|
|
32
|
+
]);
|
|
33
|
+
const GREP_OPTION_VALUE = new Set(['-e', '--regexp', '-f', '--file', '-m', '--max-count', '-A', '-B', '-C', '--after-context', '--before-context', '--context']);
|
|
34
|
+
const SED_OPTION_VALUE = new Set(['-e', '--expression', '-f', '--file']);
|
|
35
|
+
const AWK_OPTION_VALUE = new Set(['-f', '--file', '-v', '--assign', '-F', '--field-separator']);
|
|
36
|
+
const XARGS_OPTION_VALUE = new Set(['-a', '--arg-file', '-d', '--delimiter', '-E', '--eof', '-I', '--replace', '-L', '--max-lines', '-n', '--max-args', '-P', '--max-procs', '-s', '--max-chars']);
|
|
37
|
+
|
|
38
|
+
function expansionAt(source, index) {
|
|
39
|
+
const next = source[index + 1];
|
|
40
|
+
return Boolean(next && (next === '{' || next === '('
|
|
41
|
+
|| '*@#$?!-0123456789'.includes(next) || /[A-Za-z_]/.test(next)));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function matchingParen(source, start) {
|
|
45
|
+
let depth = 1;
|
|
46
|
+
let quote = null;
|
|
47
|
+
let escaped = false;
|
|
48
|
+
for (let index = start + 1; index < source.length; index += 1) {
|
|
49
|
+
const ch = source[index];
|
|
50
|
+
if (escaped) { escaped = false; continue; }
|
|
51
|
+
if (quote === "'") { if (ch === "'") quote = null; continue; }
|
|
52
|
+
if (quote === '"') {
|
|
53
|
+
if (ch === '\\') { escaped = true; continue; }
|
|
54
|
+
if (ch === '"') quote = null;
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
if (ch === '\\') { escaped = true; continue; }
|
|
58
|
+
if (ch === "'" || ch === '"') { quote = ch; continue; }
|
|
59
|
+
if (ch === '(') depth += 1;
|
|
60
|
+
if (ch === ')' && --depth === 0) return index;
|
|
61
|
+
}
|
|
62
|
+
return -1;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function matchingBacktick(source, start) {
|
|
66
|
+
let escaped = false;
|
|
67
|
+
for (let index = start + 1; index < source.length; index += 1) {
|
|
68
|
+
const ch = source[index];
|
|
69
|
+
if (escaped) { escaped = false; continue; }
|
|
70
|
+
if (ch === '\\') { escaped = true; continue; }
|
|
71
|
+
if (ch === '\x60') return index;
|
|
72
|
+
}
|
|
73
|
+
return -1;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function decodeAnsiQuote(source, start) {
|
|
77
|
+
let value = '';
|
|
78
|
+
for (let index = start + 2; index < source.length; index += 1) {
|
|
79
|
+
const ch = source[index];
|
|
80
|
+
if (ch === "'") return { value, end: index };
|
|
81
|
+
if (ch !== '\\') { value += ch; continue; }
|
|
82
|
+
const next = source[index + 1];
|
|
83
|
+
if (!next) return null;
|
|
84
|
+
if (next === 'x') {
|
|
85
|
+
const match = source.slice(index + 2).match(/^[0-9a-fA-F]{1,2}/);
|
|
86
|
+
if (match) { value += String.fromCodePoint(parseInt(match[0], 16)); index += 1 + match[0].length; continue; }
|
|
87
|
+
}
|
|
88
|
+
if (next === 'u' || next === 'U') {
|
|
89
|
+
const width = next === 'u' ? 4 : 8;
|
|
90
|
+
const digits = source.slice(index + 2, index + 2 + width);
|
|
91
|
+
if (new RegExp(`^[0-9a-fA-F]{${width}}$`).test(digits)) {
|
|
92
|
+
value += String.fromCodePoint(parseInt(digits, 16));
|
|
93
|
+
index += 1 + width;
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
const octal = source.slice(index + 1).match(/^[0-7]{1,3}/);
|
|
98
|
+
if (octal) { value += String.fromCodePoint(parseInt(octal[0], 8)); index += octal[0].length; continue; }
|
|
99
|
+
const escapes = { a: '\x07', b: '\b', e: '\x1b', E: '\x1b', f: '\f', n: '\n', r: '\r', t: '\t', v: '\v', '\\': '\\', "'": "'", '"': '"' };
|
|
100
|
+
value += Object.prototype.hasOwnProperty.call(escapes, next) ? escapes[next] : next;
|
|
101
|
+
index += 1;
|
|
102
|
+
}
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function scanShell(command) {
|
|
107
|
+
const source = String(command).replace(/\\\r?\n/g, '');
|
|
108
|
+
const segments = [];
|
|
109
|
+
const nested = [];
|
|
110
|
+
let words = [];
|
|
111
|
+
let word = { text: '', activeExpansion: false, started: false };
|
|
112
|
+
let quote = null;
|
|
113
|
+
let escaped = false;
|
|
114
|
+
let malformed = false;
|
|
115
|
+
const append = (value) => { word.text += value; word.started = true; };
|
|
116
|
+
const flushWord = () => {
|
|
117
|
+
if (word.started || word.text) words.push(word);
|
|
118
|
+
word = { text: '', activeExpansion: false, started: false };
|
|
119
|
+
};
|
|
120
|
+
const flushSegment = () => { flushWord(); if (words.length) segments.push(words); words = []; };
|
|
121
|
+
|
|
122
|
+
for (let index = 0; index < source.length; index += 1) {
|
|
123
|
+
const ch = source[index];
|
|
124
|
+
if (quote === "'") { if (ch === "'") quote = null; else append(ch); continue; }
|
|
125
|
+
if (escaped) { append(ch); escaped = false; continue; }
|
|
126
|
+
if (ch === '\\') { escaped = true; word.started = true; continue; }
|
|
127
|
+
if (quote === '"') {
|
|
128
|
+
if (ch === '"') { quote = null; continue; }
|
|
129
|
+
if (ch === '$' && expansionAt(source, index)) {
|
|
130
|
+
word.activeExpansion = true;
|
|
131
|
+
append('$');
|
|
132
|
+
if (source[index + 1] === '(') {
|
|
133
|
+
const end = matchingParen(source, index + 1);
|
|
134
|
+
if (end < 0) { malformed = true; continue; }
|
|
135
|
+
nested.push(source.slice(index + 2, end));
|
|
136
|
+
append('()');
|
|
137
|
+
index = end;
|
|
138
|
+
}
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
if (ch === '\x60') {
|
|
142
|
+
word.activeExpansion = true;
|
|
143
|
+
const end = matchingBacktick(source, index);
|
|
144
|
+
if (end < 0) { malformed = true; continue; }
|
|
145
|
+
nested.push(source.slice(index + 1, end));
|
|
146
|
+
append('\x60\x60');
|
|
147
|
+
index = end;
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
append(ch);
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
if (ch === '$' && source[index + 1] === "'") {
|
|
154
|
+
const decoded = decodeAnsiQuote(source, index);
|
|
155
|
+
if (!decoded) { malformed = true; append(ch); continue; }
|
|
156
|
+
append(decoded.value);
|
|
157
|
+
index = decoded.end;
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
if (ch === '"' || ch === "'") { quote = ch; word.started = true; continue; }
|
|
161
|
+
if (ch === '$' && expansionAt(source, index)) {
|
|
162
|
+
word.activeExpansion = true;
|
|
163
|
+
append('$');
|
|
164
|
+
if (source[index + 1] === '(') {
|
|
165
|
+
const end = matchingParen(source, index + 1);
|
|
166
|
+
if (end < 0) { malformed = true; continue; }
|
|
167
|
+
nested.push(source.slice(index + 2, end));
|
|
168
|
+
append('()');
|
|
169
|
+
index = end;
|
|
170
|
+
}
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
if (ch === '\x60') {
|
|
174
|
+
word.activeExpansion = true;
|
|
175
|
+
const end = matchingBacktick(source, index);
|
|
176
|
+
if (end < 0) { malformed = true; continue; }
|
|
177
|
+
nested.push(source.slice(index + 1, end));
|
|
178
|
+
append('\x60\x60');
|
|
179
|
+
index = end;
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
if ((ch === '<' || ch === '>') && source[index + 1] === '(') {
|
|
183
|
+
word.activeExpansion = true;
|
|
184
|
+
const end = matchingParen(source, index + 1);
|
|
185
|
+
if (end < 0) { malformed = true; continue; }
|
|
186
|
+
nested.push(source.slice(index + 2, end));
|
|
187
|
+
append(`${ch}()`);
|
|
188
|
+
index = end;
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
if ((ch === '~' && !word.text) || '*?{['.includes(ch)) { word.activeExpansion = true; append(ch); continue; }
|
|
192
|
+
if (';|&\n()'.includes(ch)) { flushSegment(); continue; }
|
|
193
|
+
if (ch === '<') { flushWord(); words.push({ text: '<', activeExpansion: false, started: true }); continue; }
|
|
194
|
+
if (ch === '>') { flushWord(); continue; }
|
|
195
|
+
if (' \t\r'.includes(ch)) { flushWord(); continue; }
|
|
196
|
+
append(ch);
|
|
197
|
+
}
|
|
198
|
+
if (quote || escaped) malformed = true;
|
|
199
|
+
flushSegment();
|
|
200
|
+
return { segments, nested, malformed };
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function commandName(word) {
|
|
204
|
+
return String(word?.text || '').replace(/^[([{]+|[)},;]+$/g, '').split(/[\\/]/).pop().toLowerCase();
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function assignment(word) {
|
|
208
|
+
return /^[A-Za-z_][A-Za-z0-9_]*=/.test(String(word?.text || ''));
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function skipOption(words, index, valueOptions = WRAPPER_OPTION_VALUE) {
|
|
212
|
+
const value = words[index]?.text || '';
|
|
213
|
+
if (value === '--') return index + 1;
|
|
214
|
+
if (!value.startsWith('-') || value === '-') return index;
|
|
215
|
+
if (value.includes('=') || /^-[A-Za-z]\S+/.test(value)) return index + 1;
|
|
216
|
+
return valueOptions.has(value) ? index + 2 : index + 1;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function unwrap(words) {
|
|
220
|
+
let index = 0;
|
|
221
|
+
while (index < words.length && assignment(words[index])) index += 1;
|
|
222
|
+
for (let depth = 0; depth < 12 && index < words.length; depth += 1) {
|
|
223
|
+
const name = commandName(words[index]);
|
|
224
|
+
if (!WRAPPERS.has(name)) return { index, name };
|
|
225
|
+
index += 1;
|
|
226
|
+
if (name === 'command' && ['-v', '-V'].includes(words[index]?.text)) return { index: -1, name: '' };
|
|
227
|
+
if (name === 'timeout') {
|
|
228
|
+
while (index < words.length && words[index].text.startsWith('-')) index = skipOption(words, index);
|
|
229
|
+
index += 1;
|
|
230
|
+
} else if (name === 'env') {
|
|
231
|
+
while (index < words.length) {
|
|
232
|
+
if (assignment(words[index])) { index += 1; continue; }
|
|
233
|
+
if (!words[index].text.startsWith('-') || words[index].text === '-') break;
|
|
234
|
+
index = skipOption(words, index);
|
|
235
|
+
}
|
|
236
|
+
} else {
|
|
237
|
+
while (index < words.length && words[index].text.startsWith('-')) index = skipOption(words, index);
|
|
238
|
+
}
|
|
239
|
+
while (index < words.length && assignment(words[index])) index += 1;
|
|
240
|
+
}
|
|
241
|
+
return index < words.length ? { index, name: commandName(words[index]) } : { index: -1, name: '' };
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function addOperand(result, word) {
|
|
245
|
+
if (!word) return;
|
|
246
|
+
if (word.activeExpansion) result.dynamic = true;
|
|
247
|
+
else if (word.text && word.text !== '-') result.paths.add(word.text);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function genericOperands(words, start, result, {
|
|
251
|
+
skipFirst = 0,
|
|
252
|
+
pathOptions = new Set(),
|
|
253
|
+
valueOptions = new Set(),
|
|
254
|
+
} = {}) {
|
|
255
|
+
let positional = 0;
|
|
256
|
+
for (let index = start; index < words.length;) {
|
|
257
|
+
const word = words[index];
|
|
258
|
+
if (word.text === '--') { index += 1; while (index < words.length) addOperand(result, words[index++]); break; }
|
|
259
|
+
if (word.text.startsWith('-') && word.text !== '-') {
|
|
260
|
+
const option = word.text.split('=')[0];
|
|
261
|
+
if (pathOptions.has(option)) {
|
|
262
|
+
if (word.text.includes('=')) addOperand(result, { text: word.text.slice(word.text.indexOf('=') + 1), activeExpansion: word.activeExpansion });
|
|
263
|
+
else addOperand(result, words[index + 1]);
|
|
264
|
+
}
|
|
265
|
+
index = skipOption(words, index, valueOptions);
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
if (positional++ >= skipFirst) addOperand(result, word);
|
|
269
|
+
index += 1;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function merge(target, source) {
|
|
274
|
+
source.paths.forEach((value) => target.paths.add(value));
|
|
275
|
+
target.dynamic ||= source.dynamic;
|
|
276
|
+
target.malformed ||= source.malformed;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function analyzeWords(words, result, depth) {
|
|
280
|
+
for (let index = 0; index + 1 < words.length; index += 1) {
|
|
281
|
+
if (words[index].text === '<') addOperand(result, words[index + 1]);
|
|
282
|
+
}
|
|
283
|
+
const command = unwrap(words);
|
|
284
|
+
if (command.index < 0) return;
|
|
285
|
+
const start = command.index + 1;
|
|
286
|
+
const name = command.name;
|
|
287
|
+
if (SIMPLE_READERS.has(name)) {
|
|
288
|
+
genericOperands(words, start, result, { valueOptions: SIMPLE_OPTION_VALUES.get(name) || new Set() });
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
if (GREP_READERS.has(name)) {
|
|
292
|
+
const patternOption = words.slice(start).some((word) => ['-e', '--regexp', '-f', '--file'].includes(word.text.split('=')[0]));
|
|
293
|
+
genericOperands(words, start, result, {
|
|
294
|
+
skipFirst: patternOption ? 0 : 1,
|
|
295
|
+
pathOptions: new Set(['-f', '--file']),
|
|
296
|
+
valueOptions: GREP_OPTION_VALUE,
|
|
297
|
+
});
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
if (name === 'sed') {
|
|
301
|
+
const scriptOption = words.slice(start).some((word) => ['-e', '--expression', '-f', '--file'].includes(word.text.split('=')[0]));
|
|
302
|
+
genericOperands(words, start, result, {
|
|
303
|
+
skipFirst: scriptOption ? 0 : 1,
|
|
304
|
+
pathOptions: new Set(['-f', '--file']),
|
|
305
|
+
valueOptions: SED_OPTION_VALUE,
|
|
306
|
+
});
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
if (AWK_READERS.has(name)) {
|
|
310
|
+
const programFile = words.slice(start).some((word) => ['-f', '--file'].includes(word.text.split('=')[0]));
|
|
311
|
+
genericOperands(words, start, result, {
|
|
312
|
+
skipFirst: programFile ? 0 : 1,
|
|
313
|
+
pathOptions: new Set(['-f', '--file']),
|
|
314
|
+
valueOptions: AWK_OPTION_VALUE,
|
|
315
|
+
});
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
if (name === 'source' || name === '.') { genericOperands(words, start, result); return; }
|
|
319
|
+
if (SHELLS.has(name)) {
|
|
320
|
+
for (let index = start; index + 1 < words.length; index += 1) {
|
|
321
|
+
if (words[index].text === '-c' || words[index].text === '--command') {
|
|
322
|
+
merge(result, analyzeCommand(words[index + 1].text, depth + 1));
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
genericOperands(words, start, result);
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
if (name === 'eval') {
|
|
330
|
+
merge(result, analyzeCommand(words.slice(start).map((word) => word.text).join(' '), depth + 1));
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
if (name === 'find') {
|
|
334
|
+
for (let index = start; index < words.length; index += 1) {
|
|
335
|
+
if (words[index].text !== '-exec' && words[index].text !== '-execdir') continue;
|
|
336
|
+
const nested = [];
|
|
337
|
+
for (index += 1; index < words.length && ![';', '+'].includes(words[index].text); index += 1) nested.push(words[index]);
|
|
338
|
+
analyzeWords(nested, result, depth + 1);
|
|
339
|
+
}
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
if (name === 'xargs') {
|
|
343
|
+
let nestedStart = start;
|
|
344
|
+
for (let index = start; index < words.length;) {
|
|
345
|
+
const option = words[index].text.split('=')[0];
|
|
346
|
+
if (option === '-a' || option === '--arg-file') {
|
|
347
|
+
if (words[index].text.includes('=')) addOperand(result, { text: words[index].text.slice(words[index].text.indexOf('=') + 1), activeExpansion: words[index].activeExpansion });
|
|
348
|
+
else addOperand(result, words[index + 1]);
|
|
349
|
+
}
|
|
350
|
+
if (!words[index].text.startsWith('-')) { nestedStart = index; break; }
|
|
351
|
+
index = skipOption(words, index, XARGS_OPTION_VALUE);
|
|
352
|
+
nestedStart = index;
|
|
353
|
+
}
|
|
354
|
+
if (nestedStart < words.length) analyzeWords(words.slice(nestedStart), result, depth + 1);
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
if (INLINE_RUNTIMES.has(name)) {
|
|
358
|
+
for (let index = start; index + 1 < words.length; index += 1) {
|
|
359
|
+
if (!['-c', '-e', '--eval'].includes(words[index].text)) continue;
|
|
360
|
+
const code = words[index + 1].text;
|
|
361
|
+
const reads = /\b(?:open|readFile(?:Sync)?|read_text|read_bytes|File\.read|IO\.read|file_get_contents)\s*\(/.test(code);
|
|
362
|
+
if (!reads) return;
|
|
363
|
+
for (const match of code.matchAll(/(['"])([^'"\r\n]+)\1/g)) result.paths.add(match[2]);
|
|
364
|
+
if (/\b(?:process\.env|process\.argv|os\.environ|sys\.argv|getenv|ENV\s*\[|ARGV\b)/.test(code)) result.dynamic = true;
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
if (name === 'openssl') genericOperands(words, start, result, { pathOptions: new Set(['-in', '-key', '-cert', '-config']) });
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function analyzeCommand(command, depth = 0) {
|
|
373
|
+
const result = { paths: new Set(), dynamic: false, malformed: false };
|
|
374
|
+
if (depth > MAX_DEPTH) { result.dynamic = true; return result; }
|
|
375
|
+
const parsed = scanShell(command);
|
|
376
|
+
result.malformed = parsed.malformed;
|
|
377
|
+
for (const words of parsed.segments) analyzeWords(words, result, depth);
|
|
378
|
+
for (const nested of parsed.nested) merge(result, analyzeCommand(nested, depth + 1));
|
|
379
|
+
if (result.malformed && parsed.segments.some((words) => {
|
|
380
|
+
const commandInfo = unwrap(words);
|
|
381
|
+
return SIMPLE_READERS.has(commandInfo.name) || GREP_READERS.has(commandInfo.name)
|
|
382
|
+
|| AWK_READERS.has(commandInfo.name) || SHELLS.has(commandInfo.name)
|
|
383
|
+
|| INLINE_RUNTIMES.has(commandInfo.name) || ['sed', 'source', '.', 'eval', 'xargs'].includes(commandInfo.name);
|
|
384
|
+
})) result.dynamic = true;
|
|
385
|
+
return result;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function commandAccess(command) {
|
|
389
|
+
const result = analyzeCommand(command);
|
|
390
|
+
return { paths: [...result.paths], dynamic: result.dynamic, malformed: result.malformed };
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
module.exports = { DYNAMIC_READ_PATH, commandAccess };
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const lstatSync = fs.lstatSync.bind(fs);
|
|
6
|
+
const readFileSync = fs.readFileSync.bind(fs);
|
|
7
|
+
const realpathSync = fs.realpathSync.bind(fs);
|
|
8
|
+
|
|
9
|
+
function inside(root, target) {
|
|
10
|
+
const relative = path.relative(path.resolve(root), path.resolve(target));
|
|
11
|
+
return relative === '' || (relative !== '..'
|
|
12
|
+
&& !relative.startsWith(`..${path.sep}`)
|
|
13
|
+
&& !path.isAbsolute(relative));
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function canonicalDirectory(directory, label = 'runtime root') {
|
|
17
|
+
const requested = path.resolve(directory);
|
|
18
|
+
let stat;
|
|
19
|
+
try { stat = lstatSync(requested); }
|
|
20
|
+
catch (error) { throw new Error(`${label} cannot be inspected (${error.code || error.message})`); }
|
|
21
|
+
if (stat.isSymbolicLink() || !stat.isDirectory()) throw new Error(`${label} must be a non-symlink directory`);
|
|
22
|
+
return realpathSync(requested);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function assertComponentPath(root, target, { allowMissing = false } = {}) {
|
|
26
|
+
const requestedRoot = path.resolve(root);
|
|
27
|
+
const requestedTarget = path.resolve(target);
|
|
28
|
+
if (!inside(requestedRoot, requestedTarget)) throw new Error(`path escapes trusted root: ${requestedTarget}`);
|
|
29
|
+
let cursor = requestedRoot;
|
|
30
|
+
for (const part of path.relative(requestedRoot, requestedTarget).split(path.sep).filter(Boolean)) {
|
|
31
|
+
cursor = path.join(cursor, part);
|
|
32
|
+
try {
|
|
33
|
+
if (lstatSync(cursor).isSymbolicLink()) throw new Error(`symlink component rejected: ${cursor}`);
|
|
34
|
+
} catch (error) {
|
|
35
|
+
if (allowMissing && error.code === 'ENOENT') return null;
|
|
36
|
+
throw error;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return requestedTarget;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function canonicalRegularFile(root, target, label = 'runtime file') {
|
|
43
|
+
const canonicalRoot = canonicalDirectory(root);
|
|
44
|
+
const requested = assertComponentPath(root, target);
|
|
45
|
+
const stat = lstatSync(requested);
|
|
46
|
+
if (stat.isSymbolicLink() || !stat.isFile()) throw new Error(`${label} must be a non-symlink regular file`);
|
|
47
|
+
const canonical = realpathSync(requested);
|
|
48
|
+
if (!inside(canonicalRoot, canonical)) throw new Error(`${label} escapes the trusted root`);
|
|
49
|
+
return canonical;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function resolveLocalDependency(root, sourceFile, request) {
|
|
53
|
+
const unresolved = path.resolve(path.dirname(sourceFile), request);
|
|
54
|
+
if (!inside(root, unresolved)) throw new Error(`runtime dependency escapes root: ${sourceFile} -> ${request}`);
|
|
55
|
+
const candidates = path.extname(unresolved)
|
|
56
|
+
? [unresolved]
|
|
57
|
+
: [unresolved, `${unresolved}.cjs`, `${unresolved}.js`, `${unresolved}.json`,
|
|
58
|
+
path.join(unresolved, 'index.cjs'), path.join(unresolved, 'index.js')];
|
|
59
|
+
for (const candidate of candidates) {
|
|
60
|
+
try { return canonicalRegularFile(root, candidate, 'runtime dependency'); }
|
|
61
|
+
catch (error) {
|
|
62
|
+
if (error.code === 'ENOENT' || error.code === 'ENOTDIR'
|
|
63
|
+
|| /cannot be inspected \((?:ENOENT|ENOTDIR)\)/.test(error.message)) continue;
|
|
64
|
+
throw error;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
throw new Error(`runtime dependency is missing: ${sourceFile} -> ${request}`);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function relativeRequires(sourceFile) {
|
|
71
|
+
if (path.extname(sourceFile) === '.json') return [];
|
|
72
|
+
const source = readFileSync(sourceFile, 'utf8');
|
|
73
|
+
if (/\brequire\s*\(\s*path\.(?:join|resolve)\s*\(\s*__dirname\b/.test(source)) {
|
|
74
|
+
throw new Error(`dynamic local require is not allowed: ${sourceFile}`);
|
|
75
|
+
}
|
|
76
|
+
const localBindings = new Set();
|
|
77
|
+
for (const match of source.matchAll(
|
|
78
|
+
/\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*(?:String\s*\(\s*)?(['"])(\.\.?\/[^'"]*)\2\s*\)?/g,
|
|
79
|
+
)) localBindings.add(match[1]);
|
|
80
|
+
const requests = new Set();
|
|
81
|
+
for (const match of source.matchAll(/\brequire\s*\(\s*([^()\r\n]+?)\s*\)/g)) {
|
|
82
|
+
const expression = match[1].trim();
|
|
83
|
+
const literal = expression.match(/^(['"])([^'"]+)\1$/);
|
|
84
|
+
if (literal) {
|
|
85
|
+
if (literal[2].startsWith('.')) requests.add(literal[2]);
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
const identifier = expression.match(/^[A-Za-z_$][\w$]*$/)?.[0];
|
|
89
|
+
const visiblyLocal = /^(['"])(\.\.?\/)/.test(expression)
|
|
90
|
+
|| /^`\.\.?\//.test(expression)
|
|
91
|
+
|| (identifier && localBindings.has(identifier));
|
|
92
|
+
if (visiblyLocal) throw new Error(`dynamic local require is not allowed: ${sourceFile}`);
|
|
93
|
+
}
|
|
94
|
+
return [...requests].sort();
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function verifyCommonJsClosure(root, entrypoint) {
|
|
98
|
+
const canonicalRoot = canonicalDirectory(root);
|
|
99
|
+
const pending = [canonicalRegularFile(root, entrypoint, 'runtime entrypoint')];
|
|
100
|
+
const verified = new Set();
|
|
101
|
+
while (pending.length) {
|
|
102
|
+
const sourceFile = pending.shift();
|
|
103
|
+
if (verified.has(sourceFile)) continue;
|
|
104
|
+
verified.add(sourceFile);
|
|
105
|
+
for (const request of relativeRequires(sourceFile)) {
|
|
106
|
+
const dependency = resolveLocalDependency(canonicalRoot, sourceFile, request);
|
|
107
|
+
if (!verified.has(dependency)) pending.push(dependency);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return [...verified].sort();
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
module.exports = {
|
|
114
|
+
assertComponentPath,
|
|
115
|
+
canonicalDirectory,
|
|
116
|
+
canonicalRegularFile,
|
|
117
|
+
inside,
|
|
118
|
+
verifyCommonJsClosure,
|
|
119
|
+
};
|