@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.
Files changed (247) hide show
  1. package/CHANGELOG.md +1 -1
  2. package/LICENSE +21 -0
  3. package/emit/operations.mjs +1 -1
  4. package/emit/type-schema.mjs +2 -1
  5. package/lint/run.mjs +82 -8
  6. package/package.json +3 -2
  7. package/report/order.mjs +2 -0
  8. package/runtime/config.example.yaml +187 -0
  9. package/runtime/engine/by-code-unit.mjs +12 -0
  10. package/runtime/engine/config.mjs +247 -273
  11. package/runtime/engine/invalid-config.mjs +5 -5
  12. package/runtime/engine/model-config.mjs +90 -0
  13. package/runtime/engine/orca-config.mjs +3 -1
  14. package/runtime/engine/removed-vocabulary.mjs +28 -0
  15. package/runtime/engine/resources-config.mjs +52 -0
  16. package/runtime/engine/runtime-root.mjs +17 -0
  17. package/runtime/engine/secrets.mjs +134 -0
  18. package/runtime/engine/sonar-config.mjs +21 -0
  19. package/runtime/engine/temp-root.mjs +32 -0
  20. package/runtime/knowledge/hfs/canon-pins.yaml +18 -18
  21. package/runtime/knowledge/hfs/peer-integrations.yaml +2 -3
  22. package/runtime/knowledge/hfs/rules.yaml +102 -69
  23. package/runtime/knowledge/hfs/slots.yaml +11 -14
  24. package/runtime/knowledge/patterns/be/api.yaml +7 -24
  25. package/runtime/knowledge/patterns/be/cli.yaml +3 -12
  26. package/runtime/knowledge/patterns/be/realtime.yaml +1 -14
  27. package/runtime/knowledge/patterns/be/webhooks.yaml +0 -19
  28. package/runtime/modules/kernel/failure-codes.yaml +1 -1
  29. package/runtime/modules/kernel/removed-vocabulary.yaml +168 -0
  30. package/runtime/modules/models/registry.yaml +25 -397
  31. package/runtime/modules/models/runtimes.yaml +92 -262
  32. package/runtime/modules/models/tiers.yaml +65 -0
  33. package/runtime/scripts/api/fs/claim-file.mjs +30 -0
  34. package/runtime/scripts/api/fs/ensure-temp-root.mjs +27 -0
  35. package/runtime/scripts/api/fs/forbidden-root.mjs +2 -1
  36. package/runtime/scripts/api/fs/lib.mjs +6 -0
  37. package/runtime/scripts/api/fs/make-temp-dir.mjs +11 -0
  38. package/runtime/scripts/api/fs/safe-remove.mjs +52 -35
  39. package/runtime/scripts/api/git/lib.mjs +36 -1
  40. package/runtime/scripts/api/process/resolve-real-tool.mjs +52 -0
  41. package/runtime/scripts/api/process/run-program.mjs +8 -0
  42. package/runtime/scripts/api/sops/decrypt.mjs +9 -16
  43. package/runtime/scripts/api/sops/lib.mjs +309 -11
  44. package/runtime/scripts/api/sops/seal.mjs +8 -4
  45. package/runtime/scripts/hfs/allows.mjs +3 -3
  46. package/runtime/scripts/hfs/architecture/ast-walks.mjs +63 -50
  47. package/runtime/scripts/hfs/architecture/automatic-gates.mjs +96 -0
  48. package/runtime/scripts/hfs/architecture/backend.mjs +261 -175
  49. package/runtime/scripts/hfs/architecture/background-unowned.mjs +69 -34
  50. package/runtime/scripts/hfs/architecture/client-reaches-server.mjs +55 -54
  51. package/runtime/scripts/hfs/architecture/clones.mjs +179 -106
  52. package/runtime/scripts/hfs/architecture/config-unread.mjs +5 -22
  53. package/runtime/scripts/hfs/architecture/config.mjs +95 -80
  54. package/runtime/scripts/hfs/architecture/connection-map.mjs +233 -141
  55. package/runtime/scripts/hfs/architecture/constructor-deps.mjs +15 -9
  56. package/runtime/scripts/hfs/architecture/context-coupling.mjs +76 -52
  57. package/runtime/scripts/hfs/architecture/context-map.mjs +215 -141
  58. package/runtime/scripts/hfs/architecture/context-owner.mjs +39 -30
  59. package/runtime/scripts/hfs/architecture/context-platform-tables.mjs +31 -17
  60. package/runtime/scripts/hfs/architecture/context-transaction.mjs +38 -25
  61. package/runtime/scripts/hfs/architecture/contract-fixture-guard.mjs +86 -40
  62. package/runtime/scripts/hfs/architecture/contracts-readonly.mjs +342 -0
  63. package/runtime/scripts/hfs/architecture/contracts.mjs +263 -455
  64. package/runtime/scripts/hfs/architecture/cross-app-duplicate.mjs +43 -34
  65. package/runtime/scripts/hfs/architecture/dead-exports.mjs +201 -154
  66. package/runtime/scripts/hfs/architecture/default-deny.mjs +118 -93
  67. package/runtime/scripts/hfs/architecture/doc-language.mjs +2 -1
  68. package/runtime/scripts/hfs/architecture/entrypoint.mjs +45 -30
  69. package/runtime/scripts/hfs/architecture/error-codes.mjs +19 -12
  70. package/runtime/scripts/hfs/architecture/error-masked.mjs +39 -26
  71. package/runtime/scripts/hfs/architecture/fe-slot-allows.mjs +3 -2
  72. package/runtime/scripts/hfs/architecture/feature-shape.mjs +26 -20
  73. package/runtime/scripts/hfs/architecture/framework-pinned.mjs +1 -1
  74. package/runtime/scripts/hfs/architecture/frontend-grammar.mjs +190 -0
  75. package/runtime/scripts/hfs/architecture/frontend-routing.mjs +186 -0
  76. package/runtime/scripts/hfs/architecture/frontend-world-render.mjs +311 -0
  77. package/runtime/scripts/hfs/architecture/frontend-world.mjs +328 -0
  78. package/runtime/scripts/hfs/architecture/frontend.mjs +109 -829
  79. package/runtime/scripts/hfs/architecture/hfs-graph.mjs +14 -2
  80. package/runtime/scripts/hfs/architecture/hfs.mjs +120 -288
  81. package/runtime/scripts/hfs/architecture/hooks-are-hooks.mjs +46 -26
  82. package/runtime/scripts/hfs/architecture/i18n-keys.mjs +114 -76
  83. package/runtime/scripts/hfs/architecture/index.mjs +158 -102
  84. package/runtime/scripts/hfs/architecture/injection-token-exported.mjs +33 -28
  85. package/runtime/scripts/hfs/architecture/machine-ast.mjs +180 -144
  86. package/runtime/scripts/hfs/architecture/managed-scripts.mjs +8 -2
  87. package/runtime/scripts/hfs/architecture/module-per-transport.mjs +150 -101
  88. package/runtime/scripts/hfs/architecture/next-data-contract.mjs +51 -43
  89. package/runtime/scripts/hfs/architecture/next-data-key.mjs +338 -0
  90. package/runtime/scripts/hfs/architecture/next-data.mjs +179 -427
  91. package/runtime/scripts/hfs/architecture/owners.mjs +27 -13
  92. package/runtime/scripts/hfs/architecture/package-shape.mjs +51 -23
  93. package/runtime/scripts/hfs/architecture/presentation.mjs +165 -0
  94. package/runtime/scripts/hfs/architecture/reachability.mjs +127 -61
  95. package/runtime/scripts/hfs/architecture/register-once.mjs +112 -63
  96. package/runtime/scripts/hfs/architecture/registration.mjs +187 -110
  97. package/runtime/scripts/hfs/architecture/required-files.mjs +100 -68
  98. package/runtime/scripts/hfs/architecture/route-files-thin.mjs +83 -58
  99. package/runtime/scripts/hfs/architecture/schema-owner.mjs +182 -108
  100. package/runtime/scripts/hfs/architecture/source-names-shape.mjs +318 -0
  101. package/runtime/scripts/hfs/architecture/source-names.mjs +83 -248
  102. package/runtime/scripts/hfs/architecture/sql-owner.mjs +175 -118
  103. package/runtime/scripts/hfs/architecture/sql-returning.mjs +39 -28
  104. package/runtime/scripts/hfs/architecture/sql-tokens.mjs +302 -172
  105. package/runtime/scripts/hfs/architecture/supabase-ast.mjs +1 -1
  106. package/runtime/scripts/hfs/architecture/supabase-be.mjs +55 -45
  107. package/runtime/scripts/hfs/architecture/supabase-results.mjs +182 -0
  108. package/runtime/scripts/hfs/architecture/supabase-tables.mjs +20 -12
  109. package/runtime/scripts/hfs/architecture/supabase.mjs +191 -263
  110. package/runtime/scripts/hfs/architecture/symbols.mjs +175 -99
  111. package/runtime/scripts/hfs/architecture/test-world-files.mjs +82 -41
  112. package/runtime/scripts/hfs/architecture/tiers.mjs +120 -69
  113. package/runtime/scripts/hfs/architecture/transport-owner.mjs +84 -57
  114. package/runtime/scripts/hfs/architecture/type-context.mjs +282 -0
  115. package/runtime/scripts/hfs/architecture/typescript.mjs +116 -276
  116. package/runtime/scripts/hfs/architecture/unit-spec-providers.mjs +84 -62
  117. package/runtime/scripts/hfs/architecture.mjs +3 -4
  118. package/runtime/scripts/hfs/check.mjs +61 -70
  119. package/runtime/scripts/hfs/coverage-scope.mjs +84 -38
  120. package/runtime/scripts/hfs/declaration-shape.mjs +77 -36
  121. package/runtime/scripts/hfs/declaration-slots.mjs +1 -1
  122. package/runtime/scripts/hfs/edition-slots.mjs +5 -3
  123. package/runtime/scripts/hfs/linear-text.mjs +30 -0
  124. package/runtime/scripts/hfs/manifest-shape.mjs +164 -63
  125. package/runtime/scripts/hfs/path-findings.mjs +58 -38
  126. package/runtime/scripts/hfs/pin-findings.mjs +31 -0
  127. package/runtime/scripts/hfs/repo-identity.mjs +5 -2
  128. package/runtime/scripts/hfs/rule-catalog.mjs +194 -0
  129. package/runtime/scripts/hfs/rule-params-shape.mjs +33 -27
  130. package/runtime/scripts/hfs/rules/cli.mjs +5 -1
  131. package/runtime/scripts/hfs/rules/contract-compat.mjs +40 -22
  132. package/runtime/scripts/hfs/rules/contract.mjs +35 -27
  133. package/runtime/scripts/hfs/rules/database-config.mjs +57 -32
  134. package/runtime/scripts/hfs/rules/database-migrations.mjs +52 -31
  135. package/runtime/scripts/hfs/rules/database-plpgsql.mjs +106 -91
  136. package/runtime/scripts/hfs/rules/database-sql.mjs +254 -173
  137. package/runtime/scripts/hfs/rules/database.mjs +36 -23
  138. package/runtime/scripts/hfs/rules/deps.mjs +64 -32
  139. package/runtime/scripts/hfs/rules/docker.mjs +113 -65
  140. package/runtime/scripts/hfs/rules/edition.mjs +118 -111
  141. package/runtime/scripts/hfs/rules/event-bus.mjs +91 -45
  142. package/runtime/scripts/hfs/rules/fe-contract-documents.mjs +17 -16
  143. package/runtime/scripts/hfs/rules/fe-no-tests.mjs +36 -16
  144. package/runtime/scripts/hfs/rules/frontend-tree.mjs +4 -3
  145. package/runtime/scripts/hfs/rules/integration-specs.mjs +75 -41
  146. package/runtime/scripts/hfs/rules/kinds.mjs +59 -25
  147. package/runtime/scripts/hfs/rules/lint-suppression.mjs +9 -3
  148. package/runtime/scripts/hfs/rules/monorepo.mjs +82 -45
  149. package/runtime/scripts/hfs/rules/peer-integrations.mjs +24 -3
  150. package/runtime/scripts/hfs/rules/pipeline.mjs +58 -9
  151. package/runtime/scripts/hfs/rules/proof-commands.mjs +14 -8
  152. package/runtime/scripts/hfs/rules/repo-local-checks.mjs +11 -6
  153. package/runtime/scripts/hfs/rules/saga.mjs +128 -61
  154. package/runtime/scripts/hfs/rules/secrets.mjs +4 -3
  155. package/runtime/scripts/hfs/rules/services.mjs +34 -16
  156. package/runtime/scripts/hfs/rules/stacks.mjs +21 -14
  157. package/runtime/scripts/hfs/rules/supabase-secrets.mjs +32 -23
  158. package/runtime/scripts/hfs/rules/test-topology.mjs +25 -11
  159. package/runtime/scripts/hfs/secret.mjs +90 -45
  160. package/runtime/scripts/hfs/slot-app-view.mjs +102 -0
  161. package/runtime/scripts/hfs/slot-classify.mjs +65 -0
  162. package/runtime/scripts/hfs/slot-errors.mjs +11 -0
  163. package/runtime/scripts/hfs/slot-imports.mjs +76 -0
  164. package/runtime/scripts/hfs/slot-manifest-shape.mjs +73 -0
  165. package/runtime/scripts/hfs/slot-match.mjs +153 -0
  166. package/runtime/scripts/hfs/slot-path.mjs +8 -0
  167. package/runtime/scripts/hfs/slot-required.mjs +76 -0
  168. package/runtime/scripts/hfs/slot-semantic-problems.mjs +148 -0
  169. package/runtime/scripts/hfs/slot-side-problems.mjs +28 -0
  170. package/runtime/scripts/hfs/slots.mjs +72 -693
  171. package/runtime/scripts/hfs/sql/pg-parse.mjs +5 -2
  172. package/runtime/scripts/hfs/trailing-slashes.mjs +8 -0
  173. package/runtime/scripts/hfs/tree.mjs +19 -14
  174. package/runtime/scripts/hfs/typescript-programs.mjs +6 -4
  175. package/runtime/scripts/hfs/view.mjs +1 -1
  176. package/runtime/scripts/lib/dockerfile.mjs +56 -33
  177. package/runtime/scripts/lib/event-contract.mjs +17 -13
  178. package/runtime/scripts/lib/fs-kind.mjs +14 -4
  179. package/runtime/scripts/lib/git.mjs +2 -2
  180. package/runtime/scripts/lib/graphql-contract.mjs +158 -322
  181. package/runtime/scripts/lib/graphql-sdl.mjs +262 -0
  182. package/runtime/scripts/lib/i18n.mjs +3 -2
  183. package/runtime/scripts/lib/in-order.mjs +71 -0
  184. package/runtime/scripts/lib/language.mjs +3 -3
  185. package/runtime/scripts/lib/list.mjs +8 -1
  186. package/runtime/scripts/lib/mutation-fence.mjs +15 -0
  187. package/runtime/scripts/lib/path-key.mjs +42 -9
  188. package/runtime/scripts/lib/pid-alive.mjs +7 -0
  189. package/runtime/scripts/lib/regex.mjs +1 -1
  190. package/runtime/scripts/lib/same-text.mjs +1 -1
  191. package/runtime/scripts/lib/secret-patterns.mjs +6 -2
  192. package/runtime/scripts/lib/sleep-sync.mjs +2 -2
  193. package/runtime/scripts/lib/sops-envelope.mjs +103 -2
  194. package/runtime/scripts/lib/stack-services.mjs +1 -2
  195. package/runtime/scripts/lib/ts-ast.mjs +9 -0
  196. package/runtime/scripts/lib/walk.mjs +6 -1
  197. package/scaffold/add-cli-lite.mjs +2 -1
  198. package/scaffold/add-table.mjs +2 -2
  199. package/scaffold/add.mjs +3 -2
  200. package/scaffold/app.mjs +4 -2
  201. package/scaffold/edition-gate.mjs +25 -25
  202. package/scaffold/lite-exports.mjs +2 -1
  203. package/scaffold/service.mjs +5 -4
  204. package/sync/hygiene.mjs +15 -9
  205. package/sync/index.mjs +7 -3
  206. package/templates/app/ci-workflows/github/workflows/ci.yml +4 -4
  207. package/templates/app/ci-workflows/github/workflows/e2e.yml +3 -3
  208. package/templates/app/ci-workflows/github/workflows/images.yml +3 -3
  209. package/templates/app/ci-workflows/sonar-steps.yml +2 -2
  210. package/templates/app/ci-workflows-lite/github/workflows/ci.yml +6 -6
  211. package/templates/app/ci-workflows-lite/github/workflows/db-deploy.yml +3 -3
  212. package/templates/app/ci-workflows-lite/github/workflows/images.yml +3 -3
  213. package/templates/app/starciwork.gitignore +1 -1
  214. package/templates/be/image/api/Dockerfile +1 -1
  215. package/templates/be/image/cli/Dockerfile +1 -1
  216. package/templates/be/image/worker/Dockerfile +1 -1
  217. package/templates/be/patterns/cli/group.cli.spec.ts.tpl +1 -1
  218. package/templates/be/patterns/cli/group.cli.ts.tpl +2 -2
  219. package/templates/be/patterns/event-bus/platform/event-runner.service.spec.ts.tpl +13 -0
  220. package/templates/be/patterns/event-bus/platform/event-runner.service.ts.tpl +7 -2
  221. package/templates/be/patterns/event-bus/platform/event.policy.ts.tpl +4 -1
  222. package/templates/be/patterns/event-bus/platform/kafka-event-transport.client.ts.tpl +2 -1
  223. package/templates/be/patterns/outbox/platform/outbox-relay.policy.ts.tpl +24 -9
  224. package/templates/be/patterns/queues/platform/queue-relay.service.ts.tpl +3 -2
  225. package/templates/be/patterns/queues/platform/queue-worker.service.ts.tpl +8 -5
  226. package/templates/be/skeleton/src/features/cli/migrate/migrate.cli.spec.ts +1 -1
  227. package/templates/be/skeleton/src/features/cli/migrate/migrate.cli.ts +2 -2
  228. package/templates/be/skeleton/src/features/cli/seed/seed.cli.spec.ts +1 -1
  229. package/templates/be/skeleton/src/features/cli/seed/seed.cli.ts +2 -2
  230. package/templates/be/skeleton/src/modules/platform/database/migrate-connections.client.ts +3 -2
  231. package/templates/be/skeleton/src/modules/platform/database/seed-connections.client.ts +5 -4
  232. package/templates/be/skeleton/src/modules/platform/primitives/index.ts +1 -0
  233. package/templates/be/skeleton/src/modules/platform/primitives/sequence.policy.spec.ts +47 -0
  234. package/templates/be/skeleton/src/modules/platform/primitives/sequence.policy.ts +16 -0
  235. package/templates/be/skeleton-lite/apps/api/Dockerfile +1 -0
  236. package/templates/be/skeleton-lite/src/modules/platform/primitives/index.ts +2 -0
  237. package/templates/fe/image/next/Dockerfile +2 -1
  238. package/templates/fe/skeleton/apps/app/src/app/[locale]/page.tsx +2 -2
  239. package/templates/fe/skeleton/apps/landing/src/app/[locale]/page.tsx +2 -2
  240. package/templates/fe/skeleton/packages/__project__-i18n/src/index.ts +1 -2
  241. package/templates/fe/skeleton-lite/apps/web/src/app/[locale]/layout.tsx +1 -2
  242. package/templates/fe/skeleton-lite/apps/web/src/app/[locale]/page.tsx +2 -2
  243. package/templates/fe/skeleton-lite/apps/web/src/components/blocks/SignInForm/index.tsx +7 -1
  244. package/templates/fe/skeleton-lite/apps/web/src/modules/db/auth/write-sign-out.ts +1 -1
  245. package/templates/fe/skeleton-lite/apps/web/src/modules/db/validation/validation.mapper.ts +10 -2
  246. package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/request.ts +10 -3
  247. package/upgrade/index.mjs +9 -2
@@ -1,9 +1,12 @@
1
- // scripts/api/sops/lib.mjs — what the sops call files beside it share: the input type a custody file's name states.
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
- /** The sops binary: PATH, then winget's Links and Packages (spawn without a shell ignores PATHEXT). */
23
- export function resolveSops(env = process.env) {
24
- const win = process.platform === 'win32';
25
- const names = win ? ['sops.exe', 'sops'] : ['sops'];
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 && env.LOCALAPPDATA) {
28
- const winget = path.join(env.LOCALAPPDATA, 'Microsoft', 'WinGet');
29
- dirs.push(path.join(winget, 'Links'));
30
- const packages = path.join(winget, 'Packages');
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 { resolveSops } from './lib.mjs';
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 exe = bin ?? resolveSops(env);
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).find(matcher => matcher.expression.test(relative));
40
- const forbidden = matchersOf(slot.forbids, classified.bindings).find(matcher => matcher.expression.test(relative) || matcher.expression.test(`${relative}/`));
41
- return Boolean(hit) && !forbidden;
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
- while (expression && predicates.some((predicate) => predicate?.(expression))) expression = expression.expression;
9
- return expression;
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 ?? null;
14
+ export function normalizedSymbolValue(ts, checker, value = null) {
15
+ let symbol = value;
15
16
  const seen = new Set();
16
- while (symbol && (symbol.flags & ts.SymbolFlags.Alias) && !seen.has(symbol)) {
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.getCombinedNodeFlags(declarations[0].parent) & ts.NodeFlags.Const)) {
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
- expression = unwrapExpression(ts, expression);
110
- const selected = selectedNode(ts, expression);
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 ?? ((ts2, c, { symbol: s, targets: t }) => t.get(s) ?? null))(ts, checker, { symbol, selected, targets }) : null;
147
+ const direct = symbol ? (directOf ?? defaultDirectOf)(ts, checker, { symbol, selected, targets }) : null;
113
148
  if (direct) return new Set([direct]);
114
- const opts = { depth: depth + 1, directOf, shorthandOf, newExpression };
115
- if (ts.isArrowFunction(expression) || ts.isFunctionExpression(expression)) {
116
- const kinds = new Set();
117
- for (const returned of returnedExpressions(ts, expression)) {
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(expression)) {
127
- return tracedFrameworkKinds(ts, checker, expression.expression, targets, { ...opts, seen });
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
- const nextSeen = new Set(seen).add(symbol);
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
- return kinds.size === 1 ? [...kinds][0] : kinds.size ? 'multiple framework' : null;
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.getCombinedNodeFlags(declarations[0].parent) & ts.NodeFlags.Const)) return null;
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
- const moduleSpecifier = (ts.isImportDeclaration(statement) || ts.isExportDeclaration(statement))
182
- && statement.moduleSpecifier && ts.isStringLiteralLike(statement.moduleSpecifier) ? statement.moduleSpecifier : null;
183
- const importEquals = ts.isImportEqualsDeclaration(statement) && ts.isExternalModuleReference(statement.moduleReference)
184
- && statement.moduleReference.expression && ts.isStringLiteralLike(statement.moduleReference.expression)
185
- ? statement.moduleReference.expression : null;
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, label = null) {
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)) {