@scopebond/hook 0.8.0 → 0.9.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 (102) hide show
  1. package/README.md +210 -0
  2. package/dist/budget-load.d.ts +68 -0
  3. package/dist/budget-load.d.ts.map +1 -0
  4. package/dist/budget-load.js +164 -0
  5. package/dist/budget-load.js.map +1 -0
  6. package/dist/capabilities.d.ts +103 -0
  7. package/dist/capabilities.d.ts.map +1 -0
  8. package/dist/capabilities.js +212 -0
  9. package/dist/capabilities.js.map +1 -0
  10. package/dist/classify.d.ts +17 -0
  11. package/dist/classify.d.ts.map +1 -0
  12. package/dist/classify.js +78 -0
  13. package/dist/classify.js.map +1 -0
  14. package/dist/cli.d.ts.map +1 -1
  15. package/dist/cli.js +546 -16
  16. package/dist/cli.js.map +1 -1
  17. package/dist/cloud.d.ts +8 -0
  18. package/dist/cloud.d.ts.map +1 -1
  19. package/dist/cloud.js.map +1 -1
  20. package/dist/dispatch-cli.d.ts +2 -0
  21. package/dist/dispatch-cli.d.ts.map +1 -0
  22. package/dist/dispatch-cli.js +128 -0
  23. package/dist/dispatch-cli.js.map +1 -0
  24. package/dist/explain.d.ts.map +1 -1
  25. package/dist/explain.js +2 -1
  26. package/dist/explain.js.map +1 -1
  27. package/dist/group.d.ts +12 -0
  28. package/dist/group.d.ts.map +1 -0
  29. package/dist/group.js +55 -0
  30. package/dist/group.js.map +1 -0
  31. package/dist/index.d.ts +25 -0
  32. package/dist/index.d.ts.map +1 -1
  33. package/dist/index.js +17 -0
  34. package/dist/index.js.map +1 -1
  35. package/dist/install.d.ts +10 -0
  36. package/dist/install.d.ts.map +1 -1
  37. package/dist/install.js +56 -0
  38. package/dist/install.js.map +1 -1
  39. package/dist/map.d.ts +4 -0
  40. package/dist/map.d.ts.map +1 -1
  41. package/dist/map.js +29 -1
  42. package/dist/map.js.map +1 -1
  43. package/dist/obs-emitter.d.ts +132 -0
  44. package/dist/obs-emitter.d.ts.map +1 -0
  45. package/dist/obs-emitter.js +415 -0
  46. package/dist/obs-emitter.js.map +1 -0
  47. package/dist/obs-store.d.ts +160 -0
  48. package/dist/obs-store.d.ts.map +1 -0
  49. package/dist/obs-store.js +367 -0
  50. package/dist/obs-store.js.map +1 -0
  51. package/dist/obs-upload.d.ts +24 -0
  52. package/dist/obs-upload.d.ts.map +1 -0
  53. package/dist/obs-upload.js +231 -0
  54. package/dist/obs-upload.js.map +1 -0
  55. package/dist/observation.d.ts +232 -0
  56. package/dist/observation.d.ts.map +1 -0
  57. package/dist/observation.js +308 -0
  58. package/dist/observation.js.map +1 -0
  59. package/dist/paths.d.ts +15 -0
  60. package/dist/paths.d.ts.map +1 -0
  61. package/dist/paths.js +79 -0
  62. package/dist/paths.js.map +1 -0
  63. package/dist/policy-load.d.ts +60 -0
  64. package/dist/policy-load.d.ts.map +1 -0
  65. package/dist/policy-load.js +151 -0
  66. package/dist/policy-load.js.map +1 -0
  67. package/dist/proof.d.ts +30 -0
  68. package/dist/proof.d.ts.map +1 -0
  69. package/dist/proof.js +186 -0
  70. package/dist/proof.js.map +1 -0
  71. package/dist/rules.d.ts +20 -0
  72. package/dist/rules.d.ts.map +1 -1
  73. package/dist/rules.js +50 -0
  74. package/dist/rules.js.map +1 -1
  75. package/dist/runtime.d.ts +18 -1
  76. package/dist/runtime.d.ts.map +1 -1
  77. package/dist/runtime.js +74 -10
  78. package/dist/runtime.js.map +1 -1
  79. package/dist/scan.d.ts +7 -0
  80. package/dist/scan.d.ts.map +1 -0
  81. package/dist/scan.js +38 -0
  82. package/dist/scan.js.map +1 -0
  83. package/dist/shell.d.ts.map +1 -1
  84. package/dist/shell.js +12 -3
  85. package/dist/shell.js.map +1 -1
  86. package/dist/sql-classify.d.ts +11 -0
  87. package/dist/sql-classify.d.ts.map +1 -0
  88. package/dist/sql-classify.js +251 -0
  89. package/dist/sql-classify.js.map +1 -0
  90. package/dist/typed-infra.d.ts +73 -0
  91. package/dist/typed-infra.d.ts.map +1 -0
  92. package/dist/typed-infra.js +758 -0
  93. package/dist/typed-infra.js.map +1 -0
  94. package/dist/typed-ops.d.ts +140 -0
  95. package/dist/typed-ops.d.ts.map +1 -0
  96. package/dist/typed-ops.js +549 -0
  97. package/dist/typed-ops.js.map +1 -0
  98. package/dist/vectors.d.ts +40 -0
  99. package/dist/vectors.d.ts.map +1 -0
  100. package/dist/vectors.js +219 -0
  101. package/dist/vectors.js.map +1 -0
  102. package/package.json +4 -4
@@ -0,0 +1,758 @@
1
+ // Typed `network`, `cloudflare_resource` and `database` operations, derived from the ACTUAL
2
+ // command line or tool input before it runs.
3
+ //
4
+ // Same rules as typed-ops.ts: a fact that cannot be read from the request stays unknown or
5
+ // leaves no operation at all, and nothing raw is exported. What each operation carries:
6
+ //
7
+ // network scheme, lowercase IDNA host, effective port, method and read/write/upload. Never
8
+ // the path, query, credentials, headers or body. A redirect hop is not followed
9
+ // or bound (`redirect_binding` is always omitted), so a redirected request is
10
+ // not qualified against the origin's approval.
11
+ // cloudflare resource kind, verb, keyed account and resource ids, an artifact digest of the
12
+ // local files a Pages deploy or an R2 put sends. `wrangler` has no DNS command,
13
+ // so dns_record and Worker/KV/secret changes made any other way are not seen.
14
+ // database provider, verb, predicate class and a keyed digest of the statement or the
15
+ // local migration set. Never SQL text, table or column names, or rows. A
16
+ // statement the classifier cannot place leaves no operation (the closed
17
+ // schema has no "unknown" verb).
18
+ import { createHash } from "node:crypto";
19
+ import { readdirSync, readFileSync, statSync } from "node:fs";
20
+ import { isAbsolute, join } from "node:path";
21
+ import { canonProgram, decomposeShell } from "./shell.js";
22
+ import { classifySql } from "./sql-classify.js";
23
+ import { scan, flagValue } from "./scan.js";
24
+ const MAX_FILE = 20 * 1024 * 1024;
25
+ const MAX_TREE_FILES = 5000;
26
+ const MAX_TREE_BYTES = 100 * 1024 * 1024;
27
+ export const systemFiles = {
28
+ readText(path, max) {
29
+ try {
30
+ const st = statSync(path);
31
+ if (!st.isFile() || st.size > max)
32
+ return null;
33
+ return readFileSync(path, "utf8");
34
+ }
35
+ catch {
36
+ return null;
37
+ }
38
+ },
39
+ sha256File(path) {
40
+ try {
41
+ const st = statSync(path);
42
+ if (!st.isFile() || st.size > MAX_FILE)
43
+ return null;
44
+ return createHash("sha256").update(readFileSync(path)).digest("hex");
45
+ }
46
+ catch {
47
+ return null;
48
+ }
49
+ },
50
+ sha256Dir(path) {
51
+ try {
52
+ const files = [];
53
+ const walk = (dir, rel) => {
54
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
55
+ const r = rel === "" ? entry.name : `${rel}/${entry.name}`;
56
+ if (entry.isDirectory())
57
+ walk(join(dir, entry.name), r);
58
+ else if (entry.isFile())
59
+ files.push(r);
60
+ if (files.length > MAX_TREE_FILES)
61
+ throw new Error("too many files");
62
+ }
63
+ };
64
+ if (!statSync(path).isDirectory())
65
+ return null;
66
+ walk(path, "");
67
+ if (files.length === 0)
68
+ return null;
69
+ files.sort();
70
+ const hash = createHash("sha256");
71
+ let total = 0;
72
+ for (const f of files) {
73
+ const bytes = readFileSync(join(path, f));
74
+ total += bytes.length;
75
+ if (total > MAX_TREE_BYTES)
76
+ return null;
77
+ hash.update(`${f}\0${bytes.length}\0`).update(bytes);
78
+ }
79
+ return hash.digest("hex");
80
+ }
81
+ catch {
82
+ return null;
83
+ }
84
+ },
85
+ listSql(path) {
86
+ try {
87
+ const names = readdirSync(path).filter((n) => /\.sql$/i.test(n)).sort();
88
+ if (names.length === 0)
89
+ return null;
90
+ const out = [];
91
+ for (const name of names) {
92
+ const text = systemFiles.readText(join(path, name), 5 * 1024 * 1024);
93
+ if (text === null)
94
+ return null;
95
+ out.push({ name, text });
96
+ }
97
+ return out;
98
+ }
99
+ catch {
100
+ return null;
101
+ }
102
+ },
103
+ };
104
+ /** No files, deterministic: what the proof fixtures read, so a proof never depends on the machine it runs on. */
105
+ export const fixtureFiles = { readText: () => null, sha256File: () => null, sha256Dir: () => null, listSql: () => null };
106
+ const UNBOUND = "unbound";
107
+ const filesOf = (ctx) => ctx.files ?? systemFiles;
108
+ const envOf = (ctx, name) => (ctx.env ? ctx.env(name) : process.env[name]);
109
+ const resolvePath = (cwd, p) => (isAbsolute(p) || /^[A-Za-z]:[\\/]/.test(p) ? p : join(cwd, p));
110
+ function common(ctx, request, environment_class) {
111
+ return {
112
+ environment_class, reference_set_version: ctx.referenceSetVersion,
113
+ request_digest: ctx.key.requestDigest(request), digest_key_generation: ctx.key.generation,
114
+ };
115
+ }
116
+ // ---- network -------------------------------------------------------------------------------------------
117
+ const HOST_OK = /^[a-z0-9](?:[a-z0-9.-]{0,251}[a-z0-9])?$/;
118
+ /** A URL reduced to scheme, IDNA-lowercase host and effective port. Null for anything else
119
+ * (an unexpanded variable, a glob, an IPv6 literal, a non-HTTP scheme, a host that is not a
120
+ * plain hostname). A scheme-less operand reads as `http`, as curl does. */
121
+ export function parseDestination(raw, assumeHttp) {
122
+ let text = raw.trim();
123
+ if (text === "" || /[$`{}\[\]*\s]/.test(text.replace(/^[a-z][a-z0-9+.-]*:\/\/[^/]*@/i, "")))
124
+ return null;
125
+ if (!/^[a-z][a-z0-9+.-]*:\/\//i.test(text)) {
126
+ if (!assumeHttp || text.startsWith("//") || text.startsWith("/"))
127
+ return null;
128
+ text = `http://${text}`;
129
+ }
130
+ let u;
131
+ try {
132
+ u = new URL(text);
133
+ }
134
+ catch {
135
+ return null;
136
+ }
137
+ if (u.protocol !== "http:" && u.protocol !== "https:")
138
+ return null;
139
+ const host = u.hostname.replace(/\.$/, "");
140
+ if (!HOST_OK.test(host) || host.includes(".."))
141
+ return null;
142
+ const scheme = u.protocol === "https:" ? "https" : "http";
143
+ const port = u.port !== "" ? Number(u.port) : scheme === "https" ? 443 : 80;
144
+ return Number.isInteger(port) && port >= 1 && port <= 65535 ? { scheme, host, port } : null;
145
+ }
146
+ const METHODS = new Set(["GET", "HEAD", "OPTIONS", "POST", "PUT", "PATCH", "DELETE"]);
147
+ function networkOperation(dest, method, hasBody, ctx, request) {
148
+ if (!METHODS.has(method))
149
+ return null;
150
+ const net_operation = method === "GET" || method === "HEAD" || method === "OPTIONS" ? "read"
151
+ : method === "DELETE" ? "write" : hasBody ? "upload" : "write";
152
+ return {
153
+ type: "network", resource_id: ctx.key.resourceId("net-dest", `${dest.host}:${dest.port}`), ...common(ctx, request, "unknown"),
154
+ scheme: dest.scheme, host: dest.host, port: dest.port, method, net_operation,
155
+ };
156
+ }
157
+ /** A WebFetch tool call: a GET of the URL in its input. */
158
+ export function fetchOperation(url, ctx, request) {
159
+ const dest = parseDestination(url, false);
160
+ return dest ? networkOperation(dest, "GET", false, ctx, request) : null;
161
+ }
162
+ // Curl options whose next argument is a value (so it is never read as the URL).
163
+ const CURL_VALUED = new Set(["-X", "--request", "-H", "--header", "-d", "--data", "--data-ascii", "--data-binary", "--data-raw", "--data-urlencode", "--json", "-F", "--form", "--form-string",
164
+ "-u", "--user", "-o", "--output", "-T", "--upload-file", "-A", "--user-agent", "-e", "--referer", "-b", "--cookie", "-c", "--cookie-jar", "-w", "--write-out", "-m", "--max-time", "--connect-timeout",
165
+ "-r", "--range", "-y", "--speed-time", "-Y", "--speed-limit", "-z", "--time-cond", "-E", "--cert", "--key", "--cacert", "--capath", "--retry", "--retry-delay", "--retry-max-time", "--max-redirs",
166
+ "--limit-rate", "--oauth2-bearer", "--proxy-user", "-U", "--interface", "--dns-servers", "--aws-sigv4", "--cert-type", "--key-type", "--pass", "--proto", "--proto-redir", "--ciphers",
167
+ "--tls-max", "--expect100-timeout", "-P", "--ftp-port", "--stderr", "--trace", "--trace-ascii", "--create-file-mode", "--max-filesize", "--url-query", "--variable", "--etag-save", "--etag-compare", "--hsts", "--alt-svc", "--happy-eyeballs-timeout-ms"]);
168
+ // Options that send the request somewhere other than the URL names, or read more requests from elsewhere.
169
+ const CURL_REROUTE = new Set(["-x", "--proxy", "--preproxy", "--connect-to", "--resolve", "--unix-socket", "--abstract-unix-socket", "-K", "--config", "--next", "-:", "--doh-url", "-q", "--disable"]);
170
+ const CURL_BODY = new Set(["-d", "--data", "--data-ascii", "--data-binary", "--data-raw", "--data-urlencode", "--json", "-F", "--form", "--form-string", "-T", "--upload-file"]);
171
+ const CURL_SHORT_VALUED = "XHdFuoTAebcwmrxyYzEUPK";
172
+ function parseCurl(argv) {
173
+ const urls = [];
174
+ let method;
175
+ let head = false;
176
+ let get = false;
177
+ let body = false;
178
+ let upload = false;
179
+ for (let i = 0; i < argv.length; i++) {
180
+ const t = argv[i];
181
+ if (t === "--") {
182
+ urls.push(...argv.slice(i + 1));
183
+ break;
184
+ }
185
+ if (t.startsWith("--")) {
186
+ const at = t.indexOf("=");
187
+ const name = at > 0 ? t.slice(0, at) : t;
188
+ let value = at > 0 ? t.slice(at + 1) : undefined;
189
+ if (CURL_REROUTE.has(name))
190
+ return null;
191
+ if (CURL_VALUED.has(name) && value === undefined)
192
+ value = argv[++i];
193
+ if (name === "--request")
194
+ method = (value ?? "").toUpperCase();
195
+ else if (name === "--url") {
196
+ if (value)
197
+ urls.push(value);
198
+ }
199
+ else if (name === "--head")
200
+ head = true;
201
+ else if (name === "--get")
202
+ get = true;
203
+ else if (name === "--upload-file") {
204
+ upload = true;
205
+ body = true;
206
+ }
207
+ else if (CURL_BODY.has(name))
208
+ body = true;
209
+ continue;
210
+ }
211
+ if (t.startsWith("-") && t.length > 1) {
212
+ if (CURL_REROUTE.has(t))
213
+ return null;
214
+ for (let k = 1; k < t.length; k++) {
215
+ const c = t[k];
216
+ if (CURL_REROUTE.has(`-${c}`))
217
+ return null;
218
+ if (CURL_SHORT_VALUED.includes(c)) {
219
+ const value = k + 1 < t.length ? t.slice(k + 1) : argv[++i];
220
+ if (c === "X")
221
+ method = (value ?? "").toUpperCase();
222
+ else if (c === "T") {
223
+ upload = true;
224
+ body = true;
225
+ }
226
+ else if (c === "d" || c === "F")
227
+ body = true;
228
+ break;
229
+ }
230
+ if (c === "I")
231
+ head = true;
232
+ else if (c === "G")
233
+ get = true;
234
+ }
235
+ continue;
236
+ }
237
+ urls.push(t);
238
+ }
239
+ const candidates = urls.filter((u) => /^[a-z][a-z0-9+.-]*:\/\//i.test(u) || /^[A-Za-z0-9.-]+\.[A-Za-z]{2,}(?::\d+)?(?:[/?#]|$)/.test(u) || /^(?:localhost|\d{1,3}(?:\.\d{1,3}){3})(?::\d+)?(?:[/?#]|$)/i.test(u));
240
+ if (candidates.length !== 1 || candidates.length !== urls.length)
241
+ return null;
242
+ const effective = method && method !== "" ? method : head ? "HEAD" : get ? "GET" : upload ? "PUT" : body ? "POST" : "GET";
243
+ return { url: candidates[0], method: effective, hasBody: (body && !get) || upload };
244
+ }
245
+ const WGET_VALUED = new Set(["-O", "--output-document", "-o", "--output-file", "-P", "--directory-prefix", "-t", "--tries", "-T", "--timeout", "-w", "--wait", "-U", "--user-agent", "--header", "--http-user", "--http-password", "--user", "--password",
246
+ "-e", "--execute", "--method", "--post-data", "--post-file", "--body-data", "--body-file", "-l", "--level", "-A", "--accept", "-R", "--reject", "-I", "--include-directories", "-X", "--exclude-directories", "--referer", "--load-cookies", "--save-cookies",
247
+ "--certificate", "--private-key", "--ca-certificate", "--ca-directory", "--bind-address", "-Q", "--quota", "--limit-rate", "--waitretry", "--connect-timeout", "--read-timeout", "--dns-timeout", "--secure-protocol", "--proxy-user", "--proxy-password"]);
248
+ function parseWget(argv) {
249
+ const urls = [];
250
+ let method;
251
+ let body = false;
252
+ let spider = false;
253
+ for (let i = 0; i < argv.length; i++) {
254
+ const t = argv[i];
255
+ if (t === "--") {
256
+ urls.push(...argv.slice(i + 1));
257
+ break;
258
+ }
259
+ if (t.startsWith("-") && t.length > 1) {
260
+ const at = t.startsWith("--") ? t.indexOf("=") : -1;
261
+ const name = at > 0 ? t.slice(0, at) : t;
262
+ let value = at > 0 ? t.slice(at + 1) : undefined;
263
+ if (["-i", "--input-file", "-B", "--base", "-e", "--execute"].includes(name))
264
+ return null;
265
+ if (name.startsWith("--")) {
266
+ if (WGET_VALUED.has(name) && value === undefined)
267
+ value = argv[++i];
268
+ if (name === "--method")
269
+ method = (value ?? "").toUpperCase();
270
+ else if (["--post-data", "--post-file", "--body-data", "--body-file"].includes(name)) {
271
+ body = true;
272
+ method ??= "POST";
273
+ }
274
+ else if (name === "--spider")
275
+ spider = true;
276
+ continue;
277
+ }
278
+ // Short options: the first letter that takes a value ends the cluster.
279
+ for (let k = 1; k < t.length; k++) {
280
+ if (WGET_VALUED.has(`-${t[k]}`)) {
281
+ if (k + 1 >= t.length)
282
+ i++;
283
+ break;
284
+ }
285
+ }
286
+ continue;
287
+ }
288
+ urls.push(t);
289
+ }
290
+ const candidates = urls.filter((u) => /^[a-z][a-z0-9+.-]*:\/\//i.test(u) || /^[A-Za-z0-9.-]+\.[A-Za-z]{2,}(?::\d+)?(?:[/?#]|$)/.test(u));
291
+ if (candidates.length !== 1 || candidates.length !== urls.length)
292
+ return null;
293
+ return { url: candidates[0], method: method ?? (spider ? "HEAD" : "GET"), hasBody: body };
294
+ }
295
+ const PS_SWITCH = new Set(["usebasicparsing", "skipcertificatecheck", "skipheadervalidation", "skiphttperrorcheck", "allowunencryptedauthentication", "passthru", "resume", "preservehttpmethodonredirect", "noproxy", "allowinsecureredirect", "usedefaultcredentials", "disablekeepalive", "verbose", "debug"]);
296
+ const PS_REROUTE = new Set(["proxy", "sessionvariable", "websession"]);
297
+ function parsePowerShell(argv) {
298
+ let uri;
299
+ let method;
300
+ let body = false;
301
+ const positional = [];
302
+ for (let i = 0; i < argv.length; i++) {
303
+ const t = argv[i];
304
+ const m = /^-([A-Za-z]+)(?::(.*))?$/.exec(t);
305
+ if (!m) {
306
+ positional.push(t);
307
+ continue;
308
+ }
309
+ const name = m[1].toLowerCase();
310
+ if (PS_REROUTE.has(name))
311
+ return null;
312
+ let value = m[2];
313
+ if (!PS_SWITCH.has(name) && value === undefined)
314
+ value = argv[++i];
315
+ if (name === "uri" || name === "u")
316
+ uri = value;
317
+ else if (name === "method" || name === "m")
318
+ method = (value ?? "").toUpperCase();
319
+ else if (name === "body" || name === "infile")
320
+ body = true;
321
+ }
322
+ if (uri === undefined) {
323
+ if (positional.length !== 1)
324
+ return null;
325
+ uri = positional[0];
326
+ }
327
+ else if (positional.length > 0)
328
+ return null;
329
+ return { url: uri, method: method ?? (body ? "POST" : "GET"), hasBody: body };
330
+ }
331
+ function fetchFromCommand(sc, dialect) {
332
+ const program = canonProgram(sc.program);
333
+ if (["invoke-webrequest", "iwr", "invoke-restmethod", "irm"].includes(program))
334
+ return parsePowerShell(sc.argv);
335
+ const exe = /\.exe$/i.test(sc.programRaw);
336
+ if (dialect === "powershell" && !exe && (program === "curl" || program === "wget"))
337
+ return parsePowerShell(sc.argv);
338
+ if (program === "curl")
339
+ return parseCurl(sc.argv);
340
+ if (program === "wget")
341
+ return parseWget(sc.argv);
342
+ return null;
343
+ }
344
+ // ---- wrangler ----------------------------------------------------------------------------------------------
345
+ /** The argument list of a `wrangler` invocation, through the usual launchers. */
346
+ function wranglerArgv(sc) {
347
+ const program = canonProgram(sc.program);
348
+ const isWrangler = (t) => t !== undefined && /^wrangler(?:@[^\s]+)?$/i.test(t);
349
+ if (program === "wrangler")
350
+ return sc.argv;
351
+ let rest;
352
+ if (program === "npx" || program === "bunx" || program === "pnpx")
353
+ rest = sc.argv;
354
+ else if (program === "pnpm" || program === "yarn" || program === "bun") {
355
+ const [sub, ...r] = sc.argv;
356
+ if (sub !== "exec" && sub !== "dlx" && sub !== "x")
357
+ return null;
358
+ rest = r;
359
+ }
360
+ else if (program === "npm") {
361
+ const [sub, ...r] = sc.argv;
362
+ if (sub !== "exec")
363
+ return null;
364
+ rest = r;
365
+ }
366
+ else
367
+ return null;
368
+ const at = rest.findIndex((t) => !t.startsWith("-") || t === "--");
369
+ if (at < 0)
370
+ return null;
371
+ const start = rest[at] === "--" ? at + 1 : at;
372
+ return isWrangler(rest[start]) ? rest.slice(start + 1) : null;
373
+ }
374
+ const WR_VALUED = ["--env", "-e", "--config", "-c", "--name", "--command", "--file", "--project-name", "--branch", "--commit-hash", "--commit-message", "--compatibility-date", "--compatibility-flags",
375
+ "--var", "--define", "--route", "--routes", "--outdir", "--tag", "--message", "--content-type", "--content-disposition", "--cache-control", "--expires", "--content-language", "--storage-class", "--jurisdiction", "-J",
376
+ "--persist-to", "--database", "--binding", "--format", "--limit", "--location", "--account-id", "--production-branch", "--assets", "--site", "--cwd"];
377
+ const WR_FLAGS = new Set([...WR_VALUED, "--remote", "--local", "--preview", "--dry-run", "--json", "--yes", "-y", "--help", "-h", "--version", "-v", "--skip-confirmation", "--keep-vars", "--no-bundle", "--node-compat",
378
+ "--experimental-json-config", "--persist", "--no-minify", "--latest", "--strict", "--force", "-f", "--no-x-remote-bindings", "--x-remote-bindings", "--experimental-provision", "--x-provision", "--unsafe", "--include-secrets", "--dev-url", "--minify", "--workers-dev", "--triggers", "--legacy-env"]);
379
+ function stripJsonc(text) {
380
+ let out = "";
381
+ for (let i = 0; i < text.length;) {
382
+ const c = text[i];
383
+ if (c === '"') {
384
+ let j = i + 1;
385
+ while (j < text.length && text[j] !== '"')
386
+ j += text[j] === "\\" ? 2 : 1;
387
+ out += text.slice(i, j + 1);
388
+ i = j + 1;
389
+ continue;
390
+ }
391
+ if (c === "/" && text[i + 1] === "/") {
392
+ while (i < text.length && text[i] !== "\n")
393
+ i++;
394
+ continue;
395
+ }
396
+ if (c === "/" && text[i + 1] === "*") {
397
+ const e = text.indexOf("*/", i + 2);
398
+ i = e < 0 ? text.length : e + 2;
399
+ continue;
400
+ }
401
+ out += c;
402
+ i++;
403
+ }
404
+ return out.replace(/,(\s*[}\]])/g, "$1");
405
+ }
406
+ function readWranglerConfig(cwd, files, configFlag, env) {
407
+ const candidates = configFlag ? [resolvePath(cwd, configFlag)] : ["wrangler.jsonc", "wrangler.json", "wrangler.toml"].map((f) => join(cwd, f));
408
+ for (const file of candidates) {
409
+ const text = files.readText(file, 1024 * 1024);
410
+ if (text === null)
411
+ continue;
412
+ try {
413
+ if (/\.toml$/i.test(file)) {
414
+ const top = text.split(/^\s*\[/m)[0];
415
+ const pick = (src, k) => new RegExp(`^\\s*${k}\\s*=\\s*"([^"\\n]+)"`, "m").exec(src)?.[1];
416
+ const base = pick(top, "name");
417
+ let name = base;
418
+ if (env) {
419
+ const section = new RegExp(`^\\s*\\[env\\.${env.replace(/[^A-Za-z0-9_-]/g, "")}\\]([\\s\\S]*?)(?=^\\s*\\[|$(?![\\s\\S]))`, "m").exec(text)?.[1];
420
+ name = (section && pick(section, "name")) ?? (base ? `${base}-${env}` : undefined);
421
+ }
422
+ const dir = /^\s*migrations_dir\s*=\s*"([^"\n]+)"/m.exec(text)?.[1];
423
+ return { name, account_id: pick(top, "account_id"), migrationsDir: dir };
424
+ }
425
+ const json = JSON.parse(stripJsonc(text));
426
+ const base = typeof json.name === "string" ? json.name : undefined;
427
+ let name = base;
428
+ if (env) {
429
+ const e = json.env?.[env]?.name;
430
+ name = typeof e === "string" ? e : base ? `${base}-${env}` : undefined;
431
+ }
432
+ const dir = json.d1_databases?.find((d) => typeof d.migrations_dir === "string")?.migrations_dir;
433
+ return { name, ...(typeof json.account_id === "string" ? { account_id: json.account_id } : {}), ...(typeof dir === "string" ? { migrationsDir: dir } : {}) };
434
+ }
435
+ catch {
436
+ return {};
437
+ }
438
+ }
439
+ return {};
440
+ }
441
+ const envClassOf = (env) => {
442
+ const e = (env ?? "").toLowerCase();
443
+ if (e === "")
444
+ return "unknown";
445
+ if (/^(?:prod|production|live)$/.test(e))
446
+ return "production";
447
+ if (/^(?:stage|staging|preprod|uat)$/.test(e))
448
+ return "staging";
449
+ if (/^(?:dev|development|test|testing|preview|local)$/.test(e))
450
+ return "development";
451
+ return "unknown";
452
+ };
453
+ /** A `KEY=value` assignment for `name` written before the program in the raw command. */
454
+ function inlineAssignment(raw, name) {
455
+ const m = new RegExp(`(?:^|\\s)${name}=(?:"([^"]*)"|'([^']*)'|([^\\s"']+))`).exec(raw);
456
+ return m ? (m[1] ?? m[2] ?? m[3]) : undefined;
457
+ }
458
+ function parseWrangler(sc, ctx) {
459
+ const argv = wranglerArgv(sc);
460
+ if (!argv)
461
+ return null;
462
+ const s = scan(argv, WR_VALUED);
463
+ // A flag this reader does not know might take a value, which would shift the positionals: no guess.
464
+ for (const f of s.flags.keys())
465
+ if (!WR_FLAGS.has(f))
466
+ return null;
467
+ const pos = s.positionals;
468
+ if (pos.length === 0)
469
+ return null;
470
+ const env = flagValue(s, "--env", "-e");
471
+ const cfg = readWranglerConfig(ctx.cwd, filesOf(ctx), flagValue(s, "--config", "-c"), env);
472
+ const accountId = inlineAssignment(sc.raw, "CLOUDFLARE_ACCOUNT_ID") ?? flagValue(s, "--account-id") ?? cfg.account_id ?? envOf(ctx, "CLOUDFLARE_ACCOUNT_ID") ?? UNBOUND;
473
+ return { area: pos[0], sub: pos[1] ?? "", pos, s, env, cfg, accountId };
474
+ }
475
+ const CF_KINDS = { worker: "worker", pages_project: "pages_project", pages_deployment: "pages_deployment", d1_database: "d1_database", r2_bucket: "r2_bucket", r2_object: "r2_object" };
476
+ function cloudflareOp(w, ctx, request, kind, verb, name, extra = {}, environmentClass) {
477
+ const bound = name !== undefined && name !== "";
478
+ return {
479
+ type: "cloudflare_resource", resource_id: ctx.key.resourceId(`cf:${kind}`, bound ? name : `\0${UNBOUND}`), ...common(ctx, request, environmentClass ?? envClassOf(w.env)),
480
+ verb, resource_kind: kind, account_id: w.accountId === UNBOUND ? UNBOUND : ctx.key.resourceId("cf:account", w.accountId),
481
+ ...(w.env ? { environment_binding: ctx.key.resourceId("cf:env", w.env) } : {}),
482
+ ...extra,
483
+ };
484
+ }
485
+ /** A local file path a Wrangler command sends, resolved and digested (plain SHA-256: the file is what is deployed). */
486
+ const fileDigest = (ctx, path) => {
487
+ if (!path)
488
+ return {};
489
+ const d = filesOf(ctx).sha256File(resolvePath(ctx.cwd, path));
490
+ return d ? { artifact_digest: d } : {};
491
+ };
492
+ const riskOf = (c) => (c.destructive || (c.verb === "update" && c.predicate_class === "all") ? "destructive" : "ordinary");
493
+ function d1Facts(w, ctx) {
494
+ const remote = w.s.flags.has("--remote") || w.s.flags.has("--preview");
495
+ const local = w.s.flags.has("--local");
496
+ if (remote && local)
497
+ return null;
498
+ const scope = remote ? "remote" : local ? "local" : "unknown";
499
+ const environment = local ? "development" : envClassOf(w.env);
500
+ const files = filesOf(ctx);
501
+ if (w.area === "d1" && w.sub === "execute") {
502
+ const db = w.pos[2];
503
+ const command = flagValue(w.s, "--command");
504
+ const file = flagValue(w.s, "--file");
505
+ if (!db || (command === undefined) === (file === undefined))
506
+ return null;
507
+ const text = command ?? files.readText(resolvePath(ctx.cwd, file), 5 * 1024 * 1024);
508
+ if (text === null)
509
+ return null;
510
+ const c = classifySql(text);
511
+ if (!c)
512
+ return null;
513
+ return { provider: "cloudflare_d1", verb: c.verb, predicate_class: c.predicate_class, risk: riskOf(c), scope, environment, databaseKey: db, databaseKind: "cf:d1_database", digestInput: { sql: text } };
514
+ }
515
+ if (w.area === "d1" && w.sub === "migrations" && w.pos[2] === "apply") {
516
+ const db = w.pos[3];
517
+ if (!db)
518
+ return null;
519
+ const dir = w.cfg.migrationsDir ?? "migrations";
520
+ const set = files.listSql(resolvePath(ctx.cwd, dir));
521
+ if (!set)
522
+ return null;
523
+ const c = classifySql(set.map((f) => f.text).join(";\n"));
524
+ return { provider: "cloudflare_d1", verb: "migrate", predicate_class: "not_applicable", risk: c ? riskOf(c) : "unknown", scope, environment, databaseKey: db, databaseKind: "cf:d1_database", digestInput: { migrations: set }, migrate: true };
525
+ }
526
+ return null;
527
+ }
528
+ const LOCAL_HOSTS = new Set(["", "localhost", "127.0.0.1", "::1", "[::1]"]);
529
+ const PSQL_VALUED_LONG = ["--host", "--port", "--username", "--dbname", "--command", "--file", "--output", "--set", "--variable", "--pset", "--field-separator", "--record-separator", "--table-attr", "--log-file"];
530
+ const PSQL_SHORT = "hpUdcfoLvPFRT";
531
+ function psqlFacts(sc, ctx) {
532
+ const commands = [];
533
+ const files = [];
534
+ let host, port, db;
535
+ const positional = [];
536
+ const argv = sc.argv;
537
+ for (let i = 0; i < argv.length; i++) {
538
+ const t = argv[i];
539
+ if (t === "--") {
540
+ positional.push(...argv.slice(i + 1));
541
+ break;
542
+ }
543
+ let name, value;
544
+ if (t.startsWith("--")) {
545
+ const at = t.indexOf("=");
546
+ name = at > 0 ? t.slice(0, at) : t;
547
+ value = at > 0 ? t.slice(at + 1) : PSQL_VALUED_LONG.includes(name) ? argv[++i] : undefined;
548
+ }
549
+ else if (t.startsWith("-") && t.length > 1) {
550
+ for (let k = 1; k < t.length; k++) {
551
+ if (PSQL_SHORT.includes(t[k])) {
552
+ name = `-${t[k]}`;
553
+ value = k + 1 < t.length ? t.slice(k + 1) : argv[++i];
554
+ break;
555
+ }
556
+ }
557
+ if (name === undefined)
558
+ continue;
559
+ }
560
+ else {
561
+ positional.push(t);
562
+ continue;
563
+ }
564
+ if (name === "-c" || name === "--command") {
565
+ if (value !== undefined)
566
+ commands.push(value);
567
+ }
568
+ else if (name === "-f" || name === "--file") {
569
+ if (value !== undefined)
570
+ files.push(value);
571
+ }
572
+ else if (name === "-h" || name === "--host")
573
+ host = value;
574
+ else if (name === "-p" || name === "--port")
575
+ port = value;
576
+ else if (name === "-d" || name === "--dbname")
577
+ db = value;
578
+ }
579
+ if (commands.length + files.length === 0)
580
+ return null;
581
+ const texts = [...commands];
582
+ for (const f of files) {
583
+ if (f === "-")
584
+ return null;
585
+ const text = filesOf(ctx).readText(resolvePath(ctx.cwd, f), 5 * 1024 * 1024);
586
+ if (text === null)
587
+ return null;
588
+ texts.push(text);
589
+ }
590
+ const c = classifySql(texts.join(";\n"));
591
+ if (!c)
592
+ return null;
593
+ // The connection: a URI, a key=value string, or a database name; credentials never read further than the host.
594
+ const target = db ?? positional[0];
595
+ if (target && /^postgres(?:ql)?:\/\//i.test(target)) {
596
+ try {
597
+ const u = new URL(target);
598
+ host ??= u.hostname;
599
+ port ??= u.port;
600
+ db = decodeURIComponent(u.pathname.replace(/^\//, ""));
601
+ }
602
+ catch {
603
+ return null;
604
+ }
605
+ }
606
+ else if (target && /(?:^|\s)(?:host|dbname|port)=/.test(target)) {
607
+ const kv = (k) => new RegExp(`(?:^|\\s)${k}=([^\\s]+)`).exec(target)?.[1];
608
+ host ??= kv("host");
609
+ port ??= kv("port");
610
+ db = kv("dbname");
611
+ }
612
+ else
613
+ db = target;
614
+ host ??= envOf(ctx, "PGHOST") ?? "";
615
+ port ??= envOf(ctx, "PGPORT") ?? "5432";
616
+ db ??= envOf(ctx, "PGDATABASE") ?? "";
617
+ const isLocal = LOCAL_HOSTS.has(host.toLowerCase());
618
+ return {
619
+ provider: "postgres", verb: c.verb, predicate_class: c.predicate_class, risk: riskOf(c), scope: isLocal ? "local" : "remote",
620
+ environment: isLocal ? "development" : "unknown", databaseKey: `${host.toLowerCase()}:${port}/${db}`, databaseKind: "pg", digestInput: { sql: texts.join(";\n") },
621
+ };
622
+ }
623
+ const SQLITE_VALUED = new Set(["-cmd", "-init", "-separator", "-newline", "-nullvalue", "-vfs", "-mmap", "-pagecache", "-lookaside", "-maxsize", "-A"]);
624
+ function sqliteFacts(sc, ctx) {
625
+ const positional = [];
626
+ const texts = [];
627
+ for (let i = 0; i < sc.argv.length; i++) {
628
+ const t = sc.argv[i];
629
+ if (t === "-init" || t === "-vfs" || t === "-A")
630
+ return null;
631
+ if (SQLITE_VALUED.has(t)) {
632
+ const v = sc.argv[++i];
633
+ if (t === "-cmd" && v !== undefined)
634
+ texts.push(v);
635
+ continue;
636
+ }
637
+ if (t.startsWith("-") && t.length > 1)
638
+ continue;
639
+ positional.push(t);
640
+ }
641
+ const [dbPath, ...sql] = positional;
642
+ if (!dbPath || sql.length === 0)
643
+ return null; // interactive, or SQL on standard input
644
+ const c = classifySql([...texts, ...sql].join(";\n"));
645
+ if (!c)
646
+ return null;
647
+ const key = dbPath === ":memory:" ? dbPath : resolvePath(ctx.cwd, dbPath).replace(/\\/g, "/").toLowerCase();
648
+ return { provider: "sqlite", verb: c.verb, predicate_class: c.predicate_class, risk: riskOf(c), scope: "local", environment: dbPath === ":memory:" ? "development" : "unknown", databaseKey: key, databaseKind: "sqlite", digestInput: { sql: [...texts, ...sql].join(";\n") } };
649
+ }
650
+ /** The database facts of a simple command, or null when it is not a readable database command. */
651
+ export function databaseFacts(sc, ctx) {
652
+ if (sc.opaque)
653
+ return null;
654
+ const program = canonProgram(sc.program);
655
+ if (program === "psql")
656
+ return psqlFacts(sc, ctx);
657
+ if (program === "sqlite3" || program === "sqlite")
658
+ return sqliteFacts(sc, ctx);
659
+ const w = parseWrangler(sc, ctx);
660
+ return w ? d1Facts(w, ctx) : null;
661
+ }
662
+ function databaseOperation(f, ctx, request) {
663
+ const id = ctx.key.resourceId(f.databaseKind, f.databaseKey);
664
+ return {
665
+ type: "database", resource_id: id, ...common(ctx, request, f.environment),
666
+ provider: f.provider, verb: f.verb, predicate_class: f.predicate_class, database_id: id, reviewed_destructive: false,
667
+ ...(f.digestInput !== undefined && (f.migrate || f.verb !== "read") ? { migration_digest: ctx.key.requestDigest({ domain: "sql", input: f.digestInput }) } : {}),
668
+ };
669
+ }
670
+ // ---- Cloudflare ------------------------------------------------------------------------------------------------
671
+ function cloudflareFromWrangler(w, ctx, request) {
672
+ const { area, sub, pos, s } = w;
673
+ if (s.flags.has("--dry-run") || s.flags.has("--help") || s.flags.has("-h"))
674
+ return null;
675
+ const name = (n) => n ?? undefined;
676
+ if (area === "deploy" || area === "publish")
677
+ return cloudflareOp(w, ctx, request, "worker", "update", name(flagValue(s, "--name") ?? w.cfg.name));
678
+ if (area === "delete" && pos.length <= 2)
679
+ return cloudflareOp(w, ctx, request, "worker", "delete", name(flagValue(s, "--name") ?? pos[1] ?? w.cfg.name));
680
+ if (area === "pages" && sub === "deploy") {
681
+ const dir = pos[2];
682
+ if (!dir)
683
+ return null;
684
+ const digest = filesOf(ctx).sha256Dir(resolvePath(ctx.cwd, dir));
685
+ const branch = flagValue(s, "--branch");
686
+ return cloudflareOp(w, ctx, request, "pages_deployment", "create", name(flagValue(s, "--project-name")), {
687
+ ...(digest ? { artifact_digest: digest } : {}), ...(branch ? { environment_binding: ctx.key.resourceId("cf:pages-branch", branch) } : {}),
688
+ });
689
+ }
690
+ if (area === "pages" && sub === "project" && (pos[2] === "create" || pos[2] === "delete") && pos[3])
691
+ return cloudflareOp(w, ctx, request, "pages_project", pos[2], pos[3]);
692
+ if (area === "d1" && (sub === "create" || sub === "delete") && pos[2])
693
+ return cloudflareOp(w, ctx, request, "d1_database", sub, pos[2]);
694
+ if (area === "r2" && sub === "bucket" && (pos[2] === "create" || pos[2] === "delete") && pos[3])
695
+ return cloudflareOp(w, ctx, request, "r2_bucket", pos[2], pos[3]);
696
+ if (area === "r2" && sub === "bucket" && pos[2] === "dev-url" && (pos[3] === "enable" || pos[3] === "disable") && pos[4]) {
697
+ return cloudflareOp(w, ctx, request, "r2_bucket", "set_visibility", pos[4], pos[3] === "enable"
698
+ ? { visibility_before: "unknown", visibility_after: "public" } : { visibility_before: "unknown", visibility_after: "private" });
699
+ }
700
+ if (area === "r2" && sub === "object" && (pos[2] === "put" || pos[2] === "delete") && pos[3]) {
701
+ return cloudflareOp(w, ctx, request, "r2_object", pos[2] === "put" ? "write" : "delete", pos[3], pos[2] === "put" ? fileDigest(ctx, flagValue(s, "--file")) : {});
702
+ }
703
+ return null;
704
+ }
705
+ // ---- entry points -------------------------------------------------------------------------------------------------
706
+ /** The typed operation for one simple command: a Wrangler or database CLI call, or a shell fetcher. */
707
+ export function deriveInfraOperation(sc, ctx, request, dialect = "posix", networkOnly = false) {
708
+ if (sc.opaque)
709
+ return null;
710
+ if (networkOnly)
711
+ return networkFromCommand(sc, ctx, request, dialect);
712
+ const facts = databaseFacts(sc, ctx);
713
+ if (facts)
714
+ return databaseOperation(facts, ctx, request);
715
+ const w = parseWrangler(sc, ctx);
716
+ if (w)
717
+ return cloudflareFromWrangler(w, ctx, request);
718
+ return networkFromCommand(sc, ctx, request, dialect);
719
+ }
720
+ function networkFromCommand(sc, ctx, request, dialect) {
721
+ const fetch = fetchFromCommand(sc, dialect);
722
+ if (!fetch)
723
+ return null;
724
+ const dest = parseDestination(fetch.url, true);
725
+ return dest ? networkOperation(dest, fetch.method, fetch.hasBody, ctx, request) : null;
726
+ }
727
+ export function databaseGuardActions(command, dialect, ctx) {
728
+ const src = dialect === "powershell" ? command.replace(/`(.)/g, "$1").replace(/\\/g, "/") : command;
729
+ const out = [];
730
+ for (const sc of decomposeShell(src)) {
731
+ if (sc.opaque)
732
+ continue;
733
+ let facts = null;
734
+ try {
735
+ facts = databaseFacts(sc, ctx);
736
+ }
737
+ catch {
738
+ facts = null;
739
+ }
740
+ if (facts) {
741
+ if (facts.scope !== "local")
742
+ out.push({ provider: facts.provider, verb: facts.verb, scope: facts.scope, risk: facts.risk });
743
+ continue;
744
+ }
745
+ // A wrangler d1 command whose SQL could not be read is not silently allowed on a remote database.
746
+ const w = parseWrangler(sc, ctx);
747
+ if (w && w.area === "d1" && (w.sub === "execute" || (w.sub === "migrations" && w.pos[2] === "apply")) && !w.s.flags.has("--local")) {
748
+ out.push({ provider: "cloudflare_d1", verb: "unknown", scope: w.s.flags.has("--remote") ? "remote" : "unknown", risk: "unknown" });
749
+ }
750
+ else if (canonProgram(sc.program) === "psql") {
751
+ const raw = sc.argv.join(" ");
752
+ if (!/(?:^|\s)(?:-h\s*|--host[= ])(?:localhost|127\.0\.0\.1)\b/.test(raw) && /\s-[a-zA-Z]*[cf]|--command|--file/.test(` ${raw}`))
753
+ out.push({ provider: "postgres", verb: "unknown", scope: "unknown", risk: "unknown" });
754
+ }
755
+ }
756
+ return out;
757
+ }
758
+ //# sourceMappingURL=typed-infra.js.map