@entrorove/skill 0.1.52 → 0.1.54
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/CAPABILITY-MAP.md +14 -2
- package/CHANGELOG.md +12 -0
- package/DELIVERY-BOUNDARIES.md +4 -2
- package/IDE-EXECUTION.md +25 -3
- package/SKILL.md +8 -4
- package/cli.mjs +4 -6
- package/cloud-mutation-authority.mjs +96 -0
- package/connected-cli.mjs +20 -0
- package/ide-code-mode.mjs +30 -8
- package/ide-command.mjs +27 -1
- package/ide-hook.mjs +70 -17
- package/ide-results.mjs +8 -6
- package/ide-session.mjs +44 -1
- package/local-approved-plan.mjs +3 -21
- package/local-approved-process.mjs +54 -0
- package/local-approved-supervisor.mjs +99 -0
- package/local-cli.mjs +13 -2
- package/local-contracts.mjs +2 -0
- package/local-delegated-modify-analysis.mjs +232 -0
- package/local-discipline-actions.mjs +2 -0
- package/local-discipline-gate.mjs +3 -1
- package/local-error.mjs +3 -0
- package/local-execution.mjs +1 -1
- package/local-mutation-authority.mjs +23 -5
- package/local-mutation-batch.mjs +1 -0
- package/local-mutation-binding.mjs +14 -5
- package/local-mutation-consumption.mjs +93 -0
- package/local-mutation-pass-store.mjs +6 -4
- package/local-mutation-wire.mjs +21 -4
- package/local-preserved-mutation-protection.mjs +51 -0
- package/local-preserved-mutation-signer.mjs +81 -0
- package/local-preserved-mutation-state.mjs +96 -0
- package/local-preserved-mutation-wire.mjs +49 -0
- package/local-scope-create-require.mjs +142 -0
- package/local-scope-import-expressions.mjs +2 -2
- package/local-scope-repository.mjs +42 -15
- package/local-scope-require-resolution.mjs +78 -0
- package/manifest.json +1 -1
- package/mutation-client.mjs +290 -0
- package/package.json +21 -2
- package/release-state.json +48 -29
- package/release.mjs +29 -0
- package/releases.json +12 -0
- package/remote-client.mjs +2 -1
- package/workflow-observation-client.mjs +44 -0
- package/workflow-observation-connection.mjs +35 -0
- package/workflow-observation-remote.mjs +84 -0
- package/workflow-observation-runner.mjs +87 -0
- package/workflow-observation-state.mjs +36 -0
- package/workflow-observation-wire.mjs +69 -0
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { promises as fs } from 'node:fs';
|
|
2
|
+
import { canonical } from './local-integrity.mjs';
|
|
3
|
+
import { scopeRead, scopeTarget } from './local-scope-files.mjs';
|
|
4
|
+
import { readDisciplineSnapshot } from './local-discipline-snapshot.mjs';
|
|
5
|
+
import { preservedMutationDelegation, preservedMutationFail } from './local-preserved-mutation-wire.mjs';
|
|
6
|
+
import { preservedMutationProtectedFiles } from './local-preserved-mutation-state.mjs';
|
|
7
|
+
import { scopeCandidateImports } from './local-scope-repository.mjs';
|
|
8
|
+
|
|
9
|
+
function unknownTarget(path, site) {
|
|
10
|
+
const reason = typeof site?.reason === 'string' && /^[a-z-]+$/.test(site.reason) ? site.reason : 'unknown-import-source';
|
|
11
|
+
throw Object.assign(new Error(`Preserved mutation cannot modify unknown dependency source ${JSON.stringify(path)}: ${reason}`),
|
|
12
|
+
{ code: 'ENTROROVE_MUTATION_DELEGATION_INVALID', path, reason });
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async function observe(input, root, expected) {
|
|
16
|
+
const before = await scopeTarget(root, expected.path, false, true), resolvedPath = await fs.realpath(before.target);
|
|
17
|
+
const read = await scopeRead(root, expected.path, input), after = await scopeTarget(root, expected.path, false, true);
|
|
18
|
+
if (read.sha256 !== expected.sha256 || before.stat.dev !== after.stat.dev || before.stat.ino !== after.stat.ino
|
|
19
|
+
|| before.stat.mode !== after.stat.mode || resolvedPath !== await fs.realpath(after.target)) {
|
|
20
|
+
preservedMutationFail(`Preserved mutation dependency ${JSON.stringify(expected.path)} changed bytes, permissions or physical identity`);
|
|
21
|
+
}
|
|
22
|
+
return { ...expected, mode: after.stat.mode & 0o777, resolvedPath };
|
|
23
|
+
}
|
|
24
|
+
export async function capturePreservedMutationProtection(input, root, authority, snapshot) {
|
|
25
|
+
const delegation = preservedMutationDelegation(authority), review = await readDisciplineSnapshot(snapshot);
|
|
26
|
+
for (const expected of delegation.targetFiles) {
|
|
27
|
+
const file = snapshot.manifest.files.find(item => item.path === expected.path);
|
|
28
|
+
if (!file?.exists || file.sha256 !== expected.sha256) preservedMutationFail('Preserved mutation target must retain its exact existing gate baseline');
|
|
29
|
+
const imports = await scopeCandidateImports(root, { path: expected.path,
|
|
30
|
+
content: (await scopeRead(root, expected.path, input)).content.toString('utf8') }, input);
|
|
31
|
+
const declared = delegation.targetImports.find(item => item.path === expected.path);
|
|
32
|
+
if (imports.dynamic) unknownTarget(expected.path, imports.unresolvedImports?.[0]);
|
|
33
|
+
if (canonical([...new Set(imports.edges.map(edge => edge.specifier))].sort()) !== canonical(declared.specifiers)) {
|
|
34
|
+
preservedMutationFail('Preserved mutation targets cannot contain unknown or changed imports');
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
const unknown = review.analysis.unresolved.find(item => authority.paths.includes(item.path) && /(?:^import-|dynamic-import|dynamic-reference)/.test(item.reason));
|
|
38
|
+
if (unknown) unknownTarget(unknown.path, unknown);
|
|
39
|
+
const result = [];
|
|
40
|
+
for (const expected of delegation.protectedFiles) result.push(await observe(input, root, expected));
|
|
41
|
+
return result;
|
|
42
|
+
}
|
|
43
|
+
export async function assertPreservedMutationProtection(input, root, authority) {
|
|
44
|
+
const delegation = preservedMutationDelegation(authority), original = await preservedMutationProtectedFiles(input, authority);
|
|
45
|
+
if (canonical(original.map(({ path, sha256 }) => ({ path, sha256 }))) !== canonical(delegation.protectedFiles)) {
|
|
46
|
+
preservedMutationFail('Preserved mutation protected file manifest differs from its signed delegation');
|
|
47
|
+
}
|
|
48
|
+
for (const expected of original) {
|
|
49
|
+
if (canonical(await observe(input, root, expected)) !== canonical(expected)) preservedMutationFail(`Preserved mutation dependency ${JSON.stringify(expected.path)} changed permissions or physical identity`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { createPrivateKey, createPublicKey, generateKeyPairSync, randomUUID, sign } from 'node:crypto';
|
|
2
|
+
import { promises as fs } from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { canonical, sha256 } from './local-integrity.mjs';
|
|
5
|
+
import { mutationHome14, mutationRoot14, mutationStateRoot14, mutationId14, privateMutationDirectory14,
|
|
6
|
+
readMutationJson14, atomicMutationJson14, mutationTarget14 } from './local-mutation-files.mjs';
|
|
7
|
+
import { preservedMutationFail, preservedMutationPublicKey } from './local-preserved-mutation-wire.mjs';
|
|
8
|
+
import { withStateLock } from './local-state-lock.mjs';
|
|
9
|
+
import { regular } from './local-task-handoff-storage.mjs';
|
|
10
|
+
|
|
11
|
+
export async function preservedMutationLocation(input) {
|
|
12
|
+
const root = await mutationRoot14(input.repositoryRoot), home = await mutationHome14(input.home);
|
|
13
|
+
const relative = path.relative(root, home);
|
|
14
|
+
if (!relative || relative !== '..' && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative)) {
|
|
15
|
+
preservedMutationFail('Preserved mutation signer home must be outside the repository');
|
|
16
|
+
}
|
|
17
|
+
const stateRoot = await mutationStateRoot14({ ...input, home });
|
|
18
|
+
const repositoryDigest = sha256(root).slice(0, 32), flowId = mutationId14(input.flowId, 'flowId');
|
|
19
|
+
const directory = path.join(stateRoot, 'mutation-delegates', repositoryDigest, flowId);
|
|
20
|
+
await privateMutationDirectory14(directory, { privateFrom: stateRoot });
|
|
21
|
+
return { root, home, stateRoot, repositoryDigest, flowId, directory, bindingPath: path.join(directory, 'binding.json'),
|
|
22
|
+
signerConfigPath: path.join(directory, 'signer.json'), fencePath: path.join(directory, 'fence.json') };
|
|
23
|
+
}
|
|
24
|
+
export async function readPreservedMutationBinding(input) {
|
|
25
|
+
const location = await preservedMutationLocation(input), binding = await readMutationJson14(location.bindingPath);
|
|
26
|
+
const publicKey = preservedMutationPublicKey(binding.publicKey);
|
|
27
|
+
if (canonical(Object.keys(binding).sort()) !== canonical(['schemaVersion', 'flowId', 'repositoryDigest', 'home', 'keyId', 'publicKey'].sort())
|
|
28
|
+
|| binding.schemaVersion !== 'entrorove.mutation-delegate-binding/1.0' || binding.flowId !== location.flowId
|
|
29
|
+
|| binding.repositoryDigest !== location.repositoryDigest || binding.home !== location.home || binding.keyId !== publicKey.keyId) {
|
|
30
|
+
preservedMutationFail('Preserved mutation signer lost its original repository, home or flow binding');
|
|
31
|
+
}
|
|
32
|
+
return { ...location, binding };
|
|
33
|
+
}
|
|
34
|
+
export async function preparePreservedMutationSigner(input) {
|
|
35
|
+
const location = await preservedMutationLocation(input);
|
|
36
|
+
return withStateLock(path.join(location.directory, 'prepare'), () => prepareSigner(input, location));
|
|
37
|
+
}
|
|
38
|
+
async function prepareSigner(input, location) {
|
|
39
|
+
try { await mutationTarget14(location.directory, 'binding.json'); }
|
|
40
|
+
catch (error) {
|
|
41
|
+
if (error.code !== 'ENOENT') throw error;
|
|
42
|
+
const pair = generateKeyPairSync('ed25519'), publicKey = pair.publicKey.export({ format: 'der', type: 'spki' }).toString('base64');
|
|
43
|
+
const stage = path.join(location.directory, `.prepare-${randomUUID()}`);
|
|
44
|
+
await privateMutationDirectory14(stage, { exclusive: true, privateFrom: location.stateRoot });
|
|
45
|
+
const privateKeyPath = path.join(location.directory, 'private.pem');
|
|
46
|
+
try {
|
|
47
|
+
await fs.writeFile(path.join(stage, 'private.pem'), pair.privateKey.export({ format: 'pem', type: 'pkcs8' }), { flag: 'wx', mode: 0o600 });
|
|
48
|
+
await atomicMutationJson14(path.join(stage, 'signer.json'), { schemaVersion: 'entrorove.mutation-delegate-signer/1.0',
|
|
49
|
+
privateKeyPath, keyId: sha256(Buffer.from(publicKey, 'base64')), receiptTtlMs: 60_000 }, { privateFrom: location.stateRoot });
|
|
50
|
+
await atomicMutationJson14(path.join(stage, 'binding.json'), { schemaVersion: 'entrorove.mutation-delegate-binding/1.0',
|
|
51
|
+
flowId: location.flowId, repositoryDigest: location.repositoryDigest, home: location.home,
|
|
52
|
+
publicKey, keyId: sha256(Buffer.from(publicKey, 'base64')) }, { privateFrom: location.stateRoot });
|
|
53
|
+
const fence = { schemaVersion: 'entrorove.preserved-mutation-fence/1.0', epoch: 0, identity: null, grants: [] };
|
|
54
|
+
await atomicMutationJson14(path.join(stage, 'fence.json'), { ...fence,
|
|
55
|
+
signature: sign(null, Buffer.from(canonical(fence)), pair.privateKey).toString('base64url') }, { privateFrom: location.stateRoot });
|
|
56
|
+
for (const file of ['private.pem', 'signer.json', 'binding.json', 'fence.json']) await fs.link(path.join(stage, file), path.join(location.directory, file));
|
|
57
|
+
} finally { await fs.rm(stage, { recursive: true, force: true }); }
|
|
58
|
+
}
|
|
59
|
+
const actual = await readPreservedMutationBinding(input);
|
|
60
|
+
await mutationTarget14(actual.directory, 'fence.json');
|
|
61
|
+
await loadPreservedMutationSigner(input, actual.signerConfigPath, actual.binding.keyId);
|
|
62
|
+
return { schemaVersion: 'entrorove.local-execution/1.0', status: 'reported', purpose: 'preserved-existing-modify',
|
|
63
|
+
mutationPublicKey: actual.binding.publicKey, mutationKeyId: actual.binding.keyId, keyId: actual.binding.keyId, signerConfigPath: actual.signerConfigPath };
|
|
64
|
+
}
|
|
65
|
+
export async function loadPreservedMutationSigner(input, file, keyId) {
|
|
66
|
+
const location = await readPreservedMutationBinding(input);
|
|
67
|
+
if (file !== location.signerConfigPath || location.binding.keyId !== keyId) preservedMutationFail('Preserved mutation signer differs from the delegated physical key');
|
|
68
|
+
const config = await readMutationJson14(file);
|
|
69
|
+
if (canonical(Object.keys(config).sort()) !== canonical(['schemaVersion', 'privateKeyPath', 'keyId', 'receiptTtlMs'].sort())
|
|
70
|
+
|| config.schemaVersion !== 'entrorove.mutation-delegate-signer/1.0' || config.keyId !== keyId
|
|
71
|
+
|| config.privateKeyPath !== path.join(location.directory, 'private.pem') || !Number.isSafeInteger(config.receiptTtlMs)
|
|
72
|
+
|| config.receiptTtlMs < 1 || config.receiptTtlMs > 90_000) preservedMutationFail('Preserved mutation signer configuration is invalid');
|
|
73
|
+
const target = await mutationTarget14(location.directory, 'private.pem'), { bytes, stat } = await regular(target.target, 16 * 1024);
|
|
74
|
+
try {
|
|
75
|
+
if (stat.nlink !== 1 || process.platform !== 'win32' && (stat.mode & 0o077)
|
|
76
|
+
|| process.getuid && stat.uid !== process.getuid()) preservedMutationFail('Preserved mutation signer must be an owned private file');
|
|
77
|
+
const privateKey = createPrivateKey(bytes), der = createPublicKey(privateKey).export({ format: 'der', type: 'spki' });
|
|
78
|
+
if (privateKey.asymmetricKeyType !== 'ed25519' || sha256(der) !== keyId) preservedMutationFail('Preserved mutation signer key fingerprint is invalid');
|
|
79
|
+
return { privateKey, config };
|
|
80
|
+
} finally { bytes.fill(0); }
|
|
81
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { promises as fs } from 'node:fs';
|
|
3
|
+
import { sign, verify } from 'node:crypto';
|
|
4
|
+
import { canonical, sha256 } from './local-integrity.mjs';
|
|
5
|
+
import { withStateLock } from './local-state-lock.mjs';
|
|
6
|
+
import { readMutationJson14, atomicMutationJson14 } from './local-mutation-files.mjs';
|
|
7
|
+
import { readPreservedMutationBinding, loadPreservedMutationSigner } from './local-preserved-mutation-signer.mjs';
|
|
8
|
+
import { preservedMutationDelegation, preservedMutationFail, preservedMutationPublicKey } from './local-preserved-mutation-wire.mjs';
|
|
9
|
+
|
|
10
|
+
const identity = authority => Object.fromEntries(['flowId', 'taskId', 'ownerId', 'taskDigest', 'chainDigest',
|
|
11
|
+
'repositoryDigest', 'runnerKeyId', 'runnerId', 'repositoryId', 'receiverDigest'].map(field => [field, authority[field]]));
|
|
12
|
+
async function rejectGenericHistory(location) {
|
|
13
|
+
const anchor = process.env.ENTROROVE_TASK_HANDOFF_STATE_DIRECTORY;
|
|
14
|
+
if (!anchor) return;
|
|
15
|
+
const file = path.join(anchor, 'flows', location.repositoryDigest, location.flowId, 'fence.json');
|
|
16
|
+
try {
|
|
17
|
+
await fs.lstat(file);
|
|
18
|
+
preservedMutationFail('Preserved mutation cannot replace an existing generic task execution or handoff history');
|
|
19
|
+
} catch (error) { if (error.code !== 'ENOENT') throw error; }
|
|
20
|
+
}
|
|
21
|
+
export async function preservedMutationOriginalBinding(input, authority) {
|
|
22
|
+
const location = await readPreservedMutationBinding(input), delegation = preservedMutationDelegation(authority);
|
|
23
|
+
if (authority.flowId !== input.flowId || authority.stepId !== input.stepId || authority.repositoryDigest !== location.repositoryDigest
|
|
24
|
+
|| input.taskId !== undefined && input.taskId !== authority.taskId || input.generation !== undefined && input.generation !== authority.generation
|
|
25
|
+
|| input.leaseId !== undefined && input.leaseId !== authority.leaseId || input.executionAttemptNonce !== undefined
|
|
26
|
+
|| delegation.mutationKeyId !== location.binding.keyId || delegation.mutationPublicKey !== location.binding.publicKey) {
|
|
27
|
+
preservedMutationFail('Preserved mutation lost its original physical signer, task, lease or repository binding');
|
|
28
|
+
}
|
|
29
|
+
await rejectGenericHistory(location);
|
|
30
|
+
let state;
|
|
31
|
+
try { state = await readMutationJson14(location.fencePath); }
|
|
32
|
+
catch (error) { if (error.code !== 'ENOENT') throw error; preservedMutationFail('Preserved mutation fence history is missing; automatic reinitialization is forbidden'); }
|
|
33
|
+
if (state) {
|
|
34
|
+
const { signature, ...core } = state, bytes = typeof signature === 'string' ? Buffer.from(signature, 'base64url') : Buffer.alloc(0);
|
|
35
|
+
if (bytes.length !== 64 || bytes.toString('base64url') !== signature
|
|
36
|
+
|| !verify(null, Buffer.from(canonical(core)), preservedMutationPublicKey(location.binding.publicKey).key, bytes)) {
|
|
37
|
+
preservedMutationFail('Preserved mutation fence integrity is invalid');
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
if (state.schemaVersion !== 'entrorove.preserved-mutation-fence/1.0' || state.epoch !== 0
|
|
41
|
+
|| state.identity !== null && canonical(state.identity) !== canonical(identity(authority))
|
|
42
|
+
|| !Array.isArray(state.grants) || state.grants.length > 10000 || state.identity === null && state.grants.length) {
|
|
43
|
+
preservedMutationFail('Preserved mutation fence cannot replace original task identity or epoch history');
|
|
44
|
+
}
|
|
45
|
+
return { ...location, context: null, state: { epoch: 0, status: 'active' }, preservedState: state };
|
|
46
|
+
}
|
|
47
|
+
function grantFor(state, authority) {
|
|
48
|
+
const grant = state.grants.find(item => item.challengeId === authority.challengeId);
|
|
49
|
+
const nonce = authority.passId ? authority.challengeNonce : authority.nonce;
|
|
50
|
+
const digest = authority.passId ? authority.challengeDigest : sha256(canonical(authority));
|
|
51
|
+
if (state.grants.some(item => item.nonce === nonce && item !== grant)
|
|
52
|
+
|| grant && (grant.nonce !== nonce || grant.challengeDigest !== digest || grant.leaseId !== authority.leaseId
|
|
53
|
+
|| grant.generation !== authority.generation || grant.stepId !== authority.stepId)) {
|
|
54
|
+
preservedMutationFail('Preserved mutation nonce cannot replace its original challenge or lease');
|
|
55
|
+
}
|
|
56
|
+
return grant;
|
|
57
|
+
}
|
|
58
|
+
export async function assertPreservedMutationIssued(input, pass) {
|
|
59
|
+
const binding = await preservedMutationOriginalBinding(input, pass), grant = grantFor(binding.preservedState, pass);
|
|
60
|
+
if (!grant || grant.passDigest !== sha256(canonical(pass))) preservedMutationFail('Preserved mutation requires its original issued pass and nonce fence');
|
|
61
|
+
return binding;
|
|
62
|
+
}
|
|
63
|
+
export async function preservedMutationProtectedFiles(input, authority) {
|
|
64
|
+
const binding = await preservedMutationOriginalBinding(input, authority), grant = grantFor(binding.preservedState, authority);
|
|
65
|
+
if (!grant?.protectedFiles) preservedMutationFail('Preserved mutation requires its original protected file observations');
|
|
66
|
+
return grant.protectedFiles;
|
|
67
|
+
}
|
|
68
|
+
async function persist(input, binding, state) {
|
|
69
|
+
const { signature: previous, ...core } = state;
|
|
70
|
+
const signer = await loadPreservedMutationSigner(input, binding.signerConfigPath, binding.binding.keyId);
|
|
71
|
+
const signature = sign(null, Buffer.from(canonical(core)), signer.privateKey).toString('base64url');
|
|
72
|
+
await atomicMutationJson14(binding.fencePath, { ...core, signature }, { privateFrom: binding.stateRoot, replace: true });
|
|
73
|
+
}
|
|
74
|
+
export async function withPreservedMutationFence(input, authority, action, { attest = false, issue = false, protectedFiles } = {}) {
|
|
75
|
+
const original = await preservedMutationOriginalBinding(input, authority);
|
|
76
|
+
return withStateLock(original.fencePath, async () => {
|
|
77
|
+
const binding = await preservedMutationOriginalBinding(input, authority), state = binding.preservedState;
|
|
78
|
+
if (state.identity === null) state.identity = identity(authority);
|
|
79
|
+
let grant = grantFor(state, authority);
|
|
80
|
+
if (!grant) {
|
|
81
|
+
if (!attest || authority.passId) preservedMutationFail('Preserved mutation has no original attested challenge');
|
|
82
|
+
grant = { challengeId: authority.challengeId, nonce: authority.nonce, challengeDigest: sha256(canonical(authority)),
|
|
83
|
+
leaseId: authority.leaseId, generation: authority.generation, stepId: authority.stepId, passDigest: null, protectedFiles };
|
|
84
|
+
state.grants.push(grant);
|
|
85
|
+
}
|
|
86
|
+
if (attest && canonical(grant.protectedFiles) !== canonical(protectedFiles)) preservedMutationFail('Preserved mutation protected baseline changed during attestation');
|
|
87
|
+
if (authority.passId) {
|
|
88
|
+
const passDigest = sha256(canonical(authority));
|
|
89
|
+
if (grant.passDigest !== passDigest && (!issue || grant.passDigest !== null)) preservedMutationFail('Preserved mutation challenge was already bound to another pass');
|
|
90
|
+
if (issue) grant.passDigest = passDigest;
|
|
91
|
+
else if (!grant.passDigest) preservedMutationFail('Preserved mutation pass is not issued');
|
|
92
|
+
}
|
|
93
|
+
await persist(input, binding, state);
|
|
94
|
+
return action(binding.state);
|
|
95
|
+
});
|
|
96
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { createPublicKey } from 'node:crypto';
|
|
2
|
+
import { canonical, sha256 } from './local-integrity.mjs';
|
|
3
|
+
import { mutationFail14, mutationPath14 } from './local-mutation-files.mjs';
|
|
4
|
+
|
|
5
|
+
export const preservedMutationFail = message => mutationFail14('ENTROROVE_MUTATION_DELEGATION_INVALID', message);
|
|
6
|
+
function exact(value, names) {
|
|
7
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)
|
|
8
|
+
|| canonical(Object.keys(value).sort()) !== canonical([...names].sort())) preservedMutationFail('Preserved mutation delegation fields are invalid');
|
|
9
|
+
}
|
|
10
|
+
export function preservedMutationPublicKey(encoded) {
|
|
11
|
+
try {
|
|
12
|
+
const bytes = Buffer.from(encoded, 'base64'), key = createPublicKey({ key: bytes, format: 'der', type: 'spki' });
|
|
13
|
+
if (typeof encoded !== 'string' || bytes.toString('base64') !== encoded || key.asymmetricKeyType !== 'ed25519'
|
|
14
|
+
|| key.export({ format: 'der', type: 'spki' }).toString('base64') !== encoded) throw new Error();
|
|
15
|
+
return { key, keyId: sha256(bytes) };
|
|
16
|
+
} catch { preservedMutationFail('Preserved mutation requires a canonical Ed25519 public key'); }
|
|
17
|
+
}
|
|
18
|
+
function files(values) {
|
|
19
|
+
if (!Array.isArray(values) || values.length > 2000) preservedMutationFail('Preserved mutation file manifest is invalid');
|
|
20
|
+
for (const [index, file] of values.entries()) {
|
|
21
|
+
exact(file, ['path', 'sha256']); mutationPath14(file.path);
|
|
22
|
+
if (!/^[0-9a-f]{64}$/.test(file.sha256) || index && values[index - 1].path >= file.path) {
|
|
23
|
+
preservedMutationFail('Preserved mutation file manifest must be sorted and unique');
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return values;
|
|
27
|
+
}
|
|
28
|
+
export function preservedMutationDelegation(authority) {
|
|
29
|
+
const value = authority.writeDelegation;
|
|
30
|
+
exact(value, ['schemaVersion', 'purpose', 'scopeKeyId', 'mutationKeyId', 'mutationPublicKey',
|
|
31
|
+
'gateEvidenceDigest', 'importGraphDigest', 'protectedFiles', 'targetFiles', 'targetImports']);
|
|
32
|
+
const publicKey = preservedMutationPublicKey(value.mutationPublicKey);
|
|
33
|
+
if (value.schemaVersion !== 'entrorove.preserved-write-delegation/1.0' || value.purpose !== 'preserved-existing-modify'
|
|
34
|
+
|| ['scopeKeyId', 'mutationKeyId', 'gateEvidenceDigest', 'importGraphDigest'].some(field => !/^[0-9a-f]{64}$/.test(value[field]))
|
|
35
|
+
|| publicKey.keyId !== value.mutationKeyId || new Set([authority.runnerKeyId, value.scopeKeyId, value.mutationKeyId]).size !== 3
|
|
36
|
+
|| authority.epoch !== 0 || authority.newFilePaths.length !== 0) preservedMutationFail('Preserved mutation role or original epoch is invalid');
|
|
37
|
+
files(value.protectedFiles); files(value.targetFiles);
|
|
38
|
+
if (canonical(value.targetFiles.map(file => file.path)) !== canonical(authority.paths)
|
|
39
|
+
|| value.protectedFiles.some(file => authority.paths.includes(file.path))
|
|
40
|
+
|| !Array.isArray(value.targetImports) || canonical(value.targetImports.map(file => file?.path)) !== canonical(authority.paths)) {
|
|
41
|
+
preservedMutationFail('Preserved mutation must retain its exact existing target and protected path sets');
|
|
42
|
+
}
|
|
43
|
+
for (const file of value.targetImports) {
|
|
44
|
+
exact(file, ['path', 'specifiers']);
|
|
45
|
+
if (!Array.isArray(file.specifiers) || file.specifiers.some(item => typeof item !== 'string' || !item || /[\u0000-\u001f\u007f]/.test(item))
|
|
46
|
+
|| canonical([...new Set(file.specifiers)].sort()) !== canonical(file.specifiers)) preservedMutationFail('Preserved mutation import manifest is invalid');
|
|
47
|
+
}
|
|
48
|
+
return value;
|
|
49
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
3
|
+
import ts from 'typescript';
|
|
4
|
+
import { createScopeImportResolver, scopeSourceChecker } from './local-scope-import-expressions.mjs';
|
|
5
|
+
import { scopeFail, scopePath } from './local-scope-files.mjs';
|
|
6
|
+
|
|
7
|
+
const purePathMethods = new Set(['join', 'resolve', 'basename', 'dirname', 'extname', 'isAbsolute', 'normalize',
|
|
8
|
+
'relative', 'parse', 'format', 'toNamespacedPath', 'matchesGlob']);
|
|
9
|
+
function unwrap(node) { while (node && ts.isParenthesizedExpression(node)) node = node.expression; return node; }
|
|
10
|
+
function imported(declaration) {
|
|
11
|
+
if (ts.isImportSpecifier(declaration)) return { imported: declaration.propertyName?.text ?? declaration.name.text,
|
|
12
|
+
module: declaration.parent.parent.parent.moduleSpecifier.text };
|
|
13
|
+
if (ts.isImportClause(declaration)) return { imported: 'default', module: declaration.parent.moduleSpecifier.text };
|
|
14
|
+
if (ts.isNamespaceImport(declaration)) return { imported: '*', module: declaration.parent.parent.moduleSpecifier.text };
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
function importName(node) {
|
|
18
|
+
for (let parent = node; parent; parent = parent.parent) {
|
|
19
|
+
if (ts.isImportDeclaration(parent)) return true;
|
|
20
|
+
if (ts.isStatement(parent)) return false;
|
|
21
|
+
}
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
function immutable(declaration, source) {
|
|
25
|
+
return declaration && ts.isVariableDeclaration(declaration) && ts.isVariableDeclarationList(declaration.parent)
|
|
26
|
+
&& declaration.parent.flags & ts.NodeFlags.Const && declaration.initializer && declaration.getSourceFile() === source
|
|
27
|
+
&& !(ts.getCombinedModifierFlags(declaration) & ts.ModifierFlags.Ambient) && !source.isDeclarationFile;
|
|
28
|
+
}
|
|
29
|
+
function staticAnchor(call, source, repositoryRoot, filename, declaration, pathMethod) {
|
|
30
|
+
if (call.arguments.length !== 1) return null;
|
|
31
|
+
let visited = 0;
|
|
32
|
+
const value = (raw, seen = new Set(), depth = 0) => {
|
|
33
|
+
const node = unwrap(raw);
|
|
34
|
+
if (!node || depth > 32 || ++visited > 256) return null;
|
|
35
|
+
if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)) return node.text;
|
|
36
|
+
if (ts.isPropertyAccessExpression(node) && ts.isMetaProperty(node.expression)
|
|
37
|
+
&& node.expression.keywordToken === ts.SyntaxKind.ImportKeyword && node.expression.name.text === 'meta') {
|
|
38
|
+
const file = path.resolve(repositoryRoot, filename);
|
|
39
|
+
return node.name.text === 'dirname' ? path.dirname(file) : node.name.text === 'filename' ? file
|
|
40
|
+
: node.name.text === 'url' ? pathToFileURL(file).href : null;
|
|
41
|
+
}
|
|
42
|
+
if (ts.isCallExpression(node)) {
|
|
43
|
+
const method = pathMethod(node.expression);
|
|
44
|
+
if (!['join', 'resolve'].includes(method) || node.arguments.length < 1 || node.arguments.length > 32) return null;
|
|
45
|
+
const args = node.arguments.map(item => value(item, seen, depth + 1));
|
|
46
|
+
if (args.some(item => typeof item !== 'string') || method === 'resolve' && !args.some(item => path.isAbsolute(item))) return null;
|
|
47
|
+
return path[method](...args);
|
|
48
|
+
}
|
|
49
|
+
if (!ts.isIdentifier(node)) return null;
|
|
50
|
+
const bound = declaration(node);
|
|
51
|
+
return immutable(bound, source) && !seen.has(bound) ? value(bound.initializer, new Set([...seen, bound]), depth + 1) : null;
|
|
52
|
+
};
|
|
53
|
+
let absolute = value(call.arguments[0]);
|
|
54
|
+
if (typeof absolute !== 'string' || !absolute || /[\u0000-\u001f\u007f]/.test(absolute)) return null;
|
|
55
|
+
if (absolute.startsWith('file:')) { try { absolute = fileURLToPath(absolute); } catch { return null; } }
|
|
56
|
+
if (!path.isAbsolute(absolute)) return null;
|
|
57
|
+
const relative = path.relative(repositoryRoot, absolute).split(path.sep).join('/');
|
|
58
|
+
if (!relative || relative === '..' || relative.startsWith('../') || path.isAbsolute(relative)
|
|
59
|
+
|| relative.split('/').some(part => ['.git', '.entrorove', '.aimlock', '.archguard'].includes(part))) {
|
|
60
|
+
scopeFail('ENTROROVE_PATH_UNSAFE', `createRequire in "${filename}" resolves its anchor outside the authorized repository or into a reserved control directory`);
|
|
61
|
+
}
|
|
62
|
+
return scopePath(relative);
|
|
63
|
+
}
|
|
64
|
+
/** Infer only lexical builtin factories and immutable path expressions; never run scanned code. */
|
|
65
|
+
export function createScopeRequireContexts(source, repositoryRoot, filename) {
|
|
66
|
+
let checker, safety;
|
|
67
|
+
const declaration = node => {
|
|
68
|
+
checker ??= scopeSourceChecker(source);
|
|
69
|
+
const declarations = checker.getSymbolAtLocation(node)?.declarations;
|
|
70
|
+
return declarations?.length === 1 && declarations[0].getSourceFile() === source ? declarations[0] : null;
|
|
71
|
+
};
|
|
72
|
+
const origin = node => ts.isIdentifier(node) ? imported(declaration(node) ?? source) : null;
|
|
73
|
+
const factory = node => { const value = node && origin(unwrap(node));
|
|
74
|
+
return value?.imported === 'createRequire' && ['node:module', 'module'].includes(value.module); };
|
|
75
|
+
const pathObject = node => { const value = node && origin(unwrap(node));
|
|
76
|
+
return ['default', '*'].includes(value?.imported) && ['node:path', 'path'].includes(value?.module); };
|
|
77
|
+
const pathMethod = node => {
|
|
78
|
+
node = unwrap(node);
|
|
79
|
+
if (node && ts.isPropertyAccessExpression(node) && pathObject(node.expression)) return node.name.text;
|
|
80
|
+
const value = node && origin(node);
|
|
81
|
+
return value && ['node:path', 'path'].includes(value.module) ? value.imported : null;
|
|
82
|
+
};
|
|
83
|
+
const factoryCall = node => { node = unwrap(node);
|
|
84
|
+
return node && ts.isCallExpression(node) && factory(node.expression) ? node : null; };
|
|
85
|
+
const moduleArguments = createScopeImportResolver(source);
|
|
86
|
+
const mutableBuiltinLoad = node => {
|
|
87
|
+
if (!ts.isCallExpression(node) || !node.arguments.length) return false;
|
|
88
|
+
const callee = unwrap(node.expression), bound = ts.isIdentifier(callee) && declaration(callee);
|
|
89
|
+
const loading = callee.kind === ts.SyntaxKind.ImportKeyword || ts.isIdentifier(callee) && callee.text === 'require'
|
|
90
|
+
|| factoryCall(callee) || bound && ts.isVariableDeclaration(bound) && factoryCall(bound.initializer);
|
|
91
|
+
return loading && moduleArguments(node.arguments[0])?.some(name => ['path', 'node:path', 'module', 'node:module'].includes(name));
|
|
92
|
+
};
|
|
93
|
+
function safeBuiltins() {
|
|
94
|
+
if (safety !== undefined) return safety;
|
|
95
|
+
safety = true;
|
|
96
|
+
const visit = node => {
|
|
97
|
+
if (mutableBuiltinLoad(node)) safety = false;
|
|
98
|
+
if (ts.isIdentifier(node) && !importName(node)) {
|
|
99
|
+
if (node.text === 'syncBuiltinESMExports') safety = false;
|
|
100
|
+
if (pathObject(node)) {
|
|
101
|
+
const member = node.parent, call = member?.parent;
|
|
102
|
+
if (!ts.isPropertyAccessExpression(member) || member.expression !== node
|
|
103
|
+
|| !(purePathMethods.has(member.name.text) && ts.isCallExpression(call) && call.expression === member)
|
|
104
|
+
&& !['sep', 'delimiter'].includes(member.name.text)) safety = false;
|
|
105
|
+
}
|
|
106
|
+
const method = origin(node);
|
|
107
|
+
if (method && ['node:module', 'module'].includes(method.module)
|
|
108
|
+
&& ['default', '*', 'register', 'registerHooks', 'syncBuiltinESMExports'].includes(method.imported)) safety = false;
|
|
109
|
+
if (method && ['node:path', 'path'].includes(method.module) && purePathMethods.has(method.imported)
|
|
110
|
+
&& !(ts.isCallExpression(node.parent) && node.parent.expression === node)) safety = false;
|
|
111
|
+
if (factory(node) && !(ts.isCallExpression(node.parent) && node.parent.expression === node)) safety = false;
|
|
112
|
+
}
|
|
113
|
+
ts.forEachChild(node, visit);
|
|
114
|
+
};
|
|
115
|
+
visit(source); return safety;
|
|
116
|
+
}
|
|
117
|
+
const anchor = call => repositoryRoot && safeBuiltins() ? staticAnchor(call, source, repositoryRoot, filename, declaration, pathMethod) : null;
|
|
118
|
+
function loader(node) {
|
|
119
|
+
node = unwrap(node);
|
|
120
|
+
const direct = factoryCall(node);
|
|
121
|
+
if (direct) return { anchor: anchor(direct), call: direct };
|
|
122
|
+
if (!node || !ts.isIdentifier(node)) return null;
|
|
123
|
+
const bound = declaration(node), creation = bound && ts.isVariableDeclaration(bound) && factoryCall(bound.initializer);
|
|
124
|
+
if (creation) return { anchor: immutable(bound, source) ? anchor(creation) : null, call: creation, declaration: bound };
|
|
125
|
+
if (node.text === 'require' && bound) return { anchor: null };
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
return { loader,
|
|
129
|
+
knownFactoryReference(node) {
|
|
130
|
+
const named = ts.isImportSpecifier(node.parent) && imported(node.parent);
|
|
131
|
+
return importName(node) && (factory(node) || named?.imported === 'createRequire' && ['node:module', 'module'].includes(named.module))
|
|
132
|
+
|| factory(node) && ts.isCallExpression(node.parent)
|
|
133
|
+
&& node.parent.expression === node && anchor(node.parent) !== null;
|
|
134
|
+
},
|
|
135
|
+
loaderReference(node) {
|
|
136
|
+
if (!ts.isIdentifier(node) || importName(node)) return null;
|
|
137
|
+
const found = loader(node);
|
|
138
|
+
if (!found) return null;
|
|
139
|
+
const declared = found.declaration?.name === node;
|
|
140
|
+
return { safe: Boolean(found.anchor) && (declared || ts.isCallExpression(node.parent) && node.parent.expression === node) };
|
|
141
|
+
} };
|
|
142
|
+
}
|
|
@@ -7,7 +7,7 @@ function unwrap(node) {
|
|
|
7
7
|
|| ts.isTypeAssertionExpression(node) || ts.isNonNullExpression(node))) node = node.expression;
|
|
8
8
|
return node;
|
|
9
9
|
}
|
|
10
|
-
function
|
|
10
|
+
export function scopeSourceChecker(source) {
|
|
11
11
|
const name = path.posix.resolve('/', source.fileName.replaceAll('\\', '/'));
|
|
12
12
|
const matches = value => path.posix.resolve('/', value.replaceAll('\\', '/')) === name;
|
|
13
13
|
const host = { getSourceFile: filename => matches(filename) ? source : undefined,
|
|
@@ -47,7 +47,7 @@ export function createScopeImportResolver(source, suppliedChecker) {
|
|
|
47
47
|
return result;
|
|
48
48
|
}
|
|
49
49
|
if (!ts.isIdentifier(node)) return null;
|
|
50
|
-
checker ??=
|
|
50
|
+
checker ??= scopeSourceChecker(source);
|
|
51
51
|
const declarations = checker.getSymbolAtLocation(node)?.declarations;
|
|
52
52
|
if (declarations?.length !== 1) return null;
|
|
53
53
|
const declaration = declarations[0];
|
|
@@ -15,6 +15,8 @@ import { scopeTsconfigImports } from './local-scope-config-imports.mjs';
|
|
|
15
15
|
import { scopeGitExecutable } from './local-git-executable.mjs';
|
|
16
16
|
import { createScopeImportResolver, unresolvedImportSite } from './local-scope-import-expressions.mjs';
|
|
17
17
|
import { isTypeOnlyReference, isDynamicFunctionReference } from './local-typescript-function-reference.mjs';
|
|
18
|
+
import { createScopeRequireContexts } from './local-scope-create-require.mjs';
|
|
19
|
+
import { resolveCreatedRequire } from './local-scope-require-resolution.mjs';
|
|
18
20
|
|
|
19
21
|
const execute = promisify(execFile);
|
|
20
22
|
async function git(root, args, allowed = [], input = { repositoryRoot: root }) {
|
|
@@ -107,11 +109,17 @@ function scriptImports(source, filename, context = {}) {
|
|
|
107
109
|
const parsed = ts.createSourceFile(filename, source, ts.ScriptTarget.Latest, true,
|
|
108
110
|
scriptKinds.get(path.extname(filename).toLowerCase()) ?? ts.ScriptKind.TS);
|
|
109
111
|
if (parsed.parseDiagnostics.length) scopeFail('ENTROROVE_IMPORT_INVALID', 'source has invalid or unterminated syntax');
|
|
110
|
-
const found = new Set(), unresolvedImports = [], resolve = createScopeImportResolver(parsed); let dynamic = false;
|
|
111
|
-
const
|
|
112
|
+
const found = new Set(), requires = new Map(), unresolvedImports = [], resolve = createScopeImportResolver(parsed); let dynamic = false;
|
|
113
|
+
const contexts = createScopeRequireContexts(parsed, context.repositoryRoot, context.sourceFilename ?? filename);
|
|
114
|
+
const unresolved = (expression, reason, message) => { dynamic = true;
|
|
115
|
+
unresolvedImports.push({ ...unresolvedImportSite(parsed, expression), reason, message }); };
|
|
116
|
+
const add = (expression, anchor) => {
|
|
112
117
|
const values = resolve(expression);
|
|
113
118
|
if (values === null) { dynamic = true; unresolvedImports.push(unresolvedImportSite(parsed, expression)); }
|
|
114
|
-
else for (const value of values)
|
|
119
|
+
else for (const value of values) {
|
|
120
|
+
if (anchor) requires.set(`${anchor}\0${value}`, { anchor, specifier: value });
|
|
121
|
+
else found.add(value);
|
|
122
|
+
}
|
|
115
123
|
};
|
|
116
124
|
const visit = node => {
|
|
117
125
|
if (ts.isImportDeclaration(node) || ts.isExportDeclaration(node)) { if (node.moduleSpecifier) add(node.moduleSpecifier); }
|
|
@@ -120,21 +128,28 @@ function scriptImports(source, filename, context = {}) {
|
|
|
120
128
|
else if (ts.isCallExpression(node)) {
|
|
121
129
|
const importer = node.expression.kind === ts.SyntaxKind.ImportKeyword;
|
|
122
130
|
const requireCall = ts.isIdentifier(node.expression) && node.expression.text === 'require';
|
|
123
|
-
|
|
131
|
+
const created = importer ? null : contexts.loader(node.expression);
|
|
132
|
+
if (importer || requireCall || created) {
|
|
124
133
|
if (!node.arguments.length || node.arguments.length > (importer ? 2 : 1)) {
|
|
125
134
|
dynamic = true;
|
|
126
135
|
unresolvedImports.push({ ...unresolvedImportSite(parsed, node), reason: 'import-argument-list-not-supported',
|
|
127
136
|
message: 'Import inspection requires one module argument and at most one import-attributes argument' });
|
|
128
137
|
}
|
|
129
|
-
else
|
|
138
|
+
else if (created && !created.anchor) unresolved(node, 'create-require-context-not-statically-proven',
|
|
139
|
+
'Require uses a shadowed, mutable or unproven factory/path context; no dependency lookup is authorized');
|
|
140
|
+
else add(node.arguments[0], created?.anchor);
|
|
130
141
|
}
|
|
131
142
|
}
|
|
132
143
|
const runtimeReference = !isTypeOnlyReference(node);
|
|
133
144
|
if (runtimeReference && ts.isIdentifier(node)) {
|
|
134
|
-
if (dynamicNames.has(node.text) && (node.text !== 'Function' || isDynamicFunctionReference(node, context))
|
|
145
|
+
if (dynamicNames.has(node.text) && (node.text !== 'Function' || isDynamicFunctionReference(node, context))
|
|
146
|
+
&& !(node.text === 'createRequire' && contexts.knownFactoryReference(node))) dynamic = true;
|
|
147
|
+
const loader = contexts.loaderReference(node);
|
|
148
|
+
if (loader && !loader.safe) unresolved(node, 'require-loader-escapes-static-context',
|
|
149
|
+
'Require loader aliases, mutable bindings and uses outside direct calls remain unresolved');
|
|
135
150
|
if (node.text === 'require' && !(ts.isCallExpression(node.parent) && node.parent.expression === node)
|
|
136
151
|
&& !(ts.isPropertyAccessExpression(node.parent) && node.parent.name === node)
|
|
137
|
-
&& !(ts.isMethodDeclaration(node.parent) && node.parent.name === node)) dynamic = true;
|
|
152
|
+
&& !(ts.isMethodDeclaration(node.parent) && node.parent.name === node) && !loader?.safe) dynamic = true;
|
|
138
153
|
}
|
|
139
154
|
if (runtimeReference && ts.isElementAccessExpression(node)) {
|
|
140
155
|
const member = staticString(node.argumentExpression);
|
|
@@ -143,7 +158,7 @@ function scriptImports(source, filename, context = {}) {
|
|
|
143
158
|
if (runtimeReference && ts.isPropertyAccessExpression(node) && node.name.text === 'require' && globalObject(node.expression)) dynamic = true;
|
|
144
159
|
ts.forEachChild(node, visit);
|
|
145
160
|
};
|
|
146
|
-
visit(parsed); return { specifiers: [...found].sort(), dynamic, unresolvedImports };
|
|
161
|
+
visit(parsed); return { specifiers: [...found].sort(), requires: [...requires.values()], dynamic, unresolvedImports };
|
|
147
162
|
}
|
|
148
163
|
function vueScripts(source, filename) {
|
|
149
164
|
const { descriptor, errors } = parseVue(source, { filename, sourceMap: false,
|
|
@@ -176,14 +191,15 @@ function vueScripts(source, filename) {
|
|
|
176
191
|
};
|
|
177
192
|
walk(descriptor.template?.ast); return scripts;
|
|
178
193
|
}
|
|
179
|
-
function importSpecifiers(content, filename) {
|
|
194
|
+
function importSpecifiers(content, filename, repositoryRoot) {
|
|
180
195
|
try {
|
|
181
196
|
const source = content.toString('utf8'), parts = path.extname(filename).toLowerCase() === '.vue' ? vueScripts(source, filename) : [{ source, filename }];
|
|
182
|
-
const specifiers = new Set(), unresolvedImports = []; let dynamic = false;
|
|
183
|
-
for (const part of parts) { const parsed = scriptImports(part.source, part.filename, part); dynamic ||= parsed.dynamic;
|
|
197
|
+
const specifiers = new Set(), requires = new Map(), unresolvedImports = []; let dynamic = false;
|
|
198
|
+
for (const part of parts) { const parsed = scriptImports(part.source, part.filename, { ...part, repositoryRoot, sourceFilename: filename }); dynamic ||= parsed.dynamic;
|
|
184
199
|
unresolvedImports.push(...parsed.unresolvedImports.map(site => ({ ...site, line: site.line + (part.lineOffset ?? 0) })));
|
|
200
|
+
for (const require of parsed.requires) requires.set(`${require.anchor}\0${require.specifier}`, require);
|
|
185
201
|
for (const specifier of parsed.specifiers) specifiers.add(specifier); }
|
|
186
|
-
return { specifiers: [...specifiers].sort(), dynamic, unresolvedImports };
|
|
202
|
+
return { specifiers: [...specifiers].sort(), requires: [...requires.values()], dynamic, unresolvedImports };
|
|
187
203
|
} catch (error) {
|
|
188
204
|
if (typeof error?.code === 'string' && error.code.startsWith('ENTROROVE_')) throw error;
|
|
189
205
|
if (error instanceof RangeError) scopeFail('ENTROROVE_SCOPE_LIMIT', 'dependency syntax exceeds the supported parser or traversal bound');
|
|
@@ -280,7 +296,7 @@ export async function importGraph(root, originals, input = { repositoryRoot: roo
|
|
|
280
296
|
const aliases = await scopeTsconfigImports(target => configMetadata(root, target, input), relative, configPath);
|
|
281
297
|
for (const observed of aliases.observations) metadata.set(observed.path, observed);
|
|
282
298
|
const declarations = declarationModules(read.content, relative);
|
|
283
|
-
const imports = importSpecifiers(read.content, relative); dynamic ||= imports.dynamic;
|
|
299
|
+
const imports = importSpecifiers(read.content, relative, root); dynamic ||= imports.dynamic;
|
|
284
300
|
unresolvedImports.push(...imports.unresolvedImports.map(site => ({ path: relative, ...site })));
|
|
285
301
|
if (imports.dynamic) dynamicSources.push({ path: relative, sha256: read.sha256 });
|
|
286
302
|
for (const specifier of imports.specifiers.sort()) {
|
|
@@ -289,6 +305,12 @@ export async function importGraph(root, originals, input = { repositoryRoot: roo
|
|
|
289
305
|
...(declarations.has(specifier) ? { declarationOnly: true, declarationPath: relative } : {}) });
|
|
290
306
|
if (target !== null && !visited.has(target)) pending.push(target);
|
|
291
307
|
}
|
|
308
|
+
for (const request of imports.requires) {
|
|
309
|
+
const resolved = await resolveCreatedRequire(relative, request.anchor, request.specifier, target => configMetadata(root, target, input));
|
|
310
|
+
for (const file of resolved.metadata) metadata.set(file.path, file);
|
|
311
|
+
edges.push({ from: relative, to: resolved.external ? null : resolved.target, external: resolved.external, specifier: request.specifier });
|
|
312
|
+
if (!resolved.external && resolved.target !== null && !visited.has(resolved.target)) pending.push(resolved.target);
|
|
313
|
+
}
|
|
292
314
|
}
|
|
293
315
|
const nodes = [...visited.values()].sort((a, b) => a.path.localeCompare(b.path));
|
|
294
316
|
await stableScope(root, nodes, input);
|
|
@@ -299,7 +321,7 @@ export async function importGraph(root, originals, input = { repositoryRoot: roo
|
|
|
299
321
|
...(unresolvedImports.length ? { unresolvedImports } : {}) };
|
|
300
322
|
}
|
|
301
323
|
export async function scopeCandidateImports(root, file, input = { repositoryRoot: root }) {
|
|
302
|
-
const parsed = importSpecifiers(Buffer.from(file.content), file.path);
|
|
324
|
+
const parsed = importSpecifiers(Buffer.from(file.content), file.path, root);
|
|
303
325
|
const workspace = await workspaceIndex(root, input);
|
|
304
326
|
const context = (input.importContexts ?? []).filter(item => file.path === item.sourcePrefix || file.path.startsWith(`${item.sourcePrefix}/`))
|
|
305
327
|
.sort((a, b) => b.sourcePrefix.length - a.sourcePrefix.length)[0];
|
|
@@ -309,7 +331,12 @@ export async function scopeCandidateImports(root, file, input = { repositoryRoot
|
|
|
309
331
|
const target = await resolveImport(root, file.path, specifier, workspace, input, aliases, context?.configPath, declarations);
|
|
310
332
|
edges.push({ from: file.path, to: target, external: target === null, specifier });
|
|
311
333
|
}
|
|
312
|
-
|
|
334
|
+
for (const request of parsed.requires) {
|
|
335
|
+
const resolved = await resolveCreatedRequire(file.path, request.anchor, request.specifier, target => configMetadata(root, target, input));
|
|
336
|
+
await stableScope(root, resolved.metadata, input);
|
|
337
|
+
edges.push({ from: file.path, to: resolved.external ? null : resolved.target, external: resolved.external, specifier: request.specifier });
|
|
338
|
+
}
|
|
339
|
+
return { edges: edges.sort((a, b) => `${a.from}:${a.specifier}`.localeCompare(`${b.from}:${b.specifier}`)), dynamic: parsed.dynamic,
|
|
313
340
|
...(parsed.unresolvedImports.length ? { unresolvedImports: parsed.unresolvedImports.map(site => ({ path: file.path, ...site })) } : {}) };
|
|
314
341
|
}
|
|
315
342
|
export async function collectScopeFacts(root, requested, baselineRef, knownFiles = [], input = { repositoryRoot: root }) {
|