saasaloy 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -91,9 +91,9 @@ Modules come in tiers. A **capability** scaffolds a workspace and sets conventio
91
91
 
92
92
  | Tier | Modules |
93
93
  |---|---|
94
- | Capability | `api`, `database`, `validators`, `logger`, `auth`, `admin`, `email`, `sms`, `queue`, `infra` |
95
- | Feature | `waitlist`, `teams`, `email-react` |
96
- | Provider | `email-console`, `email-cloudflare`, `email-plunk`, `logger-console`, `sms-console`, `queue-cloudflare`, `queue-memory` |
94
+ | Capability | `api`, `database`, `validators`, `logger`, `auth`, `admin`, `email`, `sms`, `queue`, `kv`, `infra` |
95
+ | Feature | `waitlist`, `teams`, `email-react`, `ratelimit`, `feature-flags` |
96
+ | Provider | `email-console`, `email-cloudflare`, `email-plunk`, `logger-console`, `sms-console`, `queue-cloudflare`, `queue-memory`, `kv-cloudflare`, `kv-memory` |
97
97
  | Driver | `database-d1`, `database-postgres` |
98
98
 
99
99
  `saasaloy add <name> --dry-run` prints what a module would do to your project before it does it. The one-table map of every module, what it gives you, and what it depends on is on the [Modules](https://github.com/mimukit/saasaloy/blob/main/docs/wiki/modules.md) page.
@@ -113,8 +113,9 @@ A few modules need something the free tier does not cover:
113
113
  | `database-postgres` | a Postgres server reachable from a Worker, with its URL in `DATABASE_URL`. Install instead of `database-d1`, never alongside |
114
114
  | `sms` | a third-party SMS account for any real send. Cloudflare has no SMS product. `sms-console` is free |
115
115
  | `queue-cloudflare` | a Workers paid plan, and the two queues created once with `wrangler queues create`. Install `queue-memory` instead for local work |
116
+ | `kv-cloudflare` | a Workers KV namespace created with `wrangler kv namespace create`, and its id pasted into `wrangler.jsonc`. `kv-memory` needs nothing |
116
117
 
117
- The local providers (`email-console`, `sms-console`, `logger-console`, `queue-memory`) log or run inline instead of calling a service, so local development needs no plan, domain, or key. Details for each are in the [Reference](https://github.com/mimukit/saasaloy/blob/main/docs/wiki/reference.md#email-providers).
118
+ The local providers (`email-console`, `sms-console`, `logger-console`, `queue-memory`, `kv-memory`) log or run inline instead of calling a service, so local development needs no plan, domain, or key. Details for each are in the [Reference](https://github.com/mimukit/saasaloy/blob/main/docs/wiki/reference.md#email-providers).
118
119
 
119
120
  ## Deploy
120
121
 
package/dist/index.js CHANGED
@@ -490,7 +490,7 @@ function insertChainedRoute(source, patch) {
490
490
  return source;
491
491
  }
492
492
  if (!(patch.import.name in mod.imports)) {
493
- mod.imports.$add({
493
+ mod.imports.$prepend({
494
494
  from: patch.import.from,
495
495
  imported: patch.import.name,
496
496
  local: patch.import.name
@@ -864,6 +864,193 @@ function toDiff(before, after, filename) {
864
864
  return patch;
865
865
  }
866
866
 
867
+ // src/lib/patch/drizzle-column.ts
868
+ import { generateCode as generateCode2, parseModule as parseModule3 } from "magicast";
869
+ var COLUMNS_ARG = 1;
870
+ function insertDrizzleColumn(source, patch) {
871
+ const mod = parseModule3(source);
872
+ const columns = columnsObject(mod, patch);
873
+ if (!columns) {
874
+ return source;
875
+ }
876
+ if (indexOfColumn(columns, patch.column) !== -1) {
877
+ return source;
878
+ }
879
+ parseExpressionNode(patch.value);
880
+ if (patch.import && foreignBinding(mod.imports, patch.import)) {
881
+ return source;
882
+ }
883
+ const withColumn = spliceColumn(source, columns, patch);
884
+ if (!withColumn) {
885
+ return source;
886
+ }
887
+ return addImport(withColumn, patch);
888
+ }
889
+ function removeDrizzleColumn(source, patch) {
890
+ const mod = parseModule3(source);
891
+ const columns = columnsObject(mod, patch);
892
+ const at = columns ? indexOfColumn(columns, patch.column) : -1;
893
+ if (!columns || at === -1) {
894
+ return source;
895
+ }
896
+ if (valueDrift(columns, at, patch)) {
897
+ return source;
898
+ }
899
+ if (patch.import && removeBindingConflict2(mod.imports, patch.import)) {
900
+ return source;
901
+ }
902
+ const withoutColumn = cutColumn(source, columns, at);
903
+ if (!withoutColumn) {
904
+ return source;
905
+ }
906
+ return dropImport(withoutColumn, patch);
907
+ }
908
+ function spliceColumn(source, columns, patch) {
909
+ const last = columns.properties.at(-1);
910
+ const end = offset(last, "end");
911
+ if (end === void 0) {
912
+ return void 0;
913
+ }
914
+ const indent = indentOf(source, offset(last, "start") ?? end);
915
+ const comma = source.slice(end).trimStart().startsWith(",");
916
+ const at = comma ? end + source.slice(end).indexOf(",") + 1 : end;
917
+ const line = `${indent}${patch.column}: ${patch.value}`;
918
+ return comma ? `${source.slice(0, at)}
919
+ ${line},${source.slice(at)}` : `${source.slice(0, at)},
920
+ ${line}${source.slice(at)}`;
921
+ }
922
+ function cutColumn(source, columns, at) {
923
+ const property = columns.properties[at];
924
+ const start = offset(property, "start");
925
+ const end = offset(property, "end");
926
+ if (start === void 0 || end === void 0) {
927
+ return void 0;
928
+ }
929
+ const from = source.lastIndexOf("\n", start) + 1;
930
+ const after = source.indexOf("\n", end);
931
+ const to = after === -1 ? source.length : after + 1;
932
+ const head = source.slice(from, start);
933
+ const tail = source.slice(end, to);
934
+ if (head.trim() !== "" || tail.replace(/^,/u, "").trim() !== "") {
935
+ return void 0;
936
+ }
937
+ return source.slice(0, from) + source.slice(to);
938
+ }
939
+ function indentOf(source, at) {
940
+ const from = source.lastIndexOf("\n", at) + 1;
941
+ return /^[\t ]*/u.exec(source.slice(from, at))?.[0] ?? "";
942
+ }
943
+ function offset(node, end) {
944
+ const value = node?.[end];
945
+ return typeof value === "number" ? value : void 0;
946
+ }
947
+ function addImport(source, patch) {
948
+ if (!patch.import) {
949
+ return source;
950
+ }
951
+ const mod = parseModule3(source);
952
+ if (patch.import.name in mod.imports) {
953
+ return source;
954
+ }
955
+ mod.imports.$prepend({
956
+ from: patch.import.from,
957
+ imported: patch.import.name,
958
+ local: patch.import.name
959
+ });
960
+ return print(source, mod);
961
+ }
962
+ function dropImport(source, patch) {
963
+ if (!patch.import) {
964
+ return source;
965
+ }
966
+ const mod = parseModule3(source);
967
+ const program = mod.$ast;
968
+ if (!(patch.import.name in mod.imports) || isReferenced(program, patch.import.name)) {
969
+ return source;
970
+ }
971
+ delete mod.imports[patch.import.name];
972
+ return print(source, mod);
973
+ }
974
+ function drizzleColumnInsertRefusal(source, patch) {
975
+ const mod = parseModule3(source);
976
+ if (!mod.exports[patch.exportName]) {
977
+ return `drizzle-column could not find an export named ${JSON.stringify(patch.exportName)}`;
978
+ }
979
+ const columns = columnsObject(mod, patch);
980
+ if (!columns) {
981
+ return `${JSON.stringify(patch.exportName)} is not a Drizzle table call with a columns object as its second argument`;
982
+ }
983
+ const at = indexOfColumn(columns, patch.column);
984
+ if (at !== -1 && valueDrift(columns, at, patch)) {
985
+ return `${JSON.stringify(patch.column)} on ${JSON.stringify(patch.exportName)} holds ${printed(columns, at)}, not the ${patch.value} this patch adds`;
986
+ }
987
+ if (at === -1 && patch.import) {
988
+ return insertBindingConflict2(mod.imports, patch);
989
+ }
990
+ return void 0;
991
+ }
992
+ function drizzleColumnRemoveRefusal(source, patch) {
993
+ const mod = parseModule3(source);
994
+ const columns = columnsObject(mod, patch);
995
+ const at = columns ? indexOfColumn(columns, patch.column) : -1;
996
+ if (!columns || at === -1) {
997
+ return void 0;
998
+ }
999
+ return valueDrift(columns, at, patch) ?? (patch.import ? removeBindingConflict2(mod.imports, patch.import) : void 0);
1000
+ }
1001
+ function columnsObject(mod, patch) {
1002
+ const { exports } = mod;
1003
+ const node = exports[patch.exportName]?.$args?.[COLUMNS_ARG]?.$ast;
1004
+ return node?.type === "ObjectExpression" ? node : void 0;
1005
+ }
1006
+ function indexOfColumn(columns, column) {
1007
+ return columns.properties.findIndex(
1008
+ (property) => property.key?.name === column || property.key?.value === column
1009
+ );
1010
+ }
1011
+ function parseExpressionNode(code) {
1012
+ const declaration = parseModule3(`const __drizzleColumn = ${code};`).$ast;
1013
+ const init = declaration.body.length === 1 ? declaration.body[0]?.declarations?.[0]?.init : void 0;
1014
+ if (!init) {
1015
+ throw new TypeError(
1016
+ `drizzle-column value is not a single expression: ${JSON.stringify(code)}`
1017
+ );
1018
+ }
1019
+ return init;
1020
+ }
1021
+ function printed(columns, at) {
1022
+ return generateCode2(columns.properties[at]?.value).code;
1023
+ }
1024
+ function valueDrift(columns, at, patch) {
1025
+ if (collapse(printed(columns, at)) === collapse(patch.value)) {
1026
+ return void 0;
1027
+ }
1028
+ return `${JSON.stringify(patch.column)} on ${JSON.stringify(patch.exportName)} reads ${printed(columns, at)} now, not the ${patch.value} that was applied`;
1029
+ }
1030
+ function collapse(code) {
1031
+ return code.replaceAll(/\s+/gu, "");
1032
+ }
1033
+ function insertBindingConflict2(imports, patch) {
1034
+ const held = patch.import ? foreignBinding(imports, patch.import) : void 0;
1035
+ if (!held || !patch.import) {
1036
+ return void 0;
1037
+ }
1038
+ return `${describeBinding(patch.import.name, held)}, so ${JSON.stringify(patch.column)} would read the wrong builder`;
1039
+ }
1040
+ function removeBindingConflict2(imports, want) {
1041
+ const held = foreignBinding(imports, want);
1042
+ if (!held) {
1043
+ return void 0;
1044
+ }
1045
+ return `${describeBinding(want.name, held)} now, not from ${JSON.stringify(want.from)}, so the column and its import are not the ones that were applied`;
1046
+ }
1047
+ function print(source, mod) {
1048
+ return keepTerminator(
1049
+ source,
1050
+ generateCode2(mod, { format: { objectCurlySpacing: true } }).code
1051
+ );
1052
+ }
1053
+
867
1054
  // src/lib/patch/jsonc.ts
868
1055
  import {
869
1056
  applyEdits,
@@ -1138,9 +1325,9 @@ function upsertPackageJsonScript(source, patch) {
1138
1325
  }
1139
1326
 
1140
1327
  // src/lib/patch/ts-module.ts
1141
- import { builders as builders2, generateCode as generateCode2, parseModule as parseModule3 } from "magicast";
1328
+ import { builders as builders2, generateCode as generateCode3, parseModule as parseModule4 } from "magicast";
1142
1329
  function insertIntoPluginArray(source, patch) {
1143
- const mod = parseModule3(source);
1330
+ const mod = parseModule4(source);
1144
1331
  const exported = mod.exports[patch.exportName];
1145
1332
  const callArg = exported?.$args?.[0];
1146
1333
  if (!callArg) {
@@ -1151,7 +1338,7 @@ function insertIntoPluginArray(source, patch) {
1151
1338
  return source;
1152
1339
  }
1153
1340
  if (!(patch.import.name in mod.imports)) {
1154
- mod.imports.$add({
1341
+ mod.imports.$prepend({
1155
1342
  from: patch.import.from,
1156
1343
  imported: patch.import.name,
1157
1344
  local: patch.import.name
@@ -1163,10 +1350,10 @@ function insertIntoPluginArray(source, patch) {
1163
1350
  } else {
1164
1351
  callArg[patch.arrayProp] = [newCall];
1165
1352
  }
1166
- return print(source, mod);
1353
+ return print2(source, mod);
1167
1354
  }
1168
1355
  function removeFromPluginArray(source, patch) {
1169
- const mod = parseModule3(source);
1356
+ const mod = parseModule4(source);
1170
1357
  const exported = mod.exports[patch.exportName];
1171
1358
  const callArg = exported?.$args?.[0];
1172
1359
  if (!callArg) {
@@ -1183,7 +1370,7 @@ function removeFromPluginArray(source, patch) {
1183
1370
  if (argumentDrift(array[index], patch)) {
1184
1371
  return source;
1185
1372
  }
1186
- if (removeBindingConflict2(mod.imports, patch)) {
1373
+ if (removeBindingConflict3(mod.imports, patch)) {
1187
1374
  return source;
1188
1375
  }
1189
1376
  array.splice(index, 1);
@@ -1191,10 +1378,10 @@ function removeFromPluginArray(source, patch) {
1191
1378
  if (patch.import.name in mod.imports && !isReferenced(program, patch.import.name)) {
1192
1379
  delete mod.imports[patch.import.name];
1193
1380
  }
1194
- return print(source, mod);
1381
+ return print2(source, mod);
1195
1382
  }
1196
1383
  function pluginArrayRemoveRefusal(source, patch) {
1197
- const mod = parseModule3(source);
1384
+ const mod = parseModule4(source);
1198
1385
  const callArg = mod.exports[patch.exportName]?.$args?.[0];
1199
1386
  if (!callArg) {
1200
1387
  return void 0;
@@ -1207,12 +1394,12 @@ function pluginArrayRemoveRefusal(source, patch) {
1207
1394
  if (index === -1) {
1208
1395
  return void 0;
1209
1396
  }
1210
- return argumentDrift(array[index], patch) ?? removeBindingConflict2(mod.imports, patch);
1397
+ return argumentDrift(array[index], patch) ?? removeBindingConflict3(mod.imports, patch);
1211
1398
  }
1212
- function print(source, mod) {
1399
+ function print2(source, mod) {
1213
1400
  return keepTerminator(
1214
1401
  source,
1215
- generateCode2(mod, { format: { objectCurlySpacing: true } }).code
1402
+ generateCode3(mod, { format: { objectCurlySpacing: true } }).code
1216
1403
  );
1217
1404
  }
1218
1405
  function findCallIndex(array, call) {
@@ -1230,7 +1417,7 @@ function argumentDrift(element, patch) {
1230
1417
  }
1231
1418
  return `${patch.call}() in ${JSON.stringify(patch.arrayProp)} takes arguments now, not the bare call that was applied`;
1232
1419
  }
1233
- function removeBindingConflict2(imports, patch) {
1420
+ function removeBindingConflict3(imports, patch) {
1234
1421
  const held = foreignBinding(imports, patch.import);
1235
1422
  if (!held) {
1236
1423
  return void 0;
@@ -1245,6 +1432,7 @@ function isFunctionCall(value) {
1245
1432
  var PATCH_KIND_KEYS = {
1246
1433
  "chained-route": true,
1247
1434
  "const-array": true,
1435
+ "drizzle-column": true,
1248
1436
  "package-json-dependency": true,
1249
1437
  "package-json-script": true,
1250
1438
  "plugin-array": true,
@@ -1281,6 +1469,9 @@ function applyCodemod(source, patch) {
1281
1469
  case "const-array": {
1282
1470
  return insertIntoConstArray(source, patch);
1283
1471
  }
1472
+ case "drizzle-column": {
1473
+ return insertDrizzleColumn(source, patch);
1474
+ }
1284
1475
  case "wrangler-binding": {
1285
1476
  return upsertWranglerBinding(source, patch);
1286
1477
  }
@@ -1305,6 +1496,7 @@ function applyCodemod(source, patch) {
1305
1496
  var INVERSES = {
1306
1497
  "chained-route": removeChainedRoute,
1307
1498
  "const-array": removeFromConstArray,
1499
+ "drizzle-column": removeDrizzleColumn,
1308
1500
  "plugin-array": removeFromPluginArray,
1309
1501
  "wrangler-binding": removeWranglerBinding
1310
1502
  };
@@ -1328,10 +1520,12 @@ function reversePatch(source, patch, filename) {
1328
1520
  var REFUSALS = {
1329
1521
  "chained-route": chainedRouteInsertRefusal,
1330
1522
  "const-array": constArrayInsertRefusal,
1523
+ "drizzle-column": drizzleColumnInsertRefusal,
1331
1524
  "package-json-script": packageJsonScriptRefusal
1332
1525
  };
1333
1526
  var REVERSAL_REFUSALS = {
1334
1527
  "chained-route": chainedRouteRemoveRefusal,
1528
+ "drizzle-column": drizzleColumnRemoveRefusal,
1335
1529
  "plugin-array": pluginArrayRemoveRefusal,
1336
1530
  "wrangler-binding": wranglerBindingRemoveRefusal
1337
1531
  };
@@ -3687,7 +3881,7 @@ import { fileURLToPath as fileURLToPath3 } from "url";
3687
3881
  // package.json
3688
3882
  var package_default = {
3689
3883
  name: "saasaloy",
3690
- version: "0.2.0",
3884
+ version: "0.3.0",
3691
3885
  description: "Composable SaaS accelerator kit.",
3692
3886
  keywords: [
3693
3887
  "saas",
@@ -3738,19 +3932,19 @@ var package_default = {
3738
3932
  },
3739
3933
  devDependencies: {
3740
3934
  "@release-it/conventional-changelog": "12.0.0",
3741
- "@types/node": "26.1.1",
3742
- "@vitest/coverage-v8": "4.1.10",
3935
+ "@types/node": "26.4.1",
3936
+ "@vitest/coverage-v8": "5.0.0",
3743
3937
  "release-it": "21.0.2",
3744
3938
  tsup: "8.5.1",
3745
- vitest: "4.1.10"
3939
+ vitest: "5.0.0"
3746
3940
  },
3747
3941
  dependencies: {
3748
3942
  "@clack/prompts": "1.7.0",
3749
- ajv: "8.17.1",
3943
+ ajv: "8.20.0",
3750
3944
  diff: "9.0.0",
3751
- giget: "3.3.0",
3945
+ giget: "3.3.1",
3752
3946
  "jsonc-parser": "3.3.1",
3753
- magicast: "0.5.3",
3947
+ magicast: "0.5.4",
3754
3948
  picocolors: "1.1.1"
3755
3949
  }
3756
3950
  };
@@ -4055,6 +4249,8 @@ async function baseUpdateInput(args) {
4055
4249
  }
4056
4250
 
4057
4251
  // src/lib/doctor.ts
4252
+ import { findNodeAtLocation as findNodeAtLocation4, getNodeValue as getNodeValue4, parseTree as parseTree4 } from "jsonc-parser";
4253
+ import { parseModule as parseModule5 } from "magicast";
4058
4254
  import { readFile as readFile13 } from "fs/promises";
4059
4255
  import { basename, dirname as dirname6, join as join11, posix as posix7, resolve as resolve3 } from "path";
4060
4256
  var SKILL_PREFIX = "saasaloy-";
@@ -4311,6 +4507,200 @@ function checkProject(args) {
4311
4507
  )
4312
4508
  );
4313
4509
  }
4510
+ var KV_INDEX_FILE = "packages/kv/src/index.ts";
4511
+ var WRANGLER_FILE = "apps/api/wrangler.jsonc";
4512
+ var CLOUDFLARE_KV_MODULE = "kv-cloudflare";
4513
+ function checkPolicyBindings(args) {
4514
+ const bound = new Set(rateLimitBindingNames(args.wrangler));
4515
+ const findings = [];
4516
+ for (const entry of registeredPolicies(args)) {
4517
+ if (entry.name === void 0) {
4518
+ findings.push(
4519
+ finding(
4520
+ "ratelimit",
4521
+ `/policies/${String(entry.index)}`,
4522
+ `the policy at index ${String(entry.index)} of the \`policies\` array in ${KV_INDEX_FILE} has no name this check can read, so its RL_<NAME> binding in ${WRANGLER_FILE} was not checked. Write it as \`definePolicy({ name: "..." })\`, or as a factory exported from a file under packages/kv/src/policies/.`
4523
+ )
4524
+ );
4525
+ continue;
4526
+ }
4527
+ if (!bound.has(bindingFor(entry.name))) {
4528
+ findings.push(
4529
+ finding(
4530
+ "ratelimit",
4531
+ `/policies/${entry.name}`,
4532
+ `policy "${entry.name}" has no ${bindingFor(entry.name)} entry in ${WRANGLER_FILE} \u2014 consume({ policy: "${entry.name}" }) throws not_supported on the first request. Add the binding, or drop the policy from ${KV_INDEX_FILE}.`
4533
+ )
4534
+ );
4535
+ }
4536
+ }
4537
+ return findings;
4538
+ }
4539
+ function bindingFor(policy) {
4540
+ return `RL_${policy.toUpperCase()}`;
4541
+ }
4542
+ function rateLimitBindingNames(wrangler) {
4543
+ const root = parseTree4(wrangler);
4544
+ if (!root) {
4545
+ return [];
4546
+ }
4547
+ const node = findNodeAtLocation4(root, ["ratelimits"]);
4548
+ if (node?.type !== "array") {
4549
+ return [];
4550
+ }
4551
+ const names = [];
4552
+ for (const child of node.children ?? []) {
4553
+ const value = getNodeValue4(child);
4554
+ const name = asRecord(value).name;
4555
+ if (typeof name === "string") {
4556
+ names.push(name);
4557
+ }
4558
+ }
4559
+ return names;
4560
+ }
4561
+ function registeredPolicies(args) {
4562
+ let mod;
4563
+ try {
4564
+ mod = parseModule5(args.index);
4565
+ } catch {
4566
+ return [];
4567
+ }
4568
+ const callArg = mod.exports.kv?.$args?.[0];
4569
+ const array = callArg?.policies;
4570
+ if (!Array.isArray(array)) {
4571
+ return [];
4572
+ }
4573
+ const imports = mod.imports;
4574
+ const policies = [];
4575
+ for (let i = 0; i < array.length; i++) {
4576
+ const element = array[i];
4577
+ policies.push({ index: i, name: policyNameOf(element, args, imports) });
4578
+ }
4579
+ return policies;
4580
+ }
4581
+ function policyNameOf(element, args, imports) {
4582
+ const callee = calleeName(element);
4583
+ if (callee === void 0) {
4584
+ return void 0;
4585
+ }
4586
+ const inline = inlineNameArgument(element);
4587
+ if (inline !== void 0) {
4588
+ return inline;
4589
+ }
4590
+ const from = imports[callee]?.from;
4591
+ const source = typeof from === "string" ? args.policySources[from] : void 0;
4592
+ return source === void 0 ? void 0 : policyNameIn(source, callee);
4593
+ }
4594
+ function inlineNameArgument(element) {
4595
+ const args = element.$args;
4596
+ if (!Array.isArray(args)) {
4597
+ return void 0;
4598
+ }
4599
+ const name = asRecord(args[0]).name;
4600
+ return typeof name === "string" ? name : void 0;
4601
+ }
4602
+ function calleeName(element) {
4603
+ if (typeof element !== "object" || element === null) {
4604
+ return void 0;
4605
+ }
4606
+ const record = element;
4607
+ return record.$type === "function-call" && typeof record.$callee === "string" ? record.$callee : void 0;
4608
+ }
4609
+ function policyNameIn(source, callee) {
4610
+ let program;
4611
+ try {
4612
+ program = parseModule5(source).$ast;
4613
+ } catch {
4614
+ return void 0;
4615
+ }
4616
+ const factory = findNode(
4617
+ program,
4618
+ (node) => (node.type === "FunctionDeclaration" || node.type === "VariableDeclarator") && asRecord(node.id).name === callee
4619
+ );
4620
+ if (!factory) {
4621
+ return void 0;
4622
+ }
4623
+ const call = findNode(
4624
+ factory,
4625
+ (node) => node.type === "CallExpression" && asRecord(node.callee).name === "definePolicy"
4626
+ );
4627
+ const argument = asArray(call?.arguments)[0];
4628
+ const property = asArray(asRecord(argument).properties).find(
4629
+ (entry) => asRecord(asRecord(entry).key).name === "name"
4630
+ );
4631
+ const value = asRecord(asRecord(property).value).value;
4632
+ return typeof value === "string" ? value : void 0;
4633
+ }
4634
+ function findNode(root, accept) {
4635
+ if (Array.isArray(root)) {
4636
+ for (const child of root) {
4637
+ const found = findNode(child, accept);
4638
+ if (found) {
4639
+ return found;
4640
+ }
4641
+ }
4642
+ return void 0;
4643
+ }
4644
+ if (typeof root !== "object" || root === null) {
4645
+ return void 0;
4646
+ }
4647
+ const node = root;
4648
+ if (typeof node.type === "string") {
4649
+ const typed = node;
4650
+ if (accept(typed)) {
4651
+ return typed;
4652
+ }
4653
+ }
4654
+ for (const [key, value] of Object.entries(node)) {
4655
+ if (key === "loc" || key === "comments" || key === "original") {
4656
+ continue;
4657
+ }
4658
+ const found = findNode(value, accept);
4659
+ if (found) {
4660
+ return found;
4661
+ }
4662
+ }
4663
+ return void 0;
4664
+ }
4665
+ async function readPolicyState(root, installed) {
4666
+ if (!installed.includes(CLOUDFLARE_KV_MODULE)) {
4667
+ return void 0;
4668
+ }
4669
+ const index = await readIfPresent(resolveWithinRoot(root, KV_INDEX_FILE));
4670
+ const wrangler = await readIfPresent(resolveWithinRoot(root, WRANGLER_FILE));
4671
+ if (index === void 0 || wrangler === void 0) {
4672
+ return void 0;
4673
+ }
4674
+ const policySources = {};
4675
+ for (const specifier of relativeImportsIn(index)) {
4676
+ const source = await readIfPresent(
4677
+ resolveWithinRoot(
4678
+ root,
4679
+ posix7.join(posix7.dirname(KV_INDEX_FILE), `${specifier}.ts`)
4680
+ )
4681
+ );
4682
+ if (source !== void 0) {
4683
+ policySources[specifier] = source;
4684
+ }
4685
+ }
4686
+ return { index, policySources, wrangler };
4687
+ }
4688
+ function relativeImportsIn(index) {
4689
+ let mod;
4690
+ try {
4691
+ mod = parseModule5(index);
4692
+ } catch {
4693
+ return [];
4694
+ }
4695
+ const imports = mod.imports;
4696
+ const specifiers = /* @__PURE__ */ new Set();
4697
+ for (const held of Object.values(imports)) {
4698
+ if (typeof held?.from === "string" && held.from.startsWith(".")) {
4699
+ specifiers.add(held.from);
4700
+ }
4701
+ }
4702
+ return [...specifiers];
4703
+ }
4314
4704
  async function resolveDoctorTarget(path) {
4315
4705
  const dir = resolve3(path);
4316
4706
  if (await pathExists(join11(dir, "registry-item.json"))) {
@@ -4456,9 +4846,11 @@ async function reportProject(path) {
4456
4846
  seedFiles: (await readBaseDeclaration(await baseTemplateDir())).seedFiles
4457
4847
  });
4458
4848
  note2(wrapForNote(renderBase(base)), "Base");
4849
+ const policyState = await readPolicyState(path, config.installed);
4459
4850
  const findings = [
4460
4851
  ...checkProject({ config, manifest }),
4461
- ...checkPartialInstalls({ installed: config.installed, manifest })
4852
+ ...checkPartialInstalls({ installed: config.installed, manifest }),
4853
+ ...policyState ? checkPolicyBindings(policyState) : []
4462
4854
  ];
4463
4855
  if (findings.length === 0) {
4464
4856
  const count = config.installed.length;