naro-mcp 0.18.1 → 0.20.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 +25 -24
  2. package/dist/index.js +377 -63
  3. package/package.json +4 -4
package/README.md CHANGED
@@ -55,29 +55,30 @@ Headless/CI agents can skip `naro login` and set a token instead:
55
55
 
56
56
  ## Tools
57
57
 
58
- | Tool | Purpose |
59
- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
60
- | `whoami`, `list_teams` | Account / team context |
61
- | `list_projects`, `get_project`, `link_project` | Project discovery + `.naro/project.json` linking |
62
- | `get_team_usage` | Team usage for a UTC month (`month`, the current one by default): requests, builds and custom domains per day and per project, the plan's `limits`, the enforcement mode, and `summary` (each metric against its limit, and `status` paused/over/near). `team` defaults to the team linked in `directory`, then your only team; otherwise `needsTeam` with the list. Owner, billing and enterprise_viewer only (403 `FORBIDDEN` otherwise). Read-only |
63
- | `deploy` | Local Next.js build → upload → live URL (preview, `production: true`, or a named environment with `environment: "qa"` — not together with `production`: both are refused with `VALIDATION_FAILED` `environment and production cannot be used together.` before any value is read, asked for or sent and before the deployment is created; `production` and `preview` are not environment names). The result's `target` is `production`, `preview` or the environment's slug, and `environment` is the slug or null. Local .env files are never uploaded; unset keys come back as `missingEnvKeys` |
64
- | `list_deployments`, `get_deployment` | Deployment status / history |
65
- | `get_build_logs`, `get_runtime_logs` | Debugging failed builds and live workers |
66
- | `list_environment_variables`, `set_environment_variable`, `remove_environment_variable` | Env management (secret values never returned). `target` is `production`, `preview`, `all` or an environment's slug (`all` by default when setting); without `target`, `remove_environment_variable` removes the `production`, `preview` and `all` rows and leaves an environment's value alone. A name that cannot be a slug is refused as `Invalid target "<value>" — use production, preview, all or an environment name.`. For `set_environment_variable`, a slug that is not an environment answers `404 ENVIRONMENT_NOT_FOUND`; `remove_environment_variable` reads the list and answers `ENV_NOT_FOUND` (`No KEY variable found for the <slug> environment.`). Without `target`, a key only environments hold answers `ENV_NOT_FOUND` `No KEY variable found outside named environments. Pass the environment to remove it: qa.` with `key` and `environments`, and nothing is removed |
67
- | `list_domains`, `add_domain`, `verify_domain`, `remove_domain` | Custom domains: the one CNAME to create (naro connects it within ~5 minutes), plus a re-check that says which of ownership / certificate / routing is still missing. An apex is attached as an apex+www pair by default (`withWww: false` to opt out) |
68
- | `promote_deployment`, `rollback_deployment` | Production traffic control |
69
- | `cancel_deployment` | Stops a build that is still running (QUEUED–BUILDING). Does not undo a deployment that already went live — use `rollback_deployment` for that |
70
- | `list_environments`, `create_environment`, `update_environment`, `delete_environment` | Named environments (QA, staging), served at `{subdomain}--{slug}` apart from production and the preview. `list_environments` returns `{ projectId, environments, limit }` (`limit` is what the plan allows per project, null for no limit); `create_environment` takes `slug` and optionally `branch` (pushes to it deploy there), `copyEnvFrom` (`preview` by default, `production` or `none`) and `database` (`schema` by default, or `empty`); `update_environment` takes the environment's slug and `branch` (an empty string takes it off) and/or `protectionEnabled`. **`update_environment` is destructive on its `protectionEnabled` argument** (`false` opens the environment to anyone with the link — like `set_preview_protection`) and **`delete_environment` is destructive** (removes its worker and protection key). Deploy to one with `deploy`'s `environment`; set its variables with the env tools' `target` |
71
- | `enable_database`, `get_database`, `list_tables` | The project's D1 database: turn it on, status/size/limits, schema (verbose: true for columns, indexes and foreign keys) |
72
- | `execute_sql` | Run SQL (argument `query`, optional `params` for a single statement; several statements run as one transaction). Destructive; under `--read-only` it stays but accepts a single read statement only |
73
- | `apply_migration`, `list_migrations`, `generate_typescript_types` | Numbered migrations in the `d1_migrations` ledger wrangler also uses (each file all-or-nothing), and TypeScript types for the schema (text only; its `Database` is what `createClient<Database>()` from `naro-js` takes) |
74
- | `create_table`, `alter_table`, `drop_table`, `create_index`, `drop_index` | Schema changes without SQL: naro renders checked DDL, applies it as one numbered migration (`NNNN_schema_<op>_<table>.sql`) and writes the file like `apply_migration`. `alter_table` adds, renames or drops a column or renames the table. Dropping a table or column needs `confirm`; `alter_table`, `drop_table` and `drop_index` are destructive and hidden under `--read-only`. `dryRun: true` returns the SQL only |
75
- | `export_database` | SQL dump of the database (`schemaOnly`, `dataOnly`, `tables`): waits up to 5 minutes and returns a signed download link plus `atBookmark`, the point the dump reflects |
76
- | `restore_database` | Time Travel restore to a `bookmark` or a `timestamp` (last 30 days). Destructive; hidden under `--read-only`. Returns `previousBookmark`, which undoes it |
77
- | `get_speed_insights`, `set_speed_insights` | Speed Insights: each Web Vital's p75, the selected metric by page, device, browser, operating system, country, element and deployment, or one route's paths; the page load's p75 and the mean of each step (`pageLoad`); production or preview (`environment`); narrowed to one page, country, device, browser or operating system (`path`, `country`, `device`, `browser`, `os`); turn measuring on or off (not destructive: nothing is deleted) |
78
- | `get_analytics` | Web Analytics: visitors and page views over `range` (24h, 7d or 30d), the trend, and the top 10 pages, referrers, countries, devices, browsers and operating systems by page views; narrowed to one value of each dimension (`path`, `referrer`, `country`, `device`, `browser`, `os`), all given must match. Production only: there is no `environment`, and one is refused. Read-only |
79
- | `get_preview_protection`, `set_preview_protection` | Preview protection: whether the preview URL (`{subdomain}-preview`) is open to project members only, and whether a bypass secret is set; turn it on or off. **`set_preview_protection` is destructive** (turning protection off publishes unreleased work). Bypass secrets are made in the dashboard or with `naro protection bypass`, never by a tool, so they stay out of the conversation |
80
- | `get_storage`, `enable_storage`, `list_storage_buckets`, `create_storage_bucket`, `update_storage_bucket`, `delete_storage_bucket`, `list_storage_objects`, `create_storage_signed_url`, `delete_storage_objects` | Naro Storage: on or off, limits, usage, today's operations and whether uploads are stopped, and buckets; turn it on; make, change and remove buckets (`delete_storage_bucket` is destructive: `confirm` is the bucket's name, `empty: true` deletes its objects first; `update_storage_bucket` is destructive on its `public` argument, like `deploy` on `production`: `public: true` needs the same `confirm`); list a folder's files (`search` keeps the names that start with a text; a backend from before it answers `INVALID_REQUEST` — run `naro backend enable` once), make a signed link of up to an hour, and delete files (`delete_storage_objects` is destructive: `confirm` is the bucket's name) — through the control plane, so no service key reaches the MCP process. Files go in through naro-js or `naro storage cp`, not through MCP |
58
+ | Tool | Purpose |
59
+ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
60
+ | `whoami`, `list_teams` | Account / team context |
61
+ | `list_projects`, `get_project`, `link_project` | Project discovery + `.naro/project.json` linking |
62
+ | `get_team_usage` | Team usage for a UTC month (`month`, the current one by default): requests, builds and custom domains per day and per project, the plan's `limits`, the enforcement mode, and `summary` (each metric against its limit, and `status` paused/over/near). `team` defaults to the team linked in `directory`, then your only team; otherwise `needsTeam` with the list. Owner, billing and enterprise_viewer only (403 `FORBIDDEN` otherwise). Read-only |
63
+ | `deploy` | Local Next.js build → upload → live URL (preview, `production: true`, or a named environment with `environment: "qa"` — not together with `production`: both are refused with `VALIDATION_FAILED` `environment and production cannot be used together.` before any value is read, asked for or sent and before the deployment is created; `production` and `preview` are not environment names). The result's `target` is `production`, `preview` or the environment's slug, and `environment` is the slug or null. Local .env files are never uploaded; unset keys come back as `missingEnvKeys` |
64
+ | `list_deployments`, `get_deployment` | Deployment status / history |
65
+ | `get_build_logs`, `get_runtime_logs` | Debugging failed builds and live workers |
66
+ | `list_environment_variables`, `set_environment_variable`, `remove_environment_variable` | Env management (secret values never returned). `target` is `production`, `preview`, `all` or an environment's slug (`all` by default when setting); without `target`, `remove_environment_variable` removes the `production`, `preview` and `all` rows and leaves an environment's value alone. A name that cannot be a slug is refused as `Invalid target "<value>" — use production, preview, all or an environment name.`. For `set_environment_variable`, a slug that is not an environment answers `404 ENVIRONMENT_NOT_FOUND`; `remove_environment_variable` reads the list and answers `ENV_NOT_FOUND` (`No KEY variable found for the <slug> environment.`). Without `target`, a key only environments hold answers `ENV_NOT_FOUND` `No KEY variable found outside named environments. Pass the environment to remove it: qa.` with `key` and `environments`, and nothing is removed |
67
+ | `list_domains`, `add_domain`, `verify_domain`, `remove_domain` | Custom domains: the one CNAME to create (naro connects it within ~5 minutes), plus a re-check that says which of ownership / certificate / routing is still missing. An apex is attached as an apex+www pair by default (`withWww: false` to opt out) |
68
+ | `promote_deployment` | Publishes a READY deployment to production, or to a named environment with `target: "qa"` (`target` is `production` by default; `preview` and an empty name are refused with `VALIDATION_FAILED` `target must be production or an environment name.` before any request). Promoting between surfaces when one of them is an environment (preview → qa, qa → production) rebuilds the same commit with the target surface's variables (`mode: "rebuild"`, waits up to 15 minutes; `reupload` publishes the stored build and waits up to 5). The result's `target` is the name you gave or null, and `mode` is `reupload` or `rebuild`. A control plane that predates promote targets answers `PROMOTE_TARGET_UNSUPPORTED` and promotes nothing |
69
+ | `rollback_deployment` | Rolls production back to the previous READY production build, or to the `deploymentId` you name. The result's `mode` is `reupload` or `rebuild` |
70
+ | `cancel_deployment` | Stops a build that is still running (QUEUED–BUILDING). Does not undo a deployment that already went live — use `rollback_deployment` for that |
71
+ | `list_environments`, `create_environment`, `update_environment`, `delete_environment` | Named environments (QA, staging), served at `{subdomain}--{slug}` apart from production and the preview. `list_environments` returns `{ projectId, environments, limit }` (`limit` is what the plan allows per project, null for no limit); `create_environment` takes `slug` and optionally `branch` (pushes to it deploy there), `copyEnvFrom` (`preview` by default, `production` or `none`) and `database` (`schema` by default, or `empty`); `update_environment` takes the environment's slug and `branch` (an empty string takes it off) and/or `protectionEnabled`. **`update_environment` is destructive on its `protectionEnabled` argument** (`false` opens the environment to anyone with the link — like `set_preview_protection`) and **`delete_environment` is destructive** (removes its worker and protection key). Deploy to one with `deploy`'s `environment`; set its variables with the env tools' `target` |
72
+ | `enable_database`, `get_database`, `list_tables` | The project's D1 database: turn it on, status/size/limits, schema (verbose: true for columns, indexes and foreign keys) |
73
+ | `execute_sql` | Run SQL (argument `query`, optional `params` for a single statement; several statements run as one transaction). Destructive; under `--read-only` it stays but accepts a single read statement only |
74
+ | `apply_migration`, `list_migrations`, `generate_typescript_types` | Numbered migrations in the `d1_migrations` ledger wrangler also uses (each file all-or-nothing), and TypeScript types for the schema (text only; its `Database` is what `createClient<Database>()` from `naro-js` takes) |
75
+ | `create_table`, `alter_table`, `drop_table`, `create_index`, `drop_index` | Schema changes without SQL: naro renders checked DDL, applies it as one numbered migration (`NNNN_schema_<op>_<table>.sql`) and writes the file like `apply_migration`. `alter_table` adds, renames or drops a column or renames the table. Dropping a table or column needs `confirm`; `alter_table`, `drop_table` and `drop_index` are destructive and hidden under `--read-only`. `dryRun: true` returns the SQL only |
76
+ | `export_database` | SQL dump of the database (`schemaOnly`, `dataOnly`, `tables`): waits up to 5 minutes and returns a signed download link plus `atBookmark`, the point the dump reflects |
77
+ | `restore_database` | Time Travel restore to a `bookmark` or a `timestamp` (last 30 days). Destructive; hidden under `--read-only`. Returns `previousBookmark`, which undoes it |
78
+ | `get_speed_insights`, `set_speed_insights` | Speed Insights: each Web Vital's p75, the selected metric by page, device, browser, operating system, country, element and deployment, or one route's paths; the page load's p75 and the mean of each step (`pageLoad`); production or preview (`environment`); narrowed to one page, country, device, browser or operating system (`path`, `country`, `device`, `browser`, `os`); turn measuring on or off (not destructive: nothing is deleted) |
79
+ | `get_analytics` | Web Analytics: visitors and page views over `range` (24h, 7d or 30d), the trend, and the top 10 pages, referrers, countries, devices, browsers and operating systems by page views; narrowed to one value of each dimension (`path`, `referrer`, `country`, `device`, `browser`, `os`), all given must match. Production only: there is no `environment`, and one is refused. Read-only |
80
+ | `get_preview_protection`, `set_preview_protection` | Preview protection: whether the preview URL (`{subdomain}-preview`) is open to project members only, and whether a bypass secret is set; turn it on or off. **`set_preview_protection` is destructive** (turning protection off publishes unreleased work). Bypass secrets are made in the dashboard or with `naro protection bypass`, never by a tool, so they stay out of the conversation |
81
+ | `get_storage`, `enable_storage`, `disable_storage`, `list_storage_buckets`, `create_storage_bucket`, `update_storage_bucket`, `delete_storage_bucket`, `list_storage_objects`, `create_storage_signed_url`, `delete_storage_objects` | Naro Storage: on or off, limits, usage, today's operations and whether uploads are stopped, and buckets; turn it on; turn it off and keep the files (`disable_storage` is destructive: `confirm` is the project's Storage bucket, `naro-storage-<projectId>`; the app's Storage answers 404 until `enable_storage`, and bucket writes answer `STORAGE_OFF`); make, change and remove buckets (`delete_storage_bucket` is destructive: `confirm` is the bucket's name, `empty: true` deletes its objects first; `update_storage_bucket` is destructive on its `public` argument, like `deploy` on `production`: `public: true` needs the same `confirm`); list a folder's files (`search` keeps the names that start with a text; a backend from before it answers `INVALID_REQUEST` — run `naro backend enable` once), make a signed link of up to an hour, and delete files (`delete_storage_objects` is destructive: `confirm` is the bucket's name) — through the control plane, so no service key reaches the MCP process. Files go in through naro-js or `naro storage cp`, not through MCP |
81
82
 
82
83
  Destructive tools say so in their descriptions; agents should confirm with
83
84
  the user before calling them. `set_preview_protection` is one of them: turning
@@ -109,7 +110,7 @@ strategy's failure is in the message.
109
110
  | `--features <a,b,…>` | `NARO_MCP_FEATURES` | Only these groups: `account`, `deployments`, `logs`, `env`, `domains`, `git`, `database`, `insights`, `protection`, `storage`. An unknown group is a startup error, not an empty server |
110
111
 
111
112
  Results of the `database` tools, of `get_speed_insights`, of `get_analytics`,
112
- of every Storage tool that answers bucket rows (`get_storage`, `enable_storage`,
113
+ of every Storage tool that answers bucket rows (`get_storage`, `enable_storage`, `disable_storage`,
113
114
  `list_storage_buckets`, `create_storage_bucket`, `update_storage_bucket`) and of
114
115
  `list_storage_objects` are wrapped in `<untrusted-data-…>` boundaries with
115
116
  a random id per call — rows, identifiers and SQLite error messages are bytes
package/dist/index.js CHANGED
@@ -40,6 +40,14 @@ var NaroError = class extends Error {
40
40
  if (options2?.details !== void 0) this.details = options2.details;
41
41
  }
42
42
  };
43
+ var PROMOTE_TARGET_UNSUPPORTED_MESSAGE = "This control plane cannot promote to an environment yet. Deploy the latest web first, then try again.";
44
+ function promoteTargetUnsupported(cause) {
45
+ return new NaroError(
46
+ "PROMOTE_TARGET_UNSUPPORTED",
47
+ PROMOTE_TARGET_UNSUPPORTED_MESSAGE,
48
+ cause !== void 0 ? { cause } : void 0
49
+ );
50
+ }
43
51
  function isNaroError(error51) {
44
52
  return error51 instanceof NaroError || error51 instanceof Error && error51.name === "NaroError" && typeof error51.code === "string";
45
53
  }
@@ -142,7 +150,7 @@ var STYLES = `
142
150
  *{box-sizing:border-box}
143
151
  :root{
144
152
  --bg:#000;--surface:#0a0a0a;--surface-2:#111;--border:#232323;
145
- --text:#ededed;--muted:#a1a1a1;--faint:#6f6f6f;
153
+ --text:#ededed;--muted:#a1a1a1;--faint:#808080;
146
154
  --sans:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;
147
155
  --mono:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;
148
156
  }
@@ -580,6 +588,9 @@ function schemaMigrationTitle(operation) {
580
588
  return normalizeMigrationName(`schema ${op} ${target}`).slice(0, MAX_SCHEMA_TITLE_LENGTH).replace(/_+$/, "");
581
589
  }
582
590
 
591
+ // ../../packages/core/src/platform-mail.ts
592
+ var encoder = new TextEncoder();
593
+
583
594
  // ../../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/classic/external.js
584
595
  var external_exports = {};
585
596
  __export(external_exports, {
@@ -15289,6 +15300,8 @@ function deploymentInfo(row) {
15289
15300
  errorMessage: row.error_message,
15290
15301
  gitCommitSha: row.git_commit_sha,
15291
15302
  gitCommitMessage: row.git_commit_message,
15303
+ pinnedCommitSha: row.pinned_commit_sha ?? null,
15304
+ rebuiltFromDeploymentId: row.rebuilt_from_deployment_id ?? null,
15292
15305
  createdAt: row.created_at,
15293
15306
  startedAt: row.started_at,
15294
15307
  finishedAt: row.finished_at
@@ -15321,6 +15334,7 @@ var NaroClient = class {
15321
15334
  db;
15322
15335
  backend;
15323
15336
  storage;
15337
+ realtime;
15324
15338
  insights;
15325
15339
  analytics;
15326
15340
  protection;
@@ -15340,6 +15354,7 @@ var NaroClient = class {
15340
15354
  this.db = new DbApi(this);
15341
15355
  this.backend = new BackendApi(this);
15342
15356
  this.storage = new StorageApi(this);
15357
+ this.realtime = new RealtimeApi(this);
15343
15358
  this.insights = new InsightsApi(this);
15344
15359
  this.analytics = new AnalyticsApi(this);
15345
15360
  this.protection = new ProtectionApi(this);
@@ -15540,16 +15555,65 @@ var DeploymentsApi = class {
15540
15555
  return { current, target: production.slice(1).find(differs) ?? null };
15541
15556
  }
15542
15557
  /**
15543
- * Redeploys an existing READY deployment's artifact to production.
15544
- * Used by both `naro promote` (preview → production) and `naro rollback`
15545
- * (older production build → production).
15558
+ * Promotes an existing READY deployment: with no `target` (or
15559
+ * `production`) it goes to production — `naro promote` (preview →
15560
+ * production) and `naro rollback` (older production build → production).
15561
+ * A deployment from an environment is rebuilt from its commit with
15562
+ * production's variables (`mode: "rebuild"`); any other is published as
15563
+ * stored (`mode: "reupload"`). With an environment `target` the control
15564
+ * plane likewise rebuilds the deployment's commit with that environment's
15565
+ * variables or publishes the stored build.
15566
+ *
15567
+ * No `target` sends no body, exactly as clients before targets did. A
15568
+ * control plane from before targets ignores `target` and promotes to
15569
+ * production, so an environment target is first checked with
15570
+ * `promoteCheck`: a 405 there (no such route) stops with
15571
+ * `PROMOTE_TARGET_UNSUPPORTED` before anything is promoted. The target is
15572
+ * trimmed once, as the control plane trims it, so `" production"` is
15573
+ * production and asks no preview.
15574
+ */
15575
+ async promote(projectId, deploymentId, options2 = {}) {
15576
+ const target = options2.target?.trim();
15577
+ if (target !== void 0 && target !== "production") {
15578
+ try {
15579
+ await this.promoteCheck(projectId, deploymentId, target);
15580
+ } catch (error51) {
15581
+ if (isNaroError(error51) && error51.status === 405) {
15582
+ throw promoteTargetUnsupported(error51);
15583
+ }
15584
+ throw error51;
15585
+ }
15586
+ }
15587
+ const body = await this.client.request(this.promotePath(projectId, deploymentId), {
15588
+ method: "POST",
15589
+ ...target !== void 0 ? { body: { target } } : {},
15590
+ context: "Promote deployment"
15591
+ });
15592
+ return {
15593
+ ...deploymentInfo(body.deployment),
15594
+ mode: body.mode ?? "reupload"
15595
+ };
15596
+ }
15597
+ /**
15598
+ * What promoting to `target` would do, changing nothing: how it gets its
15599
+ * build, the migrations the target's database has not applied (null when
15600
+ * they cannot be compared) and the warnings to show first. Answers 405 from
15601
+ * a control plane that predates promote targets.
15546
15602
  */
15547
- async promote(projectId, deploymentId) {
15603
+ async promoteCheck(projectId, deploymentId, target) {
15604
+ const query = target !== void 0 ? `?target=${encodeURIComponent(target)}` : "";
15548
15605
  const body = await this.client.request(
15549
- `/api/projects/${encodeURIComponent(projectId)}/deployments/${encodeURIComponent(deploymentId)}/promote`,
15550
- { method: "POST", context: "Promote deployment" }
15606
+ `${this.promotePath(projectId, deploymentId)}${query}`,
15607
+ { context: "Check promotion" }
15551
15608
  );
15552
- return deploymentInfo(body.deployment);
15609
+ return {
15610
+ mode: body.mode ?? "reupload",
15611
+ pendingMigrations: body.pendingMigrations ?? null,
15612
+ warnings: body.warnings ?? []
15613
+ };
15614
+ }
15615
+ promotePath(projectId, deploymentId) {
15616
+ return `/api/projects/${encodeURIComponent(projectId)}/deployments/${encodeURIComponent(deploymentId)}/promote`;
15553
15617
  }
15554
15618
  /**
15555
15619
  * Stops an in-flight build (QUEUED through BUILDING) and marks the row
@@ -16304,6 +16368,7 @@ function storageInfo(body) {
16304
16368
  return {
16305
16369
  enabled: body.enabled,
16306
16370
  bucket: body.bucket ?? null,
16371
+ disabledAt: body.disabledAt ?? null,
16307
16372
  limits: {
16308
16373
  bytes: body.limits.bytes,
16309
16374
  objectBytes: body.limits.objectBytes,
@@ -16315,6 +16380,18 @@ function storageInfo(body) {
16315
16380
  buckets: (body.buckets ?? []).map(storageBucket)
16316
16381
  };
16317
16382
  }
16383
+ function provisionFailure(error51, feature) {
16384
+ const details = isNaroError(error51) ? error51.details : void 0;
16385
+ if (isNaroError(error51) && error51.code === "BACKEND_PROVISION_FAILED" && isRecord(details?.[feature]) && isRecord(details.backend))
16386
+ return new NaroError(error51.code, error51.message, {
16387
+ ...error51.status !== void 0 ? { status: error51.status } : {},
16388
+ details: {
16389
+ ...details,
16390
+ backend: backendInfo(details.backend)
16391
+ }
16392
+ });
16393
+ return error51;
16394
+ }
16318
16395
  function bucketFields(fields, into = {}) {
16319
16396
  for (const key of ["public", "fileSizeLimit", "allowedMimeTypes"])
16320
16397
  if (fields[key] !== void 0) into[key] = fields[key];
@@ -16743,16 +16820,45 @@ var StorageApi = class {
16743
16820
  }
16744
16821
  );
16745
16822
  } catch (error51) {
16746
- const details = isNaroError(error51) ? error51.details : void 0;
16747
- if (isNaroError(error51) && error51.code === "BACKEND_PROVISION_FAILED" && isRecord(details?.storage) && isRecord(details.backend))
16748
- throw new NaroError(error51.code, error51.message, {
16749
- ...error51.status !== void 0 ? { status: error51.status } : {},
16750
- details: {
16751
- ...details,
16752
- backend: backendInfo(details.backend)
16753
- }
16754
- });
16755
- throw error51;
16823
+ throw provisionFailure(error51, "storage");
16824
+ }
16825
+ return storageInfo(body.storage);
16826
+ }
16827
+ /**
16828
+ * Turns Storage off and keeps its files (Storage 3-1k): the Naro worker is
16829
+ * uploaded again without the bucket, so the app's Storage calls and file
16830
+ * URLs answer `404 STORAGE_NOT_ENABLED` until `enable` binds the same
16831
+ * bucket again. The bucket, its files and their storage cost stay; while
16832
+ * it is off, bucket writes and the object routes answer `409
16833
+ * STORAGE_OFF`, and reads go on. Safe to repeat: Storage never on, or
16834
+ * already off, comes back as it is, and a turn-off that stopped is
16835
+ * finished. Two minutes, like `enable`.
16836
+ *
16837
+ * Refusals, in the order they are checked: `BACKEND_DISABLED`,
16838
+ * `FORBIDDEN` (`storage.manage`), `DATABASE_NOT_ENABLED`,
16839
+ * `BACKEND_NOT_READY` (no backend), `BACKEND_OFF`, `BACKEND_BUSY` (another
16840
+ * change holds the backend — or turning Storage on again took this
16841
+ * turn-off over, and Storage is on), `STORAGE_DISABLE_FAILED` (502: the
16842
+ * turn-off could not be recorded; nothing changed), and
16843
+ * `BACKEND_PROVISION_FAILED` (502) when it is
16844
+ * recorded but the worker's upload — or a step after it — stopped:
16845
+ * `details` holds `{ storage: { applied: false, enabled: false }, backend
16846
+ * }`, and `disable` again finishes. Never `STORAGE_DISABLED`: the control
16847
+ * plane's Storage switch does not hold a removal back.
16848
+ */
16849
+ async disable(projectId) {
16850
+ let body;
16851
+ try {
16852
+ body = await this.client.request(
16853
+ this.path(projectId),
16854
+ {
16855
+ method: "DELETE",
16856
+ context: "Turn off Storage",
16857
+ timeoutMs: PROVISION_TIMEOUT_MS
16858
+ }
16859
+ );
16860
+ } catch (error51) {
16861
+ throw provisionFailure(error51, "storage");
16756
16862
  }
16757
16863
  return storageInfo(body.storage);
16758
16864
  }
@@ -16911,6 +17017,114 @@ var StorageApi = class {
16911
17017
  return body.objects;
16912
17018
  }
16913
17019
  };
17020
+ var RealtimeApi = class {
17021
+ constructor(client) {
17022
+ this.client = client;
17023
+ }
17024
+ path(projectId, suffix = "") {
17025
+ return `/api/projects/${encodeURIComponent(projectId)}/backend/realtime${suffix}`;
17026
+ }
17027
+ /**
17028
+ * Like `StorageApi.bucketPath`: URL parsing folds `.` and `..`, an empty
17029
+ * name is the collection, and `encodeURIComponent` throws on a lone
17030
+ * surrogate — so a name no URL can carry is refused here, before a
17031
+ * request. SQLite does make tables named `.` and `..`, and one with the
17032
+ * empty name (`CREATE TABLE "" (a)`); such a table cannot be published
17033
+ * or unpublished through this API.
17034
+ */
17035
+ tablePath(projectId, table) {
17036
+ if (table === "" || table === "." || table === ".." || LONE_SURROGATE.test(table))
17037
+ throw new NaroError("INVALID_PATH", "The table name is not valid.");
17038
+ return this.path(projectId, `/tables/${encodeURIComponent(table)}`);
17039
+ }
17040
+ /**
17041
+ * Whether Realtime is on, the limits the hub enforces, the published
17042
+ * tables and the hub's own numbers. A project that never turned it on
17043
+ * answers `enabled: false` with no tables.
17044
+ */
17045
+ async info(projectId) {
17046
+ const body = await this.client.request(
17047
+ this.path(projectId),
17048
+ { context: "Read realtime" }
17049
+ );
17050
+ return body.realtime;
17051
+ }
17052
+ /**
17053
+ * Turns Realtime on: the request and the plan's limits are recorded, and
17054
+ * the Naro worker is uploaded again with its hub (a Durable Object). Safe
17055
+ * to repeat, and a repair — also after a plan change. The project must have
17056
+ * a backend, turned on, with its database. A backend in `error` or with a
17057
+ * run that stalled is not refused: its provisioning is resumed on the way.
17058
+ * Two minutes, like `backend.enable`.
17059
+ *
17060
+ * Refusals, in the order they are checked: `BACKEND_DISABLED` (the control
17061
+ * plane's backend switch), `FORBIDDEN` (`database.write`),
17062
+ * `REALTIME_DISABLED` (the control plane's Realtime switch),
17063
+ * `DATABASE_NOT_ENABLED` (the project's database is external),
17064
+ * `BACKEND_NOT_READY` (409, only when the project has no backend at all),
17065
+ * `BACKEND_OFF`, `BACKEND_BUSY`, `REALTIME_ENABLE_FAILED` (502: the request
17066
+ * could not be recorded; nothing changed), and `BACKEND_PROVISION_FAILED`
17067
+ * (502) when Realtime is recorded as on but the worker's upload stopped.
17068
+ * The last one's `details` hold a `RealtimeProvisionFailure` — `{ realtime:
17069
+ * { applied: false, enabled: true }, backend }`, the backend read like
17070
+ * `backend.info` — and `enable` again finishes.
17071
+ */
17072
+ async enable(projectId) {
17073
+ let body;
17074
+ try {
17075
+ body = await this.client.request(
17076
+ this.path(projectId),
17077
+ {
17078
+ method: "POST",
17079
+ context: "Turn on Realtime",
17080
+ timeoutMs: PROVISION_TIMEOUT_MS
17081
+ }
17082
+ );
17083
+ } catch (error51) {
17084
+ throw provisionFailure(error51, "realtime");
17085
+ }
17086
+ return body.realtime;
17087
+ }
17088
+ /**
17089
+ * The tables whose Data API writes are published, in the order they were
17090
+ * published. `exists: false` is a published name whose table was dropped.
17091
+ * `REALTIME_NOT_ENABLED` (409) before `enable`; it answers while the
17092
+ * backend is off, since the list is in the project's database.
17093
+ */
17094
+ async listTables(projectId) {
17095
+ const body = await this.client.request(
17096
+ this.path(projectId, "/tables"),
17097
+ { context: "List realtime tables" }
17098
+ );
17099
+ return body.tables;
17100
+ }
17101
+ /**
17102
+ * Publishes a table's changes to the subscribers of its channel. Safe to
17103
+ * repeat. Only changes made through the Data API are published — writes
17104
+ * the app makes with `env.DB` are not (`warnings` says so).
17105
+ * `TABLE_NOT_FOUND` (404) for a table that does not exist;
17106
+ * `VALIDATION_FAILED` (400) for a Naro system table, a virtual table, a
17107
+ * name over 256 characters, or the 100-table cap; `REALTIME_NOT_ENABLED`
17108
+ * (409) before `enable`.
17109
+ */
17110
+ async publishTable(projectId, table) {
17111
+ return this.client.request(
17112
+ this.tablePath(projectId, table),
17113
+ { method: "PUT", context: "Publish a realtime table" }
17114
+ );
17115
+ }
17116
+ /**
17117
+ * Stops publishing a table's changes; a table dropped since can still be
17118
+ * unpublished. `TABLE_NOT_PUBLISHED` (404) for a table that does not
17119
+ * publish, `REALTIME_NOT_ENABLED` (409) before `enable`.
17120
+ */
17121
+ async unpublishTable(projectId, table) {
17122
+ return this.client.request(
17123
+ this.tablePath(projectId, table),
17124
+ { method: "DELETE", context: "Stop publishing a realtime table" }
17125
+ );
17126
+ }
17127
+ };
16914
17128
 
16915
17129
  // ../../packages/sdk/src/database.ts
16916
17130
  var BOOKMARK_RE = /^[0-9a-f]{8}-[0-9a-f]{8}-[0-9a-f]{8}-[0-9a-f]{32}$/;
@@ -16937,6 +17151,39 @@ var MAX_FILE_BYTES2 = 1024 * 1024;
16937
17151
  import { existsSync as existsSync3, readFileSync as readFileSync4 } from "fs";
16938
17152
  import { join as join4 } from "path";
16939
17153
 
17154
+ // ../../packages/data-core/src/system-tables.ts
17155
+ var SYSTEM_TABLE_PREFIXES = [
17156
+ "_naro_",
17157
+ "_cf_",
17158
+ "sqlite_"
17159
+ ];
17160
+ var SYSTEM_TABLE_NAMES = ["d1_migrations"];
17161
+ function asciiLower(text) {
17162
+ return text.replace(/[A-Z]+/g, (run) => run.toLowerCase());
17163
+ }
17164
+ function isSystemTable(name) {
17165
+ const lower = asciiLower(name);
17166
+ return SYSTEM_TABLE_PREFIXES.some((prefix) => lower.startsWith(prefix)) || SYSTEM_TABLE_NAMES.includes(lower);
17167
+ }
17168
+ function likeLiteral(text) {
17169
+ return text.replace(/[\\%_]/g, (char) => `\\${char}`).replace(/'/g, "''");
17170
+ }
17171
+ var COLUMN_REFERENCE = /^[A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*)?$/;
17172
+ function systemTablePredicate(column = "name") {
17173
+ if (!COLUMN_REFERENCE.test(column))
17174
+ throw new Error(`Not a column reference: ${column}`);
17175
+ return [
17176
+ ...SYSTEM_TABLE_PREFIXES.map(
17177
+ (prefix) => `${column} NOT LIKE '${likeLiteral(prefix)}%' ESCAPE '\\'`
17178
+ ),
17179
+ ...SYSTEM_TABLE_NAMES.map(
17180
+ (name) => `${column} NOT LIKE '${likeLiteral(name)}' ESCAPE '\\'`
17181
+ ),
17182
+ `${column} NOT IN (SELECT name FROM pragma_table_list WHERE schema = 'main' AND type = 'shadow')`
17183
+ ].join(" AND ");
17184
+ }
17185
+ var SYSTEM_TABLE_SQL_PREDICATE = systemTablePredicate("name");
17186
+
16940
17187
  // ../../packages/data-core/src/storage-path.ts
16941
17188
  var STORAGE_RESERVED_WORDS = [
16942
17189
  "authenticated",
@@ -17014,39 +17261,6 @@ var STORAGE_BUCKET_DELETING_COLUMN = {
17014
17261
  definition: "TEXT"
17015
17262
  };
17016
17263
 
17017
- // ../../packages/data-core/src/system-tables.ts
17018
- var SYSTEM_TABLE_PREFIXES = [
17019
- "_naro_",
17020
- "_cf_",
17021
- "sqlite_"
17022
- ];
17023
- var SYSTEM_TABLE_NAMES = ["d1_migrations"];
17024
- function asciiLower(text) {
17025
- return text.replace(/[A-Z]+/g, (run) => run.toLowerCase());
17026
- }
17027
- function isSystemTable(name) {
17028
- const lower = asciiLower(name);
17029
- return SYSTEM_TABLE_PREFIXES.some((prefix) => lower.startsWith(prefix)) || SYSTEM_TABLE_NAMES.includes(lower);
17030
- }
17031
- function likeLiteral(text) {
17032
- return text.replace(/[\\%_]/g, (char) => `\\${char}`).replace(/'/g, "''");
17033
- }
17034
- var COLUMN_REFERENCE = /^[A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*)?$/;
17035
- function systemTablePredicate(column = "name") {
17036
- if (!COLUMN_REFERENCE.test(column))
17037
- throw new Error(`Not a column reference: ${column}`);
17038
- return [
17039
- ...SYSTEM_TABLE_PREFIXES.map(
17040
- (prefix) => `${column} NOT LIKE '${likeLiteral(prefix)}%' ESCAPE '\\'`
17041
- ),
17042
- ...SYSTEM_TABLE_NAMES.map(
17043
- (name) => `${column} NOT LIKE '${likeLiteral(name)}' ESCAPE '\\'`
17044
- ),
17045
- `${column} NOT IN (SELECT name FROM pragma_table_list WHERE schema = 'main' AND type = 'shadow')`
17046
- ].join(" AND ");
17047
- }
17048
- var SYSTEM_TABLE_SQL_PREDICATE = systemTablePredicate("name");
17049
-
17050
17264
  // ../../packages/data-core/src/errors.ts
17051
17265
  var STORAGE_MIGRATION_PENDING = new RegExp(
17052
17266
  `^no such column: (?:b\\.|${STORAGE_BUCKET_DELETING_COLUMN.table}\\.)?${STORAGE_BUCKET_DELETING_COLUMN.column}\\b`
@@ -19292,8 +19506,34 @@ var MAX_STATEMENT_BYTES = 1e5;
19292
19506
  // ../../packages/data-core/src/policy-index.ts
19293
19507
  var INDEX_SQL = `SELECT t.name AS table_name, il.name AS index_name, il."partial" AS partial, ix.name AS column_name, ix.coll AS collation FROM pragma_table_list AS t JOIN pragma_index_list(t.name, t.schema) AS il JOIN pragma_index_xinfo(il.name, t.schema) AS ix WHERE t.schema = 'main' AND t.type = 'table' AND ${systemTablePredicate("t.name")} AND ix.seqno = 0`;
19294
19508
 
19509
+ // ../../packages/data-core/src/realtime-filter.ts
19510
+ var FILTER_REASONS = {
19511
+ bytes: "must be a string of up to 1024 bytes in UTF-8",
19512
+ control: "must have no control character",
19513
+ syntax: "supports eq, neq, lt, lte, gt, gte and in, one condition",
19514
+ list: "needs in.(a,b) with 1 to 100 values"
19515
+ };
19516
+
19295
19517
  // ../../packages/data-core/src/realtime-protocol.ts
19296
19518
  var REALTIME_PROTOCOL_VERSION = "1.0.0";
19519
+ var MAX_CHANGE_CHANNELS_PER_TABLE = 4;
19520
+ var JOIN_CONFIG_REASONS = {
19521
+ object: "must be an object",
19522
+ unsupported: "is not supported",
19523
+ boolean: "must be true or false",
19524
+ presenceKey: "must be 1 to 128 characters",
19525
+ changes: "must be an array of up to 16 subscriptions",
19526
+ changeEvent: "must be *, INSERT, UPDATE or DELETE",
19527
+ schema: "must be public",
19528
+ table: "must be a table name of 1 to 128 characters, or *",
19529
+ filter: FILTER_REASONS.bytes,
19530
+ filterControl: FILTER_REASONS.control,
19531
+ filterSyntax: FILTER_REASONS.syntax,
19532
+ filterList: FILTER_REASONS.list,
19533
+ filterTable: "needs a table: a filter cannot go with *",
19534
+ /** The hub's, after `config.postgres_changes[<i>].table` (3-2c-1): `MAX_CHANGE_CHANNELS_PER_TABLE` against the connection's joins. */
19535
+ changeChannels: `would put more than ${String(MAX_CHANGE_CHANNELS_PER_TABLE)} channels of this connection on one table's changes (* counts for every table): add the subscription to a channel it already has`
19536
+ };
19297
19537
  var REALTIME_CLOSE = {
19298
19538
  UNSUPPORTED_PROTOCOL: {
19299
19539
  code: 4e3,
@@ -19382,6 +19622,12 @@ var LIVE_BUCKET = `${STORAGE_BUCKET_DELETING_COLUMN.column} IS NULL`;
19382
19622
  var STORAGE_PROBE_NONCE = "f".repeat(STORAGE_NONCE_MAX_DIGITS);
19383
19623
  var REALTIME_PROBE_NONCE = "f".repeat(64);
19384
19624
 
19625
+ // ../../packages/data-core/src/realtime-publication.ts
19626
+ var REALTIME_TABLES = "_naro_realtime_tables";
19627
+ var REALTIME_TABLES_SQL = `CREATE TABLE IF NOT EXISTS ${REALTIME_TABLES} (table_name TEXT NOT NULL PRIMARY KEY COLLATE NOCASE, created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')))`;
19628
+ var MAX_PUBLISHED_TABLES = 100;
19629
+ var PUBLISHED_TABLES_SQL = `SELECT table_name FROM ${REALTIME_TABLES} ORDER BY rowid LIMIT ${String(MAX_PUBLISHED_TABLES)}`;
19630
+
19385
19631
  // ../../packages/data-core/src/system-sql.ts
19386
19632
  async function convertLegacyPolicies(db, found) {
19387
19633
  const rows = rowsOf(await db.prepare(POLICY_SQL.list).all());
@@ -19415,6 +19661,7 @@ var MIGRATIONS_TABLE = "CREATE TABLE IF NOT EXISTS _naro_migrations (version INT
19415
19661
  var STORAGE_SUBQUERIES_VERSION = 7;
19416
19662
  var STORAGE_SUBQUERIES_NAME = "storage_subquery_policies";
19417
19663
  var STORAGE_SUBQUERIES_SQL = `SELECT version FROM _naro_migrations WHERE version = ${String(STORAGE_SUBQUERIES_VERSION)} AND name = '${STORAGE_SUBQUERIES_NAME}'`;
19664
+ var REALTIME_TABLES_VERSION = 8;
19418
19665
  var SYSTEM_MIGRATIONS = [
19419
19666
  {
19420
19667
  version: 1,
@@ -19503,6 +19750,14 @@ var SYSTEM_MIGRATIONS = [
19503
19750
  version: STORAGE_SUBQUERIES_VERSION,
19504
19751
  name: STORAGE_SUBQUERIES_NAME,
19505
19752
  statements: []
19753
+ },
19754
+ {
19755
+ // Realtime 3-2b (original spec §9.1, design §7.1): the tables whose
19756
+ // Data API writes reach the hub. Empty until the publish API (3-2c)
19757
+ // writes it. A worker from before it never reads it.
19758
+ version: REALTIME_TABLES_VERSION,
19759
+ name: "realtime",
19760
+ statements: [REALTIME_TABLES_SQL]
19506
19761
  }
19507
19762
  ];
19508
19763
 
@@ -19544,6 +19799,10 @@ var ENTERPRISE_REALTIME_LIMITS = Object.freeze({
19544
19799
  restRequestsPerSecond: 100
19545
19800
  });
19546
19801
 
19802
+ // ../../packages/data-core/src/realtime-changes.ts
19803
+ var CHANGE_TYPES = Object.freeze({ insert: "INSERT", update: "UPDATE", delete: "DELETE" });
19804
+ var CHANGE_EVENT_CODES = Object.freeze({ "*": 0, INSERT: 1, UPDATE: 2, DELETE: 3 });
19805
+
19547
19806
  // ../../packages/data-core/src/storage-multipart.ts
19548
19807
  var MiB = 1024 * 1024;
19549
19808
  var MULTIPART_PART_SIZES = [16 * MiB, 32 * MiB, 64 * MiB];
@@ -20630,7 +20889,7 @@ var DATABASE_TOOLS = [
20630
20889
  destructiveHint: true,
20631
20890
  idempotentHint: false
20632
20891
  },
20633
- description: `${DESTRUCTIVE}executes raw SQL in the D1 database \u2014 it can change or delete data. Use \`apply_migration\` instead for DDL operations. ${UNTRUSTED} Pass values through params (bound to ? placeholders) instead of building them into the SQL text; params work with a single statement only. Several statements in one call run as one transaction: if one fails, none of them is applied. CREATE/DROP POLICY work here too (see \`apply_migration\`; they take no params), and result.warnings, when present, says what a policy needs. ${SHARED_DATABASE} ${LIMITS} ${SINGLE_THREADED}`,
20892
+ description: `${DESTRUCTIVE}executes raw SQL in the D1 database \u2014 it can change or delete data. Use \`apply_migration\` instead for DDL operations. ${UNTRUSTED} Pass values through params (bound to ? placeholders) instead of building them into the SQL text; params work with a single statement only. Several statements in one call run as one transaction: if one fails, none of them is applied. CREATE/DROP POLICY work here too (see \`apply_migration\`; they take no params), and result.warnings, when present, says what a policy needs. An ALTER TABLE that adds a generated column computes it for the rows already there, as in \`apply_migration\`, and takes no params either. ${SHARED_DATABASE} ${LIMITS} ${SINGLE_THREADED}`,
20634
20893
  readOnlyVariant: {
20635
20894
  description: `Executes a single read-only SQL statement in the D1 database. This server runs with --read-only: writes, DDL and multi-statement batches are refused with DATABASE_READ_ONLY. ${UNTRUSTED} Pass values through params (bound to ? placeholders) instead of building them into the SQL text. ${SHARED_DATABASE} ${LIMITS} ${SINGLE_THREADED}`,
20636
20895
  annotations: {
@@ -20691,7 +20950,7 @@ var DATABASE_TOOLS = [
20691
20950
  destructiveHint: true,
20692
20951
  idempotentHint: false
20693
20952
  },
20694
- description: `${DESTRUCTIVE}applies a migration (DDL) to the D1 database and records it in the d1_migrations ledger \u2014 the same ledger wrangler uses. \`name\` is a short snake_case title; naro numbers it (NNNN_name.sql) and, when the project directory is known, writes the file to ./migrations so \`naro db push\` sees it as applied. All-or-nothing: if a statement fails, the whole migration is rolled back and not recorded. Write CREATE POLICY in the same migration as the table, as in Supabase: it is applied with the table, all or nothing. A table without a policy refuses every anon and signed-in request to the Data API. DROP POLICY [IF EXISTS], COMMENT ON POLICY p ON t IS 'naro:owner <column>' and ALTER TABLE t ENABLE ROW LEVEL SECURITY are accepted too; DISABLE ROW LEVEL SECURITY, GRANT, REVOKE and ALTER POLICY are refused before anything runs. Dropping a table drops its policies, so a SQLite table rebuild (create __new_x, copy, drop x, rename) must create the policies again after the rename: warnings lists the ones it removed, with their SQL. ALTER TABLE a RENAME TO b moves a's policies to b and rewrites the policies of other tables that read a, as in Postgres: do not create them again on b, where one of the same name is refused. ALTER TABLE t RENAME COLUMN c TO d rewrites every policy that reads t.c, an owner rule's column included, to read t.d; a rename such a policy cannot follow is refused before anything runs. ${UNTRUSTED} ${SHARED_DATABASE} ${LIMITS}`,
20953
+ description: `${DESTRUCTIVE}applies a migration (DDL) to the D1 database and records it in the d1_migrations ledger \u2014 the same ledger wrangler uses. \`name\` is a short snake_case title; naro numbers it (NNNN_name.sql) and, when the project directory is known, writes the file to ./migrations so \`naro db push\` sees it as applied. All-or-nothing: if a statement fails, the whole migration is rolled back and not recorded. Write CREATE POLICY in the same migration as the table, as in Supabase: it is applied with the table, all or nothing. A table without a policy refuses every anon and signed-in request to the Data API. DROP POLICY [IF EXISTS], COMMENT ON POLICY p ON t IS 'naro:owner <column>' and ALTER TABLE t ENABLE ROW LEVEL SECURITY are accepted too; DISABLE ROW LEVEL SECURITY, GRANT, REVOKE and ALTER POLICY are refused before anything runs. Dropping a table drops its policies, so a SQLite table rebuild (create __new_x, copy, drop x, rename) must create the policies again after the rename: warnings lists the ones it removed, with their SQL. ALTER TABLE a RENAME TO b moves a's policies to b and rewrites the policies of other tables that read a, as in Postgres: do not create them again on b, where one of the same name is refused. ALTER TABLE t RENAME COLUMN c TO d rewrites every policy that reads t.c, an owner rule's column included, to read t.d; a rename such a policy cannot follow is refused before anything runs. ALTER TABLE t ADD COLUMN c \u2026 AS (\u2026) computes the generated column c for the rows already in t, as Postgres does: a row it cannot be computed for (json_extract over text that is not JSON) fails the whole migration with SQLite's error (malformed JSON) and nothing is applied, so fix those rows in an earlier statement. ${UNTRUSTED} ${SHARED_DATABASE} ${LIMITS}`,
20695
20954
  inputSchema: {
20696
20955
  type: "object",
20697
20956
  properties: {
@@ -21593,7 +21852,7 @@ var STORAGE_TOOLS = [
21593
21852
  },
21594
21853
  untrusted: true,
21595
21854
  untrustedSubject: "the Storage query",
21596
- description: `Reads the project's Naro Storage: whether it is on, its R2 bucket, the limits its backend enforces (bytes in all, bytes per object, 50 MiB per request; larger files go in parts through naro-js, and through naro storage cp under 4 GiB), what is stored now (usage is null while the backend is not ready), ${STORAGE_METER}, and every bucket with its objects, bytes, public flag and limits. Who may read and write objects is decided by CREATE POLICY \u2026 ON storage.objects in a migration (apply_migration), as in Supabase. ${UNTRUSTED_BUCKETS}`,
21855
+ description: `Reads the project's Naro Storage: whether it is on, its R2 bucket, the limits its backend enforces (bytes in all, bytes per object, 50 MiB per request; larger files go in parts through naro-js, and through naro storage cp under 4 GiB), what is stored now (usage is null while the backend is not ready), disabledAt \u2014 when Storage was turned off with its files kept (then enabled is false, and enable_storage turns it on again only when the user asks) \u2014 ${STORAGE_METER}, and every bucket with its objects, bytes, public flag and limits. Who may read and write objects is decided by CREATE POLICY \u2026 ON storage.objects in a migration (apply_migration), as in Supabase. ${UNTRUSTED_BUCKETS}`,
21597
21856
  inputSchema: { type: "object", properties: { ...PROJECT_ARGS } },
21598
21857
  handler: async (args, deps) => {
21599
21858
  const projectId = await projectOf(args, deps);
@@ -21613,7 +21872,7 @@ var STORAGE_TOOLS = [
21613
21872
  },
21614
21873
  untrusted: true,
21615
21874
  untrustedSubject: "the Storage query",
21616
- description: `Turns Naro Storage on for the project: makes its R2 bucket and uploads the backend again with it, which can take up to 2 minutes. The project's backend must be on (BACKEND_NOT_READY or BACKEND_OFF otherwise). Safe to call again: it resumes a run that stopped and applies a plan change's limits. STORAGE_DISABLED means Storage is not switched on for the control plane yet. It answers the Storage state as get_storage does, with ${STORAGE_METER}. ${UNTRUSTED_BUCKETS}`,
21875
+ description: `Turns Naro Storage on for the project: makes its R2 bucket and uploads the backend again with it, which can take up to 2 minutes. The project's backend must be on (BACKEND_NOT_READY or BACKEND_OFF otherwise). Turned off with disable_storage, it binds the same bucket again and the files come back. Safe to call again: it resumes a run that stopped and applies a plan change's limits. Storage that is off (disabledAt) was turned off on purpose: turn it on again only when the user asks. STORAGE_DISABLED means Storage is not switched on for the control plane yet. It answers the Storage state as get_storage does, with ${STORAGE_METER}. ${UNTRUSTED_BUCKETS}`,
21617
21876
  inputSchema: { type: "object", properties: { ...PROJECT_ARGS } },
21618
21877
  handler: async (args, deps) => {
21619
21878
  const projectId = await projectOf(args, deps);
@@ -21623,6 +21882,40 @@ var STORAGE_TOOLS = [
21623
21882
  };
21624
21883
  }
21625
21884
  },
21885
+ {
21886
+ name: "disable_storage",
21887
+ group: "storage",
21888
+ annotations: {
21889
+ readOnlyHint: false,
21890
+ destructiveHint: true,
21891
+ idempotentHint: true
21892
+ },
21893
+ untrusted: true,
21894
+ untrustedSubject: "the Storage query",
21895
+ description: `${DESTRUCTIVE}turns Naro Storage off for the project and keeps its files: the backend is uploaded again without its R2 bucket, which can take up to 2 minutes, and from then on the app's Storage calls and file URLs answer 404 until enable_storage turns it on again with the same bucket. The buckets and files stay, and so does their storage cost; while it is off, bucket writes and the file tools answer STORAGE_OFF and get_storage shows disabledAt. To delete the files instead, call delete_storage_bucket with empty: true while Storage is still on. Needs confirm set to the project's Storage bucket as get_storage shows it (naro-storage-<projectId>) \u2014 confirm with the user first. Storage that was never turned on is answered as it is; calling it on Storage that is already off finishes a turn-off that stopped. The project's backend must be on (BACKEND_OFF otherwise). It answers the Storage state as get_storage does. ${UNTRUSTED_BUCKETS}`,
21896
+ inputSchema: {
21897
+ type: "object",
21898
+ properties: {
21899
+ ...PROJECT_ARGS,
21900
+ confirm: {
21901
+ type: "string",
21902
+ description: "The project's Storage bucket, as get_storage shows it (naro-storage-<projectId>). Needed while Storage is on."
21903
+ }
21904
+ }
21905
+ },
21906
+ handler: async (args, deps) => {
21907
+ const projectId = await projectOf(args, deps);
21908
+ const client = await deps.client();
21909
+ const current = await client.storage.info(projectId);
21910
+ if (current.bucket === null) return { projectId, storage: current };
21911
+ if (current.disabledAt === null && args.confirm !== current.bucket)
21912
+ throw new NaroError(
21913
+ "CONFIRMATION_REQUIRED",
21914
+ `disable_storage needs confirm set to the project's Storage bucket, ${current.bucket}.`
21915
+ );
21916
+ return { projectId, storage: await client.storage.disable(projectId) };
21917
+ }
21918
+ },
21626
21919
  {
21627
21920
  name: "list_storage_buckets",
21628
21921
  group: "storage",
@@ -21653,7 +21946,7 @@ var STORAGE_TOOLS = [
21653
21946
  },
21654
21947
  untrusted: true,
21655
21948
  untrustedSubject: "the Storage query",
21656
- description: `Creates a Storage bucket, optionally public, with a largest object and the types it takes. The name cannot change later and cannot be authenticated, copy, info, list, move, public, render, sign or upload; a project has up to 100 buckets. STORAGE_NOT_ENABLED means Storage is off: call enable_storage first. Objects go in with naro-js or naro storage cp, under the storage.objects policies. It answers the bucket as stored. ${UNTRUSTED_BUCKETS}`,
21949
+ description: `Creates a Storage bucket, optionally public, with a largest object and the types it takes. The name cannot change later and cannot be authenticated, copy, info, list, move, public, render, sign or upload; a project has up to 100 buckets. STORAGE_NOT_ENABLED means Storage was never turned on: call enable_storage first. STORAGE_OFF means the user turned Storage off on purpose (disable_storage): tell the user, and call enable_storage only if they ask to turn it back on. Objects go in with naro-js or naro storage cp, under the storage.objects policies. It answers the bucket as stored. ${UNTRUSTED_BUCKETS}`,
21657
21950
  inputSchema: {
21658
21951
  type: "object",
21659
21952
  properties: { ...PROJECT_ARGS, name: NAME, ...FIELDS },
@@ -21984,6 +22277,18 @@ function envTarget(value) {
21984
22277
  `Invalid target ${JSON.stringify(value)} \u2014 use production, preview, all or an environment name.`
21985
22278
  );
21986
22279
  }
22280
+ var REBUILD_TIMEOUT_MS = 15 * 6e4;
22281
+ var REUPLOAD_TIMEOUT_MS = 5 * 6e4;
22282
+ function promoteTarget(value) {
22283
+ if (value === void 0 || value === null) return void 0;
22284
+ const name = typeof value === "string" ? value.trim() : "";
22285
+ if (name === "" || name === "preview")
22286
+ throw new NaroError(
22287
+ "VALIDATION_FAILED",
22288
+ "target must be production or an environment name."
22289
+ );
22290
+ return name;
22291
+ }
21987
22292
  var REGISTERED = [
21988
22293
  {
21989
22294
  name: "whoami",
@@ -22607,7 +22912,7 @@ var REGISTERED = [
22607
22912
  destructiveHint: true,
22608
22913
  idempotentHint: false
22609
22914
  },
22610
- description: `${DESTRUCTIVE}publishes an existing READY deployment (e.g. a preview build) to production. Production traffic switches to it once READY.`,
22915
+ description: `${DESTRUCTIVE}publishes an existing READY deployment (e.g. a preview build). Without target it goes to production, and production traffic switches to it once READY; with target set to an environment's name (list_environments) it goes to that environment instead. The result's mode says how it got its build: reupload publishes the stored build again, rebuild builds the same commit again with the target's variables (a promotion between surfaces when one of them is an environment, such as preview \u2192 qa or qa \u2192 production) and only works for a deployment built from Git (PROMOTE_NEEDS_GIT_SOURCE otherwise). target in the result is null when it was left out. If the call times out on the client side, the promotion keeps going: look it up with list_deployments or get_deployment. A control plane that predates promote targets answers PROMOTE_TARGET_UNSUPPORTED and promotes nothing: ask the user to deploy the latest web first.`,
22611
22916
  inputSchema: {
22612
22917
  type: "object",
22613
22918
  properties: {
@@ -22615,6 +22920,10 @@ var REGISTERED = [
22615
22920
  deploymentId: {
22616
22921
  type: "string",
22617
22922
  description: "The READY deployment to promote."
22923
+ },
22924
+ target: {
22925
+ type: "string",
22926
+ description: `Where to promote: production (the default when omitted; switches production traffic to the deployment), or an environment name such as qa (list_environments); preview is not a target. Promoting between surfaces when one of them is an environment (preview \u2192 qa, qa \u2192 production) rebuilds the same commit with the target surface's variables; a rebuild takes up to 15 minutes.`
22618
22927
  }
22619
22928
  },
22620
22929
  required: ["deploymentId"]
@@ -22623,6 +22932,7 @@ var REGISTERED = [
22623
22932
  const deploymentId = str(args.deploymentId);
22624
22933
  if (!deploymentId)
22625
22934
  throw new NaroError("VALIDATION_FAILED", "deploymentId is required.");
22935
+ const target = promoteTarget(args.target);
22626
22936
  const projectId = await deps.resolveProjectId({
22627
22937
  projectId: str(args.projectId),
22628
22938
  directory: str(args.directory)
@@ -22630,17 +22940,20 @@ var REGISTERED = [
22630
22940
  const client = await deps.client();
22631
22941
  const promoted = await client.deployments.promote(
22632
22942
  projectId,
22633
- deploymentId
22943
+ deploymentId,
22944
+ target !== void 0 ? { target } : void 0
22634
22945
  );
22635
22946
  const detail = await client.deployments.wait(promoted.id, {
22636
- timeoutMs: 5 * 6e4
22947
+ timeoutMs: promoted.mode === "rebuild" ? REBUILD_TIMEOUT_MS : REUPLOAD_TIMEOUT_MS
22637
22948
  });
22638
22949
  return {
22639
22950
  deploymentId: promoted.id,
22640
22951
  sourceDeploymentId: deploymentId,
22641
22952
  status: detail.deployment.status,
22642
22953
  url: detail.deployment.url,
22643
- errorCode: detail.deployment.errorCode
22954
+ errorCode: detail.deployment.errorCode,
22955
+ target: target ?? null,
22956
+ mode: promoted.mode
22644
22957
  };
22645
22958
  }
22646
22959
  },
@@ -22687,14 +23000,15 @@ var REGISTERED = [
22687
23000
  }
22688
23001
  const promoted = await client.deployments.promote(projectId, target);
22689
23002
  const detail = await client.deployments.wait(promoted.id, {
22690
- timeoutMs: 5 * 6e4
23003
+ timeoutMs: promoted.mode === "rebuild" ? REBUILD_TIMEOUT_MS : REUPLOAD_TIMEOUT_MS
22691
23004
  });
22692
23005
  return {
22693
23006
  deploymentId: promoted.id,
22694
23007
  rolledBackTo: target,
22695
23008
  status: detail.deployment.status,
22696
23009
  url: detail.deployment.url,
22697
- errorCode: detail.deployment.errorCode
23010
+ errorCode: detail.deployment.errorCode,
23011
+ mode: promoted.mode
22698
23012
  };
22699
23013
  }
22700
23014
  },
@@ -22851,7 +23165,7 @@ async function openClaims(client, projectId) {
22851
23165
  }
22852
23166
 
22853
23167
  // src/server.ts
22854
- var SERVER_VERSION = "0.18.1";
23168
+ var SERVER_VERSION = "0.20.0";
22855
23169
  function visibleTools(options2) {
22856
23170
  const { features, readOnly } = options2;
22857
23171
  return TOOLS.flatMap((tool) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "naro-mcp",
3
- "version": "0.18.1",
3
+ "version": "0.20.0",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "naro-mcp": "./dist/index.js"
@@ -18,15 +18,15 @@
18
18
  "dependencies": {
19
19
  "@modelcontextprotocol/sdk": "^1.0.0",
20
20
  "@supabase/supabase-js": "2.112.3",
21
- "naro-cli": "0.19.1"
21
+ "naro-cli": "0.21.0"
22
22
  },
23
23
  "devDependencies": {
24
24
  "tsup": "8.5.0",
25
25
  "tsx": "4.23.12",
26
26
  "typescript": "7.0.2",
27
27
  "vitest": "3.2.7",
28
- "@next-deploy/sdk": "0.1.0",
29
- "@next-deploy/config": "0.1.0"
28
+ "@next-deploy/config": "0.1.0",
29
+ "@next-deploy/sdk": "0.1.0"
30
30
  },
31
31
  "scripts": {
32
32
  "build": "tsup",