@mnstry/atelier 0.2.0-alpha.1

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 (260) hide show
  1. package/CHANGELOG.md +383 -0
  2. package/LICENSE +202 -0
  3. package/NOTICE +5 -0
  4. package/README.md +262 -0
  5. package/SECURITY.md +93 -0
  6. package/TRADEMARKS.md +88 -0
  7. package/announcements/2026-08-06-announcements-channel.v1.json +12 -0
  8. package/announcements/keys/mnstry-announcements.public.v1.json +9 -0
  9. package/bin/atelier.mjs +2 -0
  10. package/bin/mnstry-atelier.mjs +3 -0
  11. package/contracts/analysis-adapter.v1.schema.json +248 -0
  12. package/contracts/atelier-action-intent.v1.schema.json +143 -0
  13. package/contracts/atelier-analysis-adapter.v1.schema.json +360 -0
  14. package/contracts/atelier-attestation.v1.schema.json +239 -0
  15. package/contracts/atelier-boundary-policy.v1.schema.json +339 -0
  16. package/contracts/atelier-claim.v1.schema.json +97 -0
  17. package/contracts/atelier-export.v1.schema.json +1015 -0
  18. package/contracts/atelier-extension-pack.v1.schema.json +290 -0
  19. package/contracts/atelier-kit-manifest.v1.schema.json +273 -0
  20. package/contracts/atelier-lock.v1.schema.json +364 -0
  21. package/contracts/atelier-migration.v1.schema.json +162 -0
  22. package/contracts/atelier-project-config.v1.schema.json +352 -0
  23. package/contracts/atelier-readiness-protocol.v1.schema.json +517 -0
  24. package/contracts/atelier-readiness-run.v1.schema.json +229 -0
  25. package/contracts/atelier-readiness.v1.schema.json +457 -0
  26. package/contracts/compat-baseline.json +10 -0
  27. package/contracts/git-promote-event.v1.schema.json +145 -0
  28. package/contracts/knowledge-source-sidecar.v1.schema.json +157 -0
  29. package/contracts/mnstry-atelier-semantic-profile.v1.json +254 -0
  30. package/docs/announcements.md +101 -0
  31. package/docs/atelier-runtime.md +34 -0
  32. package/docs/attestation.md +201 -0
  33. package/docs/continuity.md +55 -0
  34. package/docs/contract-stability.md +111 -0
  35. package/docs/distributions.md +220 -0
  36. package/docs/install.md +140 -0
  37. package/docs/knowledge-graph.md +140 -0
  38. package/docs/ontology.md +206 -0
  39. package/docs/release-engineering.md +151 -0
  40. package/docs/repo-boundary-guard.md +199 -0
  41. package/docs/tenant-readiness.md +64 -0
  42. package/docs/upgrade.md +124 -0
  43. package/fixtures/analysis/invalid/native-graph.v1.json +4 -0
  44. package/fixtures/analysis/invalid/promoted-claim.v1.json +17 -0
  45. package/fixtures/analysis/invalid/runtime-import.v1.json +20 -0
  46. package/fixtures/analysis/valid/output.claims.v1.json +17 -0
  47. package/fixtures/analysis-adapter/invalid/manifest-hidden-provider.v1.json +24 -0
  48. package/fixtures/analysis-adapter/invalid/output-frontmatter-mutation.v1.json +21 -0
  49. package/fixtures/analysis-adapter/invalid/output-native-graph.v1.json +14 -0
  50. package/fixtures/analysis-adapter/manifest.disabled.v1.json +22 -0
  51. package/fixtures/analysis-adapter/manifest.enabled-local.v1.json +23 -0
  52. package/fixtures/analysis-adapter/output.claims.v1.json +21 -0
  53. package/fixtures/atelier-action-intent/invalid/runtime-mutation-authority.v1.json +16 -0
  54. package/fixtures/atelier-action-intent/invalid/unknown-top-level-field.v1.json +17 -0
  55. package/fixtures/atelier-action-intent/invalid/writes-beyond-clipboard.v1.json +16 -0
  56. package/fixtures/atelier-action-intent/valid/copy-agent-prompt.v1.json +16 -0
  57. package/fixtures/atelier-action-intent/valid/copy-repo-path.v1.json +22 -0
  58. package/fixtures/atelier-analysis-adapter/invalid/hidden-model-provider.v1.json +24 -0
  59. package/fixtures/atelier-analysis-adapter/invalid/inert-with-execute-command.v1.json +24 -0
  60. package/fixtures/atelier-analysis-adapter/invalid/proposals-with-runtime-import.v1.json +30 -0
  61. package/fixtures/atelier-analysis-adapter/valid/manifest-inert.v1.json +24 -0
  62. package/fixtures/atelier-analysis-adapter/valid/proposals-claim-only.v1.json +27 -0
  63. package/fixtures/atelier-attestation/invalid/bad-digest.v1.json +23 -0
  64. package/fixtures/atelier-attestation/invalid/conformance-scope.v1.json +23 -0
  65. package/fixtures/atelier-attestation/invalid/missing-signature.v1.json +22 -0
  66. package/fixtures/atelier-attestation/invalid/unknown-root-field.v1.json +24 -0
  67. package/fixtures/atelier-attestation/keys/roundtrip-issuer.public.v1.json +9 -0
  68. package/fixtures/atelier-attestation/valid/rejected-with-reasons.v1.json +49 -0
  69. package/fixtures/atelier-attestation/valid/signed-admitted.v1.json +35 -0
  70. package/fixtures/atelier-attestation/valid/signed-roundtrip.v1.json +29 -0
  71. package/fixtures/atelier-attestation/valid/unsigned-advisory.v1.json +23 -0
  72. package/fixtures/atelier-claim/invalid/unknown-provider.v1.json +15 -0
  73. package/fixtures/atelier-claim/invalid/unknown-top-level-field.v1.json +16 -0
  74. package/fixtures/atelier-claim/invalid/wrong-schema-const.v1.json +15 -0
  75. package/fixtures/atelier-claim/valid/adapter-style-proposed-relation.v1.json +19 -0
  76. package/fixtures/atelier-claim/valid/codex-proposed-relation.v1.json +16 -0
  77. package/fixtures/atelier-claim/valid/manual-proposed-relation.v1.json +16 -0
  78. package/fixtures/atelier-claim/valid/readiness-proposed-relation.v1.json +18 -0
  79. package/fixtures/atelier-export/invalid/dirty-unforced-empty-taint.v1.json +19 -0
  80. package/fixtures/atelier-export/invalid/object-local-audience-visibility.v1.json +11 -0
  81. package/fixtures/atelier-export/invalid/open-object-class-owner.v1.json +23 -0
  82. package/fixtures/atelier-export/invalid/pricing-legacy-amount.v1.json +23 -0
  83. package/fixtures/atelier-export/invalid/public-disguised-internal-source-ref.v1.json +11 -0
  84. package/fixtures/atelier-export/invalid/public-nested-private-source-ref.v1.json +17 -0
  85. package/fixtures/atelier-export/invalid/public-resolved-private-source-ref.v1.json +13 -0
  86. package/fixtures/atelier-export/invalid/public-resolved-sensitive-source-ref.v1.json +13 -0
  87. package/fixtures/atelier-export/invalid/public-source-ref-missing-provenance.v1.json +20 -0
  88. package/fixtures/atelier-export/invalid/public-source-team-kg.json +12 -0
  89. package/fixtures/atelier-export/invalid/source-node-legacy-visibility.v1.json +21 -0
  90. package/fixtures/atelier-export/invalid/source-node-unknown-field.v1.json +18 -0
  91. package/fixtures/atelier-export/invalid/unresolved-public-source-ref.v1.json +11 -0
  92. package/fixtures/atelier-export/sample-studio-offer.v1.json +397 -0
  93. package/fixtures/atelier-extension-pack/invalid/bundled-slug-collision.v1.json +22 -0
  94. package/fixtures/atelier-extension-pack/invalid/escapes-root.v1.json +22 -0
  95. package/fixtures/atelier-extension-pack/invalid/posture-tampered.v1.json +22 -0
  96. package/fixtures/atelier-extension-pack/invalid/protocols/autonomous-authority.v1.json +118 -0
  97. package/fixtures/atelier-extension-pack/invalid/protocols/offer-map.v1.json +109 -0
  98. package/fixtures/atelier-extension-pack/invalid/redefines-runtime-term.v1.json +19 -0
  99. package/fixtures/atelier-extension-pack/invalid/wrong-namespace.v1.json +19 -0
  100. package/fixtures/atelier-extension-pack/valid/protocols/contract-gate.v1.json +109 -0
  101. package/fixtures/atelier-extension-pack/valid/sample-pack.v1.json +48 -0
  102. package/fixtures/atelier-kit-manifest/invalid/runtime-copy-without-canonical-role.v1.json +53 -0
  103. package/fixtures/atelier-kit-manifest/invalid/telemetry-send-path.v1.json +21 -0
  104. package/fixtures/atelier-kit-manifest/valid/mnstry-atelier.valid.v1.json +76 -0
  105. package/fixtures/atelier-lock/invalid/absolute-contract-path.v1.json +40 -0
  106. package/fixtures/atelier-lock/invalid/runtime-mutation-enabled.v1.json +41 -0
  107. package/fixtures/atelier-lock/invalid/unknown-top-level-field.v1.json +42 -0
  108. package/fixtures/atelier-lock/valid/extension-pack.v1.json +55 -0
  109. package/fixtures/atelier-lock/valid/minimal.v1.json +44 -0
  110. package/fixtures/atelier-migration/invalid/live-apply-mode.v1.json +21 -0
  111. package/fixtures/atelier-migration/invalid/missing-review-evidence.v1.json +20 -0
  112. package/fixtures/atelier-migration/invalid/runtime-mutation-enabled.v1.json +21 -0
  113. package/fixtures/atelier-migration/valid/minimal.v1.json +32 -0
  114. package/fixtures/atelier-project-config/invalid/absolute-repo-path.v1.json +11 -0
  115. package/fixtures/atelier-project-config/invalid/ext-not-object.v1.json +12 -0
  116. package/fixtures/atelier-project-config/invalid/external-kind-read-boundary.v1.json +12 -0
  117. package/fixtures/atelier-project-config/invalid/missing-repos-and-alignment.v1.json +7 -0
  118. package/fixtures/atelier-project-config/invalid/unknown-top-level-field.v1.json +14 -0
  119. package/fixtures/atelier-project-config/valid/alignment-scaffold.v1.json +15 -0
  120. package/fixtures/atelier-project-config/valid/ext-extension-packs.v1.json +36 -0
  121. package/fixtures/atelier-project-config/valid/repo-workspace.v1.json +23 -0
  122. package/fixtures/atelier-project-config/validator-only/ext-member-not-object.v1.json +14 -0
  123. package/fixtures/atelier-readiness/invalid/dry-run-only-false.v1.json +87 -0
  124. package/fixtures/atelier-readiness/invalid/support-bundle-send-path.v1.json +87 -0
  125. package/fixtures/atelier-readiness/invalid/unknown-top-level-field.v1.json +88 -0
  126. package/fixtures/atelier-readiness/valid/ready-workspace.v1.json +84 -0
  127. package/fixtures/boundary-policy/invalid/content-rule-exception-blanket-path.v1.json +67 -0
  128. package/fixtures/boundary-policy/invalid/invalid-actor-private-repo-mapping.v1.json +20 -0
  129. package/fixtures/boundary-policy/invalid/legacy-warning-missing-warning.v1.json +20 -0
  130. package/fixtures/boundary-policy/invalid/missing-repo-coverage.v1.json +19 -0
  131. package/fixtures/boundary-policy/invalid/unknown-key.v1.json +12 -0
  132. package/fixtures/boundary-policy/valid/content-rule-exception.v1.json +67 -0
  133. package/fixtures/boundary-policy/valid/legacy-warning.v1.json +33 -0
  134. package/fixtures/boundary-policy/valid/strict-private-shared.v1.json +40 -0
  135. package/fixtures/git-promote/invalid/provision-named-revocable.v1.json +29 -0
  136. package/fixtures/git-promote/private-to-team.v1.json +29 -0
  137. package/fixtures/knowledge-source-sidecar/invalid/legacy-visibility-field.v1.json +17 -0
  138. package/fixtures/knowledge-source-sidecar/invalid/missing-audience.v1.json +15 -0
  139. package/fixtures/knowledge-source-sidecar/invalid/unknown-relation-kind.v1.json +18 -0
  140. package/fixtures/knowledge-source-sidecar/valid/html-source.v1.json +18 -0
  141. package/fixtures/knowledge-source-sidecar/valid/pdf-evidence.v1.json +18 -0
  142. package/fixtures/projects/neutral-project/atelier.project.json +15 -0
  143. package/fixtures/projects/neutral-project/ops/config/repo-access.v1.json +12 -0
  144. package/fixtures/projects/neutral-project/project-knowledge.graph.json +69 -0
  145. package/fixtures/projects/sample-workspace/atelier.project.json +23 -0
  146. package/fixtures/projects/sample-workspace/boundary-policy.v1.json +59 -0
  147. package/fixtures/projects/sample-workspace/content/README.md +16 -0
  148. package/fixtures/projects/sample-workspace/content/source.html +5 -0
  149. package/fixtures/projects/sample-workspace/content/source.html.kg.json +15 -0
  150. package/fixtures/projects/sample-workspace/repo-access.v1.json +9 -0
  151. package/fixtures/projects/source-formats-workspace/atelier.project.json +23 -0
  152. package/fixtures/projects/source-formats-workspace/content/README.md +17 -0
  153. package/fixtures/projects/source-formats-workspace/content/data.json +7 -0
  154. package/fixtures/projects/source-formats-workspace/content/data.json.kg.json +15 -0
  155. package/fixtures/projects/source-formats-workspace/content/logo.png.kg.json +15 -0
  156. package/fixtures/projects/source-formats-workspace/content/metrics.csv +3 -0
  157. package/fixtures/projects/source-formats-workspace/content/metrics.csv.kg.json +15 -0
  158. package/fixtures/projects/source-formats-workspace/content/pipeline.yaml +4 -0
  159. package/fixtures/projects/source-formats-workspace/content/pipeline.yaml.kg.json +15 -0
  160. package/fixtures/projects/source-formats-workspace/repo-access.v1.json +9 -0
  161. package/fixtures/readiness-protocols/extension-pack/invalid/protocol-missing-path.v1.json +20 -0
  162. package/fixtures/readiness-protocols/extension-pack/valid/with-protocol.v1.json +24 -0
  163. package/fixtures/readiness-protocols/protocol/invalid/missing-check-target.v1.json +103 -0
  164. package/fixtures/readiness-protocols/protocol/invalid/runtime-mutation-enabled.v1.json +107 -0
  165. package/fixtures/readiness-protocols/protocol/valid/minimal.v1.json +100 -0
  166. package/fixtures/readiness-protocols/run/invalid/missing-evidence.v1.json +37 -0
  167. package/fixtures/readiness-protocols/run/invalid/pass-with-blocker.v1.json +22 -0
  168. package/fixtures/readiness-protocols/run/invalid/runtime-mutation-enabled.v1.json +22 -0
  169. package/fixtures/readiness-protocols/run/valid/pass.v1.json +44 -0
  170. package/fixtures/support/invalid/leaky-support.v1.json +13 -0
  171. package/fixtures/support/valid/minimal-safe.v1.json +20 -0
  172. package/package.json +98 -0
  173. package/skills/claude/atelier-open/SKILL.md +68 -0
  174. package/skills/claude/mnstry-readiness/SKILL.md +41 -0
  175. package/skills/codex/atelier-open/SKILL.md +68 -0
  176. package/skills/codex/mnstry-readiness/SKILL.md +41 -0
  177. package/src/analysis/adapter.mjs +20 -0
  178. package/src/analysis/analysis-claim-contract.mjs +335 -0
  179. package/src/attestation/jcs.mjs +64 -0
  180. package/src/attestation/sign.mjs +305 -0
  181. package/src/boundary/content-rules.mjs +262 -0
  182. package/src/boundary/policy.mjs +806 -0
  183. package/src/check-atelier-export-contract.mjs +116 -0
  184. package/src/cli/run.mjs +283 -0
  185. package/src/collaboration/proposals.mjs +262 -0
  186. package/src/commands/analysis.mjs +8 -0
  187. package/src/commands/announcements.mjs +308 -0
  188. package/src/commands/attestation.mjs +198 -0
  189. package/src/commands/boundary.mjs +25 -0
  190. package/src/commands/config.mjs +4 -0
  191. package/src/commands/context.mjs +3 -0
  192. package/src/commands/distribution.mjs +224 -0
  193. package/src/commands/egress.mjs +11 -0
  194. package/src/commands/extension-pack.mjs +209 -0
  195. package/src/commands/feedback.mjs +3 -0
  196. package/src/commands/graph.mjs +3 -0
  197. package/src/commands/init.mjs +130 -0
  198. package/src/commands/lock.mjs +3 -0
  199. package/src/commands/project.mjs +3 -0
  200. package/src/commands/promote.mjs +4 -0
  201. package/src/commands/readiness.mjs +3 -0
  202. package/src/commands/server.mjs +6 -0
  203. package/src/commands/setup.mjs +250 -0
  204. package/src/commands/support.mjs +3 -0
  205. package/src/commands/upgrade.mjs +3 -0
  206. package/src/contracts/corpus.mjs +171 -0
  207. package/src/egress/check.mjs +45 -0
  208. package/src/egress/forbidden-egress.mjs +323 -0
  209. package/src/export/atelier-export-contract.mjs +722 -0
  210. package/src/extension-packs/loader.mjs +491 -0
  211. package/src/graph/graph.mjs +370 -0
  212. package/src/graph/knowledge-graph.mjs +887 -0
  213. package/src/harness/context-client.mjs +144 -0
  214. package/src/harness/context.mjs +27 -0
  215. package/src/index.mjs +88 -0
  216. package/src/project/config.mjs +557 -0
  217. package/src/project/file-class.mjs +113 -0
  218. package/src/project/git-ignore.mjs +31 -0
  219. package/src/project/path-match.mjs +30 -0
  220. package/src/project/repo-identity.mjs +176 -0
  221. package/src/projection/alignment-projection.mjs +169 -0
  222. package/src/projection/policy.mjs +90 -0
  223. package/src/projection/project.mjs +152 -0
  224. package/src/readiness/readiness.mjs +312 -0
  225. package/src/readiness-protocols/bundled-pack.mjs +766 -0
  226. package/src/readiness-protocols/runtime.mjs +419 -0
  227. package/src/server/local-sidecar.mjs +640 -0
  228. package/src/server/security.mjs +229 -0
  229. package/src/server/server.mjs +42 -0
  230. package/src/support/feedback-report.mjs +414 -0
  231. package/src/support/support-bundle.mjs +226 -0
  232. package/src/ui/html-primitives.mjs +104 -0
  233. package/src/upgrade/upgrade.mjs +701 -0
  234. package/src/validate-atelier-export-dry-run.mjs +75 -0
  235. package/templates/atelier.project.example.json +23 -0
  236. package/templates/distribution-workspace/.claude/launch.json +12 -0
  237. package/templates/distribution-workspace/README.md +86 -0
  238. package/templates/distribution-workspace/atelier.project.json +36 -0
  239. package/templates/distribution-workspace/boundary-policy.v1.json +61 -0
  240. package/templates/distribution-workspace/gitignore +29 -0
  241. package/templates/distribution-workspace/governance/repo-boundary-ledger.md +5 -0
  242. package/templates/distribution-workspace/project/README.md +19 -0
  243. package/templates/distribution-workspace/repo-access.v1.json +9 -0
  244. package/templates/launch.json +12 -0
  245. package/templates/private-domain-workspace/.claude/launch.json +12 -0
  246. package/templates/private-domain-workspace/README.md +50 -0
  247. package/templates/private-domain-workspace/atelier.project.json +28 -0
  248. package/templates/private-domain-workspace/boundary-policy.v1.json +54 -0
  249. package/templates/private-domain-workspace/domain/README.md +19 -0
  250. package/templates/private-domain-workspace/gitignore +29 -0
  251. package/templates/private-domain-workspace/governance/repo-boundary-ledger.md +4 -0
  252. package/templates/private-domain-workspace/repo-access.v1.json +9 -0
  253. package/templates/shared-project-workspace/.claude/launch.json +12 -0
  254. package/templates/shared-project-workspace/README.md +51 -0
  255. package/templates/shared-project-workspace/atelier.project.json +28 -0
  256. package/templates/shared-project-workspace/boundary-policy.v1.json +61 -0
  257. package/templates/shared-project-workspace/gitignore +29 -0
  258. package/templates/shared-project-workspace/governance/repo-boundary-ledger.md +5 -0
  259. package/templates/shared-project-workspace/project/README.md +19 -0
  260. package/templates/shared-project-workspace/repo-access.v1.json +9 -0
@@ -0,0 +1,323 @@
1
+ #!/usr/bin/env node
2
+
3
+ import fs from 'node:fs'
4
+ import path from 'node:path'
5
+ import { fileURLToPath } from 'node:url'
6
+
7
+ const __dirname = path.dirname(fileURLToPath(import.meta.url))
8
+ const packageRoot = path.resolve(__dirname, '../..')
9
+
10
+ export const TEST_FIXTURE_ALLOW_MARKER = '@atelier-egress-allow-test-fixture'
11
+ export const LOCAL_COMPUTED_ALLOW_MARKER = '@atelier-egress-local-computed'
12
+
13
+ export const DEFAULT_EGRESS_SCAN_PATHS = [
14
+ 'src',
15
+ 'scripts',
16
+ 'bin',
17
+ 'templates',
18
+ 'examples',
19
+ 'skills',
20
+ ]
21
+
22
+ const SCRIPT_EXTS = new Set(['.js', '.mjs', '.cjs', '.ts', '.mts', '.sh'])
23
+ const MARKUP_EXTS = new Set(['.html', '.htm', '.svg'])
24
+ const SKIP_DIRS = new Set(['node_modules', '.git'])
25
+ const INTERNAL_SCANNER_FILES = new Set(['forbidden-egress.mjs'])
26
+
27
+ function isTestFile(file) {
28
+ return /(?:^|[/.])test(?:s)?[/.]/.test(file) || /\.(?:test|spec)\.[cm]?[jt]s$/.test(file)
29
+ }
30
+
31
+ function isMarkedTestFixture(file, text) {
32
+ return isTestFile(file) && text.includes(TEST_FIXTURE_ALLOW_MARKER)
33
+ }
34
+
35
+ function shouldScanFile(file, { includeTests = false } = {}) {
36
+ const ext = path.extname(file)
37
+ if (!SCRIPT_EXTS.has(ext) && !MARKUP_EXTS.has(ext)) return false
38
+ if (INTERNAL_SCANNER_FILES.has(path.basename(file))) return false
39
+ if (!includeTests && isTestFile(file)) return false
40
+ return true
41
+ }
42
+
43
+ function walk(dir, options = {}) {
44
+ if (!fs.existsSync(dir)) return []
45
+ const out = []
46
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
47
+ if (entry.isDirectory()) {
48
+ if (!SKIP_DIRS.has(entry.name)) out.push(...walk(path.join(dir, entry.name), options))
49
+ } else {
50
+ const file = path.join(dir, entry.name)
51
+ if (shouldScanFile(file, options)) out.push(file)
52
+ }
53
+ }
54
+ return out
55
+ }
56
+
57
+ function uniqueFiles(files) {
58
+ return [...new Set(files.map((file) => path.resolve(file)))]
59
+ }
60
+
61
+ export function discoverForbiddenEgressScanFiles({
62
+ root = packageRoot,
63
+ scanPaths = DEFAULT_EGRESS_SCAN_PATHS,
64
+ includeTests = false,
65
+ } = {}) {
66
+ const files = []
67
+ for (const rel of scanPaths) {
68
+ const abs = path.resolve(root, rel)
69
+ if (!fs.existsSync(abs)) continue
70
+ const stat = fs.statSync(abs)
71
+ if (stat.isDirectory()) files.push(...walk(abs, { includeTests }))
72
+ else if (shouldScanFile(abs, { includeTests })) files.push(abs)
73
+ }
74
+ return uniqueFiles(files)
75
+ }
76
+
77
+ function hostnameFromNetworkUrl(value) {
78
+ try {
79
+ return new URL(value).hostname
80
+ } catch {
81
+ const match = String(value || '').match(/^(?:https?|wss?):\/\/(\[[^\]]+\]|[^/:${}\s]+)/i)
82
+ if (match) return match[1].replace(/^\[|\]$/g, '')
83
+ return ''
84
+ }
85
+ }
86
+
87
+ function isLocalHostname(hostname) {
88
+ const clean = String(hostname || '').toLowerCase()
89
+ return clean === 'localhost' || clean === '::1' || /^127(?:\.\d{1,3}){3}$/.test(clean)
90
+ }
91
+
92
+ function isNetworkUrl(value) {
93
+ return /^(?:https?|wss?):\/\//i.test(String(value || ''))
94
+ }
95
+
96
+ function isLocalUrl(value) {
97
+ if (!isNetworkUrl(value)) return false
98
+ return isLocalHostname(hostnameFromNetworkUrl(value))
99
+ }
100
+
101
+ function stringLiterals(text) {
102
+ const literals = []
103
+ const re = /(['"`])((?:\\.|(?!\1)[\s\S])*)\1/g
104
+ let match
105
+ while ((match = re.exec(text))) {
106
+ literals.push(match[2])
107
+ }
108
+ return literals
109
+ }
110
+
111
+ function firstNonLocalNetworkLiteral(text) {
112
+ return stringLiterals(text).find((literal) => isNetworkUrl(literal) && !isLocalUrl(literal))
113
+ }
114
+
115
+ function firstLocalNetworkLiteral(text) {
116
+ return stringLiterals(text).find((literal) => isLocalUrl(literal))
117
+ }
118
+
119
+ function firstRelativePathLiteral(text) {
120
+ return stringLiterals(text).find((literal) => /^\/(?!\/)/.test(literal))
121
+ }
122
+
123
+ function firstNonLocalHostLiteral(text) {
124
+ return stringLiterals(text).find((literal) => {
125
+ if (!/^[a-z0-9.-]+\.[a-z]{2,}$/i.test(literal)) return false
126
+ return !isLocalHostname(literal)
127
+ })
128
+ }
129
+
130
+ function firstLocalHostLiteral(text) {
131
+ return stringLiterals(text).find((literal) => isLocalHostname(literal))
132
+ }
133
+
134
+ function callBlock(lines, index) {
135
+ return lines.slice(index, Math.min(lines.length, index + 5)).join('\n')
136
+ }
137
+
138
+ function isFixtureAllowed(lines, index, file) {
139
+ return isTestFile(file) && callBlock(lines, Math.max(0, index - 2)).includes(TEST_FIXTURE_ALLOW_MARKER)
140
+ }
141
+
142
+ function isLocalComputedAllowed(lines, index) {
143
+ return callBlock(lines, Math.max(0, index - 2)).includes(LOCAL_COMPUTED_ALLOW_MARKER)
144
+ }
145
+
146
+ function trimmedCodeLine(line) {
147
+ return line.replace(/\/\/.*$/, '').trim()
148
+ }
149
+
150
+ function addFinding(findings, file, line, type, detail) {
151
+ findings.push({ file, line, type, detail })
152
+ }
153
+
154
+ function jsFindingsForBlock(block, { findings, file, line, allowLocalComputed = false }) {
155
+ const nonLocalNetwork = firstNonLocalNetworkLiteral(block)
156
+ const localNetwork = firstLocalNetworkLiteral(block)
157
+ const relativePath = firstRelativePathLiteral(block)
158
+ const nonLocalHost = firstNonLocalHostLiteral(block)
159
+ const localHost = firstLocalHostLiteral(block)
160
+
161
+ if (/\bfetch\s*\(/.test(block)) {
162
+ if (nonLocalNetwork) addFinding(findings, file, line, 'non-localhost-fetch', nonLocalNetwork)
163
+ else if (!localNetwork && !relativePath && !allowLocalComputed) addFinding(findings, file, line, 'fetch-unresolved', 'computed fetch target')
164
+ }
165
+ if (/\bnew\s+WebSocket\s*\(/.test(block)) {
166
+ if (nonLocalNetwork) addFinding(findings, file, line, 'websocket-egress', nonLocalNetwork)
167
+ else if (!localNetwork && !relativePath && !allowLocalComputed) addFinding(findings, file, line, 'websocket-unresolved', 'computed websocket target')
168
+ }
169
+ if (/\bhttps?\.(?:request|get)\s*\(/.test(block) || /\bhttp2\.connect\s*\(/.test(block)) {
170
+ if (nonLocalNetwork || nonLocalHost) addFinding(findings, file, line, 'non-localhost-http-request', nonLocalNetwork || nonLocalHost)
171
+ else if (!localNetwork && !localHost && !allowLocalComputed) addFinding(findings, file, line, 'http-request-unresolved', 'computed HTTP request target')
172
+ }
173
+ if (/\bnavigator\.sendBeacon\s*\(/.test(block) || /\bnew\s+EventSource\s*\(/.test(block)) {
174
+ if (nonLocalNetwork) addFinding(findings, file, line, 'beacon-egress', nonLocalNetwork)
175
+ else if (!localNetwork && !relativePath && !allowLocalComputed) addFinding(findings, file, line, 'beacon-unresolved', 'computed beacon target')
176
+ }
177
+ if (/\bimport\s*\(/.test(block) && nonLocalNetwork) {
178
+ addFinding(findings, file, line, 'dynamic-import-egress', nonLocalNetwork)
179
+ }
180
+ if ((/\bnew\s+XMLHttpRequest\b/.test(block) || /\bnew\s+Image\s*\(/.test(block)) && nonLocalNetwork) {
181
+ addFinding(findings, file, line, 'browser-request-egress', nonLocalNetwork)
182
+ }
183
+ if (/\bnet\.connect\s*\(/.test(block) || /\bnet\.createConnection\s*\(/.test(block)) {
184
+ if (nonLocalHost || nonLocalNetwork) addFinding(findings, file, line, 'net-connect-egress', nonLocalHost || nonLocalNetwork)
185
+ else if (!localHost && !localNetwork && !allowLocalComputed) addFinding(findings, file, line, 'net-connect-unresolved', 'computed socket target')
186
+ }
187
+ if (/\bdns\.(?:resolve|lookup|promises\.resolve|promises\.lookup)\s*\(/.test(block)) {
188
+ if (nonLocalHost) addFinding(findings, file, line, 'dns-egress', nonLocalHost)
189
+ else if (!localHost && !allowLocalComputed) addFinding(findings, file, line, 'dns-unresolved', 'computed DNS target')
190
+ }
191
+ }
192
+
193
+ function shellWords(line) {
194
+ return line.match(/(?:[^\s'"`]+|'[^']*'|"[^"]*")+/g) || []
195
+ }
196
+
197
+ function unquote(value) {
198
+ return String(value || '').replace(/^['"]|['"]$/g, '')
199
+ }
200
+
201
+ function firstNonLocalNetworkWord(line) {
202
+ return shellWords(line).map(unquote).find((word) => isNetworkUrl(word) && !isLocalUrl(word))
203
+ }
204
+
205
+ function firstLocalNetworkWord(line) {
206
+ return shellWords(line).map(unquote).find((word) => isLocalUrl(word))
207
+ }
208
+
209
+ function firstShellHostArg(line, commandRe) {
210
+ if (!commandRe.test(line)) return ''
211
+ const words = shellWords(line).map(unquote)
212
+ for (const word of words.slice(1)) {
213
+ const candidate = word.includes(':') ? word.split(':')[0] : word
214
+ if (/^[a-z0-9.-]+\.[a-z]{2,}$/i.test(candidate) && !isLocalHostname(candidate)) return candidate
215
+ }
216
+ return ''
217
+ }
218
+
219
+ function shellFindingsForLine(line, { findings, file, lineNumber }) {
220
+ const clean = String(line || '').trim()
221
+ if (!clean || clean.startsWith('#')) return
222
+ if (/\b(?:curl|wget)\b/.test(clean)) {
223
+ const nonLocal = firstNonLocalNetworkWord(clean)
224
+ const local = firstLocalNetworkWord(clean)
225
+ if (nonLocal) addFinding(findings, file, lineNumber, 'shell-http-egress', nonLocal)
226
+ else if (!local && /\bhttps?:\/\//.test(clean)) addFinding(findings, file, lineNumber, 'shell-http-unresolved', 'computed shell HTTP target')
227
+ }
228
+ const socketHost = firstShellHostArg(clean, /\b(?:nc|ncat|telnet)\b/)
229
+ if (socketHost) addFinding(findings, file, lineNumber, 'shell-socket-egress', socketHost)
230
+ const opensslHost = firstShellHostArg(clean, /\bopenssl\s+s_client\b/)
231
+ if (opensslHost) addFinding(findings, file, lineNumber, 'shell-socket-egress', opensslHost)
232
+ }
233
+
234
+ const HTTP_CLIENT_MODULES = /\b(?:import|require)\b[^\n]*['"](?:undici|axios|node-fetch|got|superagent|needle|phin)['"]/
235
+
236
+ function lineFindingsForCode(lineCode, { findings, file, lineNumber }) {
237
+ // A CSP directive string that names an external origin authorizes egress
238
+ // from every page it is served with, without any call site to match.
239
+ if (/(?:-src|content-security-policy)/i.test(lineCode)) {
240
+ const external = firstNonLocalNetworkLiteral(lineCode) ?? (isNetworkUrl(lineCode.match(/https?:\/\/[^\s'"`;]+/i)?.[0]) && !isLocalUrl(lineCode.match(/https?:\/\/[^\s'"`;]+/i)?.[0]) ? lineCode.match(/https?:\/\/[^\s'"`;]+/i)[0] : null)
241
+ if (external) addFinding(findings, file, lineNumber, 'csp-external-origin', external)
242
+ }
243
+ if (HTTP_CLIENT_MODULES.test(lineCode)) {
244
+ addFinding(findings, file, lineNumber, 'http-client-import', 'third-party HTTP client module')
245
+ }
246
+ }
247
+
248
+ function markupFindingsForLine(line, { findings, file, lineNumber }) {
249
+ const attrRe = /(?:src|href|action|data|poster|formaction)\s*=\s*["']?((?:https?:)?\/\/[^"'\s>]+)/gi
250
+ let match
251
+ while ((match = attrRe.exec(line))) {
252
+ const url = match[1].startsWith('//') ? `https:${match[1]}` : match[1]
253
+ if (isNetworkUrl(url) && !isLocalUrl(url)) {
254
+ addFinding(findings, file, lineNumber, 'markup-external-resource', url)
255
+ }
256
+ }
257
+ }
258
+
259
+ export function forbiddenEgressFindingsForText(text, { file = 'input' } = {}) {
260
+ if (isMarkedTestFixture(file, text)) return []
261
+ const findings = []
262
+ const lines = String(text || '').split(/\r?\n/)
263
+ const isMarkup = MARKUP_EXTS.has(path.extname(file))
264
+
265
+ for (let index = 0; index < lines.length; index += 1) {
266
+ if (isFixtureAllowed(lines, index, file)) continue
267
+ const lineCode = trimmedCodeLine(lines[index])
268
+ const hasJsPrimitive = /\b(?:fetch|https?\.(?:request|get)|http2\.connect|navigator\.sendBeacon|new\s+EventSource|new\s+WebSocket|net\.connect|net\.createConnection|dns\.(?:resolve|lookup|promises\.resolve|promises\.lookup)|import)\s*\(/.test(lineCode)
269
+ || /\bnew\s+(?:XMLHttpRequest|Image)\b/.test(lineCode)
270
+ const block = callBlock(lines, index)
271
+ if (hasJsPrimitive) {
272
+ jsFindingsForBlock(block, {
273
+ findings,
274
+ file,
275
+ line: index + 1,
276
+ allowLocalComputed: isLocalComputedAllowed(lines, index),
277
+ })
278
+ }
279
+ // Raw line, not the comment-stripped one: a URL's own // reads as a
280
+ // comment and would truncate the literal these checks exist to find.
281
+ lineFindingsForCode(lines[index], { findings, file, lineNumber: index + 1 })
282
+ if (isMarkup) markupFindingsForLine(lines[index], { findings, file, lineNumber: index + 1 })
283
+ shellFindingsForLine(lines[index], { findings, file, lineNumber: index + 1 })
284
+ }
285
+
286
+ const seen = new Set()
287
+ return findings.filter((finding) => {
288
+ const key = `${finding.file}:${finding.line}:${finding.type}:${finding.detail}`
289
+ if (seen.has(key)) return false
290
+ seen.add(key)
291
+ return true
292
+ })
293
+ }
294
+
295
+ export function checkForbiddenEgress({
296
+ root = packageRoot,
297
+ scanPaths = DEFAULT_EGRESS_SCAN_PATHS,
298
+ includeTests = false,
299
+ } = {}) {
300
+ const findings = []
301
+ for (const file of discoverForbiddenEgressScanFiles({ root, scanPaths, includeTests })) {
302
+ const rel = path.relative(root, file)
303
+ findings.push(...forbiddenEgressFindingsForText(fs.readFileSync(file, 'utf8'), { file: rel }))
304
+ }
305
+ return findings
306
+ }
307
+
308
+ function main() {
309
+ const findings = checkForbiddenEgress()
310
+ if (findings.length) {
311
+ for (const finding of findings) {
312
+ console.error(`[atelier-egress] ${finding.file}:${finding.line} ${finding.type}: ${finding.detail}`)
313
+ }
314
+ process.exitCode = 1
315
+ return
316
+ }
317
+ console.log('[atelier-egress] no forbidden network egress found')
318
+ }
319
+
320
+ const currentFile = fileURLToPath(import.meta.url)
321
+ if (process.argv[1] && path.resolve(process.argv[1]) === currentFile) {
322
+ main()
323
+ }