@keithadler/frostjs 0.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 (179) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +568 -0
  3. package/dist/baseline.d.ts +18 -0
  4. package/dist/baseline.d.ts.map +1 -0
  5. package/dist/baseline.js +56 -0
  6. package/dist/baseline.js.map +1 -0
  7. package/dist/changed.d.ts +15 -0
  8. package/dist/changed.d.ts.map +1 -0
  9. package/dist/changed.js +93 -0
  10. package/dist/changed.js.map +1 -0
  11. package/dist/cli/args.d.ts +37 -0
  12. package/dist/cli/args.d.ts.map +1 -0
  13. package/dist/cli/args.js +200 -0
  14. package/dist/cli/args.js.map +1 -0
  15. package/dist/cli/main.d.ts +3 -0
  16. package/dist/cli/main.d.ts.map +1 -0
  17. package/dist/cli/main.js +8 -0
  18. package/dist/cli/main.js.map +1 -0
  19. package/dist/cli/run.d.ts +9 -0
  20. package/dist/cli/run.d.ts.map +1 -0
  21. package/dist/cli/run.js +404 -0
  22. package/dist/cli/run.js.map +1 -0
  23. package/dist/discover/index.d.ts +15 -0
  24. package/dist/discover/index.d.ts.map +1 -0
  25. package/dist/discover/index.js +70 -0
  26. package/dist/discover/index.js.map +1 -0
  27. package/dist/eslint.d.ts +103 -0
  28. package/dist/eslint.d.ts.map +1 -0
  29. package/dist/eslint.js +111 -0
  30. package/dist/eslint.js.map +1 -0
  31. package/dist/extract/annotations.d.ts +5 -0
  32. package/dist/extract/annotations.d.ts.map +1 -0
  33. package/dist/extract/annotations.js +5 -0
  34. package/dist/extract/annotations.js.map +1 -0
  35. package/dist/extract/ast.d.ts +45 -0
  36. package/dist/extract/ast.d.ts.map +1 -0
  37. package/dist/extract/ast.js +60 -0
  38. package/dist/extract/ast.js.map +1 -0
  39. package/dist/extract/capability.d.ts +22 -0
  40. package/dist/extract/capability.d.ts.map +1 -0
  41. package/dist/extract/capability.js +2 -0
  42. package/dist/extract/capability.js.map +1 -0
  43. package/dist/extract/html.d.ts +15 -0
  44. package/dist/extract/html.d.ts.map +1 -0
  45. package/dist/extract/html.js +69 -0
  46. package/dist/extract/html.js.map +1 -0
  47. package/dist/extract/index.d.ts +9 -0
  48. package/dist/extract/index.d.ts.map +1 -0
  49. package/dist/extract/index.js +95 -0
  50. package/dist/extract/index.js.map +1 -0
  51. package/dist/extract/recognizers/codegen.d.ts +10 -0
  52. package/dist/extract/recognizers/codegen.d.ts.map +1 -0
  53. package/dist/extract/recognizers/codegen.js +39 -0
  54. package/dist/extract/recognizers/codegen.js.map +1 -0
  55. package/dist/extract/recognizers/dom-escape.d.ts +11 -0
  56. package/dist/extract/recognizers/dom-escape.d.ts.map +1 -0
  57. package/dist/extract/recognizers/dom-escape.js +58 -0
  58. package/dist/extract/recognizers/dom-escape.js.map +1 -0
  59. package/dist/extract/recognizers/globals.d.ts +9 -0
  60. package/dist/extract/recognizers/globals.d.ts.map +1 -0
  61. package/dist/extract/recognizers/globals.js +121 -0
  62. package/dist/extract/recognizers/globals.js.map +1 -0
  63. package/dist/extract/recognizers/identity.d.ts +10 -0
  64. package/dist/extract/recognizers/identity.d.ts.map +1 -0
  65. package/dist/extract/recognizers/identity.js +79 -0
  66. package/dist/extract/recognizers/identity.js.map +1 -0
  67. package/dist/extract/recognizers/navigation.d.ts +12 -0
  68. package/dist/extract/recognizers/navigation.d.ts.map +1 -0
  69. package/dist/extract/recognizers/navigation.js +110 -0
  70. package/dist/extract/recognizers/navigation.js.map +1 -0
  71. package/dist/extract/recognizers/network.d.ts +11 -0
  72. package/dist/extract/recognizers/network.d.ts.map +1 -0
  73. package/dist/extract/recognizers/network.js +41 -0
  74. package/dist/extract/recognizers/network.js.map +1 -0
  75. package/dist/extract/recognizers/resolve.d.ts +37 -0
  76. package/dist/extract/recognizers/resolve.d.ts.map +1 -0
  77. package/dist/extract/recognizers/resolve.js +72 -0
  78. package/dist/extract/recognizers/resolve.js.map +1 -0
  79. package/dist/extract/recognizers/storage.d.ts +9 -0
  80. package/dist/extract/recognizers/storage.d.ts.map +1 -0
  81. package/dist/extract/recognizers/storage.js +37 -0
  82. package/dist/extract/recognizers/storage.js.map +1 -0
  83. package/dist/extract/recognizers/types.d.ts +38 -0
  84. package/dist/extract/recognizers/types.d.ts.map +1 -0
  85. package/dist/extract/recognizers/types.js +25 -0
  86. package/dist/extract/recognizers/types.js.map +1 -0
  87. package/dist/extract/recognizers/worker.d.ts +11 -0
  88. package/dist/extract/recognizers/worker.d.ts.map +1 -0
  89. package/dist/extract/recognizers/worker.js +41 -0
  90. package/dist/extract/recognizers/worker.js.map +1 -0
  91. package/dist/extract/scope.d.ts +23 -0
  92. package/dist/extract/scope.d.ts.map +1 -0
  93. package/dist/extract/scope.js +299 -0
  94. package/dist/extract/scope.js.map +1 -0
  95. package/dist/extract/suppress.d.ts +21 -0
  96. package/dist/extract/suppress.d.ts.map +1 -0
  97. package/dist/extract/suppress.js +42 -0
  98. package/dist/extract/suppress.js.map +1 -0
  99. package/dist/extract/target.d.ts +34 -0
  100. package/dist/extract/target.d.ts.map +1 -0
  101. package/dist/extract/target.js +81 -0
  102. package/dist/extract/target.js.map +1 -0
  103. package/dist/extract/typescript.d.ts +12 -0
  104. package/dist/extract/typescript.d.ts.map +1 -0
  105. package/dist/extract/typescript.js +56 -0
  106. package/dist/extract/typescript.js.map +1 -0
  107. package/dist/extract/walk.d.ts +14 -0
  108. package/dist/extract/walk.d.ts.map +1 -0
  109. package/dist/extract/walk.js +70 -0
  110. package/dist/extract/walk.js.map +1 -0
  111. package/dist/index.d.ts +4 -0
  112. package/dist/index.d.ts.map +1 -0
  113. package/dist/index.js +4 -0
  114. package/dist/index.js.map +1 -0
  115. package/dist/init.d.ts +10 -0
  116. package/dist/init.d.ts.map +1 -0
  117. package/dist/init.js +79 -0
  118. package/dist/init.js.map +1 -0
  119. package/dist/policy/compile.d.ts +56 -0
  120. package/dist/policy/compile.d.ts.map +1 -0
  121. package/dist/policy/compile.js +78 -0
  122. package/dist/policy/compile.js.map +1 -0
  123. package/dist/policy/config.d.ts +18 -0
  124. package/dist/policy/config.d.ts.map +1 -0
  125. package/dist/policy/config.js +70 -0
  126. package/dist/policy/config.js.map +1 -0
  127. package/dist/policy/csp.d.ts +19 -0
  128. package/dist/policy/csp.d.ts.map +1 -0
  129. package/dist/policy/csp.js +47 -0
  130. package/dist/policy/csp.js.map +1 -0
  131. package/dist/policy/glob.d.ts +12 -0
  132. package/dist/policy/glob.d.ts.map +1 -0
  133. package/dist/policy/glob.js +63 -0
  134. package/dist/policy/glob.js.map +1 -0
  135. package/dist/policy/index.d.ts +47 -0
  136. package/dist/policy/index.d.ts.map +1 -0
  137. package/dist/policy/index.js +34 -0
  138. package/dist/policy/index.js.map +1 -0
  139. package/dist/policy/parse.d.ts +51 -0
  140. package/dist/policy/parse.d.ts.map +1 -0
  141. package/dist/policy/parse.js +352 -0
  142. package/dist/policy/parse.js.map +1 -0
  143. package/dist/policy/vocabulary.d.ts +15 -0
  144. package/dist/policy/vocabulary.d.ts.map +1 -0
  145. package/dist/policy/vocabulary.js +99 -0
  146. package/dist/policy/vocabulary.js.map +1 -0
  147. package/dist/registry.d.ts +45 -0
  148. package/dist/registry.d.ts.map +1 -0
  149. package/dist/registry.js +85 -0
  150. package/dist/registry.js.map +1 -0
  151. package/dist/report/github.d.ts +10 -0
  152. package/dist/report/github.d.ts.map +1 -0
  153. package/dist/report/github.js +31 -0
  154. package/dist/report/github.js.map +1 -0
  155. package/dist/report/json.d.ts +44 -0
  156. package/dist/report/json.d.ts.map +1 -0
  157. package/dist/report/json.js +40 -0
  158. package/dist/report/json.js.map +1 -0
  159. package/dist/report/sarif.d.ts +10 -0
  160. package/dist/report/sarif.d.ts.map +1 -0
  161. package/dist/report/sarif.js +64 -0
  162. package/dist/report/sarif.js.map +1 -0
  163. package/dist/report/summary.d.ts +9 -0
  164. package/dist/report/summary.d.ts.map +1 -0
  165. package/dist/report/summary.js +67 -0
  166. package/dist/report/summary.js.map +1 -0
  167. package/dist/report/text.d.ts +37 -0
  168. package/dist/report/text.d.ts.map +1 -0
  169. package/dist/report/text.js +81 -0
  170. package/dist/report/text.js.map +1 -0
  171. package/dist/sync.d.ts +19 -0
  172. package/dist/sync.d.ts.map +1 -0
  173. package/dist/sync.js +131 -0
  174. package/dist/sync.js.map +1 -0
  175. package/dist/version.d.ts +2 -0
  176. package/dist/version.d.ts.map +1 -0
  177. package/dist/version.js +6 -0
  178. package/dist/version.js.map +1 -0
  179. package/package.json +88 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Keith Adler
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to frostjs persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,568 @@
1
+ # frostjs
2
+
3
+ **The model wrote it. Did anyone decide it could do that?**
4
+
5
+ frostjs is a deny-by-default capability gate for JavaScript. You write a
6
+ policy that fits on one screen, in plain words:
7
+
8
+ ```
9
+ may reach "api.example.com"
10
+ may use session storage
11
+ forbid cookies -- consent banner owns these
12
+ ```
13
+
14
+ and the build fails on anything the code reaches for that the policy does
15
+ not grant: reading storage, setting cookies, calling `eval`, injecting a
16
+ `<script>`, opening a WebSocket to a host you have never heard of. The
17
+ report names the file, the line, the expression, and the policy line that
18
+ said no.
19
+
20
+ ## Why every AI-assisted JavaScript project needs this in the pipeline
21
+
22
+ Code review was built for code written by a colleague at human speed. An
23
+ assistant writes a hundred lines in the time it takes to read ten, and
24
+ none of those lines arrive with an intent attached. A prompt says "cache
25
+ the results"; the model reaches for `localStorage`. A prompt says "load the
26
+ physics engine"; the model writes `import("https://cdn.skypack.dev/...")`.
27
+ A prompt says "make the markdown render"; the model assigns `innerHTML`.
28
+ Each is a reasonable reading of the words, each is a capability your
29
+ application now has, and nobody decided it.
30
+
31
+ A test suite does not catch this: the code works. A linter does not catch
32
+ this: the code is well formed. A human reviewer skims it, because the
33
+ diff is long and the code looks fine, which it is. The only thing that
34
+ catches it is a rule that says what this project may do, written down
35
+ before the code was, and a build that enforces it. That is the whole
36
+ tool.
37
+
38
+ frostjs gives you:
39
+
40
+ - **Deny by default.** Everything is off until the policy turns it on. A
41
+ new capability cannot arrive unnoticed, whoever or whatever wrote it.
42
+ - **A policy a non-engineer can read and sign off.** `frostjs summary`
43
+ prints it in English. `frostjs csp` turns the same file into your
44
+ `Content-Security-Policy` header, so the build-time gate and the runtime
45
+ backstop cannot disagree.
46
+ - **Zero false positives as the product.** Real scope analysis, so a local
47
+ named `fetch` is not a network call. Every engine change runs against 21
48
+ MB of real, pinned, hash-verified JavaScript and the finding count must
49
+ not move.
50
+ - **Adoption in one command.** `frostjs init src` writes a policy that
51
+ grants exactly what the code does today, with a note on each line saying
52
+ where. The first run passes. Then you delete what should not be allowed.
53
+ - **Dependencies by fingerprint.** Vendored files are admitted by hash with
54
+ the capabilities someone reviewed; a version bump that gains a network
55
+ destination is refused and shown as a diff.
56
+ - **Exceptions that expire.** `may use local storage in "src/legacy/*"
57
+ until 2026-12-01` warns for two weeks, then fails. Drift has a deadline.
58
+
59
+ CLI, GitHub Action, pre-commit hook, ESLint plugin. One runtime
60
+ dependency. MIT.
61
+
62
+ ## Two minutes to a gated pipeline
63
+
64
+ ```bash
65
+ npm install -D @keithadler/frostjs
66
+ npx frostjs init src # writes frostjs.policy from what the code does today
67
+ git add frostjs.policy # commit it; it is the contract
68
+ npx frostjs src # passes, because init granted what exists
69
+ ```
70
+
71
+ The package is scoped (`@keithadler/frostjs`) because npm's typosquat
72
+ rules reserve the bare name; the command it installs is `frostjs`.
73
+
74
+ Then add the check to CI:
75
+
76
+ ```yaml
77
+ # .github/workflows/frostjs.yml
78
+ on: [pull_request]
79
+ jobs:
80
+ frostjs:
81
+ runs-on: ubuntu-latest
82
+ steps:
83
+ - uses: actions/checkout@v4
84
+ with: { fetch-depth: 0 }
85
+ - uses: keithadler/frostjs@main
86
+ with:
87
+ paths: src
88
+ args: --changed-since origin/main
89
+ ```
90
+
91
+ From here on, a pull request that reaches for a capability the policy does
92
+ not grant fails with an inline annotation on the offending line. To allow
93
+ it, someone edits `frostjs.policy` in the same PR, and the reviewer sees
94
+ both changes side by side. That is the decision being made visible.
95
+
96
+ ## For AI coding agents
97
+
98
+ If an assistant writes code in your project, tell it the gate exists.
99
+ Paste this into `CLAUDE.md`, `AGENTS.md`, `.cursorrules`, or whatever your
100
+ tool reads:
101
+
102
+ ```markdown
103
+ ## frostjs
104
+
105
+ This project is gated by frostjs (https://github.com/keithadler/frostjs,
106
+ npm package @keithadler/frostjs, installed as a dev dependency).
107
+ `frostjs.policy` at the repository root says which browser capabilities
108
+ the code may use: storage, network destinations, code generation, HTML
109
+ injection, identity, navigation, globals, workers. Everything else is
110
+ denied.
111
+
112
+ - Before finishing any change to JavaScript or TypeScript, run
113
+ `npx frostjs <paths you changed>` and make it pass.
114
+ - Read a denial as a question, not an obstacle. If the task genuinely needs
115
+ the capability, add the narrowest grant that covers it to
116
+ `frostjs.policy` (scope it with `in "<file>"`, name hosts with
117
+ `may reach "<host>"`, add `until <date>` if it is temporary) and say in
118
+ your summary that you widened the policy and why. If the task does not
119
+ need it, change the code instead.
120
+ - Never add `may use everything`, never add `// frostjs: ignore` to make a
121
+ build pass, and never delete or loosen an existing `forbid` line. Those
122
+ are a person's decisions.
123
+ - `npx frostjs summary` prints the policy in plain English if you need to
124
+ explain it.
125
+ ```
126
+
127
+ The instructions above are also in [`AGENTS.md`](AGENTS.md) in this
128
+ repository, which applies them to frostjs's own code.
129
+
130
+ ## Where it has already found something
131
+
132
+ three.js 0.160.0 ships `examples/jsm/libs/ecsy.module.js`, which, if a page
133
+ that imports it is opened with `?enable-remote-devtools` in the URL, loads a
134
+ script from a third-party CDN, connects to a third-party relay, and `eval`s
135
+ whatever the relay sends. frostjs reports it as
136
+ `codegen.eval denied by default (no rule grants it): eval(data.script)` under any policy
137
+ an application would plausibly write. The same run names a runtime
138
+ `import()` of physics engine code from `cdn.skypack.dev`. The full story, the
139
+ policy, the CSP it emits and the honest count of what else the policy
140
+ flags are in [SHOWCASE.md](SHOWCASE.md).
141
+
142
+ ## Status
143
+
144
+ Pre-alpha, feature complete against [REQUIREMENTS.md](REQUIREMENTS.md): all
145
+ eight capability families, frost-dialect policies, scope analysis,
146
+ baselines, changed-lines mode, json/sarif/github output, a GitHub Action,
147
+ an ESLint plugin, a fingerprint registry for vendored code with SRI output,
148
+ TypeScript, JSX and inline HTML. Run on Excalidraw (656 files, a
149
+ TypeScript and React monorepo) it finishes in under a second and
150
+ `frostjs init` writes an 18-line policy; every finding was checked by hand.
151
+ Not yet published to npm.
152
+
153
+ ```
154
+ $ cat frostjs.policy
155
+ policy "checkout-widget"
156
+ may use session storage
157
+ may use local storage in "src/legacy/*" -- old code, rewrite by Q4
158
+ forbid cookies -- consent banner owns these
159
+ may use the cache until 2026-08-30 -- service worker experiment
160
+
161
+ $ frostjs src
162
+ src/app.js:2:1: storage.local denied by default (no rule grants it): localStorage.setItem("not-here", 1)
163
+ src/legacy/old.js:2:1: storage.cookie denied by "forbid cookies" (line 4): consent banner owns these: document.cookie
164
+
165
+ warning: frostjs.policy line 5: "may use the cache until 2026-08-30" expires in 7 days
166
+
167
+ 3 files, 2 denied, 0 unknown
168
+ $ echo $?
169
+ 1
170
+ ```
171
+
172
+ ## Starting out
173
+
174
+ ```bash
175
+ npm install -D @keithadler/frostjs
176
+ npx frostjs init src
177
+ ```
178
+
179
+ writes a `frostjs.policy` in the current directory that grants exactly
180
+ what the code under `src` does today, one line per capability, scoped to
181
+ the files that use it when there are only a few, each with a note saying
182
+ where. The first check passes. Then read the file and delete what should
183
+ not be allowed; the build starts refusing it. A network destination the
184
+ code builds at runtime is called out in a hint rather than quietly widened
185
+ to `may use the network`. For a large codebase with debt you would rather
186
+ pay down than grant, use `--baseline` instead (below).
187
+
188
+ ## Policy files
189
+
190
+ A policy is a `frostjs.policy` file in frost's policy dialect: one rule per
191
+ line, `--` or `#` comments, case-insensitive keywords. Deny-by-default, so
192
+ the file only ever grants. A trailing comment on a rule is its *hint*, and is
193
+ printed whenever that rule refuses something.
194
+
195
+ ```
196
+ policy "<name>" optional, once
197
+ ignore "<glob>", ... files not analyzed at all
198
+ vendored "<glob>", ... third-party files, checked by fingerprint
199
+ may use <capability> [in "<glob>", ...] [until YYYY-MM-DD]
200
+ may reach "<host>", ... [in "<glob>", ...] [until YYYY-MM-DD]
201
+ forbid [using] <capability> [in "<glob>", ...]
202
+ forbid reaching "<host>", ... [in "<glob>", ...]
203
+ forbid everything else optional, readability only
204
+ ```
205
+
206
+ `<capability>` is a phrase or a code. A family name grants the whole family.
207
+
208
+ | phrase | code |
209
+ | --- | --- |
210
+ | `storage` | `storage` (every member below) |
211
+ | `local storage` | `storage.local` |
212
+ | `session storage` | `storage.session` |
213
+ | `cookies` | `storage.cookie` |
214
+ | `indexeddb` | `storage.indexeddb` |
215
+ | `the cache`, `caches` | `storage.cache` |
216
+ | `navigator storage` | `storage.navigator` |
217
+ | `the network` | `network` (any destination) |
218
+ | `code generation`, `eval` | `codegen` |
219
+ | `html injection` | `dom-escape` |
220
+ | `identity`, `fingerprinting` | `identity` |
221
+ | `navigation` | `navigation` |
222
+ | `globals` | `globals` |
223
+ | `workers`, `service workers` | `worker` |
224
+ | `everything` | `*` |
225
+
226
+ Rules:
227
+
228
+ - `forbid` always wins over `may`, so `may use storage` + `forbid cookies`
229
+ grants everything in storage except cookies.
230
+ - `in` scopes a rule to path globs (`*` within a segment, `**` across
231
+ segments, a bare name matches at any depth, a plain directory matches
232
+ everything beneath it). Globs are relative to the policy file's directory.
233
+ - `ignore` skips files entirely: generated bundles, test fixtures, anything
234
+ that is not your code to police. It lives in the policy so the exception
235
+ is visible in review, where `--exclude` on the command line is not.
236
+ - `may reach` grants the network family only to the named hosts. `*` in a
237
+ host spans any characters (`*.internal`). `"same-origin"` names relative
238
+ URLs. A destination that cannot be read from the code is **not** allowed by
239
+ a host list: cannot be shown to be allowed is not allowed. Grant
240
+ `may use the network` if you really mean any destination.
241
+ - `until` puts an expiry on a grant. Inside the last 14 days the build warns;
242
+ after the date the grant denies with its own message. This is how drift is
243
+ fought: an exception has to be renewed on purpose.
244
+
245
+ `frostjs.policy` is searched for in the directory shared by all the given
246
+ paths, then upward; the nearest one wins, so a monorepo can keep one per
247
+ tenant directory. `--policy <file>` overrides the search. With no policy at
248
+ all, every capability is denied and a note says so.
249
+
250
+ ## Usage
251
+
252
+ ```
253
+ frostjs init [paths] write a starter frostjs.policy granting what the code does today
254
+ frostjs <paths...> discover and analyze .js/.mjs/.cjs/.jsx/.ts/.tsx/.mts/.cts and inline <script> in .html under paths
255
+ frostjs csp print the Content-Security-Policy header the policy implies
256
+ frostjs summary print a plain-English reading of the policy
257
+ frostjs vendor add <files> fingerprint third-party files and record their capabilities
258
+ frostjs registry sync re-admit bumped dependencies whose capabilities did not change
259
+ frostjs sri [paths] print Subresource Integrity values for registered vendored files
260
+ frostjs --exclude <name> skip directories with this name (repeatable)
261
+ frostjs --exit-zero report findings but always exit 0
262
+ frostjs --policy <file> use this policy instead of searching for frostjs.policy
263
+ frostjs --today <date> treat YYYY-MM-DD as today when checking expiry
264
+ frostjs --min-confidence <c> lowest confidence that fails: certain, probable (default), possible
265
+ frostjs --baseline <file> denials recorded in this file do not fail the build
266
+ frostjs --update-baseline write every current denial into the baseline and exit 0
267
+ frostjs --changed-since <ref> fail only on uses in lines changed since the git ref
268
+ frostjs --format <f> text (default), json, sarif, or github
269
+ frostjs --version print the version and exit
270
+ frostjs --help show usage
271
+ ```
272
+
273
+ `node_modules`, `dist`, `build`, `coverage` and `.git` are always skipped
274
+ (unless a `vendored` glob reaches into them). `.d.ts` files are skipped:
275
+ they describe globals and contain no code. A path that names a file
276
+ directly is always analyzed.
277
+
278
+ TypeScript type positions are never references (`let f: typeof fetch` is
279
+ quiet), `declare` statements neither use nor shadow the globals they
280
+ describe, and `as` / `!` / `satisfies` are looked through. In JSX,
281
+ `dangerouslySetInnerHTML={...}` and `srcdoc={...}` are html injection and
282
+ intrinsic `<script>` / `<iframe>` elements count like `createElement`;
283
+ component names and ordinary attributes are quiet.
284
+
285
+ Inline `<script>` blocks in `.html` and `.htm` files are analyzed in place:
286
+ positions refer to the HTML file, `type="module"` blocks parse as modules,
287
+ and blocks with a `src` or a non-JavaScript `type` (JSON, import maps,
288
+ templates) are data, not code. Script elements are found with a regular
289
+ expression, which is right for markup people write and wrong only for
290
+ markup written to confuse it, which the threat model already excludes.
291
+
292
+ Exit codes: `0` clean, `1` policy violations, `2` usage or input error
293
+ (bad flag, missing path, syntax error).
294
+
295
+ Uses with `possible` confidence are listed under "unknown" and never fail the
296
+ build; `certain` and `probable` uses do.
297
+
298
+ ## GitHub Action
299
+
300
+ ```yaml
301
+ - uses: keithadler/frostjs@main
302
+ with:
303
+ paths: src
304
+ args: --baseline .frostjs-baseline.json --changed-since origin/main
305
+ fail-on-findings: "true"
306
+ ```
307
+
308
+ Inputs: `paths` (default `.`), `format` (default `github`, which annotates
309
+ the pull request inline), `args` (extra flags), `fail-on-findings` (set
310
+ `"false"` for an informational run). Inputs reach the script through the
311
+ environment only, never spliced into the script body, so a hostile input is
312
+ an argument and not a command.
313
+
314
+ ## ESLint plugin
315
+
316
+ The same engine as an ESLint rule, so denials show up in the editor and on
317
+ `eslint` runs, with the same policy discovery (nearest `frostjs.policy`
318
+ above the file) and the same `frostjs: ignore` comments. `eslint-disable`
319
+ works too.
320
+
321
+ ```js
322
+ // eslint.config.js
323
+ import frostjs from "@keithadler/frostjs/eslint";
324
+ export default [frostjs.configs.recommended];
325
+ // or: [{ plugins: { frostjs }, rules: { "frostjs/capability": ["error", { reportUnknown: true }] } }]
326
+ ```
327
+
328
+ Options: `policy` (explicit file), `minConfidence`, `reportUnknown`
329
+ (also report uses the CLI would list as unknown), `today`.
330
+
331
+ ## pre-commit
332
+
333
+ ```yaml
334
+ repos:
335
+ - repo: https://github.com/keithadler/frostjs
336
+ rev: main
337
+ hooks:
338
+ - id: frostjs
339
+ ```
340
+
341
+ ## Third-party code
342
+
343
+ Dependencies are not analyzed line by line; that is a year-long project
344
+ that ends in noise. Instead the policy names which files are vendored:
345
+
346
+ ```
347
+ vendored "vendor/**", "static/lib/*.min.js"
348
+ ```
349
+
350
+ A vendored file is hashed (SHA-384, the same value SRI uses) and looked up
351
+ in `.frostjs/registry.json` beside the policy. A known hash contributes the
352
+ capability set somebody recorded for it, checked against the policy like
353
+ any first-party use. An unknown hash fails the build:
354
+
355
+ ```
356
+ vendor/widget.min.js:1:1: vendored file is not in the registry; review it with: frostjs vendor add vendor/widget.min.js
357
+ ```
358
+
359
+ `frostjs vendor add` analyzes the file once, prints what it found so a
360
+ person can look at it, and records the entry. A patch release changes the
361
+ hash, so the review happens again; that is the point. To keep that from
362
+ being a chore, `frostjs registry sync` walks the vendored paths after a
363
+ dependency bump: a new version that uses exactly the capabilities the old
364
+ one did is re-admitted automatically and noted; one that gained a
365
+ capability or a new destination is refused with the difference printed,
366
+ which is the "dependency bump silently introduces a new network
367
+ destination" case from the threat model. Entries whose file is gone are
368
+ pruned, and the lockfile's hash is recorded so the next run can say
369
+ whether anything moved. A vendored glob may reach into `node_modules`;
370
+ the walk follows it there.
371
+
372
+ `frostjs sri` prints the same SHA-384 values as `integrity` attributes
373
+ (`--format html` for ready-made script tags, `--format json` for a build
374
+ step), so the browser refuses at load time exactly what the registry never
375
+ reviewed. A vendored file that is not in the registry is refused here too.
376
+
377
+ ## One policy, three artifacts
378
+
379
+ The same `frostjs.policy` drives the linter ruleset, a CSP header, and a
380
+ reviewer's summary, so they cannot drift apart.
381
+
382
+ `frostjs csp` prints the header and nothing else, for nginx or the CDN
383
+ config. Only directives the policy determines are emitted: `connect-src`
384
+ from `may reach` hosts (`'none'` when nothing is granted, `*` for
385
+ `may use the network`), `script-src 'self'` plus `'unsafe-eval'` when code
386
+ generation is granted and the reach hosts when dynamic import is, and
387
+ `worker-src` when workers are. Expired grants do not widen it; path-scoped
388
+ grants do, because a header covers the whole page.
389
+
390
+ `frostjs summary` prints what the code may do, what it may not, and spells
391
+ out the implicit deny:
392
+
393
+ ```
394
+ Policy "proj" (frostjs.policy)
395
+
396
+ This code may:
397
+ - use session storage (line 2)
398
+ - use local storage, only in src/legacy/* (line 3) - old code, rewrite by Q4
399
+ - use the cache, until 2026-08-30 (line 5) - service worker experiment
400
+
401
+ It may not, even where a broader grant would allow it:
402
+ - use cookies (line 4) - consent banner owns these
403
+
404
+ Everything else is denied. In particular this code may not use: the network, code generation, html injection, identity, navigation, globals, workers.
405
+ ```
406
+
407
+ ## Output formats
408
+
409
+ - `text` (default): one line per denial with the policy line that denied
410
+ it, unknowns in their own section, warnings, then a summary.
411
+ - `json`: a versioned document (`schema: 1`) with every decision, the
412
+ policy used, a summary by verdict, and expiry warnings.
413
+ - `sarif`: SARIF 2.1.0 for code scanning. One rule per capability code;
414
+ denied uses are errors, unknown uses warnings, baselined uses carry
415
+ `baselineState: "unchanged"`.
416
+ - `github`: GitHub Actions workflow commands (`::error file=...`) so each
417
+ denial shows up inline on the pull request, followed by the text report.
418
+
419
+ ## Adopting frostjs on an existing codebase
420
+
421
+ ```bash
422
+ frostjs --baseline .frostjs-baseline.json --update-baseline src
423
+ ```
424
+
425
+ That records every current denial, keyed on file, capability and expression
426
+ text (never line numbers), and exits 0. From then on
427
+ `frostjs --baseline .frostjs-baseline.json src` fails only on *new* uses;
428
+ existing ones are counted as "baselined". Commit the file; shrink it as the
429
+ debt is paid down. Paths inside it are relative to the file's directory.
430
+
431
+ For pull-request checks, `frostjs --changed-since origin/main src` fails only
432
+ on uses that sit in lines the branch added or modified; the rest are counted
433
+ as "unchanged". Untracked files count as entirely changed.
434
+
435
+ ## Suppressing a single use
436
+
437
+ ```js
438
+ // frostjs: ignore[storage.local]
439
+ localStorage.setItem("draft", text);
440
+
441
+ fetch(url); // frostjs: ignore
442
+ ```
443
+
444
+ A bare `frostjs: ignore` suppresses every capability on that line; a
445
+ bracketed list suppresses only those codes or families. The comment applies
446
+ to its own line or, when it stands alone, to the line after it. Suppressed
447
+ uses are counted in the summary and never fail the build. Prefer a scoped
448
+ `may ... in "file"` line in the policy when the exception should be visible
449
+ to a reviewer; suppression is for the one-off.
450
+
451
+ ## Capabilities recognized so far
452
+
453
+ | code | what |
454
+ | --- | --- |
455
+ | `storage.local` | `localStorage` |
456
+ | `storage.session` | `sessionStorage` |
457
+ | `storage.indexeddb` | `indexedDB` |
458
+ | `storage.cache` | `caches` |
459
+ | `storage.cookie` | `document.cookie` |
460
+ | `storage.navigator` | `navigator.storage` |
461
+ | `network.fetch` | `fetch` |
462
+ | `network.xhr` | `XMLHttpRequest` |
463
+ | `network.websocket` | `WebSocket` |
464
+ | `network.eventsource` | `EventSource` |
465
+ | `network.beacon` | `navigator.sendBeacon` |
466
+ | `network.import` | dynamic `import()` of an absolute URL or an expression |
467
+ | `codegen.eval` | `eval` |
468
+ | `codegen.function` | `Function(...)`, `new Function(...)` |
469
+ | `codegen.timer` | `setTimeout` / `setInterval` with string code |
470
+ | `codegen.write` | `document.write`, `document.writeln` |
471
+ | `dom-escape.html` | assignment to `innerHTML` / `outerHTML` / `srcdoc`, `insertAdjacentHTML`, `createContextualFragment`, JSX `dangerouslySetInnerHTML` / `srcdoc` |
472
+ | `dom-escape.script` | `document.createElement("script")`, JSX `<script>` |
473
+ | `dom-escape.iframe` | `document.createElement("iframe")`, JSX `<iframe>` |
474
+ | `identity.device` | `navigator.userAgent`, `platform`, `vendor`, `plugins`, `hardwareConcurrency`, `deviceMemory`... |
475
+ | `identity.geolocation` | `navigator.geolocation` |
476
+ | `identity.media` | `navigator.mediaDevices`, `getUserMedia` |
477
+ | `identity.clipboard` | `navigator.clipboard`, `document.execCommand("copy" / "paste")` |
478
+ | `identity.credentials` | `navigator.credentials` |
479
+ | `identity.permissions` | `navigator.permissions` |
480
+ | `navigation.location` | assignment to `location` / `location.href` etc., `location.assign` / `replace` / `reload` |
481
+ | `navigation.open` | `window.open` |
482
+ | `navigation.history` | `history.pushState` / `replaceState` / `back` / `forward` / `go` |
483
+ | `navigation.postmessage` | `postMessage` to `parent` / `top` / `opener` / `contentWindow`, or with a string origin |
484
+ | `globals.window` | assignment to `window.*` / `globalThis.*`, `Object.defineProperty(window, ...)` |
485
+ | `globals.prototype` | assignment to a built-in or its prototype (`Array.prototype.x = `, `Error.prepareStackTrace = `), or `Object.defineProperty` / `assign` on one |
486
+ | `worker.dedicated` | `new Worker(url)` |
487
+ | `worker.shared` | `new SharedWorker(url)` |
488
+ | `worker.service` | `navigator.serviceWorker.register(url)` |
489
+ | `worker.worklet` | `CSS.paintWorklet.addModule(url)`, `audioWorklet.addModule(url)`... |
490
+
491
+ Each is recognized bare, via `window` / `globalThis` / `self`, and via a
492
+ computed member whose name is a string literal (`window["localStorage"]`),
493
+ a concatenation of literals, or a `const` the scope analysis can fold
494
+ (`const k = "localStorage"; window[k]`, reported as `probable`).
495
+
496
+ Scope analysis is real, with hoisting: a local named `fetch` or `window` is
497
+ not the global, so a use through it is not reported at all, while the same
498
+ name declared in a sibling function does not hide anything. Only inside a
499
+ `with` block, where nothing can be resolved, is a use reported as `possible`.
500
+ Uses via `self` are `probable` rather than `certain`, since `self` is often
501
+ a local alias for `this` in older code.
502
+
503
+ Canvas and audio fingerprinting are deliberately **not** recognized: every
504
+ charting and 3D library draws to canvases, and no static signature separates
505
+ that from fingerprinting without false positives. CSP and the network family
506
+ are the backstop for where such a fingerprint would be sent.
507
+
508
+ Network uses carry a **target** when it can be fixed statically. Frost's
509
+ rule applies: a literal that closes the authority fixes the host, and nothing
510
+ after the slash can move it. `fetch("https://api.example.com/items/" + id)`
511
+ reaches `api.example.com`; `fetch("https://" + host)` reaches nobody we can
512
+ name. Relative URLs are `same-origin`. Dynamic `import()` of a relative path
513
+ or a bare package name goes through the bundler, not the network, and is not
514
+ reported.
515
+
516
+ ## What this is not
517
+
518
+ - **Not a runtime sandbox.** No membrane, no proxied globals. A determined
519
+ attacker with code execution defeats any wrapper; that fight is not worth having.
520
+ - **Not a replacement for CSP.** It emits CSP, and CSP remains the runtime
521
+ backstop. This tool is the build-time gate.
522
+ - **Not a universal npm scanner.** Dependencies are admitted by fingerprint
523
+ against a registry, not analyzed line by line.
524
+
525
+ ## Threat model, honestly
526
+
527
+ `frostjs` catches careless or accidental use of forbidden APIs in first-party,
528
+ tenant, or model-generated code, and it catches drift over time. It does **not**
529
+ catch deliberately obfuscated code, runtime-constructed access beyond a shallow
530
+ constant fold, or anything injected after the build. The value is a high floor,
531
+ not a ceiling.
532
+
533
+ ## Development
534
+
535
+ ```
536
+ npm install
537
+ npm test vitest (npm run test:watch to keep it running)
538
+ npm run lint prettier --check, then typecheck src, test and scripts
539
+ npm run format prettier --write
540
+ npm run build tsc to dist/
541
+ npm run corpus scan the pinned corpus; fails if findings changed
542
+ npm run showcase reproduce SHOWCASE.md
543
+ ```
544
+
545
+ Zero false positives is the product. `npm run corpus` runs the extractor over
546
+ six pinned, hash-verified npm packages (about 21 MB of real JavaScript) and
547
+ diffs the findings against `corpus/expected.txt`. Any change to `src/extract/`
548
+ must leave that diff empty, or update the file deliberately with
549
+ `npm run corpus -- --update` and explain why in the commit.
550
+
551
+ - [ARCHITECTURE.md](ARCHITECTURE.md): how the code is laid out and the
552
+ contract a recognizer signs.
553
+ - [CONTRIBUTING.md](CONTRIBUTING.md): how to add a recognizer or a policy
554
+ form, and the conventions.
555
+ - [SECURITY.md](SECURITY.md): what a green run does and does not promise,
556
+ and how to report a bypass.
557
+ - [CHANGELOG.md](CHANGELOG.md).
558
+ - [REQUIREMENTS.md](REQUIREMENTS.md): the original plan, with every step
559
+ marked done and every decision recorded.
560
+
561
+ ## License
562
+
563
+ [MIT](LICENSE), the same license as [frost](https://github.com/keithadler/frost)
564
+ and [exact](https://github.com/keithadler/magic-float-linter). A build-time
565
+ linter wants the widest possible adoption and gives nobody a reason to
566
+ hesitate: no copyleft, no patent clause to have reviewed, nothing to
567
+ attribute beyond the notice. Contributions are accepted under the same
568
+ license.
@@ -0,0 +1,18 @@
1
+ export interface BaselineEntry {
2
+ file: string;
3
+ capability: string;
4
+ expression: string;
5
+ }
6
+ export interface Baseline {
7
+ version: 1;
8
+ entries: BaselineEntry[];
9
+ }
10
+ /** The key an entry is matched on: posix path, capability, normalized expression. Never a line number. */
11
+ export declare function baselineKey(file: string, capability: string, expression: string): string;
12
+ /** Read a baseline. A missing file is an empty baseline; malformed JSON or an unknown version throws. */
13
+ export declare function readBaseline(file: string): Baseline;
14
+ /** Write entries, deduplicated and sorted, and return how many were written. */
15
+ export declare function writeBaseline(file: string, entries: readonly BaselineEntry[]): number;
16
+ /** The set of keys a baseline covers. */
17
+ export declare function baselineKeys(b: Baseline): Set<string>;
18
+ //# sourceMappingURL=baseline.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"baseline.d.ts","sourceRoot":"","sources":["../src/baseline.ts"],"names":[],"mappings":"AAUA,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,QAAQ;IACvB,OAAO,EAAE,CAAC,CAAC;IACX,OAAO,EAAE,aAAa,EAAE,CAAC;CAC1B;AAOD,0GAA0G;AAC1G,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,CAExF;AAED,yGAAyG;AACzG,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,QAAQ,CAWnD;AAED,gFAAgF;AAChF,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,SAAS,aAAa,EAAE,GAAG,MAAM,CAIrF;AAiBD,yCAAyC;AACzC,wBAAgB,YAAY,CAAC,CAAC,EAAE,QAAQ,GAAG,GAAG,CAAC,MAAM,CAAC,CAErD"}
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Baseline snapshots. A baseline freezes the violations
3
+ * a codebase already has so that only new ones fail the build. Entries are
4
+ * keyed on (file, capability, expression text), never on line numbers, so
5
+ * unrelated edits do not invalidate them. Paths are relative to the
6
+ * baseline file's directory.
7
+ */
8
+ import fs from "node:fs";
9
+ import path from "node:path";
10
+ /** Collapse whitespace so a reformatted expression keeps its baseline entry. */
11
+ function normalizeExpression(expression) {
12
+ return expression.replace(/\s+/g, " ").trim();
13
+ }
14
+ /** The key an entry is matched on: posix path, capability, normalized expression. Never a line number. */
15
+ export function baselineKey(file, capability, expression) {
16
+ return `${file.split(path.sep).join("/")} ${capability} ${normalizeExpression(expression)}`;
17
+ }
18
+ /** Read a baseline. A missing file is an empty baseline; malformed JSON or an unknown version throws. */
19
+ export function readBaseline(file) {
20
+ if (!fs.existsSync(file))
21
+ return { version: 1, entries: [] };
22
+ let raw;
23
+ try {
24
+ raw = JSON.parse(fs.readFileSync(file, "utf8"));
25
+ }
26
+ catch (e) {
27
+ throw new Error(`baseline ${file} is not valid JSON: ${e.message}`);
28
+ }
29
+ const b = raw;
30
+ if (b.version !== 1 || !Array.isArray(b.entries))
31
+ throw new Error(`baseline ${file} has an unknown format`);
32
+ return { version: 1, entries: dedupe(b.entries) };
33
+ }
34
+ /** Write entries, deduplicated and sorted, and return how many were written. */
35
+ export function writeBaseline(file, entries) {
36
+ const out = { version: 1, entries: dedupe(entries) };
37
+ fs.writeFileSync(file, JSON.stringify(out, null, 2) + "\n");
38
+ return out.entries.length;
39
+ }
40
+ function dedupe(entries) {
41
+ const seen = new Map();
42
+ for (const e of entries) {
43
+ const n = { file: e.file, capability: e.capability, expression: normalizeExpression(e.expression) };
44
+ seen.set(baselineKey(n.file, n.capability, n.expression), n);
45
+ }
46
+ return [...seen.values()].sort((a, b) => a.file === b.file
47
+ ? a.capability === b.capability
48
+ ? a.expression.localeCompare(b.expression)
49
+ : a.capability.localeCompare(b.capability)
50
+ : a.file.localeCompare(b.file));
51
+ }
52
+ /** The set of keys a baseline covers. */
53
+ export function baselineKeys(b) {
54
+ return new Set(b.entries.map((e) => baselineKey(e.file, e.capability, e.expression)));
55
+ }
56
+ //# sourceMappingURL=baseline.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"baseline.js","sourceRoot":"","sources":["../src/baseline.ts"],"names":[],"mappings":"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"}