@starci/hfs 4.0.9 → 4.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +1 -1
- package/LICENSE +21 -0
- package/emit/operations.mjs +1 -1
- package/emit/type-schema.mjs +2 -1
- package/lint/run.mjs +82 -8
- package/package.json +3 -2
- package/report/order.mjs +2 -0
- package/runtime/config.example.yaml +187 -0
- package/runtime/engine/by-code-unit.mjs +12 -0
- package/runtime/engine/config.mjs +247 -273
- package/runtime/engine/invalid-config.mjs +5 -5
- package/runtime/engine/model-config.mjs +90 -0
- package/runtime/engine/orca-config.mjs +3 -1
- package/runtime/engine/removed-vocabulary.mjs +28 -0
- package/runtime/engine/resources-config.mjs +52 -0
- package/runtime/engine/runtime-root.mjs +17 -0
- package/runtime/engine/secrets.mjs +134 -0
- package/runtime/engine/sonar-config.mjs +21 -0
- package/runtime/engine/temp-root.mjs +32 -0
- package/runtime/knowledge/hfs/canon-pins.yaml +18 -18
- package/runtime/knowledge/hfs/peer-integrations.yaml +2 -3
- package/runtime/knowledge/hfs/rules.yaml +102 -69
- package/runtime/knowledge/hfs/slots.yaml +11 -14
- package/runtime/knowledge/patterns/be/api.yaml +7 -24
- package/runtime/knowledge/patterns/be/cli.yaml +3 -12
- package/runtime/knowledge/patterns/be/realtime.yaml +1 -14
- package/runtime/knowledge/patterns/be/webhooks.yaml +0 -19
- package/runtime/modules/kernel/failure-codes.yaml +1 -1
- package/runtime/modules/kernel/removed-vocabulary.yaml +168 -0
- package/runtime/modules/models/registry.yaml +25 -397
- package/runtime/modules/models/runtimes.yaml +92 -262
- package/runtime/modules/models/tiers.yaml +65 -0
- package/runtime/scripts/api/fs/claim-file.mjs +30 -0
- package/runtime/scripts/api/fs/ensure-temp-root.mjs +27 -0
- package/runtime/scripts/api/fs/forbidden-root.mjs +2 -1
- package/runtime/scripts/api/fs/lib.mjs +6 -0
- package/runtime/scripts/api/fs/make-temp-dir.mjs +11 -0
- package/runtime/scripts/api/fs/safe-remove.mjs +52 -35
- package/runtime/scripts/api/git/lib.mjs +36 -1
- package/runtime/scripts/api/process/resolve-real-tool.mjs +52 -0
- package/runtime/scripts/api/process/run-program.mjs +8 -0
- package/runtime/scripts/api/sops/decrypt.mjs +9 -16
- package/runtime/scripts/api/sops/lib.mjs +309 -11
- package/runtime/scripts/api/sops/seal.mjs +8 -4
- package/runtime/scripts/hfs/allows.mjs +3 -3
- package/runtime/scripts/hfs/architecture/ast-walks.mjs +63 -50
- package/runtime/scripts/hfs/architecture/automatic-gates.mjs +96 -0
- package/runtime/scripts/hfs/architecture/backend.mjs +261 -175
- package/runtime/scripts/hfs/architecture/background-unowned.mjs +69 -34
- package/runtime/scripts/hfs/architecture/client-reaches-server.mjs +55 -54
- package/runtime/scripts/hfs/architecture/clones.mjs +179 -106
- package/runtime/scripts/hfs/architecture/config-unread.mjs +5 -22
- package/runtime/scripts/hfs/architecture/config.mjs +95 -80
- package/runtime/scripts/hfs/architecture/connection-map.mjs +233 -141
- package/runtime/scripts/hfs/architecture/constructor-deps.mjs +15 -9
- package/runtime/scripts/hfs/architecture/context-coupling.mjs +76 -52
- package/runtime/scripts/hfs/architecture/context-map.mjs +215 -141
- package/runtime/scripts/hfs/architecture/context-owner.mjs +39 -30
- package/runtime/scripts/hfs/architecture/context-platform-tables.mjs +31 -17
- package/runtime/scripts/hfs/architecture/context-transaction.mjs +38 -25
- package/runtime/scripts/hfs/architecture/contract-fixture-guard.mjs +86 -40
- package/runtime/scripts/hfs/architecture/contracts-readonly.mjs +342 -0
- package/runtime/scripts/hfs/architecture/contracts.mjs +263 -455
- package/runtime/scripts/hfs/architecture/cross-app-duplicate.mjs +43 -34
- package/runtime/scripts/hfs/architecture/dead-exports.mjs +201 -154
- package/runtime/scripts/hfs/architecture/default-deny.mjs +118 -93
- package/runtime/scripts/hfs/architecture/doc-language.mjs +2 -1
- package/runtime/scripts/hfs/architecture/entrypoint.mjs +45 -30
- package/runtime/scripts/hfs/architecture/error-codes.mjs +19 -12
- package/runtime/scripts/hfs/architecture/error-masked.mjs +39 -26
- package/runtime/scripts/hfs/architecture/fe-slot-allows.mjs +3 -2
- package/runtime/scripts/hfs/architecture/feature-shape.mjs +26 -20
- package/runtime/scripts/hfs/architecture/framework-pinned.mjs +1 -1
- package/runtime/scripts/hfs/architecture/frontend-grammar.mjs +190 -0
- package/runtime/scripts/hfs/architecture/frontend-routing.mjs +186 -0
- package/runtime/scripts/hfs/architecture/frontend-world-render.mjs +311 -0
- package/runtime/scripts/hfs/architecture/frontend-world.mjs +328 -0
- package/runtime/scripts/hfs/architecture/frontend.mjs +109 -829
- package/runtime/scripts/hfs/architecture/hfs-graph.mjs +14 -2
- package/runtime/scripts/hfs/architecture/hfs.mjs +120 -288
- package/runtime/scripts/hfs/architecture/hooks-are-hooks.mjs +46 -26
- package/runtime/scripts/hfs/architecture/i18n-keys.mjs +114 -76
- package/runtime/scripts/hfs/architecture/index.mjs +158 -102
- package/runtime/scripts/hfs/architecture/injection-token-exported.mjs +33 -28
- package/runtime/scripts/hfs/architecture/machine-ast.mjs +180 -144
- package/runtime/scripts/hfs/architecture/managed-scripts.mjs +8 -2
- package/runtime/scripts/hfs/architecture/module-per-transport.mjs +150 -101
- package/runtime/scripts/hfs/architecture/next-data-contract.mjs +51 -43
- package/runtime/scripts/hfs/architecture/next-data-key.mjs +338 -0
- package/runtime/scripts/hfs/architecture/next-data.mjs +179 -427
- package/runtime/scripts/hfs/architecture/owners.mjs +27 -13
- package/runtime/scripts/hfs/architecture/package-shape.mjs +51 -23
- package/runtime/scripts/hfs/architecture/presentation.mjs +165 -0
- package/runtime/scripts/hfs/architecture/reachability.mjs +127 -61
- package/runtime/scripts/hfs/architecture/register-once.mjs +112 -63
- package/runtime/scripts/hfs/architecture/registration.mjs +187 -110
- package/runtime/scripts/hfs/architecture/required-files.mjs +100 -68
- package/runtime/scripts/hfs/architecture/route-files-thin.mjs +83 -58
- package/runtime/scripts/hfs/architecture/schema-owner.mjs +182 -108
- package/runtime/scripts/hfs/architecture/source-names-shape.mjs +318 -0
- package/runtime/scripts/hfs/architecture/source-names.mjs +83 -248
- package/runtime/scripts/hfs/architecture/sql-owner.mjs +175 -118
- package/runtime/scripts/hfs/architecture/sql-returning.mjs +39 -28
- package/runtime/scripts/hfs/architecture/sql-tokens.mjs +302 -172
- package/runtime/scripts/hfs/architecture/supabase-ast.mjs +1 -1
- package/runtime/scripts/hfs/architecture/supabase-be.mjs +55 -45
- package/runtime/scripts/hfs/architecture/supabase-results.mjs +182 -0
- package/runtime/scripts/hfs/architecture/supabase-tables.mjs +20 -12
- package/runtime/scripts/hfs/architecture/supabase.mjs +191 -263
- package/runtime/scripts/hfs/architecture/symbols.mjs +175 -99
- package/runtime/scripts/hfs/architecture/test-world-files.mjs +82 -41
- package/runtime/scripts/hfs/architecture/tiers.mjs +120 -69
- package/runtime/scripts/hfs/architecture/transport-owner.mjs +84 -57
- package/runtime/scripts/hfs/architecture/type-context.mjs +282 -0
- package/runtime/scripts/hfs/architecture/typescript.mjs +116 -276
- package/runtime/scripts/hfs/architecture/unit-spec-providers.mjs +84 -62
- package/runtime/scripts/hfs/architecture.mjs +3 -4
- package/runtime/scripts/hfs/check.mjs +61 -70
- package/runtime/scripts/hfs/coverage-scope.mjs +84 -38
- package/runtime/scripts/hfs/declaration-shape.mjs +77 -36
- package/runtime/scripts/hfs/declaration-slots.mjs +1 -1
- package/runtime/scripts/hfs/edition-slots.mjs +5 -3
- package/runtime/scripts/hfs/linear-text.mjs +30 -0
- package/runtime/scripts/hfs/manifest-shape.mjs +164 -63
- package/runtime/scripts/hfs/path-findings.mjs +58 -38
- package/runtime/scripts/hfs/pin-findings.mjs +31 -0
- package/runtime/scripts/hfs/repo-identity.mjs +5 -2
- package/runtime/scripts/hfs/rule-catalog.mjs +194 -0
- package/runtime/scripts/hfs/rule-params-shape.mjs +33 -27
- package/runtime/scripts/hfs/rules/cli.mjs +5 -1
- package/runtime/scripts/hfs/rules/contract-compat.mjs +40 -22
- package/runtime/scripts/hfs/rules/contract.mjs +35 -27
- package/runtime/scripts/hfs/rules/database-config.mjs +57 -32
- package/runtime/scripts/hfs/rules/database-migrations.mjs +52 -31
- package/runtime/scripts/hfs/rules/database-plpgsql.mjs +106 -91
- package/runtime/scripts/hfs/rules/database-sql.mjs +254 -173
- package/runtime/scripts/hfs/rules/database.mjs +36 -23
- package/runtime/scripts/hfs/rules/deps.mjs +64 -32
- package/runtime/scripts/hfs/rules/docker.mjs +113 -65
- package/runtime/scripts/hfs/rules/edition.mjs +118 -111
- package/runtime/scripts/hfs/rules/event-bus.mjs +91 -45
- package/runtime/scripts/hfs/rules/fe-contract-documents.mjs +17 -16
- package/runtime/scripts/hfs/rules/fe-no-tests.mjs +36 -16
- package/runtime/scripts/hfs/rules/frontend-tree.mjs +4 -3
- package/runtime/scripts/hfs/rules/integration-specs.mjs +75 -41
- package/runtime/scripts/hfs/rules/kinds.mjs +59 -25
- package/runtime/scripts/hfs/rules/lint-suppression.mjs +9 -3
- package/runtime/scripts/hfs/rules/monorepo.mjs +82 -45
- package/runtime/scripts/hfs/rules/peer-integrations.mjs +24 -3
- package/runtime/scripts/hfs/rules/pipeline.mjs +58 -9
- package/runtime/scripts/hfs/rules/proof-commands.mjs +14 -8
- package/runtime/scripts/hfs/rules/repo-local-checks.mjs +11 -6
- package/runtime/scripts/hfs/rules/saga.mjs +128 -61
- package/runtime/scripts/hfs/rules/secrets.mjs +4 -3
- package/runtime/scripts/hfs/rules/services.mjs +34 -16
- package/runtime/scripts/hfs/rules/stacks.mjs +21 -14
- package/runtime/scripts/hfs/rules/supabase-secrets.mjs +32 -23
- package/runtime/scripts/hfs/rules/test-topology.mjs +25 -11
- package/runtime/scripts/hfs/secret.mjs +90 -45
- package/runtime/scripts/hfs/slot-app-view.mjs +102 -0
- package/runtime/scripts/hfs/slot-classify.mjs +65 -0
- package/runtime/scripts/hfs/slot-errors.mjs +11 -0
- package/runtime/scripts/hfs/slot-imports.mjs +76 -0
- package/runtime/scripts/hfs/slot-manifest-shape.mjs +73 -0
- package/runtime/scripts/hfs/slot-match.mjs +153 -0
- package/runtime/scripts/hfs/slot-path.mjs +8 -0
- package/runtime/scripts/hfs/slot-required.mjs +76 -0
- package/runtime/scripts/hfs/slot-semantic-problems.mjs +148 -0
- package/runtime/scripts/hfs/slot-side-problems.mjs +28 -0
- package/runtime/scripts/hfs/slots.mjs +72 -693
- package/runtime/scripts/hfs/sql/pg-parse.mjs +5 -2
- package/runtime/scripts/hfs/trailing-slashes.mjs +8 -0
- package/runtime/scripts/hfs/tree.mjs +19 -14
- package/runtime/scripts/hfs/typescript-programs.mjs +6 -4
- package/runtime/scripts/hfs/view.mjs +1 -1
- package/runtime/scripts/lib/dockerfile.mjs +56 -33
- package/runtime/scripts/lib/event-contract.mjs +17 -13
- package/runtime/scripts/lib/fs-kind.mjs +14 -4
- package/runtime/scripts/lib/git.mjs +2 -2
- package/runtime/scripts/lib/graphql-contract.mjs +158 -322
- package/runtime/scripts/lib/graphql-sdl.mjs +262 -0
- package/runtime/scripts/lib/i18n.mjs +3 -2
- package/runtime/scripts/lib/in-order.mjs +71 -0
- package/runtime/scripts/lib/language.mjs +3 -3
- package/runtime/scripts/lib/list.mjs +8 -1
- package/runtime/scripts/lib/mutation-fence.mjs +15 -0
- package/runtime/scripts/lib/path-key.mjs +42 -9
- package/runtime/scripts/lib/pid-alive.mjs +7 -0
- package/runtime/scripts/lib/regex.mjs +1 -1
- package/runtime/scripts/lib/same-text.mjs +1 -1
- package/runtime/scripts/lib/secret-patterns.mjs +6 -2
- package/runtime/scripts/lib/sleep-sync.mjs +2 -2
- package/runtime/scripts/lib/sops-envelope.mjs +103 -2
- package/runtime/scripts/lib/stack-services.mjs +1 -2
- package/runtime/scripts/lib/ts-ast.mjs +9 -0
- package/runtime/scripts/lib/walk.mjs +6 -1
- package/scaffold/add-cli-lite.mjs +2 -1
- package/scaffold/add-table.mjs +2 -2
- package/scaffold/add.mjs +3 -2
- package/scaffold/app.mjs +4 -2
- package/scaffold/edition-gate.mjs +25 -25
- package/scaffold/lite-exports.mjs +2 -1
- package/scaffold/service.mjs +5 -4
- package/sync/hygiene.mjs +15 -9
- package/sync/index.mjs +7 -3
- package/templates/app/ci-workflows/github/workflows/ci.yml +4 -4
- package/templates/app/ci-workflows/github/workflows/e2e.yml +3 -3
- package/templates/app/ci-workflows/github/workflows/images.yml +3 -3
- package/templates/app/ci-workflows/sonar-steps.yml +2 -2
- package/templates/app/ci-workflows-lite/github/workflows/ci.yml +6 -6
- package/templates/app/ci-workflows-lite/github/workflows/db-deploy.yml +3 -3
- package/templates/app/ci-workflows-lite/github/workflows/images.yml +3 -3
- package/templates/app/starciwork.gitignore +1 -1
- package/templates/be/image/api/Dockerfile +1 -1
- package/templates/be/image/cli/Dockerfile +1 -1
- package/templates/be/image/worker/Dockerfile +1 -1
- package/templates/be/patterns/cli/group.cli.spec.ts.tpl +1 -1
- package/templates/be/patterns/cli/group.cli.ts.tpl +2 -2
- package/templates/be/patterns/event-bus/platform/event-runner.service.spec.ts.tpl +13 -0
- package/templates/be/patterns/event-bus/platform/event-runner.service.ts.tpl +7 -2
- package/templates/be/patterns/event-bus/platform/event.policy.ts.tpl +4 -1
- package/templates/be/patterns/event-bus/platform/kafka-event-transport.client.ts.tpl +2 -1
- package/templates/be/patterns/outbox/platform/outbox-relay.policy.ts.tpl +24 -9
- package/templates/be/patterns/queues/platform/queue-relay.service.ts.tpl +3 -2
- package/templates/be/patterns/queues/platform/queue-worker.service.ts.tpl +8 -5
- package/templates/be/skeleton/src/features/cli/migrate/migrate.cli.spec.ts +1 -1
- package/templates/be/skeleton/src/features/cli/migrate/migrate.cli.ts +2 -2
- package/templates/be/skeleton/src/features/cli/seed/seed.cli.spec.ts +1 -1
- package/templates/be/skeleton/src/features/cli/seed/seed.cli.ts +2 -2
- package/templates/be/skeleton/src/modules/platform/database/migrate-connections.client.ts +3 -2
- package/templates/be/skeleton/src/modules/platform/database/seed-connections.client.ts +5 -4
- package/templates/be/skeleton/src/modules/platform/primitives/index.ts +1 -0
- package/templates/be/skeleton/src/modules/platform/primitives/sequence.policy.spec.ts +47 -0
- package/templates/be/skeleton/src/modules/platform/primitives/sequence.policy.ts +16 -0
- package/templates/be/skeleton-lite/apps/api/Dockerfile +1 -0
- package/templates/be/skeleton-lite/src/modules/platform/primitives/index.ts +2 -0
- package/templates/fe/image/next/Dockerfile +2 -1
- package/templates/fe/skeleton/apps/app/src/app/[locale]/page.tsx +2 -2
- package/templates/fe/skeleton/apps/landing/src/app/[locale]/page.tsx +2 -2
- package/templates/fe/skeleton/packages/__project__-i18n/src/index.ts +1 -2
- package/templates/fe/skeleton-lite/apps/web/src/app/[locale]/layout.tsx +1 -2
- package/templates/fe/skeleton-lite/apps/web/src/app/[locale]/page.tsx +2 -2
- package/templates/fe/skeleton-lite/apps/web/src/components/blocks/SignInForm/index.tsx +7 -1
- package/templates/fe/skeleton-lite/apps/web/src/modules/db/auth/write-sign-out.ts +1 -1
- package/templates/fe/skeleton-lite/apps/web/src/modules/db/validation/validation.mapper.ts +10 -2
- package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/request.ts +10 -3
- package/upgrade/index.mjs +9 -2
|
@@ -1,9 +1,12 @@
|
|
|
1
|
-
// scripts/api/sops/lib.mjs —
|
|
1
|
+
// scripts/api/sops/lib.mjs — shared file formats, identity admission and SOPS child capture.
|
|
2
2
|
// Custody members are committed as `<name>.<fmt>.enc`; sops infers a format from the LAST extension, so `.enc` reads as
|
|
3
3
|
// binary and every read states the format (--input-type). The call files (decrypt.mjs, encrypt.mjs, exec-env.mjs) each
|
|
4
4
|
// name one sops use.
|
|
5
5
|
import fs from 'node:fs';
|
|
6
6
|
import path from 'node:path';
|
|
7
|
+
import { spawnSync } from 'node:child_process';
|
|
8
|
+
import { readSecretBytes, CREDENTIAL_FILE_MAX_BYTES, sopsIdentityEnv } from '../../../engine/secrets.mjs';
|
|
9
|
+
import { selectedAgeEnvelope } from '../../lib/sops-envelope.mjs';
|
|
7
10
|
|
|
8
11
|
const FORMATS = { yaml: 'yaml', yml: 'yaml', json: 'json', env: 'dotenv', dotenv: 'dotenv' };
|
|
9
12
|
|
|
@@ -19,17 +22,312 @@ export function custodyInputType(file, override) {
|
|
|
19
22
|
return type;
|
|
20
23
|
}
|
|
21
24
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
25
|
+
const sopsNames = (env, win, pathext) => {
|
|
26
|
+
if (!win) return ['sops'];
|
|
27
|
+
if (!pathext) return ['sops.exe', 'sops'];
|
|
28
|
+
return String(env.PATHEXT || '.EXE;.CMD;.BAT').split(';').filter(Boolean).map((ext) => `sops${ext}`);
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const nestedPackageDirs = (filesystem, paths, directory) => {
|
|
32
|
+
const dirs = [];
|
|
33
|
+
try {
|
|
34
|
+
for (const nested of filesystem.readdirSync(directory, { withFileTypes: true })) {
|
|
35
|
+
if (nested.isDirectory()) dirs.push(paths.join(directory, nested.name));
|
|
36
|
+
}
|
|
37
|
+
} catch { /* unreadable package */ }
|
|
38
|
+
return dirs;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const wingetPackageDirs = (filesystem, paths, packages, wingetPackageTree) => {
|
|
42
|
+
const dirs = [];
|
|
43
|
+
try {
|
|
44
|
+
for (const entry of filesystem.readdirSync(packages)) {
|
|
45
|
+
if (!wingetPackageTree && !/sops/i.test(entry)) continue;
|
|
46
|
+
const directory = paths.join(packages, entry);
|
|
47
|
+
dirs.push(directory);
|
|
48
|
+
if (wingetPackageTree) dirs.push(...nestedPackageDirs(filesystem, paths, directory));
|
|
49
|
+
}
|
|
50
|
+
} catch { /* no WinGet packages */ }
|
|
51
|
+
return dirs;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const wingetDirs = (filesystem, paths, env, wingetPackageTree) => {
|
|
55
|
+
const dirs = [];
|
|
56
|
+
if (!env.LOCALAPPDATA) return dirs;
|
|
57
|
+
const winget = paths.join(env.LOCALAPPDATA, 'Microsoft', 'WinGet');
|
|
58
|
+
dirs.push(paths.join(winget, 'Links'), ...wingetPackageDirs(filesystem, paths, paths.join(winget, 'Packages'), wingetPackageTree));
|
|
59
|
+
return dirs;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
/** Resolve SOPS from PATH and WinGet. Consumers select PATHEXT names and the full package tree for their launcher. */
|
|
63
|
+
export function resolveSops(env = process.env, { platform = process.platform, pathext = false, wingetPackageTree = false, filesystem = fs } = {}) {
|
|
64
|
+
const win = platform === 'win32';
|
|
65
|
+
const paths = win ? path.win32 : path.posix;
|
|
66
|
+
const names = sopsNames(env, win, pathext);
|
|
26
67
|
const dirs = String(env.PATH ?? '').split(win ? ';' : ':').filter(Boolean);
|
|
27
|
-
if (win
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
try { for (const e of fs.readdirSync(packages)) if (/sops/i.test(e)) dirs.push(path.join(packages, e)); } catch { /* none */ }
|
|
68
|
+
if (win) dirs.push(...wingetDirs(filesystem, paths, env, wingetPackageTree));
|
|
69
|
+
for (const directory of dirs) for (const name of names) {
|
|
70
|
+
const file = paths.join(directory, name);
|
|
71
|
+
try { if (filesystem.statSync(file).isFile()) return file; } catch { /* next candidate */ }
|
|
32
72
|
}
|
|
33
|
-
for (const dir of dirs) for (const n of names) { const f = path.join(dir, n); try { if (fs.statSync(f).isFile()) return f; } catch { /* next */ } }
|
|
34
73
|
return null;
|
|
35
74
|
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Select the caller's identity once, then run the encrypt/decrypt file action through its admitted SOPS path.
|
|
78
|
+
* Inline selection delegates the unchanged request to runSelectedSops; original FILE and recipient-only modes retain their existing child transport.
|
|
79
|
+
* @param {string|null} bin Explicit SOPS executable, or null to resolve it from the selected environment.
|
|
80
|
+
* @param {string[]} args The caller's SOPS argument array; no private identity is added to it.
|
|
81
|
+
* @param {'encrypt'|'decrypt'} operation The public file action, which owns identity requirements and capture defaults.
|
|
82
|
+
* @param {object} options Original identity, selected invocation ports, environment, cwd, timeout and maxBuffer.
|
|
83
|
+
* @returns {{status: number|null, stdout: string|null, stderr: string|null, error: Error|null}} The unchanged child result or typed prelaunch refusal.
|
|
84
|
+
*/
|
|
85
|
+
export function runSopsFile(bin, args, operation, options = {}) {
|
|
86
|
+
const { identity = null, invocation = null, env = process.env, cwd = undefined } = options;
|
|
87
|
+
let maxBuffer, timeout;
|
|
88
|
+
if (operation === 'decrypt') ({ maxBuffer = 16 * 1024 * 1024, timeout = undefined } = options);
|
|
89
|
+
else ({ timeout = undefined, maxBuffer = 1024 * 1024 } = options);
|
|
90
|
+
const selected = sopsIdentityEnv(env, operation === 'decrypt' ? { identity, required: true } : { identity });
|
|
91
|
+
if (selected.error?.identityRefusal === 'inline-context-unqualified') return runSelectedSops(bin, { operation, args }, { selection: selected, invocation, env, cwd, maxBuffer, timeout });
|
|
92
|
+
if (selected.error) return { status: null, stdout: '', stderr: '', error: selected.error };
|
|
93
|
+
const exe = bin ?? resolveSops(selected.env);
|
|
94
|
+
if (!exe) return { status: null, stdout: '', stderr: '', error: Object.assign(new Error('sops is not installed (Windows: winget install Mozilla.SOPS)'), { code: 'SOPS_MISSING' }) };
|
|
95
|
+
const r = spawnSync(exe, args, {
|
|
96
|
+
encoding: 'utf8', windowsHide: true, maxBuffer, timeout, env: selected.env,
|
|
97
|
+
...(operation === 'decrypt' ? { cwd } : { stdio: ['ignore', 'pipe', 'pipe'] }),
|
|
98
|
+
});
|
|
99
|
+
return { status: r.status, stdout: r.stdout, stderr: r.stderr, error: r.error ?? null };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const SELECTED_SOPS_VERSIONS = new Set(['3.13.2', '3.13.3']);
|
|
103
|
+
export const SELECTED_AGE_VERSIONS = new Set(['1.2.1', '1.3.1']);
|
|
104
|
+
const IDENTITY_TIMEOUT_MS = 20_000;
|
|
105
|
+
const CONTROLLED_ENV = new Set(['SOPS_AGE_KEY', 'SOPS_AGE_KEY_FILE', 'SOPS_AGE_KEY_CMD', 'SOPS_AGE_SSH_PRIVATE_KEY_FILE', 'SOPS_AGE_SSH_PRIVATE_KEY_CMD', 'SOPS_AGE_RECIPIENT', 'SOPS_KEYSERVICE', 'SOPS_ENABLE_LOCAL_KEYSERVICE', 'SOPS_DECRYPTION_ORDER', 'SOPS_KMS_ARN', 'SOPS_GCP_KMS_IDS', 'SOPS_HUAWEICLOUD_KMS_IDS', 'SOPS_AZURE_KEYVAULT_URLS', 'SOPS_VAULT_URIS', 'SOPS_PGP_FP', 'SOPS_AGE_RECIPIENTS', 'HOME', 'USERPROFILE', 'APPDATA', 'XDG_CONFIG_HOME']);
|
|
106
|
+
|
|
107
|
+
/** Construct only the child map; empty desktop roots make Go return directory errors, not fallback paths. */
|
|
108
|
+
export function isolatedSopsEnv(env, inlineName = 'SOPS_AGE_KEY') {
|
|
109
|
+
const out = Object.fromEntries(Object.entries(env).filter(([name]) => !CONTROLLED_ENV.has(name.toUpperCase())));
|
|
110
|
+
out.HOME = ''; out.USERPROFILE = ''; out.APPDATA = ''; out.XDG_CONFIG_HOME = '';
|
|
111
|
+
if (inlineName !== null) out.SOPS_AGE_KEY = env[inlineName];
|
|
112
|
+
return out;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const held = reason => ({ status: null, stdout: '', stderr: '', error: Object.assign(new Error(`SOPS selected identity [${reason}]: the selected invocation was not qualified`), { name: 'SopsIdentityRefusal', identityRefusal: reason }) });
|
|
116
|
+
const nativeFile = file => {
|
|
117
|
+
if (typeof file !== 'string' || !path.isAbsolute(file) || /\.(?:[cm]?js|cmd|bat|ps1|sh)$/i.test(file)) return false;
|
|
118
|
+
let fd;
|
|
119
|
+
try {
|
|
120
|
+
const before = fs.lstatSync(file);
|
|
121
|
+
if (!before.isFile() || before.isSymbolicLink()) return false;
|
|
122
|
+
fd = fs.openSync(file, fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW ?? 0));
|
|
123
|
+
const stat = fs.fstatSync(fd), head = Buffer.alloc(64);
|
|
124
|
+
if (!stat.isFile() || before.dev !== stat.dev || before.ino !== stat.ino || fs.readSync(fd, head, 0, head.length, 0) < 20) return false;
|
|
125
|
+
if (process.platform === 'win32' && head.toString('ascii', 0, 2) === 'MZ') {
|
|
126
|
+
const offset = head.readUInt32LE(60), pe = Buffer.alloc(6);
|
|
127
|
+
return offset >= 64 && offset + pe.length <= stat.size && fs.readSync(fd, pe, 0, pe.length, offset) === pe.length && pe.toString('ascii', 0, 4) === 'PE\0\0';
|
|
128
|
+
}
|
|
129
|
+
if (process.platform === 'linux') return head.subarray(0, 4).equals(Buffer.from([0x7f, 0x45, 0x4c, 0x46]));
|
|
130
|
+
if (process.platform === 'darwin') return new Set([0xfeedface, 0xfeedfacf, 0xcefaedfe, 0xcffaedfe, 0xcafebabe, 0xbebafeca, 0xcafebabf, 0xbfbafeca]).has(head.readUInt32BE(0));
|
|
131
|
+
return false;
|
|
132
|
+
} catch { return false; }
|
|
133
|
+
finally { if (fd !== undefined) { try { fs.closeSync(fd); } catch { /* no private result or success credit */ } } }
|
|
134
|
+
};
|
|
135
|
+
const auditAbsent = cwd => {
|
|
136
|
+
try { fs.lstatSync(path.resolve(cwd ?? process.cwd(), '/etc/sops/audit.yaml')); return false; }
|
|
137
|
+
catch (error) { return error.code === 'ENOENT'; }
|
|
138
|
+
};
|
|
139
|
+
const captured = r => r?.status === 0 && r.signal == null && !r.error && Buffer.isBuffer(r.stdout) && Buffer.isBuffer(r.stderr);
|
|
140
|
+
const decoded = bytes => new TextDecoder('utf-8', { fatal: true }).decode(bytes);
|
|
141
|
+
const ownedRequest = request => {
|
|
142
|
+
if (request.operation === 'seal') {
|
|
143
|
+
const p = request.params;
|
|
144
|
+
if (!p || !Array.isArray(p.recipients) || p.recipients.length !== 1 || typeof p.filenameOverride !== 'string' || !p.filenameOverride || (typeof p.plaintext !== 'string' && !Buffer.isBuffer(p.plaintext)) || Buffer.byteLength(p.plaintext) > CREDENTIAL_FILE_MAX_BYTES) return null;
|
|
145
|
+
return { inputType: p.inputType, outputType: p.inputType, file: p.filenameOverride, recipient: p.recipients[0], plaintext: p.plaintext };
|
|
146
|
+
}
|
|
147
|
+
const args = request.args;
|
|
148
|
+
if (!Array.isArray(args) || !['decrypt', '--decrypt', 'encrypt', '--encrypt'].includes(args[0]) || args.some(value => typeof value !== 'string')) return null;
|
|
149
|
+
if (!args.at(-1) || args.at(-1).startsWith('-')) return null;
|
|
150
|
+
const flags = {};
|
|
151
|
+
for (let i = 1; i < args.length - 1; i += 2) {
|
|
152
|
+
const flag = args[i];
|
|
153
|
+
if (!['--input-type', '--output-type', '--age'].includes(flag) || Object.hasOwn(flags, flag) || i + 1 >= args.length - 1) return null;
|
|
154
|
+
flags[flag] = args[i + 1];
|
|
155
|
+
}
|
|
156
|
+
if (request.operation === 'decrypt' && (args[0] !== 'decrypt' && args[0] !== '--decrypt' || Object.hasOwn(flags, '--age'))) return null;
|
|
157
|
+
if (request.operation === 'encrypt' && (args[0] !== 'encrypt' && args[0] !== '--encrypt' || !flags['--age'] || flags['--age'].includes(','))) return null;
|
|
158
|
+
return { inputType: flags['--input-type'], outputType: flags['--output-type'], file: args.at(-1), recipient: flags['--age'] };
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
/** The admitted identity key, or the unchanged refusal/failure result (admitted.key is absent then). */
|
|
162
|
+
const selectedAdmission = (selection, spec, invocation, env) => {
|
|
163
|
+
const fail = () => ({ status: null, stdout: '', stderr: '', error: selection.error });
|
|
164
|
+
if (!spec || !['binary', 'json', 'yaml', 'dotenv'].includes(spec.inputType) || !['binary', 'json', 'yaml', 'dotenv'].includes(spec.outputType)) return fail();
|
|
165
|
+
if (!invocation || typeof invocation.runProgram !== 'function' || typeof invocation.resolveRealTool !== 'function') return fail();
|
|
166
|
+
if (!['win32', 'linux', 'darwin'].includes(process.platform)) return held('unsupported-platform');
|
|
167
|
+
const key = env[selection.inlineName];
|
|
168
|
+
if (typeof key !== 'string' || !/^AGE-SECRET-KEY-1[0-9A-Z]+$/.test(key.trim())) return fail();
|
|
169
|
+
return { key };
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
const decryptArgs = (file, inputType, outputType) => ['decrypt', '--input-type', inputType, '--output-type', outputType, '--filename-override', file, '--enable-local-keyservice=true', '--decryption-order', 'age'];
|
|
173
|
+
|
|
174
|
+
const decryptSelected = (run, exe, file, input, spec, recipient) => {
|
|
175
|
+
const envelope = selectedAgeEnvelope(decoded(input), spec.inputType, recipient);
|
|
176
|
+
if (!envelope.ok) return held(envelope.reason);
|
|
177
|
+
const result = run(exe, decryptArgs(file, spec.inputType, spec.outputType), input);
|
|
178
|
+
if (!captured(result)) return held('selected-decryption-failed');
|
|
179
|
+
return { status: 0, stdout: decoded(result.stdout), stderr: '', error: null };
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
const encryptSelected = (run, exe, file, input, spec, recipient) => {
|
|
183
|
+
if (spec.recipient !== recipient) return held('recipient-mismatch');
|
|
184
|
+
const args = ['encrypt', '--input-type', spec.inputType, '--output-type', spec.outputType, '--filename-override', file, '--enable-local-keyservice=true', '--age', recipient];
|
|
185
|
+
const result = run(exe, args, input);
|
|
186
|
+
if (!captured(result)) return held('selected-encryption-failed');
|
|
187
|
+
if (result.stdout.length > CREDENTIAL_FILE_MAX_BYTES) return held('document-over-budget');
|
|
188
|
+
const text = decoded(result.stdout);
|
|
189
|
+
const envelope = selectedAgeEnvelope(text, spec.outputType, recipient);
|
|
190
|
+
if (!envelope.ok) return held(envelope.reason);
|
|
191
|
+
const readback = run(exe, decryptArgs(file, spec.outputType, spec.inputType), result.stdout);
|
|
192
|
+
if (!captured(readback)) return held('selected-readback-failed');
|
|
193
|
+
return { status: 0, stdout: text, stderr: '', error: null };
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
const wipeCaptures = (captures) => {
|
|
197
|
+
for (const result of captures) {
|
|
198
|
+
if (Buffer.isBuffer(result?.stdout)) result.stdout.fill(0);
|
|
199
|
+
if (Buffer.isBuffer(result?.stderr)) result.stderr.fill(0);
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
// The one recipient an `age -y` capture names, or null.
|
|
204
|
+
const recipientOf = (stdout) => {
|
|
205
|
+
const recipients = decoded(stdout).trim().split(/\r?\n/);
|
|
206
|
+
return recipients.length === 1 && /^age1[ac-hj-np-z02-9]+$/.test(recipients[0]) ? recipients[0] : null;
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
const toolProfileSupported = (version, ageVersion) => captured(version) && captured(ageVersion)
|
|
210
|
+
&& SELECTED_SOPS_VERSIONS.has(/^sops\s+(\d+\.\d+\.\d+)/.exec(decoded(version.stdout))?.[1])
|
|
211
|
+
&& SELECTED_AGE_VERSIONS.has(/^v?(\d+\.\d+\.\d+)/.exec(decoded(ageVersion.stdout))?.[1]);
|
|
212
|
+
|
|
213
|
+
const invalidCaptureBudget = (deadline, maxBuffer) => !Number.isInteger(deadline) || deadline < 1 || deadline > 2_147_483_647 || !Number.isInteger(maxBuffer) || maxBuffer < 1;
|
|
214
|
+
|
|
215
|
+
// The document bytes into `state.input`; the refusal when they are unavailable or over budget, else null.
|
|
216
|
+
const readSelectedInput = (state, request, spec, file) => {
|
|
217
|
+
try { state.input = request.operation === 'seal' ? Buffer.from(spec.plaintext) : readSecretBytes(file); } catch { return held('actual-document-unavailable'); }
|
|
218
|
+
return state.input.length > CREDENTIAL_FILE_MAX_BYTES ? held('document-over-budget') : null;
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
const selectedRunner = (invocation, options, captures, selectedEnv) => (program, args, bytes, childEnv = selectedEnv) => {
|
|
222
|
+
const result = invocation.runProgram(program, args, { ...options, env: childEnv, input: bytes });
|
|
223
|
+
captures.push(result); return result;
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
const performSelected = (state, { bin, request, spec, key, selection, invocation, env, cwd, maxBuffer, timeout }) => {
|
|
227
|
+
const file = path.resolve(cwd ?? process.cwd(), spec.file);
|
|
228
|
+
const unavailable = readSelectedInput(state, request, spec, file);
|
|
229
|
+
if (unavailable) return unavailable;
|
|
230
|
+
const exe = bin ?? resolveSops(env), age = invocation.resolveRealTool('age-keygen', { env });
|
|
231
|
+
if (!nativeFile(exe) || !nativeFile(age)) return held('native-tool-unavailable');
|
|
232
|
+
if (!auditAbsent(cwd)) return held('unsupported-audit-context');
|
|
233
|
+
const deadline = timeout ?? IDENTITY_TIMEOUT_MS;
|
|
234
|
+
if (invalidCaptureBudget(deadline, maxBuffer)) return held('invalid-capture-budget');
|
|
235
|
+
state.selectedEnv = isolatedSopsEnv(env, selection.inlineName);
|
|
236
|
+
const publicEnv = isolatedSopsEnv(env, null);
|
|
237
|
+
const options = { cwd, env: state.selectedEnv, shell: false, encoding: 'buffer', windowsHide: true, stdio: ['pipe', 'pipe', 'pipe'], timeout: deadline, maxBuffer };
|
|
238
|
+
const run = selectedRunner(invocation, options, state.captures, state.selectedEnv);
|
|
239
|
+
const version = run(exe, ['--disable-version-check', '--version'], undefined, publicEnv);
|
|
240
|
+
const ageVersion = run(age, ['--version'], undefined, publicEnv);
|
|
241
|
+
if (!toolProfileSupported(version, ageVersion)) return held('unsupported-tool-profile');
|
|
242
|
+
state.identity = Buffer.from(key.trim(), 'utf8');
|
|
243
|
+
const derived = run(age, ['-y'], state.identity, publicEnv);
|
|
244
|
+
if (!captured(derived)) return held('recipient-unproven');
|
|
245
|
+
const recipient = recipientOf(derived.stdout);
|
|
246
|
+
if (recipient === null) return held('recipient-unproven');
|
|
247
|
+
if (request.operation === 'decrypt') return decryptSelected(run, exe, file, state.input, spec, recipient);
|
|
248
|
+
return encryptSelected(run, exe, file, state.input, spec, recipient);
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
/** The domain composes the existing process owner: invocation={runProgram(file,args,options),resolveRealTool(program,{env})}. No API imports another system. */
|
|
252
|
+
export function runSelectedSops(bin, request, { selection, invocation, env, cwd, maxBuffer, timeout } = {}) {
|
|
253
|
+
if (selection?.error?.identityRefusal !== 'inline-context-unqualified' || typeof selection.inlineName !== 'string' || !env || typeof env !== 'object') return held('selected-context-missing');
|
|
254
|
+
const spec = ownedRequest(request);
|
|
255
|
+
const admitted = selectedAdmission(selection, spec, invocation, env);
|
|
256
|
+
if (!admitted.key) return admitted;
|
|
257
|
+
const state = { input: undefined, identity: undefined, selectedEnv: undefined, captures: [] };
|
|
258
|
+
try {
|
|
259
|
+
return performSelected(state, { bin, request, spec, key: admitted.key, selection, invocation, env, cwd, maxBuffer, timeout });
|
|
260
|
+
} catch { return held('selected-native-refused'); }
|
|
261
|
+
finally {
|
|
262
|
+
state.identity?.fill(0); state.input?.fill(0);
|
|
263
|
+
wipeCaptures(state.captures);
|
|
264
|
+
if (state.selectedEnv) delete state.selectedEnv.SOPS_AGE_KEY;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const identityRequestValid = ({ env, cwd, invocation, assertLease, consume }) =>
|
|
269
|
+
env && typeof env === 'object' && path.isAbsolute(cwd ?? '') && typeof consume === 'function'
|
|
270
|
+
&& typeof assertLease === 'function' && typeof invocation?.runProgram === 'function'
|
|
271
|
+
&& typeof invocation?.resolveRealTool === 'function';
|
|
272
|
+
const identityLinesValid = (lines, identities) => identities.length === 1 && /^AGE-SECRET-KEY-1[0-9A-Z]+$/.test(identities[0])
|
|
273
|
+
&& !lines.some(line => line.startsWith('#') && !/^# (?:created:|public key:)/.test(line));
|
|
274
|
+
const declaredRecipientMatches = (lines, recipient) => {
|
|
275
|
+
const declared = lines.filter(line => line.startsWith('# public key:'));
|
|
276
|
+
return declared.length === 1 && declared[0].slice('# public key:'.length).trim() === recipient;
|
|
277
|
+
};
|
|
278
|
+
const publicationShaped = (publication) => publication && typeof publication.ok === 'boolean' && ['none', 'unknown', 'complete'].includes(publication.effectState);
|
|
279
|
+
const publicationDurable = (publication) => [true, false].includes(publication.created) && ['file-fsync', 'file-and-parent-fsync', 'file-fsync-namespace-unqualified'].includes(publication.durability);
|
|
280
|
+
|
|
281
|
+
const identityFailure = (reason, generated) => ({ ok: false, reason, captureState: generated ? 'unknown' : 'none', effectState: generated ? 'unknown' : 'none' });
|
|
282
|
+
|
|
283
|
+
// The outcome of handing the captured identity to the installer's publication.
|
|
284
|
+
const publicationOutcome = (publication, recipient) => {
|
|
285
|
+
if (!publicationShaped(publication)) return identityFailure('publication-unknown', true);
|
|
286
|
+
if (!publication.ok || publication.effectState !== 'complete') return { ok: false, captureState: 'generated', effectState: publication.effectState,
|
|
287
|
+
reason: 'publication-held' };
|
|
288
|
+
if (!publicationDurable(publication)) return identityFailure('publication-unknown', true);
|
|
289
|
+
return { ok: true, captureState: 'generated', effectState: 'complete', created: publication.created,
|
|
290
|
+
durability: publication.durability, publicRecipient: recipient };
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
// One age-keygen run under the caller's lease, its capture kept for wiping.
|
|
294
|
+
const leasedAgeRunner = (invocation, age, options, captures, assertLease) => (args, input) => {
|
|
295
|
+
if (assertLease() !== true) throw new Error('initial age lease lost');
|
|
296
|
+
const result = invocation.runProgram(age, args, { ...options, input });
|
|
297
|
+
captures.push(result);
|
|
298
|
+
if (!captured(result) || result.stdout.length > options.maxBuffer || result.stderr.length > options.maxBuffer) throw new Error('initial age capture incomplete');
|
|
299
|
+
return result;
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
const generateAndPublish = (state, { env, cwd, invocation, assertLease, consume }) => {
|
|
303
|
+
const age = invocation.resolveRealTool('age-keygen', { env });
|
|
304
|
+
if (!nativeFile(age)) return identityFailure('native-tool-unavailable', false);
|
|
305
|
+
const options = { cwd, env: isolatedSopsEnv(env, null), shell: false, encoding: 'buffer', windowsHide: true,
|
|
306
|
+
stdio: ['pipe', 'pipe', 'pipe'], timeout: IDENTITY_TIMEOUT_MS, maxBuffer: CREDENTIAL_FILE_MAX_BYTES };
|
|
307
|
+
const run = leasedAgeRunner(invocation, age, options, state.captures, assertLease);
|
|
308
|
+
const version = run(['--version']);
|
|
309
|
+
if (!SELECTED_AGE_VERSIONS.has(/^v?(\d+\.\d+\.\d+)/.exec(decoded(version.stdout))?.[1])) return identityFailure('unsupported-tool-profile', false);
|
|
310
|
+
if (assertLease() !== true) return identityFailure('lease-lost', false);
|
|
311
|
+
state.generated = true;
|
|
312
|
+
const made = run([]), lines = decoded(made.stdout).trim().split(/\r?\n/);
|
|
313
|
+
const identities = lines.filter(line => line && !line.startsWith('#'));
|
|
314
|
+
if (!identityLinesValid(lines, identities)) return identityFailure('capture-incomplete', true);
|
|
315
|
+
state.identity = Buffer.from(identities[0], 'utf8');
|
|
316
|
+
const recipient = recipientOf(run(['-y'], state.identity).stdout);
|
|
317
|
+
if (recipient === null) return identityFailure('recipient-unproven', true);
|
|
318
|
+
if (!declaredRecipientMatches(lines, recipient) || assertLease() !== true) return identityFailure('recipient-unproven', true);
|
|
319
|
+
return publicationOutcome(consume(state.identity, recipient), recipient);
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
/** One real private capture for an admitted initial request; the installer owns its durable attempt. */
|
|
323
|
+
export function withGeneratedAgeIdentity({ env, cwd, invocation, assertLease, consume } = {}) {
|
|
324
|
+
if (!identityRequestValid({ env, cwd, invocation, assertLease, consume })) return identityFailure('invalid-request', false);
|
|
325
|
+
const state = { captures: [], generated: false, identity: undefined };
|
|
326
|
+
try {
|
|
327
|
+
return generateAndPublish(state, { env, cwd, invocation, assertLease, consume });
|
|
328
|
+
} catch { return identityFailure('capture-or-publication-unknown', state.generated); }
|
|
329
|
+
finally {
|
|
330
|
+
state.identity?.fill(0);
|
|
331
|
+
wipeCaptures(state.captures);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
@@ -2,17 +2,21 @@
|
|
|
2
2
|
// (scripts/hfs/secret.mjs). The plaintext travels on the child's stdin and never on its argument list, so it is not visible in a
|
|
3
3
|
// process listing; the ciphertext comes back on stdout.
|
|
4
4
|
import { spawnSync } from 'node:child_process';
|
|
5
|
-
import {
|
|
5
|
+
import { sopsIdentityEnv } from '../../../engine/secrets.mjs';
|
|
6
|
+
import { resolveSops, runSelectedSops } from './lib.mjs';
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* Run `<bin> encrypt` (bin null: the sops resolveSops finds) over `plaintext` of format `inputType` (json, yaml or dotenv). Sops reads stdin only with a `--filename-override`, the path the sealed
|
|
9
10
|
* file will have: recipients are age keys (`--age`), and when none are given sops picks them from the repository's .sops.yaml for that path. {status, stdout, stderr, error}.
|
|
10
11
|
*/
|
|
11
|
-
export function seal(bin, { inputType, plaintext, recipients = [], filenameOverride }, { env = process.env, cwd = undefined, maxBuffer = 16 * 1024 * 1024 } = {}) {
|
|
12
|
+
export function seal(bin, { inputType, plaintext, recipients = [], filenameOverride }, { invocation = null, env = process.env, cwd = undefined, maxBuffer = 16 * 1024 * 1024, timeout = undefined } = {}) {
|
|
12
13
|
const args = ['encrypt', '--input-type', inputType, '--output-type', inputType, '--filename-override', filenameOverride];
|
|
13
14
|
if (recipients.length) args.push('--age', recipients.join(','));
|
|
14
|
-
const
|
|
15
|
+
const selected = sopsIdentityEnv(env);
|
|
16
|
+
if (selected.error?.identityRefusal === 'inline-context-unqualified') return runSelectedSops(bin, { operation: 'seal', params: { inputType, plaintext, recipients, filenameOverride } }, { selection: selected, invocation, env, cwd, maxBuffer, timeout });
|
|
17
|
+
if (selected.error) return { status: null, stdout: '', stderr: '', error: selected.error };
|
|
18
|
+
const exe = bin ?? resolveSops(selected.env);
|
|
15
19
|
if (!exe) return { status: null, stdout: '', stderr: '', error: Object.assign(new Error('sops is not installed (Windows: winget install Mozilla.SOPS)'), { code: 'SOPS_MISSING' }) };
|
|
16
|
-
const r = spawnSync(exe, args, { cwd, env, input: plaintext, encoding: 'utf8', windowsHide: true, maxBuffer });
|
|
20
|
+
const r = spawnSync(exe, args, { cwd, env: selected.env, input: plaintext, encoding: 'utf8', windowsHide: true, maxBuffer });
|
|
17
21
|
return { status: r.status, stdout: r.stdout, stderr: r.stderr, error: r.error ?? null };
|
|
18
22
|
}
|
|
@@ -36,9 +36,9 @@ export function slotAdmitsFile(resolver, file) {
|
|
|
36
36
|
if (classified.status === 'no-slot' || classified.status === 'ambiguous' || !classified.slot) return null;
|
|
37
37
|
const slot = resolver.slot(classified.slot);
|
|
38
38
|
const relative = relativeToRoot(classified.path, classified.root);
|
|
39
|
-
const hit = matchersOf([...(slot.requires ?? []), ...(slot.allows ?? [])], classified.bindings).
|
|
40
|
-
const forbidden = matchersOf(slot.forbids, classified.bindings).
|
|
41
|
-
return
|
|
39
|
+
const hit = matchersOf([...(slot.requires ?? []), ...(slot.allows ?? [])], classified.bindings).some(matcher => matcher.expression.test(relative));
|
|
40
|
+
const forbidden = matchersOf(slot.forbids, classified.bindings).some(matcher => matcher.expression.test(relative) || matcher.expression.test(`${relative}/`));
|
|
41
|
+
return hit && !forbidden;
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
/**
|
|
@@ -1,19 +1,20 @@
|
|
|
1
1
|
// ast-walks.mjs - AST walks the architecture checkers share (moved out of typescript.mjs, which keeps the program loading and module resolution).
|
|
2
2
|
import { canonical, isInside } from './config.mjs';
|
|
3
|
-
import { sourceLocation } from '../../lib/ts-ast.mjs';
|
|
3
|
+
import { hasAnyFlag, isConstVariable, sourceLocation } from '../../lib/ts-ast.mjs';
|
|
4
4
|
import { UNPROVEN_FRAMEWORK, isUnshadowedCommonJsRequire, relativePath, unwrapExpression } from './typescript.mjs';
|
|
5
5
|
|
|
6
6
|
/** `expression` unwrapped by the caller's own predicate set (a predicate absent on an older ts reads as never). */
|
|
7
7
|
export function unwrapEach(ts, expression, predicates) {
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
let current = expression;
|
|
9
|
+
while (current && predicates.some((predicate) => predicate?.(current))) current = current.expression;
|
|
10
|
+
return current;
|
|
10
11
|
}
|
|
11
12
|
|
|
12
13
|
/** `value` with every TS alias hop resolved (a re-exported import reads as its target symbol). */
|
|
13
|
-
export function normalizedSymbolValue(ts, checker, value) {
|
|
14
|
-
let symbol = value
|
|
14
|
+
export function normalizedSymbolValue(ts, checker, value = null) {
|
|
15
|
+
let symbol = value;
|
|
15
16
|
const seen = new Set();
|
|
16
|
-
while (symbol && (symbol.flags
|
|
17
|
+
while (symbol && hasAnyFlag(symbol.flags, ts.SymbolFlags.Alias) && !seen.has(symbol)) {
|
|
17
18
|
seen.add(symbol);
|
|
18
19
|
const target = checker.getAliasedSymbol(symbol);
|
|
19
20
|
if (!target || target === symbol) break;
|
|
@@ -48,7 +49,7 @@ export function valueSymbol(ts, checker, node, seen = new Set(), { unselected =
|
|
|
48
49
|
seen.add(symbol);
|
|
49
50
|
const declarations = symbol.getDeclarations?.() ?? [];
|
|
50
51
|
if (declarations.length === 1 && ts.isVariableDeclaration(declarations[0]) && declarations[0].initializer
|
|
51
|
-
&& (ts
|
|
52
|
+
&& isConstVariable(ts, declarations[0])) {
|
|
52
53
|
return valueSymbol(ts, checker, declarations[0].initializer, seen, { unselected });
|
|
53
54
|
}
|
|
54
55
|
return symbol;
|
|
@@ -102,50 +103,59 @@ export function anyDescendant(ts, node, match) {
|
|
|
102
103
|
* selected node's symbols to a kind (default `targets.get(symbol)`); `shorthandOf` (when set) also resolves
|
|
103
104
|
* shorthand-property declarations and descends property assignments; `newExpression` descends `new X()` callees.
|
|
104
105
|
*/
|
|
106
|
+
const addKinds = (target, source) => { for (const kind of source) target.add(kind); };
|
|
107
|
+
|
|
108
|
+
const kindsFromReturns = (ts, checker, declaration, targets, options) => {
|
|
109
|
+
const kinds = new Set();
|
|
110
|
+
for (const returned of returnedExpressions(ts, declaration)) addKinds(kinds, tracedFrameworkKinds(ts, checker, returned, targets, options));
|
|
111
|
+
return kinds;
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const kindsFromDeclaration = (ts, checker, declaration, targets, options, shorthandOf) => {
|
|
115
|
+
const kinds = new Set();
|
|
116
|
+
if (ts.isVariableDeclaration(declaration) && declaration.initializer)
|
|
117
|
+
addKinds(kinds, tracedFrameworkKinds(ts, checker, declaration.initializer, targets, options));
|
|
118
|
+
if (shorthandOf && ts.isShorthandPropertyAssignment(declaration)) {
|
|
119
|
+
const target = normalizedSymbolValue(ts, checker, checker.getShorthandAssignmentValueSymbol?.(declaration));
|
|
120
|
+
const kind = shorthandOf(ts, checker, target, targets);
|
|
121
|
+
if (kind) kinds.add(kind);
|
|
122
|
+
}
|
|
123
|
+
if (shorthandOf && ts.isPropertyAssignment(declaration))
|
|
124
|
+
addKinds(kinds, tracedFrameworkKinds(ts, checker, declaration.initializer, targets, options));
|
|
125
|
+
addKinds(kinds, kindsFromReturns(ts, checker, declaration, targets, options));
|
|
126
|
+
return kinds;
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const kindsFromSymbol = (ts, checker, symbol, targets, seen, options) => {
|
|
130
|
+
const nextSeen = new Set(seen).add(symbol);
|
|
131
|
+
const declarationOptions = { ...options, seen: nextSeen };
|
|
132
|
+
const kinds = new Set();
|
|
133
|
+
for (const declaration of symbol.getDeclarations?.() ?? [])
|
|
134
|
+
addKinds(kinds, kindsFromDeclaration(ts, checker, declaration, targets, declarationOptions, options.shorthandOf));
|
|
135
|
+
return kinds;
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
const defaultDirectOf = (ts, checker, { symbol, targets }) => targets.get(symbol) ?? null;
|
|
139
|
+
|
|
105
140
|
export function tracedFrameworkKinds(ts, checker, expression, targets,
|
|
106
141
|
{ seen = new Set(), depth = 0, directOf = null, shorthandOf = null, newExpression = false } = {}) {
|
|
107
142
|
if (!expression) return new Set();
|
|
108
143
|
if (depth > 10) return new Set([UNPROVEN_FRAMEWORK]);
|
|
109
|
-
|
|
110
|
-
const selected = selectedNode(ts,
|
|
144
|
+
const unwrapped = unwrapExpression(ts, expression);
|
|
145
|
+
const selected = selectedNode(ts, unwrapped);
|
|
111
146
|
const symbol = selected ? normalizedSymbol(ts, checker, selected) : null;
|
|
112
|
-
const direct = symbol ? (directOf ??
|
|
147
|
+
const direct = symbol ? (directOf ?? defaultDirectOf)(ts, checker, { symbol, selected, targets }) : null;
|
|
113
148
|
if (direct) return new Set([direct]);
|
|
114
|
-
const
|
|
115
|
-
if (ts.isArrowFunction(
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
for (const kind of tracedFrameworkKinds(ts, checker, returned, targets, { ...opts, seen })) kinds.add(kind);
|
|
119
|
-
}
|
|
120
|
-
return kinds;
|
|
121
|
-
}
|
|
122
|
-
if (ts.isCallExpression(expression)) {
|
|
123
|
-
const kinds = tracedFrameworkKinds(ts, checker, expression.expression, targets, { ...opts, seen });
|
|
149
|
+
const options = { depth: depth + 1, directOf, shorthandOf, newExpression };
|
|
150
|
+
if (ts.isArrowFunction(unwrapped) || ts.isFunctionExpression(unwrapped)) return kindsFromReturns(ts, checker, unwrapped, targets, { ...options, seen });
|
|
151
|
+
if (ts.isCallExpression(unwrapped)) {
|
|
152
|
+
const kinds = tracedFrameworkKinds(ts, checker, unwrapped.expression, targets, { ...options, seen });
|
|
124
153
|
if (kinds.size) return kinds;
|
|
125
154
|
}
|
|
126
|
-
if (newExpression && ts.isNewExpression(
|
|
127
|
-
return tracedFrameworkKinds(ts, checker,
|
|
128
|
-
}
|
|
155
|
+
if (newExpression && ts.isNewExpression(unwrapped))
|
|
156
|
+
return tracedFrameworkKinds(ts, checker, unwrapped.expression, targets, { ...options, seen });
|
|
129
157
|
if (!symbol || seen.has(symbol)) return new Set();
|
|
130
|
-
|
|
131
|
-
const kinds = new Set();
|
|
132
|
-
for (const declaration of symbol.getDeclarations?.() ?? []) {
|
|
133
|
-
if (ts.isVariableDeclaration(declaration) && declaration.initializer) {
|
|
134
|
-
for (const kind of tracedFrameworkKinds(ts, checker, declaration.initializer, targets, { ...opts, seen: nextSeen })) kinds.add(kind);
|
|
135
|
-
}
|
|
136
|
-
if (shorthandOf && ts.isShorthandPropertyAssignment(declaration)) {
|
|
137
|
-
const target = normalizedSymbolValue(ts, checker, checker.getShorthandAssignmentValueSymbol?.(declaration));
|
|
138
|
-
const kind = shorthandOf(ts, checker, target, targets);
|
|
139
|
-
if (kind) kinds.add(kind);
|
|
140
|
-
}
|
|
141
|
-
if (shorthandOf && ts.isPropertyAssignment(declaration)) {
|
|
142
|
-
for (const kind of tracedFrameworkKinds(ts, checker, declaration.initializer, targets, { ...opts, seen: nextSeen })) kinds.add(kind);
|
|
143
|
-
}
|
|
144
|
-
for (const returned of returnedExpressions(ts, declaration)) {
|
|
145
|
-
for (const kind of tracedFrameworkKinds(ts, checker, returned, targets, { ...opts, seen: nextSeen })) kinds.add(kind);
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
return kinds;
|
|
158
|
+
return kindsFromSymbol(ts, checker, symbol, targets, seen, options);
|
|
149
159
|
}
|
|
150
160
|
|
|
151
161
|
/** 'unproven framework' | 'multiple framework' | the one kind | null, for a decorator whose expression constructs. */
|
|
@@ -153,7 +163,8 @@ export function constructedDecoratorKind(ts, checker, decorator, targets, traceO
|
|
|
153
163
|
const callee = ts.isCallExpression(decorator.expression) ? decorator.expression.expression : decorator.expression;
|
|
154
164
|
const kinds = tracedFrameworkKinds(ts, checker, callee, targets, traceOpts);
|
|
155
165
|
if (kinds.has(UNPROVEN_FRAMEWORK)) return 'unproven framework';
|
|
156
|
-
|
|
166
|
+
if (kinds.size === 1) return [...kinds][0];
|
|
167
|
+
return kinds.size ? 'multiple framework' : null;
|
|
157
168
|
}
|
|
158
169
|
|
|
159
170
|
/** The target a decorator resolves to through a `let`/`var` alias's single initializer, or null. */
|
|
@@ -163,7 +174,7 @@ export function mutableDecoratorKind(ts, checker, decorator, targets) {
|
|
|
163
174
|
const symbol = selected ? normalizedSymbol(ts, checker, selected) : null;
|
|
164
175
|
const declarations = symbol?.getDeclarations?.() ?? [];
|
|
165
176
|
if (declarations.length !== 1 || !ts.isVariableDeclaration(declarations[0]) || !declarations[0].initializer
|
|
166
|
-
|| (ts
|
|
177
|
+
|| isConstVariable(ts, declarations[0])) return null;
|
|
167
178
|
return targets.get(valueSymbol(ts, checker, declarations[0].initializer)) ?? null;
|
|
168
179
|
}
|
|
169
180
|
|
|
@@ -178,11 +189,12 @@ export function decoratorCallee(ts, decorator) {
|
|
|
178
189
|
* not resolve. Null when the statement names no static specifier.
|
|
179
190
|
*/
|
|
180
191
|
export function moduleExportsOf(ts, checker, statement) {
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
192
|
+
let moduleSpecifier = null;
|
|
193
|
+
if ((ts.isImportDeclaration(statement) || ts.isExportDeclaration(statement))
|
|
194
|
+
&& statement.moduleSpecifier && ts.isStringLiteralLike(statement.moduleSpecifier)) moduleSpecifier = statement.moduleSpecifier;
|
|
195
|
+
let importEquals = null;
|
|
196
|
+
if (ts.isImportEqualsDeclaration(statement) && ts.isExternalModuleReference(statement.moduleReference)
|
|
197
|
+
&& statement.moduleReference.expression && ts.isStringLiteralLike(statement.moduleReference.expression)) importEquals = statement.moduleReference.expression;
|
|
186
198
|
const specifierNode = moduleSpecifier ?? importEquals;
|
|
187
199
|
const specifier = specifierNode?.text;
|
|
188
200
|
if (specifier == null) return null;
|
|
@@ -208,7 +220,8 @@ export function programSourcesOf(context, checker, localFiles, { root = null } =
|
|
|
208
220
|
* The CommonJS require() caveat every framework scan reports: each `require('<specifier>')` `accepted` pushes
|
|
209
221
|
* `${filePath} uses a CommonJS <label ?? specifier> <detail>` (the import-shape reasoning cannot prove that binding).
|
|
210
222
|
*/
|
|
211
|
-
export function commonJsRequireReasons(ts, checker, sourceFile, accepted, reasons, filePath, detail
|
|
223
|
+
export function commonJsRequireReasons(ts, checker, sourceFile, accepted, reasons, filePath, detail) {
|
|
224
|
+
const label = arguments[7] ?? null;
|
|
212
225
|
const visit = node => {
|
|
213
226
|
if (ts.isCallExpression(node) && isUnshadowedCommonJsRequire(ts, checker, node.expression)
|
|
214
227
|
&& node.arguments.length === 1 && ts.isStringLiteralLike(node.arguments[0]) && accepted(node.arguments[0].text)) {
|