naro-mcp 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 (3) hide show
  1. package/README.md +17 -16
  2. package/dist/index.js +1091 -128
  3. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -48,6 +48,12 @@ async function errorFromResponse(response, context) {
48
48
  const serverMessage = typeof body?.error?.message === "string" ? body.error.message : null;
49
49
  const code = serverCode ?? (response.status === 401 ? "UNAUTHORIZED" : response.status === 404 ? "NOT_FOUND" : response.status === 429 ? "RATE_LIMITED" : "INTERNAL_ERROR");
50
50
  const details = {};
51
+ const serverDetails = body?.error?.details;
52
+ if (typeof serverDetails === "object" && serverDetails !== null && !Array.isArray(serverDetails))
53
+ Object.assign(details, serverDetails);
54
+ const retryAfter = response.headers.get("retry-after")?.trim() ?? "";
55
+ if (response.status === 429 && /^\d{1,9}$/.test(retryAfter))
56
+ details.retryAfter = Number(retryAfter);
51
57
  if (body?.error?.fields !== void 0) details.fields = body.error.fields;
52
58
  if (typeof body?.error?.installUrl === "string")
53
59
  details.installUrl = body.error.installUrl;
@@ -459,6 +465,18 @@ var TERMINAL_DEPLOYMENT_STATUSES = [
459
465
  function isTerminalStatus(status) {
460
466
  return TERMINAL_DEPLOYMENT_STATUSES.includes(status);
461
467
  }
468
+ var SCHEMA_COLUMN_TYPES = [
469
+ "INTEGER",
470
+ "REAL",
471
+ "TEXT",
472
+ "BLOB",
473
+ "NUMERIC",
474
+ "BOOLEAN",
475
+ "DATETIME",
476
+ "JSON"
477
+ ];
478
+ var STALLED_GRACE_MS = 2 * 6e4;
479
+ var POLICY_ACTIONS = ["select", "insert", "update", "delete"];
462
480
  function projectInfo(row) {
463
481
  return {
464
482
  id: row.id,
@@ -516,6 +534,7 @@ var NaroClient = class {
516
534
  domains;
517
535
  logs;
518
536
  db;
537
+ backend;
519
538
  constructor(options2) {
520
539
  this.baseUrl = (options2.baseUrl ?? controlPlaneUrl()).replace(/\/$/, "");
521
540
  this.getAccessToken = options2.getAccessToken;
@@ -529,9 +548,11 @@ var NaroClient = class {
529
548
  this.domains = new DomainsApi(this);
530
549
  this.logs = new LogsApi(this);
531
550
  this.db = new DbApi(this);
551
+ this.backend = new BackendApi(this);
532
552
  }
533
553
  async request(path, options2) {
534
554
  const token = await this.getAccessToken();
555
+ const timeoutMs = options2.timeoutMs ?? this.requestTimeoutMs;
535
556
  let response;
536
557
  try {
537
558
  response = await this.doFetch(`${this.baseUrl}${path}`, {
@@ -541,13 +562,13 @@ var NaroClient = class {
541
562
  ...options2.body !== void 0 ? { "Content-Type": "application/json" } : {}
542
563
  },
543
564
  ...options2.body !== void 0 ? { body: JSON.stringify(options2.body) } : {},
544
- signal: AbortSignal.timeout(this.requestTimeoutMs)
565
+ signal: AbortSignal.timeout(timeoutMs)
545
566
  });
546
567
  } catch (error51) {
547
568
  if (error51 instanceof Error && (error51.name === "TimeoutError" || error51.name === "AbortError")) {
548
569
  throw new NaroError(
549
570
  "TIMEOUT",
550
- `${options2.context} timed out after ${String(Math.round(this.requestTimeoutMs / 1e3))}s.`,
571
+ `${options2.context} timed out after ${String(Math.round(timeoutMs / 1e3))}s.`,
551
572
  { cause: error51 }
552
573
  );
553
574
  }
@@ -1115,6 +1136,255 @@ var DbApi = class {
1115
1136
  context: "Pull schema"
1116
1137
  });
1117
1138
  }
1139
+ /**
1140
+ * Changes the schema without SQL (spec 2026-09-24 P2-4): the server
1141
+ * renders the DDL, checks it and applies it as ONE migration recorded in
1142
+ * `d1_migrations` — all operations or none. `dryRun` returns the SQL and
1143
+ * the name without applying anything. Save `sql` as `migrations/<name>` so
1144
+ * `naro db reset` re-applies it; a refused operation's index is in
1145
+ * `details.operation`.
1146
+ */
1147
+ async schema(projectId, operations, options2 = {}) {
1148
+ return this.client.request(
1149
+ this.path(projectId, "/schema"),
1150
+ {
1151
+ method: "POST",
1152
+ // Only the keys given: the server validates each one it sees.
1153
+ body: {
1154
+ operations,
1155
+ ...options2.dryRun !== void 0 ? { dryRun: options2.dryRun } : {},
1156
+ ...options2.name !== void 0 ? { name: options2.name } : {}
1157
+ },
1158
+ context: "Change schema"
1159
+ }
1160
+ );
1161
+ }
1162
+ };
1163
+ var PROVISION_TIMEOUT_MS = 12e4;
1164
+ function backendInfo(body) {
1165
+ return {
1166
+ status: body.status,
1167
+ step: body.step,
1168
+ error: body.error,
1169
+ url: body.url,
1170
+ anonKey: body.anonKey,
1171
+ serviceKeyPrefix: body.serviceKeyPrefix,
1172
+ leaseExpiresAt: body.leaseExpiresAt ?? null,
1173
+ // Only a real verdict: without it `isStalledBackend` judges locally.
1174
+ ...typeof body.stalled === "boolean" ? { stalled: body.stalled } : {},
1175
+ // A control plane from before the toggle could not turn a backend off:
1176
+ // whatever it has is on.
1177
+ enabled: typeof body.enabled === "boolean" ? body.enabled : body.status !== "none",
1178
+ disabledAt: body.disabledAt ?? null,
1179
+ turningOff: body.turningOff === true
1180
+ };
1181
+ }
1182
+ var BackendApi = class {
1183
+ constructor(client) {
1184
+ this.client = client;
1185
+ }
1186
+ path(projectId, suffix = "") {
1187
+ return `/api/projects/${encodeURIComponent(projectId)}/backend${suffix}`;
1188
+ }
1189
+ /**
1190
+ * `status: "none"` until the backend is first turned on; `enabled` says
1191
+ * whether it is on now.
1192
+ */
1193
+ async info(projectId) {
1194
+ const body = await this.client.request(this.path(projectId), {
1195
+ context: "Read backend"
1196
+ });
1197
+ return backendInfo(body);
1198
+ }
1199
+ /**
1200
+ * Turns the backend on (toggle spec §2.3): the first time it provisions
1201
+ * the D1, the keys and the Naro worker; a backend that was turned off
1202
+ * comes back with the same keys — its worker is uploaded again, nothing
1203
+ * else is made anew. Safe to repeat, and a repair: a ready backend that
1204
+ * is on has its routes written again and its worker checked (no upload),
1205
+ * a failed run resumes at its step. A step that fails is not an
1206
+ * HTTP error — the answer is 200 with `status: "error"`, `step` and
1207
+ * `error`. While another run holds the project's lease this answers the
1208
+ * row as it stands (`provisioning`).
1209
+ */
1210
+ async enable(projectId) {
1211
+ const body = await this.client.request(
1212
+ this.path(projectId, "/enable"),
1213
+ {
1214
+ method: "POST",
1215
+ context: "Enable backend",
1216
+ timeoutMs: PROVISION_TIMEOUT_MS
1217
+ }
1218
+ );
1219
+ return backendInfo(body);
1220
+ }
1221
+ /**
1222
+ * Turns the backend off (toggle spec §2.2): the dispatcher's routes to
1223
+ * the Naro worker go, so the app's sign-ins and data requests answer 404
1224
+ * from `/_naro/*` — in up to about 90 seconds, as the dispatcher's cache
1225
+ * ages out. The D1 with its data and users, the keys, the sign-in
1226
+ * settings and the worker stay; `enable` brings it back as it was.
1227
+ * Safe to repeat, and a repair: a backend already off has its routes
1228
+ * removed again — what finishes a turn-off that stopped half-way
1229
+ * (`turningOff`) — and keeps its first `disabledAt`. No backend at all
1230
+ * answers as it stands. `BACKEND_BUSY` while another run holds the lease.
1231
+ */
1232
+ async disable(projectId) {
1233
+ const body = await this.client.request(
1234
+ this.path(projectId, "/disable"),
1235
+ { method: "POST", context: "Disable backend" }
1236
+ );
1237
+ return backendInfo(body);
1238
+ }
1239
+ /**
1240
+ * `enable` under its first name: the control plane keeps
1241
+ * `POST …/backend/provision` as the same call.
1242
+ */
1243
+ async provision(projectId) {
1244
+ const body = await this.client.request(
1245
+ this.path(projectId, "/provision"),
1246
+ {
1247
+ method: "POST",
1248
+ context: "Provision backend",
1249
+ timeoutMs: PROVISION_TIMEOUT_MS
1250
+ }
1251
+ );
1252
+ return backendInfo(body);
1253
+ }
1254
+ /** The anon key and the service key's prefix; BACKEND_NOT_READY before provisioning. */
1255
+ async keys(projectId) {
1256
+ const body = await this.client.request(
1257
+ this.path(projectId, "/keys"),
1258
+ { context: "Read API keys" }
1259
+ );
1260
+ return { anonKey: body.anonKey, serviceKeyPrefix: body.serviceKeyPrefix };
1261
+ }
1262
+ /**
1263
+ * The whole service key. It skips every policy — servers only. A role
1264
+ * without `database.write` gets `FORBIDDEN` "Your role cannot reveal the
1265
+ * service key."
1266
+ */
1267
+ async revealServiceKey(projectId) {
1268
+ const body = await this.client.request(
1269
+ this.path(projectId, "/keys?reveal=service"),
1270
+ { context: "Reveal service key" }
1271
+ );
1272
+ return body.serviceKey;
1273
+ }
1274
+ /**
1275
+ * Replaces the live key of one kind, in two phases (spec §6 "(P1-3 리뷰
1276
+ * 반영)" I3): the worker takes the new hash first, and only then does the
1277
+ * database swap keys. There is no overlap — the old key stops working once
1278
+ * both are done. Only a `ready` backend that is on rotates
1279
+ * (`BACKEND_NOT_READY`, `BACKEND_BUSY`, or `BACKEND_OFF` while it is
1280
+ * turned off).
1281
+ *
1282
+ * The database decides the outcome. Whenever the new key ends up live —
1283
+ * even if a step after the swap failed — this resolves with the key and
1284
+ * `backend.status` `error`. Otherwise it throws `BACKEND_ROTATION_FAILED`
1285
+ * with no key; that usually leaves the old key working, but a 5xx, a
1286
+ * timeout or a lost connection is not proof either way: `keys()` (and
1287
+ * `revealServiceKey()`) show which key is live, and `enable` puts the
1288
+ * stored keys back on the worker. For a few seconds during any rotation
1289
+ * the worker can already refuse the old key.
1290
+ */
1291
+ async rotateKey(projectId, kind) {
1292
+ const body = await this.client.request(this.path(projectId, "/keys/rotate"), {
1293
+ method: "POST",
1294
+ body: { kind },
1295
+ context: "Rotate API key",
1296
+ timeoutMs: PROVISION_TIMEOUT_MS
1297
+ });
1298
+ const backend = backendInfo(body);
1299
+ const key = kind === "anon" ? backend.anonKey : body.serviceKey;
1300
+ if (typeof key !== "string" || key === "")
1301
+ throw new NaroError(
1302
+ "INTERNAL_ERROR",
1303
+ "The rotation answer carried no new key. `naro backend keys` shows the key stored now (--reveal for the service key).",
1304
+ { details: { backend } }
1305
+ );
1306
+ return { backend, key };
1307
+ }
1308
+ /**
1309
+ * The sign-in settings (spec P2-6 §12): email, Google and GitHub, the
1310
+ * allowed redirect origins, whether sign-in waits for a confirmed email,
1311
+ * and the callback URLs to register with the providers. Never a secret.
1312
+ */
1313
+ async auth(projectId) {
1314
+ return this.client.request(
1315
+ this.path(projectId, "/auth"),
1316
+ {
1317
+ context: "Read sign-in settings"
1318
+ }
1319
+ );
1320
+ }
1321
+ /**
1322
+ * Changes the given settings and uploads the Naro worker again with them,
1323
+ * under the provisioning lease: `BACKEND_BUSY` while another run holds it
1324
+ * (nothing is saved), `BACKEND_NOT_READY` before the first provision,
1325
+ * `BACKEND_OFF` while the backend is turned off (nothing is saved),
1326
+ * `VERIFICATION_NEEDS_EMAIL` for a state where nobody could sign in. A
1327
+ * failed upload is not an error — `backend.status` says `error` and the
1328
+ * settings are saved; `enable` finishes. Two minutes, like enable.
1329
+ */
1330
+ async setAuth(projectId, patch) {
1331
+ const answer = await this.client.request(
1332
+ this.path(projectId, "/auth"),
1333
+ {
1334
+ method: "PUT",
1335
+ body: patch,
1336
+ context: "Change sign-in settings",
1337
+ timeoutMs: PROVISION_TIMEOUT_MS
1338
+ }
1339
+ );
1340
+ return { auth: answer.auth, backend: backendInfo(answer.backend) };
1341
+ }
1342
+ /** Removes email or one provider; the same answer as `setAuth`. */
1343
+ async removeAuth(projectId, target) {
1344
+ const answer = await this.client.request(
1345
+ this.path(projectId, `/auth?target=${encodeURIComponent(target)}`),
1346
+ {
1347
+ method: "DELETE",
1348
+ context: "Change sign-in settings",
1349
+ timeoutMs: PROVISION_TIMEOUT_MS
1350
+ }
1351
+ );
1352
+ return { auth: answer.auth, backend: backendInfo(answer.backend) };
1353
+ }
1354
+ /** Every stored policy, by table name. Tables without one deny anon and signed-in requests. */
1355
+ async policies(projectId) {
1356
+ const body = await this.client.request(
1357
+ this.path(projectId, "/policies"),
1358
+ { context: "List policies" }
1359
+ );
1360
+ return body.policies;
1361
+ }
1362
+ /**
1363
+ * Merges the given fields into the table's policy (what is left out keeps
1364
+ * its stored value, or `none`) and returns the whole row as stored, under
1365
+ * the table's own spelling. An `owner` rule needs `ownerColumn`.
1366
+ */
1367
+ async setPolicy(projectId, table, patch) {
1368
+ const body = {};
1369
+ for (const key of [...POLICY_ACTIONS, "ownerColumn"])
1370
+ if (patch[key] !== void 0) body[key] = patch[key];
1371
+ const answer = await this.client.request(
1372
+ this.path(projectId, `/policies/${encodeURIComponent(table)}`),
1373
+ { method: "PUT", body, context: "Set policy" }
1374
+ );
1375
+ return answer.policy;
1376
+ }
1377
+ /**
1378
+ * Deletes the table's policy, so it denies anon and signed-in requests
1379
+ * again. `POLICY_NOT_FOUND` when the table has none.
1380
+ */
1381
+ async removePolicy(projectId, table) {
1382
+ const answer = await this.client.request(
1383
+ this.path(projectId, `/policies/${encodeURIComponent(table)}`),
1384
+ { method: "DELETE", context: "Remove policy" }
1385
+ );
1386
+ return answer.policy;
1387
+ }
1118
1388
  };
1119
1389
 
1120
1390
  // ../../packages/sdk/src/database.ts
@@ -1142,27 +1412,203 @@ var MAX_FILE_BYTES = 1024 * 1024;
1142
1412
  import { existsSync as existsSync3, readFileSync as readFileSync4 } from "fs";
1143
1413
  import { join as join4 } from "path";
1144
1414
 
1415
+ // ../../packages/data-core/src/system-tables.ts
1416
+ var SYSTEM_TABLE_PREFIXES = [
1417
+ "_naro_",
1418
+ "_cf_",
1419
+ "sqlite_"
1420
+ ];
1421
+ var SYSTEM_TABLE_NAMES = ["d1_migrations"];
1422
+ function asciiLower(text) {
1423
+ return text.replace(/[A-Z]+/g, (run) => run.toLowerCase());
1424
+ }
1425
+ function likeLiteral(text) {
1426
+ return text.replace(/[\\%_]/g, (char) => `\\${char}`).replace(/'/g, "''");
1427
+ }
1428
+ var COLUMN_REFERENCE = /^[A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*)?$/;
1429
+ function systemTablePredicate(column = "name") {
1430
+ if (!COLUMN_REFERENCE.test(column))
1431
+ throw new Error(`Not a column reference: ${column}`);
1432
+ return [
1433
+ ...SYSTEM_TABLE_PREFIXES.map(
1434
+ (prefix) => `${column} NOT LIKE '${likeLiteral(prefix)}%' ESCAPE '\\'`
1435
+ ),
1436
+ ...SYSTEM_TABLE_NAMES.map(
1437
+ (name) => `${column} NOT LIKE '${likeLiteral(name)}' ESCAPE '\\'`
1438
+ ),
1439
+ `${column} NOT IN (SELECT name FROM pragma_table_list WHERE schema = 'main' AND type = 'shadow')`
1440
+ ].join(" AND ");
1441
+ }
1442
+ var SYSTEM_TABLE_SQL_PREDICATE = systemTablePredicate("name");
1443
+
1444
+ // ../../packages/data-core/src/query.ts
1445
+ var MAX_BODY_BYTES = 1024 * 1024;
1446
+
1447
+ // ../../packages/data-core/src/schema.ts
1448
+ var SCHEMA_SQL = [
1449
+ `SELECT t.name AS table_name, t.type AS table_type, c.cid AS cid, c.name AS column_name, c.type AS column_type, c."notnull" AS not_null, c.dflt_value AS default_value, c.pk AS pk, c.hidden AS hidden FROM pragma_table_list AS t JOIN pragma_table_xinfo(t.name, t.schema) AS c WHERE t.schema = 'main' AND t.type IN ('table', 'virtual') AND ${systemTablePredicate("t.name")} ORDER BY t.name, c.cid`,
1450
+ // Phase 2-2 (적대적 리뷰 반영): each table's CREATE TABLE text, where
1451
+ // alone a column's collation is written. A separate statement, so the
1452
+ // text comes once per table rather than once per column. D1 answers
1453
+ // `sqlite_master` reads (the database routes' `_tables.ts` reads it too).
1454
+ `SELECT name AS table_name, sql AS table_sql FROM sqlite_master WHERE type = 'table' AND ${systemTablePredicate("name")}`
1455
+ ];
1456
+
1457
+ // ../../packages/data-core/src/condition.ts
1458
+ var MAX_CONDITION_BYTES = 16 * 1024;
1459
+
1460
+ // ../../packages/data-core/src/system-sql.ts
1461
+ var MIGRATIONS_TABLE = "CREATE TABLE IF NOT EXISTS _naro_migrations (version INTEGER PRIMARY KEY, name TEXT NOT NULL, applied_at TEXT NOT NULL DEFAULT (datetime('now')))";
1462
+ var SYSTEM_MIGRATIONS = [
1463
+ {
1464
+ version: 1,
1465
+ name: "system_tables",
1466
+ statements: [
1467
+ MIGRATIONS_TABLE,
1468
+ "CREATE TABLE IF NOT EXISTS _naro_meta (key TEXT PRIMARY KEY, value TEXT NOT NULL, updated_at TEXT NOT NULL DEFAULT (datetime('now')))",
1469
+ // Spec §11.2, as written there.
1470
+ "CREATE TABLE IF NOT EXISTS _naro_policies (table_name TEXT PRIMARY KEY, select_rule TEXT NOT NULL DEFAULT 'none', insert_rule TEXT NOT NULL DEFAULT 'none', update_rule TEXT NOT NULL DEFAULT 'none', delete_rule TEXT NOT NULL DEFAULT 'none', owner_column TEXT, updated_at TEXT NOT NULL DEFAULT (datetime('now')))"
1471
+ ]
1472
+ },
1473
+ {
1474
+ // Spec P2-2 §3.1: an action whose rule column says 'condition' reads its
1475
+ // condition from these. A worker from before them reads 'condition' as
1476
+ // an unknown rule — none.
1477
+ version: 2,
1478
+ name: "policy_conditions",
1479
+ statements: [],
1480
+ addColumns: ["select", "insert", "update", "delete"].map((action) => ({
1481
+ table: "_naro_policies",
1482
+ column: `${action}_condition`,
1483
+ definition: "TEXT"
1484
+ }))
1485
+ },
1486
+ {
1487
+ // Spec P2-6 §11: the Naro worker's sign-in handoff. A provider sign-in's
1488
+ // flow (state → the app's redirect and the browser's PKCE challenge), the
1489
+ // one-time codes a redirect hands the app, the sends the per-address
1490
+ // email limit counts, and (adversarial review I3) the unconfirmed
1491
+ // accounts someone else also tried to sign up as. Times are epoch
1492
+ // milliseconds.
1493
+ version: 3,
1494
+ name: "auth_handoff",
1495
+ statements: [
1496
+ "CREATE TABLE IF NOT EXISTS _naro_auth_flows (state TEXT PRIMARY KEY, provider TEXT NOT NULL, code_challenge TEXT NOT NULL, redirect_to TEXT NOT NULL, expires_at INTEGER NOT NULL)",
1497
+ "CREATE TABLE IF NOT EXISTS _naro_auth_codes (code_hash TEXT PRIMARY KEY, code_challenge TEXT NOT NULL, session_token TEXT NOT NULL, sealed_token TEXT NOT NULL, expires_at INTEGER NOT NULL)",
1498
+ "CREATE TABLE IF NOT EXISTS _naro_auth_email_log (address_hash TEXT NOT NULL, sent_at INTEGER NOT NULL)",
1499
+ "CREATE INDEX IF NOT EXISTS _naro_auth_email_log_address ON _naro_auth_email_log (address_hash, sent_at)",
1500
+ "CREATE TABLE IF NOT EXISTS _naro_auth_contested (user_id TEXT PRIMARY KEY, contested_at INTEGER NOT NULL)"
1501
+ ]
1502
+ }
1503
+ ];
1504
+
1505
+ // ../../packages/data-core/src/write-quota.ts
1506
+ var WRITE_QUOTA_KEY = "write_quota";
1507
+ var WRITE_QUOTA_SQL = {
1508
+ read: `SELECT value FROM _naro_meta WHERE key = '${WRITE_QUOTA_KEY}'`,
1509
+ upsert: `INSERT INTO _naro_meta (key, value, updated_at) VALUES ('${WRITE_QUOTA_KEY}', ?1, ?2) ON CONFLICT (key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at`
1510
+ };
1511
+
1145
1512
  // ../../packages/sdk/src/generate-types.ts
1146
1513
  var HEADER = [
1147
1514
  "// Generated by `naro gen types`. Do not edit - re-run the command instead.",
1148
1515
  "// `D1Database` comes from @cloudflare/workers-types."
1149
1516
  ];
1150
- function valueType(declared) {
1517
+ function affinity(declared, strict) {
1151
1518
  const type = declared.toUpperCase();
1152
- if (type.trim() === "") return "number | string | ArrayBuffer";
1153
- if (type.includes("INT")) return "number";
1519
+ if (type.trim() === "") return "NONE";
1520
+ if (strict && type.trim() === "ANY") return "ANY";
1521
+ if (type.includes("INT")) return "INTEGER";
1154
1522
  if (type.includes("CHAR") || type.includes("CLOB") || type.includes("TEXT"))
1155
- return "string";
1156
- if (type.includes("BLOB")) return "ArrayBuffer";
1523
+ return "TEXT";
1524
+ if (type.includes("BLOB")) return "BLOB";
1157
1525
  if (type.includes("REAL") || type.includes("FLOA") || type.includes("DOUB"))
1158
- return "number";
1159
- return "number | string";
1526
+ return "REAL";
1527
+ if (type.includes("BOOL")) return "BOOLEAN";
1528
+ if (type.includes("JSON")) return "JSON";
1529
+ return "NUMERIC";
1530
+ }
1531
+ var READ = {
1532
+ INTEGER: "number",
1533
+ TEXT: "string",
1534
+ BLOB: "number[]",
1535
+ REAL: "number",
1536
+ NONE: "number | string | number[]",
1537
+ ANY: "number | string | number[]",
1538
+ BOOLEAN: "number",
1539
+ JSON: "number | string",
1540
+ NUMERIC: "number | string"
1541
+ };
1542
+ var WRITE = {
1543
+ INTEGER: "number",
1544
+ TEXT: "string | Json",
1545
+ BLOB: "never",
1546
+ REAL: "number",
1547
+ NONE: "number | string | Json",
1548
+ ANY: "number | string | Json",
1549
+ BOOLEAN: "number | boolean",
1550
+ JSON: "number | string | Json",
1551
+ NUMERIC: "number | string"
1552
+ };
1553
+ var JSON_TYPE = "export type Json = readonly unknown[] | { readonly [key: string]: unknown };";
1554
+ function withNull(type, nullable2) {
1555
+ if (!nullable2) return type;
1556
+ return type === "never" ? "null" : `${type} | null`;
1160
1557
  }
1161
1558
  function interfaceBase(tableName) {
1162
1559
  const pascal = tableName.split(/[^A-Za-z0-9$]+/).filter((word) => word.length > 0).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join("").replace(/[^A-Za-z0-9_$]/g, "");
1163
1560
  return pascal === "" || /^[0-9]/.test(pascal) ? `_${pascal}` : pascal;
1164
1561
  }
1562
+ function sqlSkeleton(sql) {
1563
+ let out = "";
1564
+ let index = 0;
1565
+ while (index < sql.length) {
1566
+ const char = sql[index] ?? "";
1567
+ const pair = sql.slice(index, index + 2);
1568
+ if (pair === "--") {
1569
+ const end = sql.indexOf("\n", index);
1570
+ index = end < 0 ? sql.length : end;
1571
+ out += " ";
1572
+ } else if (pair === "/*") {
1573
+ const end = sql.indexOf("*/", index + 2);
1574
+ index = end < 0 ? sql.length : end + 2;
1575
+ out += " ";
1576
+ } else if (char === "'" || char === '"' || char === "`" || char === "[") {
1577
+ const close = char === "[" ? "]" : char;
1578
+ let end = index + 1;
1579
+ for (; ; ) {
1580
+ const found = sql.indexOf(close, end);
1581
+ if (found < 0) {
1582
+ end = sql.length;
1583
+ break;
1584
+ }
1585
+ if (close !== "]" && sql[found + 1] === close) {
1586
+ end = found + 2;
1587
+ continue;
1588
+ }
1589
+ end = found + 1;
1590
+ break;
1591
+ }
1592
+ out += `${char}${close}`;
1593
+ index = end;
1594
+ } else {
1595
+ out += char;
1596
+ index += 1;
1597
+ }
1598
+ }
1599
+ return out;
1600
+ }
1601
+ function tableOptions(sql) {
1602
+ const skeleton = sql === null ? "" : sqlSkeleton(sql);
1603
+ const close = skeleton.lastIndexOf(")");
1604
+ const tail = close < 0 ? "" : skeleton.slice(close + 1);
1605
+ return {
1606
+ withoutRowid: /\bWITHOUT\s+ROWID\b/i.test(tail),
1607
+ strict: /\bSTRICT\b/i.test(tail)
1608
+ };
1609
+ }
1165
1610
  function rowidAlias(table) {
1611
+ if (tableOptions(table.sql).withoutRowid) return null;
1166
1612
  const keys = table.columns.filter((column) => column.primaryKey);
1167
1613
  const only = keys.length === 1 ? keys[0] : void 0;
1168
1614
  return only && only.type.trim().toUpperCase() === "INTEGER" ? only.name : null;
@@ -1174,34 +1620,120 @@ function keyLiteral(name) {
1174
1620
  (unit) => `${UNICODE_ESCAPE}${unit.charCodeAt(0).toString(16).padStart(4, "0")}`
1175
1621
  );
1176
1622
  }
1177
- function property(column, alias) {
1178
- const nullable2 = !column.notNull && column.name !== alias;
1179
- return ` ${keyLiteral(column.name)}: ${valueType(column.type)}${nullable2 ? " | null" : ""};`;
1623
+ function ownerFilledOnInsert(policies, table) {
1624
+ const folded = asciiLower(table);
1625
+ const matches = policies.filter(
1626
+ (policy2) => asciiLower(policy2.table) === folded
1627
+ );
1628
+ const policy = policies.find((candidate) => candidate.table === table) ?? (matches.length === 1 ? matches[0] : void 0);
1629
+ return policy?.insert === "owner" ? policy.ownerColumn : null;
1630
+ }
1631
+ async function policiesForTypes(client, projectId) {
1632
+ try {
1633
+ return { policies: await client.backend.policies(projectId) };
1634
+ } catch (error51) {
1635
+ return {
1636
+ policies: void 0,
1637
+ reason: isNaroError(error51) ? error51.code : "UNKNOWN_ERROR"
1638
+ };
1639
+ }
1640
+ }
1641
+ function isNullable(column, alias) {
1642
+ return !column.notNull && column.name !== alias;
1643
+ }
1644
+ function rowProperty(column, { alias, strict }) {
1645
+ const type = READ[affinity(column.type, strict)];
1646
+ return ` ${keyLiteral(column.name)}: ${withNull(type, isNullable(column, alias))};`;
1647
+ }
1648
+ function writeProperty(column, { alias, strict }, optional2) {
1649
+ const type = WRITE[affinity(column.type, strict)];
1650
+ return ` ${keyLiteral(column.name)}${optional2 ? "?" : ""}: ${withNull(type, isNullable(column, alias))};`;
1180
1651
  }
1181
1652
  function block(name, lines) {
1182
1653
  return lines.length === 0 ? [`export interface ${name} {}`] : [`export interface ${name} {`, ...lines, "}"];
1183
1654
  }
1184
- function generateTypes(tables) {
1655
+ function member(indent, name, lines) {
1656
+ return lines.length === 0 ? [`${indent}${name}: {};`] : [`${indent}${name}: {`, ...lines, `${indent}};`];
1657
+ }
1658
+ function tableEntry(described) {
1659
+ const { table, row, alias, owner } = described;
1660
+ const writable = table.columns.filter((column) => column.generated !== true);
1661
+ const optionalOnInsert = (column) => isNullable(column, alias) || column.defaultValue !== null || column.name === alias || asciiLower(column.name) === owner;
1662
+ return [
1663
+ ` ${keyLiteral(table.name)}: {`,
1664
+ ` Row: ${row};`,
1665
+ ...member(
1666
+ " ",
1667
+ "Insert",
1668
+ writable.map(
1669
+ (column) => writeProperty(column, described, optionalOnInsert(column))
1670
+ )
1671
+ ),
1672
+ ...member(
1673
+ " ",
1674
+ "Update",
1675
+ writable.map((column) => writeProperty(column, described, true))
1676
+ ),
1677
+ " };"
1678
+ ];
1679
+ }
1680
+ function viewEntry({ table, row }) {
1681
+ return [` ${keyLiteral(table.name)}: {`, ` Row: ${row};`, " };"];
1682
+ }
1683
+ var ALIASES = [
1684
+ 'export type Tables<Name extends keyof Database["tables"] | keyof Database["views"]> =',
1685
+ ' (Database["tables"] & Database["views"])[Name]["Row"];',
1686
+ 'export type TablesInsert<Name extends keyof Database["tables"]> =',
1687
+ ' Database["tables"][Name]["Insert"];',
1688
+ 'export type TablesUpdate<Name extends keyof Database["tables"]> =',
1689
+ ' Database["tables"][Name]["Update"];'
1690
+ ];
1691
+ function generateTypes(tables, options2 = {}) {
1185
1692
  const used = /* @__PURE__ */ new Set();
1186
1693
  const out = [...HEADER];
1187
- const map2 = [];
1694
+ const described = [];
1188
1695
  for (const table of tables) {
1189
1696
  const base = `${interfaceBase(table.name)}Row`;
1190
- let name = base;
1191
- for (let suffix = 2; used.has(name); suffix += 1)
1192
- name = `${base}_${String(suffix)}`;
1193
- used.add(name);
1194
- const alias = rowidAlias(table);
1697
+ let row = base;
1698
+ for (let suffix = 2; used.has(row); suffix += 1)
1699
+ row = `${base}_${String(suffix)}`;
1700
+ used.add(row);
1701
+ const owner = ownerFilledOnInsert(options2.policies ?? [], table.name);
1702
+ const entry = {
1703
+ table,
1704
+ row,
1705
+ alias: rowidAlias(table),
1706
+ strict: tableOptions(table.sql).strict,
1707
+ owner: owner === null ? null : asciiLower(owner)
1708
+ };
1709
+ described.push(entry);
1195
1710
  out.push(
1196
1711
  "",
1197
1712
  ...block(
1198
- name,
1199
- table.columns.map((column) => property(column, alias))
1713
+ row,
1714
+ table.columns.map((column) => rowProperty(column, entry))
1200
1715
  )
1201
1716
  );
1202
- map2.push(` ${keyLiteral(table.name)}: ${name};`);
1203
1717
  }
1204
- out.push("", ...block("Database", map2));
1718
+ out.push(
1719
+ "",
1720
+ JSON_TYPE,
1721
+ "",
1722
+ "export type Database = {",
1723
+ ...member(
1724
+ " ",
1725
+ "tables",
1726
+ described.filter(({ table }) => table.type !== "view").flatMap(tableEntry)
1727
+ ),
1728
+ ...member(
1729
+ " ",
1730
+ "views",
1731
+ described.filter(({ table }) => table.type === "view").flatMap(viewEntry)
1732
+ ),
1733
+ "};",
1734
+ "",
1735
+ ...ALIASES
1736
+ );
1205
1737
  out.push("", ...block("Env", [" DB: D1Database;"]));
1206
1738
  return `${out.join("\n")}
1207
1739
  `;
@@ -1245,6 +1777,33 @@ function pathnameOf(url2) {
1245
1777
  // ../../packages/core/src/internal-auth.ts
1246
1778
  import { Buffer as Buffer2 } from "buffer";
1247
1779
 
1780
+ // ../../packages/core/src/migration-names.ts
1781
+ function normalizeMigrationName(name) {
1782
+ return name.trim().toLowerCase().replace(/\.sql$/, "").replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
1783
+ }
1784
+ function leadingMigrationNumber(name) {
1785
+ const match = /^(\d+)/.exec(name);
1786
+ return match ? BigInt(match[1] ?? "0") : null;
1787
+ }
1788
+ function highestMigrationNumber(names) {
1789
+ let max = 0n;
1790
+ for (const name of names) {
1791
+ const number4 = leadingMigrationNumber(name);
1792
+ if (number4 !== null && number4 > max) max = number4;
1793
+ }
1794
+ return max;
1795
+ }
1796
+ function numberedMigrationName(existing, title) {
1797
+ const number4 = (highestMigrationNumber(existing) + 1n).toString().padStart(4, "0");
1798
+ return `${number4}_${title}.sql`;
1799
+ }
1800
+ var MAX_SCHEMA_TITLE_LENGTH = 180;
1801
+ function schemaMigrationTitle(operation) {
1802
+ const op = typeof operation.op === "string" ? operation.op.replace(/[A-Z]/g, (letter) => `_${letter}`) : "";
1803
+ const target = typeof operation.table === "string" ? operation.table : typeof operation.name === "string" ? operation.name : "";
1804
+ return normalizeMigrationName(`schema ${op} ${target}`).slice(0, MAX_SCHEMA_TITLE_LENGTH).replace(/_+$/, "");
1805
+ }
1806
+
1248
1807
  // ../../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/classic/external.js
1249
1808
  var external_exports = {};
1250
1809
  __export(external_exports, {
@@ -1881,7 +2440,7 @@ __export(util_exports, {
1881
2440
  getSizableOrigin: () => getSizableOrigin,
1882
2441
  hexToUint8Array: () => hexToUint8Array,
1883
2442
  isObject: () => isObject,
1884
- isPlainObject: () => isPlainObject,
2443
+ isPlainObject: () => isPlainObject2,
1885
2444
  issue: () => issue,
1886
2445
  joinValues: () => joinValues,
1887
2446
  jsonStringifyReplacer: () => jsonStringifyReplacer,
@@ -2061,7 +2620,7 @@ var allowsEval = /* @__PURE__ */ cached(() => {
2061
2620
  return false;
2062
2621
  }
2063
2622
  });
2064
- function isPlainObject(o) {
2623
+ function isPlainObject2(o) {
2065
2624
  if (isObject(o) === false)
2066
2625
  return false;
2067
2626
  const ctor = o.constructor;
@@ -2078,7 +2637,7 @@ function isPlainObject(o) {
2078
2637
  return true;
2079
2638
  }
2080
2639
  function shallowClone(o) {
2081
- if (isPlainObject(o))
2640
+ if (isPlainObject2(o))
2082
2641
  return { ...o };
2083
2642
  if (Array.isArray(o))
2084
2643
  return [...o];
@@ -2282,7 +2841,7 @@ function omit(schema, mask) {
2282
2841
  return clone(schema, def);
2283
2842
  }
2284
2843
  function extend(schema, shape) {
2285
- if (!isPlainObject(shape)) {
2844
+ if (!isPlainObject2(shape)) {
2286
2845
  throw new Error("Invalid input to extend: expected a plain object");
2287
2846
  }
2288
2847
  const checks = schema._zod.def.checks;
@@ -2305,7 +2864,7 @@ function extend(schema, shape) {
2305
2864
  return clone(schema, def);
2306
2865
  }
2307
2866
  function safeExtend(schema, shape) {
2308
- if (!isPlainObject(shape)) {
2867
+ if (!isPlainObject2(shape)) {
2309
2868
  throw new Error("Invalid input to safeExtend: expected a plain object");
2310
2869
  }
2311
2870
  const def = mergeDefs(schema._zod.def, {
@@ -3430,9 +3989,9 @@ var $ZodCheckEndsWith = /* @__PURE__ */ $constructor("$ZodCheckEndsWith", (inst,
3430
3989
  });
3431
3990
  };
3432
3991
  });
3433
- function handleCheckPropertyResult(result, payload, property2) {
3992
+ function handleCheckPropertyResult(result, payload, property) {
3434
3993
  if (result.issues.length) {
3435
- payload.issues.push(...prefixIssues(property2, result.issues));
3994
+ payload.issues.push(...prefixIssues(property, result.issues));
3436
3995
  }
3437
3996
  }
3438
3997
  var $ZodCheckProperty = /* @__PURE__ */ $constructor("$ZodCheckProperty", (inst, def) => {
@@ -4652,7 +5211,7 @@ function mergeValues(a, b) {
4652
5211
  if (a instanceof Date && b instanceof Date && +a === +b) {
4653
5212
  return { valid: true, data: a };
4654
5213
  }
4655
- if (isPlainObject(a) && isPlainObject(b)) {
5214
+ if (isPlainObject2(a) && isPlainObject2(b)) {
4656
5215
  const bKeys = Object.keys(b);
4657
5216
  const sharedKeys = Object.keys(a).filter((key) => bKeys.indexOf(key) !== -1);
4658
5217
  const newObj = { ...a, ...b };
@@ -4838,7 +5397,7 @@ var $ZodRecord = /* @__PURE__ */ $constructor("$ZodRecord", (inst, def) => {
4838
5397
  $ZodType.init(inst, def);
4839
5398
  inst._zod.parse = (payload, ctx) => {
4840
5399
  const input = payload.value;
4841
- if (!isPlainObject(input)) {
5400
+ if (!isPlainObject2(input)) {
4842
5401
  payload.issues.push({
4843
5402
  expected: "record",
4844
5403
  code: "invalid_type",
@@ -12259,10 +12818,10 @@ function _endsWith(suffix, params) {
12259
12818
  });
12260
12819
  }
12261
12820
  // @__NO_SIDE_EFFECTS__
12262
- function _property(property2, schema, params) {
12821
+ function _property(property, schema, params) {
12263
12822
  return new $ZodCheckProperty({
12264
12823
  check: "property",
12265
- property: property2,
12824
+ property,
12266
12825
  schema,
12267
12826
  ...normalizeParams(params)
12268
12827
  });
@@ -12359,28 +12918,28 @@ function _tuple(Class2, items, _paramsOrRest, _params) {
12359
12918
  });
12360
12919
  }
12361
12920
  // @__NO_SIDE_EFFECTS__
12362
- function _record(Class2, keyType, valueType2, params) {
12921
+ function _record(Class2, keyType, valueType, params) {
12363
12922
  return new Class2({
12364
12923
  type: "record",
12365
12924
  keyType,
12366
- valueType: valueType2,
12925
+ valueType,
12367
12926
  ...normalizeParams(params)
12368
12927
  });
12369
12928
  }
12370
12929
  // @__NO_SIDE_EFFECTS__
12371
- function _map(Class2, keyType, valueType2, params) {
12930
+ function _map(Class2, keyType, valueType, params) {
12372
12931
  return new Class2({
12373
12932
  type: "map",
12374
12933
  keyType,
12375
- valueType: valueType2,
12934
+ valueType,
12376
12935
  ...normalizeParams(params)
12377
12936
  });
12378
12937
  }
12379
12938
  // @__NO_SIDE_EFFECTS__
12380
- function _set(Class2, valueType2, params) {
12939
+ function _set(Class2, valueType, params) {
12381
12940
  return new Class2({
12382
12941
  type: "set",
12383
- valueType: valueType2,
12942
+ valueType,
12384
12943
  ...normalizeParams(params)
12385
12944
  });
12386
12945
  }
@@ -14746,37 +15305,37 @@ var ZodRecord = /* @__PURE__ */ $constructor("ZodRecord", (inst, def) => {
14746
15305
  inst.keyType = def.keyType;
14747
15306
  inst.valueType = def.valueType;
14748
15307
  });
14749
- function record(keyType, valueType2, params) {
14750
- if (!valueType2 || !valueType2._zod) {
15308
+ function record(keyType, valueType, params) {
15309
+ if (!valueType || !valueType._zod) {
14751
15310
  return new ZodRecord({
14752
15311
  type: "record",
14753
15312
  keyType: string2(),
14754
15313
  valueType: keyType,
14755
- ...util_exports.normalizeParams(valueType2)
15314
+ ...util_exports.normalizeParams(valueType)
14756
15315
  });
14757
15316
  }
14758
15317
  return new ZodRecord({
14759
15318
  type: "record",
14760
15319
  keyType,
14761
- valueType: valueType2,
15320
+ valueType,
14762
15321
  ...util_exports.normalizeParams(params)
14763
15322
  });
14764
15323
  }
14765
- function partialRecord(keyType, valueType2, params) {
15324
+ function partialRecord(keyType, valueType, params) {
14766
15325
  const k = clone(keyType);
14767
15326
  k._zod.values = void 0;
14768
15327
  return new ZodRecord({
14769
15328
  type: "record",
14770
15329
  keyType: k,
14771
- valueType: valueType2,
15330
+ valueType,
14772
15331
  ...util_exports.normalizeParams(params)
14773
15332
  });
14774
15333
  }
14775
- function looseRecord(keyType, valueType2, params) {
15334
+ function looseRecord(keyType, valueType, params) {
14776
15335
  return new ZodRecord({
14777
15336
  type: "record",
14778
15337
  keyType,
14779
- valueType: valueType2,
15338
+ valueType,
14780
15339
  mode: "loose",
14781
15340
  ...util_exports.normalizeParams(params)
14782
15341
  });
@@ -14792,11 +15351,11 @@ var ZodMap = /* @__PURE__ */ $constructor("ZodMap", (inst, def) => {
14792
15351
  inst.max = (...args) => inst.check(_maxSize(...args));
14793
15352
  inst.size = (...args) => inst.check(_size(...args));
14794
15353
  });
14795
- function map(keyType, valueType2, params) {
15354
+ function map(keyType, valueType, params) {
14796
15355
  return new ZodMap({
14797
15356
  type: "map",
14798
15357
  keyType,
14799
- valueType: valueType2,
15358
+ valueType,
14800
15359
  ...util_exports.normalizeParams(params)
14801
15360
  });
14802
15361
  }
@@ -14809,10 +15368,10 @@ var ZodSet = /* @__PURE__ */ $constructor("ZodSet", (inst, def) => {
14809
15368
  inst.max = (...args) => inst.check(_maxSize(...args));
14810
15369
  inst.size = (...args) => inst.check(_size(...args));
14811
15370
  });
14812
- function set(valueType2, params) {
15371
+ function set(valueType, params) {
14813
15372
  return new ZodSet({
14814
15373
  type: "set",
14815
- valueType: valueType2,
15374
+ valueType,
14816
15375
  ...util_exports.normalizeParams(params)
14817
15376
  });
14818
15377
  }
@@ -15784,12 +16343,15 @@ var RESERVED_SUBDOMAINS = [
15784
16343
  "support",
15785
16344
  "www"
15786
16345
  ];
16346
+ var BACKEND_WORKER_NAME_PREFIX = "naro-backend-";
15787
16347
  var SUBDOMAIN_PATTERN = /^[a-z0-9](-?[a-z0-9])*$/;
15788
16348
  var SUBDOMAIN_MAX_LENGTH = 54;
15789
16349
  function subdomainProblem(value) {
15790
16350
  if (value.length < 1 || value.length > SUBDOMAIN_MAX_LENGTH) return "invalid";
15791
16351
  if (!SUBDOMAIN_PATTERN.test(value)) return "invalid";
15792
16352
  if (value.endsWith("-preview")) return "reserved";
16353
+ if (value === BACKEND_WORKER_NAME_PREFIX.slice(0, -1) || value.startsWith(BACKEND_WORKER_NAME_PREFIX))
16354
+ return "reserved";
15793
16355
  if (RESERVED_SUBDOMAINS.includes(value))
15794
16356
  return "reserved";
15795
16357
  return null;
@@ -15913,29 +16475,17 @@ function resolveMigrationsDir(input) {
15913
16475
  if (input.dir) return resolve(input.baseDir, input.dir);
15914
16476
  return resolve(input.baseDir, input.rootDirectory ?? "", "migrations");
15915
16477
  }
15916
- function normalizeMigrationName(name) {
15917
- return name.trim().toLowerCase().replace(/\.sql$/, "").replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
15918
- }
15919
- function leadingNumber(name) {
15920
- const match = /^(\d+)/.exec(name);
15921
- return match ? Number(match[1]) : null;
15922
- }
15923
16478
  function sortMigrationNames(names) {
15924
16479
  return names.toSorted((a, b) => {
15925
- const left = leadingNumber(a);
15926
- const right = leadingNumber(b);
15927
- if (left !== null && right !== null && left !== right) return left - right;
16480
+ const left = leadingMigrationNumber(a);
16481
+ const right = leadingMigrationNumber(b);
16482
+ if (left !== null && right !== null && left !== right)
16483
+ return left < right ? -1 : 1;
15928
16484
  if (left === null && right !== null) return 1;
15929
16485
  if (left !== null && right === null) return -1;
15930
16486
  return a < b ? -1 : a > b ? 1 : 0;
15931
16487
  });
15932
16488
  }
15933
- function highestMigrationNumber(names) {
15934
- return names.reduce(
15935
- (max, name) => Math.max(max, leadingNumber(name) ?? 0),
15936
- 0
15937
- );
15938
- }
15939
16489
  function listMigrationFiles(dir) {
15940
16490
  if (!existsSync5(dir)) return [];
15941
16491
  return sortMigrationNames(
@@ -15949,8 +16499,57 @@ function nextMigrationFileName(existing, name) {
15949
16499
  "VALIDATION_FAILED",
15950
16500
  "Migration name must contain letters or digits."
15951
16501
  );
15952
- const number4 = String(highestMigrationNumber(existing) + 1).padStart(4, "0");
15953
- return `${number4}_${normalized}.sql`;
16502
+ return numberedMigrationName(existing, normalized);
16503
+ }
16504
+ function compareMigrations(local, applied) {
16505
+ const ledger = new Map(applied.map((row) => [row.name, row]));
16506
+ const highestApplied = highestMigrationNumber(applied.map((row) => row.name));
16507
+ const rows = [];
16508
+ for (const name of local) {
16509
+ const exact = ledger.get(name);
16510
+ if (exact) {
16511
+ ledger.delete(name);
16512
+ rows.push({ name, status: "applied", appliedAt: exact.appliedAt });
16513
+ continue;
16514
+ }
16515
+ const bare = ledger.get(name.replace(/\.sql$/, ""));
16516
+ if (bare) {
16517
+ ledger.delete(bare.name);
16518
+ rows.push({
16519
+ name,
16520
+ status: "applied",
16521
+ appliedAt: bare.appliedAt,
16522
+ warning: "ledger-name-missing-extension"
16523
+ });
16524
+ continue;
16525
+ }
16526
+ const number4 = leadingMigrationNumber(name);
16527
+ rows.push({
16528
+ name,
16529
+ status: "pending",
16530
+ ...number4 !== null && number4 < highestApplied ? { warning: "out-of-order" } : {}
16531
+ });
16532
+ }
16533
+ for (const row of ledger.values())
16534
+ rows.push({
16535
+ name: row.name,
16536
+ status: "remote-only",
16537
+ appliedAt: row.appliedAt
16538
+ });
16539
+ const order = sortMigrationNames(rows.map((row) => row.name));
16540
+ return rows.toSorted((a, b) => order.indexOf(a.name) - order.indexOf(b.name));
16541
+ }
16542
+ function pendingMigrationNames(local, applied) {
16543
+ return compareMigrations(local, applied).filter((row) => row.status === "pending").map((row) => row.name);
16544
+ }
16545
+ function migrationsPendingError(pending) {
16546
+ return new NaroError(
16547
+ "DATABASE_MIGRATIONS_PENDING",
16548
+ `Run \`naro db push\` first; ${String(pending.length)} local migration(s) are not applied yet: ${pending.join(", ")}.`
16549
+ );
16550
+ }
16551
+ function migrationNameMismatch(sent, recorded) {
16552
+ return `The control plane recorded the migration as ${recorded}, but naro sent ${sent} and wrote the file under that name. Until the two match, naro migration list shows them apart and naro db push would apply the file again.`;
15954
16553
  }
15955
16554
 
15956
16555
  // src/naro.ts
@@ -16165,6 +16764,179 @@ function staysInsideProject(projectDir, target) {
16165
16764
  return false;
16166
16765
  }
16167
16766
  }
16767
+ function assertRootDirectoryArg(args) {
16768
+ if (args.rootDirectory !== void 0 && typeof args.rootDirectory !== "string")
16769
+ throw new Error("rootDirectory must be a string.");
16770
+ }
16771
+ function migrationTarget(args, deps, projectId) {
16772
+ const { dir: projectDir, mismatch } = writableProjectDirectory(
16773
+ deps,
16774
+ str(args.directory),
16775
+ projectId
16776
+ );
16777
+ const rootDirectory = str(args.rootDirectory);
16778
+ if (rootDirectory !== void 0) {
16779
+ const escapes = projectDir !== void 0 && relative(projectDir, resolve2(projectDir, rootDirectory)).startsWith("..");
16780
+ if (isAbsolute(rootDirectory) || escapes) throw new Error(OUTSIDE_PROJECT);
16781
+ }
16782
+ const dir = projectDir ? resolveMigrationsDir({ baseDir: projectDir, rootDirectory }) : void 0;
16783
+ if (dir && projectDir && !staysInsideProject(projectDir, dir))
16784
+ throw new Error(
16785
+ rootDirectory === void 0 ? MIGRATIONS_OUTSIDE_PROJECT : OUTSIDE_PROJECT
16786
+ );
16787
+ return {
16788
+ ...projectDir === void 0 ? {} : { projectDir },
16789
+ ...dir === void 0 ? {} : { dir },
16790
+ ...mismatch === void 0 ? {} : { mismatch }
16791
+ };
16792
+ }
16793
+ async function nextMigrationName(deps, projectId, target, title, options2 = {}) {
16794
+ const local = target.dir ? listMigrationFiles(target.dir) : [];
16795
+ let ledger;
16796
+ try {
16797
+ ledger = await (await deps.client()).db.migrations(projectId);
16798
+ } catch (error51) {
16799
+ if (isNaroError(error51) && error51.code === "DATABASE_NOT_READY") ledger = [];
16800
+ else throw error51;
16801
+ }
16802
+ if (options2.refusePending) {
16803
+ const pending = pendingMigrationNames(local, ledger);
16804
+ if (pending.length > 0) throw migrationsPendingError(pending);
16805
+ }
16806
+ return nextMigrationFileName(
16807
+ [...local, ...ledger.map((migration) => migration.name)],
16808
+ title
16809
+ );
16810
+ }
16811
+ function writeMigrationFile(target, fileName, sql) {
16812
+ const { dir, projectDir } = target;
16813
+ if (dir && projectDir) {
16814
+ const relDir = relative(projectDir, dir);
16815
+ const path = join6(dir, fileName);
16816
+ try {
16817
+ const escaped = new Error(
16818
+ "the migrations directory resolves outside the project directory"
16819
+ );
16820
+ if (!staysInsideProject(projectDir, dir)) throw escaped;
16821
+ mkdirSync3(dir, { recursive: true });
16822
+ if (!staysInsideProject(projectDir, dir)) throw escaped;
16823
+ writeFileSync3(path, sql, { flag: "wx" });
16824
+ return { file: join6(relDir, fileName) };
16825
+ } catch (error51) {
16826
+ const message = error51 instanceof Error ? error51.message : String(error51);
16827
+ return {
16828
+ file: null,
16829
+ fileWarning: `Applied, but the migration file could not be written to ${join6(relDir, fileName)}: ${message}. Create it yourself so naro db push does not see a gap.`
16830
+ };
16831
+ }
16832
+ }
16833
+ if (target.mismatch)
16834
+ return {
16835
+ file: null,
16836
+ fileWarning: `Applied, but no file was written: ${target.mismatch}. Save the SQL as migrations/${fileName} yourself so naro db push does not see a gap.`
16837
+ };
16838
+ return {
16839
+ file: null,
16840
+ fileWarning: `Applied. No project directory is known, so no file was written \u2014 save the SQL as migrations/${fileName} to keep local files and the ledger in step.`
16841
+ };
16842
+ }
16843
+ async function runSchemaTool(args, deps, operation) {
16844
+ if (args.dryRun !== void 0 && typeof args.dryRun !== "boolean")
16845
+ throw new Error("dryRun must be a boolean.");
16846
+ const dryRun = args.dryRun === true;
16847
+ const projectId = await deps.resolveProjectId({
16848
+ projectId: str(args.projectId),
16849
+ directory: str(args.directory)
16850
+ });
16851
+ const target = migrationTarget(args, deps, projectId);
16852
+ const name = await nextMigrationName(
16853
+ deps,
16854
+ projectId,
16855
+ target,
16856
+ schemaMigrationTitle(operation),
16857
+ { refusePending: !dryRun }
16858
+ );
16859
+ const result = await (await deps.client()).db.schema(projectId, [operation], {
16860
+ ...dryRun ? { dryRun: true } : {},
16861
+ name
16862
+ });
16863
+ if (result.dryRun) return { projectId, ...result, file: null };
16864
+ const written = writeMigrationFile(target, name, result.sql);
16865
+ return {
16866
+ projectId,
16867
+ ...result,
16868
+ ...result.name === name ? {} : {
16869
+ warnings: [
16870
+ ...result.warnings,
16871
+ migrationNameMismatch(name, result.name)
16872
+ ]
16873
+ },
16874
+ ...written
16875
+ };
16876
+ }
16877
+ function assertSchemaArgs(args) {
16878
+ assertProjectArgs(args);
16879
+ assertRootDirectoryArg(args);
16880
+ }
16881
+ function required2(value, message) {
16882
+ const text = str(value);
16883
+ if (text === void 0) throw new Error(message);
16884
+ return text;
16885
+ }
16886
+ var SCHEMA_ARGS = {
16887
+ dryRun: {
16888
+ type: "boolean",
16889
+ description: "Return the SQL and the migration name without applying, recording or writing anything."
16890
+ },
16891
+ rootDirectory: {
16892
+ type: "string",
16893
+ description: `Sub-directory holding the app's migrations/ folder, relative to the project directory, e.g. "apps/web". Must stay inside the project directory. Defaults to the project directory itself.`
16894
+ }
16895
+ };
16896
+ var COLUMN_SCHEMA = {
16897
+ type: "object",
16898
+ properties: {
16899
+ name: { type: "string" },
16900
+ type: {
16901
+ type: "string",
16902
+ enum: [...SCHEMA_COLUMN_TYPES],
16903
+ description: "Declared type. Store JSON in TEXT: a JSON column has NUMERIC affinity and turns '123' into a number."
16904
+ },
16905
+ notNull: { type: "boolean" },
16906
+ default: {
16907
+ description: 'A literal: a string, a finite number, true, false, null, or {"keyword": "CURRENT_TIMESTAMP" | "CURRENT_DATE" | "CURRENT_TIME"}. Never an expression.'
16908
+ },
16909
+ primaryKey: {
16910
+ type: "boolean",
16911
+ description: "Several make a composite key. Every primary key but a single INTEGER one is NOT NULL."
16912
+ },
16913
+ autoIncrement: {
16914
+ type: "boolean",
16915
+ description: "Only on the table's one INTEGER primary key."
16916
+ },
16917
+ unique: { type: "boolean" },
16918
+ references: {
16919
+ type: "object",
16920
+ description: "A foreign key. The parent column must be its table's only primary key or UNIQUE.",
16921
+ properties: {
16922
+ table: { type: "string" },
16923
+ column: { type: "string" },
16924
+ onDelete: {
16925
+ type: "string",
16926
+ enum: ["CASCADE", "SET NULL", "SET DEFAULT", "RESTRICT", "NO ACTION"]
16927
+ }
16928
+ },
16929
+ required: ["table", "column"]
16930
+ }
16931
+ },
16932
+ required: ["name", "type"]
16933
+ };
16934
+ var ALTER_ACTIONS = [
16935
+ "add_column",
16936
+ "rename_column",
16937
+ "drop_column",
16938
+ "rename_table"
16939
+ ];
16168
16940
  var DATABASE_TOOLS = [
16169
16941
  {
16170
16942
  name: "enable_database",
@@ -16196,7 +16968,7 @@ var DATABASE_TOOLS = [
16196
16968
  destructiveHint: false,
16197
16969
  idempotentHint: true
16198
16970
  },
16199
- description: `Returns the project database's provider, status, size, table count and limits, plus remainingBytes before the 10 GB ceiling (which cannot be raised). ${UNTRUSTED}`,
16971
+ description: `Returns the project database's provider, status, size, table count and limits, plus remainingBytes before the 10 GB ceiling (which cannot be raised). usage is today's (UTC) rowsRead, rowsWritten, storageBytes (today's peak size, which does not drop when data is deleted) and the daily writeLimit, or null when nothing was metered yet; while usage.writesBlocked is true, execute_sql writes and apply_migration fail with DATABASE_WRITE_QUOTA_EXCEEDED until 00:00 UTC \u2014 reads still work. usage.storageWarning is true from 8 GB of usage.storageLimitBytes (10 GB), judged on the current size (cloudflare.fileSize, the one remainingBytes uses) or on today's peak when that is unknown; at the limit every write fails with DATABASE_FULL until data is deleted, so suggest deleting rows or moving large blobs to R2 before then. ${UNTRUSTED}`,
16200
16972
  inputSchema: { type: "object", properties: { ...PROJECT_ARGS } },
16201
16973
  handler: async (args, deps) => {
16202
16974
  assertProjectArgs(args);
@@ -16344,63 +17116,15 @@ var DATABASE_TOOLS = [
16344
17116
  if (!name) throw new Error("name is required.");
16345
17117
  const query = str(args.query);
16346
17118
  if (!query) throw new Error("query is required.");
16347
- if (args.rootDirectory !== void 0 && typeof args.rootDirectory !== "string")
16348
- throw new Error("rootDirectory must be a string.");
17119
+ assertRootDirectoryArg(args);
16349
17120
  const projectId = await deps.resolveProjectId({
16350
17121
  projectId: str(args.projectId),
16351
17122
  directory: str(args.directory)
16352
17123
  });
16353
- const { dir: projectDir, mismatch: directoryMismatch } = writableProjectDirectory(deps, str(args.directory), projectId);
16354
- const rootDirectory = str(args.rootDirectory);
16355
- if (rootDirectory !== void 0) {
16356
- const escapes = projectDir !== void 0 && relative(projectDir, resolve2(projectDir, rootDirectory)).startsWith(
16357
- ".."
16358
- );
16359
- if (isAbsolute(rootDirectory) || escapes)
16360
- throw new Error(OUTSIDE_PROJECT);
16361
- }
16362
- const dir = projectDir ? resolveMigrationsDir({ baseDir: projectDir, rootDirectory }) : void 0;
16363
- if (dir && projectDir && !staysInsideProject(projectDir, dir))
16364
- throw new Error(
16365
- rootDirectory === void 0 ? MIGRATIONS_OUTSIDE_PROJECT : OUTSIDE_PROJECT
16366
- );
16367
- const local = dir ? listMigrationFiles(dir) : [];
16368
- let ledger;
16369
- try {
16370
- ledger = await (await deps.client()).db.migrations(projectId);
16371
- } catch (error51) {
16372
- if (isNaroError(error51) && error51.code === "DATABASE_NOT_READY")
16373
- ledger = [];
16374
- else throw error51;
16375
- }
16376
- const fileName = nextMigrationFileName(
16377
- [...local, ...ledger.map((migration) => migration.name)],
16378
- name
16379
- );
17124
+ const target = migrationTarget(args, deps, projectId);
17125
+ const fileName = await nextMigrationName(deps, projectId, target, name);
16380
17126
  const applied = await (await deps.client()).db.applyMigration(projectId, { name: fileName, sql: query });
16381
- let file2 = null;
16382
- let fileWarning;
16383
- if (dir && projectDir) {
16384
- const relDir = relative(projectDir, dir);
16385
- const path = join6(dir, fileName);
16386
- try {
16387
- const escaped = new Error(
16388
- "the migrations directory resolves outside the project directory"
16389
- );
16390
- if (!staysInsideProject(projectDir, dir)) throw escaped;
16391
- mkdirSync3(dir, { recursive: true });
16392
- if (!staysInsideProject(projectDir, dir)) throw escaped;
16393
- writeFileSync3(path, query, { flag: "wx" });
16394
- file2 = join6(relDir, fileName);
16395
- } catch (error51) {
16396
- const message = error51 instanceof Error ? error51.message : String(error51);
16397
- fileWarning = `Applied, but the migration file could not be written to ${join6(relDir, fileName)}: ${message}. Create it yourself so naro db push does not see a gap.`;
16398
- }
16399
- } else if (directoryMismatch) {
16400
- fileWarning = `Applied, but no file was written: ${directoryMismatch}. Save the SQL as migrations/${fileName} yourself so naro db push does not see a gap.`;
16401
- } else {
16402
- fileWarning = `Applied. No project directory is known, so no file was written \u2014 save the SQL as migrations/${fileName} to keep local files and the ledger in step.`;
16403
- }
17127
+ const { file: file2, fileWarning } = writeMigrationFile(target, fileName, query);
16404
17128
  return {
16405
17129
  projectId,
16406
17130
  migration: applied.migration,
@@ -16410,6 +17134,239 @@ var DATABASE_TOOLS = [
16410
17134
  };
16411
17135
  }
16412
17136
  },
17137
+ {
17138
+ name: "create_table",
17139
+ group: "database",
17140
+ untrusted: true,
17141
+ annotations: {
17142
+ readOnlyHint: false,
17143
+ destructiveHint: false,
17144
+ idempotentHint: false
17145
+ },
17146
+ description: `Creates a table in the project's D1 database without writing SQL: naro renders the CREATE TABLE from columns, applies it as one migration recorded in d1_migrations and, when the project directory is known, writes it to ./migrations as NNNN_schema_create_table_<table>.sql so naro db push and naro db reset see it. Column types: INTEGER, REAL, TEXT, BLOB, NUMERIC, BOOLEAN, DATETIME, JSON \u2014 store JSON in TEXT, since a JSON column turns '123' into a number. default takes a literal (a string, a number, true, false, null) or {"keyword": "CURRENT_TIMESTAMP"}, never an expression. Every primary key but a single INTEGER one is NOT NULL. owner: {column} adds that column as TEXT NOT NULL and, when the project's backend is ready, an owner policy for select, insert, update and delete. Without a policy the Data API refuses every anon and signed-in request to the table; warnings says so. dryRun: true returns the SQL and the migration name without applying anything. ${UNTRUSTED} ${SHARED_DATABASE}`,
17147
+ inputSchema: {
17148
+ type: "object",
17149
+ properties: {
17150
+ ...PROJECT_ARGS,
17151
+ table: { type: "string", description: "Name of the new table." },
17152
+ columns: {
17153
+ type: "array",
17154
+ description: "1 to 100 columns (D1's limit), in order.",
17155
+ items: COLUMN_SCHEMA
17156
+ },
17157
+ owner: {
17158
+ type: "object",
17159
+ description: "Adds this column as TEXT NOT NULL (leave it out of columns) and, when the backend is ready, an owner policy on all four actions.",
17160
+ properties: { column: { type: "string" } },
17161
+ required: ["column"]
17162
+ },
17163
+ ifNotExists: {
17164
+ type: "boolean",
17165
+ description: "Leave an existing table of that name as it is."
17166
+ },
17167
+ ...SCHEMA_ARGS
17168
+ },
17169
+ required: ["table", "columns"]
17170
+ },
17171
+ handler: async (args, deps) => {
17172
+ assertSchemaArgs(args);
17173
+ const table = required2(args.table, "table is required.");
17174
+ if (!Array.isArray(args.columns)) throw new Error("columns is required.");
17175
+ return runSchemaTool(args, deps, {
17176
+ op: "createTable",
17177
+ table,
17178
+ columns: args.columns,
17179
+ ...args.owner !== void 0 ? { owner: args.owner } : {},
17180
+ ...args.ifNotExists !== void 0 ? { ifNotExists: args.ifNotExists } : {}
17181
+ });
17182
+ }
17183
+ },
17184
+ {
17185
+ name: "alter_table",
17186
+ group: "database",
17187
+ untrusted: true,
17188
+ annotations: {
17189
+ readOnlyHint: false,
17190
+ destructiveHint: true,
17191
+ idempotentHint: false
17192
+ },
17193
+ description: `${DESTRUCTIVE}changes a table of the project's D1 database without writing SQL, as one migration recorded in d1_migrations and written to ./migrations when the project directory is known. action is add_column (newColumn, shaped like a create_table column), rename_column (column, to), rename_table (to) or drop_column (column) \u2014 which deletes the column and its value in every row and needs confirm set to "<table>.<column>". SQLite cannot add a PRIMARY KEY or UNIQUE column, a NOT NULL column without a default that is not NULL, or a column defaulting to CURRENT_TIMESTAMP, and cannot drop a primary key, UNIQUE or indexed column: naro refuses those up front and says why. It also refuses to drop or rename a table or column that a trigger or view uses (read from their SQL; when the SQL leaves it open, a warning instead) \u2014 drop or recreate that trigger or view with apply_migration first. Changing a column's type is not an action \u2014 it needs a table rebuild, so write that migration with apply_migration. Data API policies are kept by table name and are not moved: read warnings after a rename or a drop. dryRun: true returns the SQL without applying anything. ${UNTRUSTED} ${SHARED_DATABASE}`,
17194
+ inputSchema: {
17195
+ type: "object",
17196
+ properties: {
17197
+ ...PROJECT_ARGS,
17198
+ table: { type: "string", description: "The table to change." },
17199
+ action: { type: "string", enum: [...ALTER_ACTIONS] },
17200
+ newColumn: {
17201
+ ...COLUMN_SCHEMA,
17202
+ description: "add_column: the column to add, shaped like a create_table column."
17203
+ },
17204
+ column: {
17205
+ type: "string",
17206
+ description: "rename_column and drop_column: the column's name."
17207
+ },
17208
+ to: {
17209
+ type: "string",
17210
+ description: "rename_column and rename_table: the new name."
17211
+ },
17212
+ confirm: {
17213
+ type: "string",
17214
+ description: 'drop_column only: "<table>.<column>", spelled as in table and column.'
17215
+ },
17216
+ ...SCHEMA_ARGS
17217
+ },
17218
+ required: ["table", "action"]
17219
+ },
17220
+ handler: async (args, deps) => {
17221
+ assertSchemaArgs(args);
17222
+ const action = args.action;
17223
+ if (typeof action !== "string" || !ALTER_ACTIONS.includes(action))
17224
+ throw new Error(`action must be one of ${ALTER_ACTIONS.join(", ")}.`);
17225
+ const table = required2(args.table, "table is required.");
17226
+ let operation;
17227
+ switch (action) {
17228
+ case "add_column":
17229
+ if (typeof args.newColumn !== "object" || args.newColumn === null)
17230
+ throw new Error("newColumn is required for add_column.");
17231
+ operation = {
17232
+ op: "addColumn",
17233
+ table,
17234
+ column: args.newColumn
17235
+ };
17236
+ break;
17237
+ case "rename_column": {
17238
+ const column = str(args.column);
17239
+ const to = str(args.to);
17240
+ if (!column || !to)
17241
+ throw new Error("column and to are required for rename_column.");
17242
+ operation = { op: "renameColumn", table, column, to };
17243
+ break;
17244
+ }
17245
+ case "drop_column":
17246
+ operation = {
17247
+ op: "dropColumn",
17248
+ table,
17249
+ column: required2(
17250
+ args.column,
17251
+ "column is required for drop_column."
17252
+ ),
17253
+ // Forwarded as given; without it the server says what to pass.
17254
+ ...args.confirm !== void 0 ? { confirm: args.confirm } : {}
17255
+ };
17256
+ break;
17257
+ case "rename_table":
17258
+ operation = {
17259
+ op: "renameTable",
17260
+ table,
17261
+ to: required2(args.to, "to is required for rename_table.")
17262
+ };
17263
+ break;
17264
+ }
17265
+ return runSchemaTool(args, deps, operation);
17266
+ }
17267
+ },
17268
+ {
17269
+ name: "drop_table",
17270
+ group: "database",
17271
+ untrusted: true,
17272
+ annotations: {
17273
+ readOnlyHint: false,
17274
+ destructiveHint: true,
17275
+ idempotentHint: false
17276
+ },
17277
+ description: `${DESTRUCTIVE}drops a table of the project's D1 database and every row in it, as one migration recorded in d1_migrations. Needs confirm set to the table's name. A table that another table's foreign key references, or that a trigger or view uses, is refused. restore_database can bring it back (Time Travel, 30 days). Its Data API policy is removed with it whenever the project has a backend, on or off (removedPolicies); otherwise it stays under the table's name: read warnings. dryRun: true returns the SQL without applying anything. ${UNTRUSTED} ${SHARED_DATABASE}`,
17278
+ inputSchema: {
17279
+ type: "object",
17280
+ properties: {
17281
+ ...PROJECT_ARGS,
17282
+ table: { type: "string", description: "The table to drop." },
17283
+ confirm: {
17284
+ type: "string",
17285
+ description: "The table's name, spelled as in table."
17286
+ },
17287
+ ...SCHEMA_ARGS
17288
+ },
17289
+ required: ["table", "confirm"]
17290
+ },
17291
+ handler: async (args, deps) => {
17292
+ assertSchemaArgs(args);
17293
+ const table = required2(args.table, "table is required.");
17294
+ return runSchemaTool(args, deps, {
17295
+ op: "dropTable",
17296
+ table,
17297
+ ...args.confirm !== void 0 ? { confirm: args.confirm } : {}
17298
+ });
17299
+ }
17300
+ },
17301
+ {
17302
+ name: "create_index",
17303
+ group: "database",
17304
+ untrusted: true,
17305
+ annotations: {
17306
+ readOnlyHint: false,
17307
+ destructiveHint: false,
17308
+ idempotentHint: false
17309
+ },
17310
+ description: `Creates an index on columns of a table in the project's D1 database, as one migration recorded in d1_migrations and written to ./migrations when the project directory is known. unique: true makes it a unique index \u2014 SQLite refuses it while the columns hold duplicates. name defaults to <table>_<columns>_idx. dryRun: true returns the SQL without applying anything. ${UNTRUSTED} ${SHARED_DATABASE}`,
17311
+ inputSchema: {
17312
+ type: "object",
17313
+ properties: {
17314
+ ...PROJECT_ARGS,
17315
+ table: { type: "string" },
17316
+ columns: {
17317
+ type: "array",
17318
+ items: { type: "string" },
17319
+ description: "1 to 16 column names, in index order."
17320
+ },
17321
+ unique: { type: "boolean" },
17322
+ name: {
17323
+ type: "string",
17324
+ description: "The index's name. Defaults to <table>_<columns>_idx."
17325
+ },
17326
+ ...SCHEMA_ARGS
17327
+ },
17328
+ required: ["table", "columns"]
17329
+ },
17330
+ handler: async (args, deps) => {
17331
+ assertSchemaArgs(args);
17332
+ const table = required2(args.table, "table is required.");
17333
+ if (!Array.isArray(args.columns)) throw new Error("columns is required.");
17334
+ return runSchemaTool(args, deps, {
17335
+ op: "createIndex",
17336
+ table,
17337
+ columns: args.columns,
17338
+ ...args.unique !== void 0 ? { unique: args.unique } : {},
17339
+ ...args.name !== void 0 ? { name: args.name } : {}
17340
+ });
17341
+ }
17342
+ },
17343
+ {
17344
+ name: "drop_index",
17345
+ group: "database",
17346
+ untrusted: true,
17347
+ annotations: {
17348
+ readOnlyHint: false,
17349
+ destructiveHint: true,
17350
+ idempotentHint: false
17351
+ },
17352
+ description: `${DESTRUCTIVE}drops an index of the project's D1 database, as one migration recorded in d1_migrations. No rows are lost, but a unique index takes its constraint with it. The index SQLite made for a UNIQUE or PRIMARY KEY constraint (sqlite_autoindex_\u2026) cannot be dropped. dryRun: true returns the SQL without applying anything. ${UNTRUSTED} ${SHARED_DATABASE}`,
17353
+ inputSchema: {
17354
+ type: "object",
17355
+ properties: {
17356
+ ...PROJECT_ARGS,
17357
+ name: { type: "string", description: "The index to drop." },
17358
+ ...SCHEMA_ARGS
17359
+ },
17360
+ required: ["name"]
17361
+ },
17362
+ handler: async (args, deps) => {
17363
+ assertSchemaArgs(args);
17364
+ return runSchemaTool(args, deps, {
17365
+ op: "dropIndex",
17366
+ name: required2(args.name, "name is required.")
17367
+ });
17368
+ }
17369
+ },
16413
17370
  {
16414
17371
  name: "list_migrations",
16415
17372
  group: "database",
@@ -16440,7 +17397,7 @@ var DATABASE_TOOLS = [
16440
17397
  destructiveHint: false,
16441
17398
  idempotentHint: true
16442
17399
  },
16443
- description: `Generates TypeScript types for the D1 schema: one interface per table, a Database map and an Env with the DB binding. Returns the text only \u2014 write it to a file yourself. A view that could not be described is an empty interface in the text and is named under the undescribed key. ${UNTRUSTED}`,
17400
+ description: `Generates TypeScript types for the D1 schema: one Row interface per table and view, a Database type with Row, Insert and Update per table that createClient<Database>() from @naro/sdk reads, the Tables, TablesInsert and TablesUpdate helpers, and an Env with the DB binding. Returns the text only \u2014 write it to a file yourself (for example src/database.types.ts). A view that could not be described is an empty interface in the text and is named under the undescribed key. Where the backend's insert rule for a table is owner, its owner column is optional in Insert (the backend fills it); when the policies cannot be read, the result says policies: "unavailable" with policiesReason, and the owner columns stay required. ${UNTRUSTED}`,
16444
17401
  inputSchema: { type: "object", properties: { ...PROJECT_ARGS } },
16445
17402
  handler: async (args, deps) => {
16446
17403
  assertProjectArgs(args);
@@ -16448,14 +17405,20 @@ var DATABASE_TOOLS = [
16448
17405
  projectId: str(args.projectId),
16449
17406
  directory: str(args.directory)
16450
17407
  });
16451
- const tables = await (await deps.client()).db.tables(projectId);
17408
+ const client = await deps.client();
17409
+ const tables = await client.db.tables(projectId);
17410
+ const read = await policiesForTypes(client, projectId);
16452
17411
  const undescribed = tables.flatMap(
16453
17412
  (table) => table.error === void 0 ? [] : [{ name: table.name, error: table.error }]
16454
17413
  );
16455
17414
  return {
16456
17415
  projectId,
16457
- types: generateTypes(tables),
16458
- ...undescribed.length > 0 ? { undescribed } : {}
17416
+ types: generateTypes(
17417
+ tables,
17418
+ read.policies === void 0 ? {} : { policies: read.policies }
17419
+ ),
17420
+ ...undescribed.length > 0 ? { undescribed } : {},
17421
+ ...read.policies === void 0 ? { policies: "unavailable", policiesReason: read.reason } : {}
16459
17422
  };
16460
17423
  }
16461
17424
  },
@@ -17376,7 +18339,7 @@ function toolErrorJson(error51) {
17376
18339
  }
17377
18340
 
17378
18341
  // src/server.ts
17379
- var SERVER_VERSION = "0.8.0";
18342
+ var SERVER_VERSION = "0.9.0";
17380
18343
  function visibleTools(options2) {
17381
18344
  const { features, readOnly } = options2;
17382
18345
  return TOOLS.flatMap((tool) => {