naro-mcp 0.18.0 → 0.19.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 +326 -102
  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`; `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.`; a slug that is not an environment answers `404 ENVIRONMENT_NOT_FOUND` |
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
@@ -15540,16 +15553,65 @@ var DeploymentsApi = class {
15540
15553
  return { current, target: production.slice(1).find(differs) ?? null };
15541
15554
  }
15542
15555
  /**
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).
15556
+ * Promotes an existing READY deployment: with no `target` (or
15557
+ * `production`) it goes to production — `naro promote` (preview →
15558
+ * production) and `naro rollback` (older production build → production).
15559
+ * A deployment from an environment is rebuilt from its commit with
15560
+ * production's variables (`mode: "rebuild"`); any other is published as
15561
+ * stored (`mode: "reupload"`). With an environment `target` the control
15562
+ * plane likewise rebuilds the deployment's commit with that environment's
15563
+ * variables or publishes the stored build.
15564
+ *
15565
+ * No `target` sends no body, exactly as clients before targets did. A
15566
+ * control plane from before targets ignores `target` and promotes to
15567
+ * production, so an environment target is first checked with
15568
+ * `promoteCheck`: a 405 there (no such route) stops with
15569
+ * `PROMOTE_TARGET_UNSUPPORTED` before anything is promoted. The target is
15570
+ * trimmed once, as the control plane trims it, so `" production"` is
15571
+ * production and asks no preview.
15572
+ */
15573
+ async promote(projectId, deploymentId, options2 = {}) {
15574
+ const target = options2.target?.trim();
15575
+ if (target !== void 0 && target !== "production") {
15576
+ try {
15577
+ await this.promoteCheck(projectId, deploymentId, target);
15578
+ } catch (error51) {
15579
+ if (isNaroError(error51) && error51.status === 405) {
15580
+ throw promoteTargetUnsupported(error51);
15581
+ }
15582
+ throw error51;
15583
+ }
15584
+ }
15585
+ const body = await this.client.request(this.promotePath(projectId, deploymentId), {
15586
+ method: "POST",
15587
+ ...target !== void 0 ? { body: { target } } : {},
15588
+ context: "Promote deployment"
15589
+ });
15590
+ return {
15591
+ ...deploymentInfo(body.deployment),
15592
+ mode: body.mode ?? "reupload"
15593
+ };
15594
+ }
15595
+ /**
15596
+ * What promoting to `target` would do, changing nothing: how it gets its
15597
+ * build, the migrations the target's database has not applied (null when
15598
+ * they cannot be compared) and the warnings to show first. Answers 405 from
15599
+ * a control plane that predates promote targets.
15546
15600
  */
15547
- async promote(projectId, deploymentId) {
15601
+ async promoteCheck(projectId, deploymentId, target) {
15602
+ const query = target !== void 0 ? `?target=${encodeURIComponent(target)}` : "";
15548
15603
  const body = await this.client.request(
15549
- `/api/projects/${encodeURIComponent(projectId)}/deployments/${encodeURIComponent(deploymentId)}/promote`,
15550
- { method: "POST", context: "Promote deployment" }
15604
+ `${this.promotePath(projectId, deploymentId)}${query}`,
15605
+ { context: "Check promotion" }
15551
15606
  );
15552
- return deploymentInfo(body.deployment);
15607
+ return {
15608
+ mode: body.mode ?? "reupload",
15609
+ pendingMigrations: body.pendingMigrations ?? null,
15610
+ warnings: body.warnings ?? []
15611
+ };
15612
+ }
15613
+ promotePath(projectId, deploymentId) {
15614
+ return `/api/projects/${encodeURIComponent(projectId)}/deployments/${encodeURIComponent(deploymentId)}/promote`;
15553
15615
  }
15554
15616
  /**
15555
15617
  * Stops an in-flight build (QUEUED through BUILDING) and marks the row
@@ -15595,6 +15657,8 @@ var DeploymentsApi = class {
15595
15657
  }
15596
15658
  }
15597
15659
  };
15660
+ var STATIC_ASSETS_BINDING = "ASSETS";
15661
+ var MANAGED_DATABASE_BINDING = "DB";
15598
15662
  var EnvApi = class {
15599
15663
  constructor(client) {
15600
15664
  this.client = client;
@@ -15624,6 +15688,37 @@ var EnvApi = class {
15624
15688
  }
15625
15689
  );
15626
15690
  }
15691
+ /**
15692
+ * The names the env route refuses as a key — `409 VALIDATION_FAILED`
15693
+ * `<KEY> is already used by a storage binding.` — read the way it reads
15694
+ * them: `ASSETS` (the static files), every binding of the project's storage
15695
+ * and, on a project using the managed database (`database_provider` is
15696
+ * `d1`), `DB`, which may not be a storage row yet. For a caller that sends
15697
+ * many entries in several requests (`naro env import`) and has to refuse
15698
+ * before the first one is saved. A control plane whose project row has no
15699
+ * `database_provider` reads as having no managed database, so `DB` is only
15700
+ * left out there.
15701
+ */
15702
+ async takenBindingNames(projectId) {
15703
+ const base = `/api/projects/${encodeURIComponent(projectId)}`;
15704
+ const [storage, project] = await Promise.all([
15705
+ this.client.request(
15706
+ `${base}/storage`,
15707
+ { context: "List storage" }
15708
+ ),
15709
+ this.client.request(
15710
+ base,
15711
+ { context: "Read project" }
15712
+ )
15713
+ ]);
15714
+ const names = /* @__PURE__ */ new Set([
15715
+ STATIC_ASSETS_BINDING,
15716
+ ...storage.storage.map((row) => row.bindingName)
15717
+ ]);
15718
+ if (project.project.database_provider === "d1")
15719
+ names.add(MANAGED_DATABASE_BINDING);
15720
+ return [...names];
15721
+ }
15627
15722
  async removeById(projectId, envVarId) {
15628
15723
  await this.client.request(
15629
15724
  `/api/projects/${encodeURIComponent(projectId)}/env/${encodeURIComponent(envVarId)}`,
@@ -15634,13 +15729,28 @@ var EnvApi = class {
15634
15729
  * Removes a key's rows: the one `target` names, or, with no target, the
15635
15730
  * `production`, `preview` and `all` rows. An environment's value goes only
15636
15731
  * when its slug is the target, so removing a key everywhere else does not
15637
- * take a QA or staging value with it.
15732
+ * take a QA or staging value with it. A key only environments hold fails
15733
+ * ENV_NOT_FOUND naming them (`details.environments`), not as if there were
15734
+ * none.
15638
15735
  */
15639
15736
  async remove(projectId, key, target) {
15640
15737
  const rows = await this.list(projectId);
15641
15738
  const matches = rows.filter(
15642
15739
  (row) => row.key === key && (target === void 0 ? isReservedEnvTarget(row.target) : row.target === target)
15643
15740
  );
15741
+ if (matches.length === 0 && target === void 0) {
15742
+ const environments = [
15743
+ ...new Set(
15744
+ rows.filter((row) => row.key === key).map((row) => row.target).filter((slug) => !isReservedEnvTarget(slug))
15745
+ )
15746
+ ].toSorted();
15747
+ if (environments.length > 0)
15748
+ throw new NaroError(
15749
+ "ENV_NOT_FOUND",
15750
+ `No ${key} variable found outside named environments. Pass the environment to remove it: ${environments.join(", ")}.`,
15751
+ { details: { key, environments } }
15752
+ );
15753
+ }
15644
15754
  if (matches.length === 0) {
15645
15755
  throw new NaroError(
15646
15756
  "ENV_NOT_FOUND",
@@ -16256,6 +16366,7 @@ function storageInfo(body) {
16256
16366
  return {
16257
16367
  enabled: body.enabled,
16258
16368
  bucket: body.bucket ?? null,
16369
+ disabledAt: body.disabledAt ?? null,
16259
16370
  limits: {
16260
16371
  bytes: body.limits.bytes,
16261
16372
  objectBytes: body.limits.objectBytes,
@@ -16267,6 +16378,18 @@ function storageInfo(body) {
16267
16378
  buckets: (body.buckets ?? []).map(storageBucket)
16268
16379
  };
16269
16380
  }
16381
+ function storageRunFailure(error51) {
16382
+ const details = isNaroError(error51) ? error51.details : void 0;
16383
+ if (isNaroError(error51) && error51.code === "BACKEND_PROVISION_FAILED" && isRecord(details?.storage) && isRecord(details.backend))
16384
+ return new NaroError(error51.code, error51.message, {
16385
+ ...error51.status !== void 0 ? { status: error51.status } : {},
16386
+ details: {
16387
+ ...details,
16388
+ backend: backendInfo(details.backend)
16389
+ }
16390
+ });
16391
+ return error51;
16392
+ }
16270
16393
  function bucketFields(fields, into = {}) {
16271
16394
  for (const key of ["public", "fileSizeLimit", "allowedMimeTypes"])
16272
16395
  if (fields[key] !== void 0) into[key] = fields[key];
@@ -16695,16 +16818,45 @@ var StorageApi = class {
16695
16818
  }
16696
16819
  );
16697
16820
  } catch (error51) {
16698
- const details = isNaroError(error51) ? error51.details : void 0;
16699
- if (isNaroError(error51) && error51.code === "BACKEND_PROVISION_FAILED" && isRecord(details?.storage) && isRecord(details.backend))
16700
- throw new NaroError(error51.code, error51.message, {
16701
- ...error51.status !== void 0 ? { status: error51.status } : {},
16702
- details: {
16703
- ...details,
16704
- backend: backendInfo(details.backend)
16705
- }
16706
- });
16707
- throw error51;
16821
+ throw storageRunFailure(error51);
16822
+ }
16823
+ return storageInfo(body.storage);
16824
+ }
16825
+ /**
16826
+ * Turns Storage off and keeps its files (Storage 3-1k): the Naro worker is
16827
+ * uploaded again without the bucket, so the app's Storage calls and file
16828
+ * URLs answer `404 STORAGE_NOT_ENABLED` until `enable` binds the same
16829
+ * bucket again. The bucket, its files and their storage cost stay; while
16830
+ * it is off, bucket writes and the object routes answer `409
16831
+ * STORAGE_OFF`, and reads go on. Safe to repeat: Storage never on, or
16832
+ * already off, comes back as it is, and a turn-off that stopped is
16833
+ * finished. Two minutes, like `enable`.
16834
+ *
16835
+ * Refusals, in the order they are checked: `BACKEND_DISABLED`,
16836
+ * `FORBIDDEN` (`storage.manage`), `DATABASE_NOT_ENABLED`,
16837
+ * `BACKEND_NOT_READY` (no backend), `BACKEND_OFF`, `BACKEND_BUSY` (another
16838
+ * change holds the backend — or turning Storage on again took this
16839
+ * turn-off over, and Storage is on), `STORAGE_DISABLE_FAILED` (502: the
16840
+ * turn-off could not be recorded; nothing changed), and
16841
+ * `BACKEND_PROVISION_FAILED` (502) when it is
16842
+ * recorded but the worker's upload — or a step after it — stopped:
16843
+ * `details` holds `{ storage: { applied: false, enabled: false }, backend
16844
+ * }`, and `disable` again finishes. Never `STORAGE_DISABLED`: the control
16845
+ * plane's Storage switch does not hold a removal back.
16846
+ */
16847
+ async disable(projectId) {
16848
+ let body;
16849
+ try {
16850
+ body = await this.client.request(
16851
+ this.path(projectId),
16852
+ {
16853
+ method: "DELETE",
16854
+ context: "Turn off Storage",
16855
+ timeoutMs: PROVISION_TIMEOUT_MS
16856
+ }
16857
+ );
16858
+ } catch (error51) {
16859
+ throw storageRunFailure(error51);
16708
16860
  }
16709
16861
  return storageInfo(body.storage);
16710
16862
  }
@@ -16912,6 +17064,7 @@ var MEDIA_TYPE = new RegExp(`^${TOKEN_CHARACTERS}+/${TOKEN_CHARACTERS}+$`);
16912
17064
  // ../../packages/data-core/src/storage-table.ts
16913
17065
  var STORAGE_OBJECTS = "storage.objects";
16914
17066
  var STORAGE_OBJECTS_TABLE = "_naro_storage_objects";
17067
+ var STORAGE_UNAVAILABLE_REASON = `${STORAGE_OBJECTS} is not available on this backend yet`;
16915
17068
  var STORAGE_BUCKETS_TABLE = "_naro_storage_buckets";
16916
17069
  var STORAGE_POLICY_COLUMNS = [
16917
17070
  "id",
@@ -17477,7 +17630,6 @@ var SPECIAL_WORDS = /* @__PURE__ */ new Set([
17477
17630
  "auth"
17478
17631
  ]);
17479
17632
  var TEXT_TYPES = /* @__PURE__ */ new Set(["text", "varchar", "uuid"]);
17480
- var STORAGE_SUBQUERY_PROBLEM = "A policy cannot read storage.objects in a subquery yet";
17481
17633
  var FOLDERNAME_INDEX_PROBLEM = `storage.foldername(name) is available only as (storage.foldername(name))[n] with n from 1 to ${String(MAX_FOLDERNAME_INDEX)}`;
17482
17634
  function storageHelperColumnProblem(fn) {
17483
17635
  return `storage.${fn}() takes the name column of storage.objects`;
@@ -17928,8 +18080,12 @@ function atRealtimeMessages(stream) {
17928
18080
  }
17929
18081
  function readTableName(stream) {
17930
18082
  const first = stream.peek();
17931
- if (atStorageObjects(stream))
17932
- throw new PolicySyntaxError(STORAGE_SUBQUERY_PROBLEM, first.position);
18083
+ if (atStorageObjects(stream)) {
18084
+ stream.next();
18085
+ stream.next();
18086
+ stream.next();
18087
+ return STORAGE_OBJECTS;
18088
+ }
17933
18089
  if (atRealtimeMessages(stream))
17934
18090
  throw new PolicySyntaxError(REALTIME_SUBQUERY_PROBLEM, first.position);
17935
18091
  let token = first;
@@ -18366,9 +18522,10 @@ function printValue(expr) {
18366
18522
  }
18367
18523
  function printSubquery(subquery) {
18368
18524
  const select = subquery.select === "*" ? "*" : printValue(subquery.select);
18525
+ const table = asciiLower(subquery.table) === STORAGE_OBJECTS ? STORAGE_OBJECTS : quoteIdent(subquery.table);
18369
18526
  const alias = subquery.alias === null ? "" : ` ${quoteIdent(subquery.alias)}`;
18370
18527
  const where = subquery.where === null ? "" : ` WHERE ${printExpr(subquery.where)}`;
18371
- return `SELECT ${select} FROM ${quoteIdent(subquery.table)}${alias}${where}`;
18528
+ return `SELECT ${select} FROM ${table}${alias}${where}`;
18372
18529
  }
18373
18530
  function printElement(element) {
18374
18531
  if (typeof element === "object") return printValue(element);
@@ -19326,70 +19483,9 @@ var PROJECT_BYTES = `(SELECT coalesce(sum(total_bytes), 0) FROM ${STORAGE_BUCKET
19326
19483
  var LIVE_BUCKET = `${STORAGE_BUCKET_DELETING_COLUMN.column} IS NULL`;
19327
19484
 
19328
19485
  // ../../packages/data-core/src/policy-check.ts
19329
- var CONTEXTS = [
19330
- { role: "anon" },
19331
- {
19332
- role: "authenticated",
19333
- userId: "naro-policy-check",
19334
- email: "check@naro.invalid",
19335
- emailVerified: true
19336
- }
19337
- ];
19338
19486
  var STORAGE_PROBE_NONCE = "f".repeat(STORAGE_NONCE_MAX_DIGITS);
19339
- var STORAGE_CONTEXTS = [
19340
- ...CONTEXTS,
19341
- { role: "authenticated", userId: "naro-policy-check" }
19342
- ];
19343
19487
  var REALTIME_PROBE_NONCE = "f".repeat(64);
19344
19488
 
19345
- // ../../packages/data-core/src/policy-migration.ts
19346
- var STORED_POLICIES_SQL = `SELECT table_name, name, kind, command, roles, using_expr, check_expr, owner_column, updated_at FROM ${NAMED_POLICY_TABLE} ORDER BY table_name, name`;
19347
- var SYSTEM_PREDICATE = systemTablePredicate("name");
19348
- var STORAGE_EXISTS = `EXISTS (SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ${sqlStringLiteral(STORAGE_OBJECTS_TABLE)})`;
19349
-
19350
- // ../../packages/data-core/src/realtime-limits.ts
19351
- var HOBBY_REALTIME_LIMITS = Object.freeze({
19352
- connections: 200,
19353
- newConnectionsPerSecond: 10,
19354
- messagesPerSecond: 100,
19355
- joinsPerSecond: 100,
19356
- presencePerSecond: 20,
19357
- broadcastPayloadBytes: 262144,
19358
- policyChecksPerSecond: 100,
19359
- restRequestsPerSecond: 10
19360
- });
19361
- var PRO_REALTIME_LIMITS = Object.freeze({
19362
- connections: 500,
19363
- newConnectionsPerSecond: 25,
19364
- messagesPerSecond: 500,
19365
- joinsPerSecond: 500,
19366
- presencePerSecond: 50,
19367
- broadcastPayloadBytes: 3072e3,
19368
- policyChecksPerSecond: 250,
19369
- restRequestsPerSecond: 25
19370
- });
19371
- var ENTERPRISE_REALTIME_LIMITS = Object.freeze({
19372
- connections: 2e3,
19373
- newConnectionsPerSecond: 100,
19374
- messagesPerSecond: 1e3,
19375
- joinsPerSecond: 1e3,
19376
- presencePerSecond: 1e3,
19377
- broadcastPayloadBytes: 3072e3,
19378
- policyChecksPerSecond: 500,
19379
- restRequestsPerSecond: 100
19380
- });
19381
-
19382
- // ../../packages/data-core/src/storage-multipart.ts
19383
- var MiB = 1024 * 1024;
19384
- var MULTIPART_PART_SIZES = [16 * MiB, 32 * MiB, 64 * MiB];
19385
- var MAX_MULTIPART_PARTS = 1e4;
19386
- var MAX_MULTIPART_BYTES = 64 * MiB * MAX_MULTIPART_PARTS;
19387
- var MULTIPART_LOCK_MS = 60 * 60 * 1e3;
19388
- var MULTIPART_PART_EXTEND_BELOW_MS = 30 * 60 * 1e3;
19389
- var MULTIPART_COMPLETE_LOCK_MS = 15 * 60 * 1e3;
19390
- var UUID = "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}";
19391
- var UPLOAD_ID = new RegExp(`^(${UUID})\\.(${UUID})$`);
19392
-
19393
19489
  // ../../packages/data-core/src/system-sql.ts
19394
19490
  async function convertLegacyPolicies(db, found) {
19395
19491
  const rows = rowsOf(await db.prepare(POLICY_SQL.list).all());
@@ -19420,6 +19516,9 @@ async function convertLegacyPolicies(db, found) {
19420
19516
  return statements;
19421
19517
  }
19422
19518
  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')))";
19519
+ var STORAGE_SUBQUERIES_VERSION = 7;
19520
+ var STORAGE_SUBQUERIES_NAME = "storage_subquery_policies";
19521
+ var STORAGE_SUBQUERIES_SQL = `SELECT version FROM _naro_migrations WHERE version = ${String(STORAGE_SUBQUERIES_VERSION)} AND name = '${STORAGE_SUBQUERIES_NAME}'`;
19423
19522
  var SYSTEM_MIGRATIONS = [
19424
19523
  {
19425
19524
  version: 1,
@@ -19488,9 +19587,78 @@ var SYSTEM_MIGRATIONS = [
19488
19587
  name: "storage_bucket_deleting",
19489
19588
  statements: [],
19490
19589
  addColumns: [STORAGE_BUCKET_DELETING_COLUMN]
19590
+ },
19591
+ {
19592
+ // Storage 3-1j (plan correction (p)): no schema — the mark that a worker
19593
+ // which reads storage.objects in a policy's subquery has migrated this
19594
+ // database. Only the Naro worker's own `internal/migrate` applies system
19595
+ // migrations, so the row is there once such a worker has run, and stays:
19596
+ // every later worker reads them too — as long as the worker bundle does
19597
+ // not go back. Deploying web from before 3-1j makes the next upgrade
19598
+ // put an older worker back, and a run that lost its lease may upload
19599
+ // one late; such a policy then answers 500 POLICY_INVALID (closed, no
19600
+ // rows) until it is dropped or the newer worker is uploaded again
19601
+ // (3-1j 최종 리뷰). Migration 5 alone cannot tell —
19602
+ // backends migrated by 3-1a to 3-1i have it, and their worker refuses
19603
+ // such a policy ("A policy cannot read storage.objects in a subquery
19604
+ // yet") and answers every request of its table 500 POLICY_INVALID until
19605
+ // the next upgrade. The control plane takes one only where this row is
19606
+ // (`STORAGE_SUBQUERIES_SQL`, `buildPolicyBatch`).
19607
+ version: STORAGE_SUBQUERIES_VERSION,
19608
+ name: STORAGE_SUBQUERIES_NAME,
19609
+ statements: []
19491
19610
  }
19492
19611
  ];
19493
19612
 
19613
+ // ../../packages/data-core/src/policy-migration.ts
19614
+ var STORED_POLICIES_SQL = `SELECT table_name, name, kind, command, roles, using_expr, check_expr, owner_column, updated_at FROM ${NAMED_POLICY_TABLE} ORDER BY table_name, name`;
19615
+ var SYSTEM_PREDICATE = systemTablePredicate("name");
19616
+ var STORAGE_EXISTS = `EXISTS (SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ${sqlStringLiteral(STORAGE_OBJECTS_TABLE)})`;
19617
+ var STORAGE_SUBQUERIES_READY = `EXISTS (${STORAGE_SUBQUERIES_SQL})`;
19618
+
19619
+ // ../../packages/data-core/src/realtime-limits.ts
19620
+ var HOBBY_REALTIME_LIMITS = Object.freeze({
19621
+ connections: 200,
19622
+ newConnectionsPerSecond: 10,
19623
+ messagesPerSecond: 100,
19624
+ joinsPerSecond: 100,
19625
+ presencePerSecond: 20,
19626
+ broadcastPayloadBytes: 262144,
19627
+ policyChecksPerSecond: 100,
19628
+ restRequestsPerSecond: 10
19629
+ });
19630
+ var PRO_REALTIME_LIMITS = Object.freeze({
19631
+ connections: 500,
19632
+ newConnectionsPerSecond: 25,
19633
+ messagesPerSecond: 500,
19634
+ joinsPerSecond: 500,
19635
+ presencePerSecond: 50,
19636
+ broadcastPayloadBytes: 3072e3,
19637
+ policyChecksPerSecond: 250,
19638
+ restRequestsPerSecond: 25
19639
+ });
19640
+ var ENTERPRISE_REALTIME_LIMITS = Object.freeze({
19641
+ connections: 2e3,
19642
+ newConnectionsPerSecond: 100,
19643
+ messagesPerSecond: 1e3,
19644
+ joinsPerSecond: 1e3,
19645
+ presencePerSecond: 1e3,
19646
+ broadcastPayloadBytes: 3072e3,
19647
+ policyChecksPerSecond: 500,
19648
+ restRequestsPerSecond: 100
19649
+ });
19650
+
19651
+ // ../../packages/data-core/src/storage-multipart.ts
19652
+ var MiB = 1024 * 1024;
19653
+ var MULTIPART_PART_SIZES = [16 * MiB, 32 * MiB, 64 * MiB];
19654
+ var MAX_MULTIPART_PARTS = 1e4;
19655
+ var MAX_MULTIPART_BYTES = 64 * MiB * MAX_MULTIPART_PARTS;
19656
+ var MULTIPART_LOCK_MS = 60 * 60 * 1e3;
19657
+ var MULTIPART_PART_EXTEND_BELOW_MS = 30 * 60 * 1e3;
19658
+ var MULTIPART_COMPLETE_LOCK_MS = 15 * 60 * 1e3;
19659
+ var UUID = "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}";
19660
+ var UPLOAD_ID = new RegExp(`^(${UUID})\\.(${UUID})$`);
19661
+
19494
19662
  // ../../packages/data-core/src/write-quota.ts
19495
19663
  var WRITE_QUOTA_KEY = "write_quota";
19496
19664
  var WRITE_QUOTA_SQL = {
@@ -20566,7 +20734,7 @@ var DATABASE_TOOLS = [
20566
20734
  destructiveHint: true,
20567
20735
  idempotentHint: false
20568
20736
  },
20569
- 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}`,
20737
+ 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}`,
20570
20738
  readOnlyVariant: {
20571
20739
  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}`,
20572
20740
  annotations: {
@@ -20627,7 +20795,7 @@ var DATABASE_TOOLS = [
20627
20795
  destructiveHint: true,
20628
20796
  idempotentHint: false
20629
20797
  },
20630
- 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. ${UNTRUSTED} ${SHARED_DATABASE} ${LIMITS}`,
20798
+ 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}`,
20631
20799
  inputSchema: {
20632
20800
  type: "object",
20633
20801
  properties: {
@@ -20731,7 +20899,7 @@ var DATABASE_TOOLS = [
20731
20899
  destructiveHint: true,
20732
20900
  idempotentHint: false
20733
20901
  },
20734
- 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. rename_table moves the table's Data API policies to the new name and rewrites the policies of other tables that read it by name, in the same migration, as in Postgres: warnings says which. A rename such a policy cannot follow (an alias in it already goes by the new name) is refused before anything runs. Policies an earlier table left under the new name are removed: removedPolicies lists them: statements has each as the CREATE POLICY that naro policy create takes back, except an owner rule (its ownerColumn is set), which CREATE POLICY cannot say \u2014 setCommand is the naro policy set command that sets those again. A rename or drop that takes a table or column from under a policy \u2014 this table's or, through a subquery, another table's \u2014 makes the requests that policy applies to answer 500 POLICY_INVALID: warnings names each policy the change breaks, with dryRun: true before anything is applied. Read warnings after a rename or a drop. dryRun: true returns the SQL without applying anything. ${UNTRUSTED} ${SHARED_DATABASE}`,
20902
+ 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. rename_table moves the table's Data API policies to the new name and rewrites the policies of other tables that read it by name, in the same migration, as in Postgres: warnings says which. rename_column rewrites every policy that reads the column, this table's or another's through a subquery, an owner rule's column included: warnings says which. A rename such a policy cannot follow (an alias in it already goes by the new name, or a policy already reads the new column name) is refused before anything runs. Policies an earlier table left under the new name are removed: removedPolicies lists them: statements has each as the CREATE POLICY that naro policy create takes back, except an owner rule (its ownerColumn is set), which CREATE POLICY cannot say \u2014 setCommand is the naro policy set command that sets those again. A rename or drop that takes a table or column from under a policy \u2014 this table's or, through a subquery, another table's \u2014 makes the requests that policy applies to answer 500 POLICY_INVALID: warnings names each policy the change breaks, with dryRun: true before anything is applied. Read warnings after a rename or a drop. dryRun: true returns the SQL without applying anything. ${UNTRUSTED} ${SHARED_DATABASE}`,
20735
20903
  inputSchema: {
20736
20904
  type: "object",
20737
20905
  properties: {
@@ -21529,7 +21697,7 @@ var STORAGE_TOOLS = [
21529
21697
  },
21530
21698
  untrusted: true,
21531
21699
  untrustedSubject: "the Storage query",
21532
- 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}`,
21700
+ 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}`,
21533
21701
  inputSchema: { type: "object", properties: { ...PROJECT_ARGS } },
21534
21702
  handler: async (args, deps) => {
21535
21703
  const projectId = await projectOf(args, deps);
@@ -21549,7 +21717,7 @@ var STORAGE_TOOLS = [
21549
21717
  },
21550
21718
  untrusted: true,
21551
21719
  untrustedSubject: "the Storage query",
21552
- 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}`,
21720
+ 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}`,
21553
21721
  inputSchema: { type: "object", properties: { ...PROJECT_ARGS } },
21554
21722
  handler: async (args, deps) => {
21555
21723
  const projectId = await projectOf(args, deps);
@@ -21559,6 +21727,40 @@ var STORAGE_TOOLS = [
21559
21727
  };
21560
21728
  }
21561
21729
  },
21730
+ {
21731
+ name: "disable_storage",
21732
+ group: "storage",
21733
+ annotations: {
21734
+ readOnlyHint: false,
21735
+ destructiveHint: true,
21736
+ idempotentHint: true
21737
+ },
21738
+ untrusted: true,
21739
+ untrustedSubject: "the Storage query",
21740
+ 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}`,
21741
+ inputSchema: {
21742
+ type: "object",
21743
+ properties: {
21744
+ ...PROJECT_ARGS,
21745
+ confirm: {
21746
+ type: "string",
21747
+ description: "The project's Storage bucket, as get_storage shows it (naro-storage-<projectId>). Needed while Storage is on."
21748
+ }
21749
+ }
21750
+ },
21751
+ handler: async (args, deps) => {
21752
+ const projectId = await projectOf(args, deps);
21753
+ const client = await deps.client();
21754
+ const current = await client.storage.info(projectId);
21755
+ if (current.bucket === null) return { projectId, storage: current };
21756
+ if (current.disabledAt === null && args.confirm !== current.bucket)
21757
+ throw new NaroError(
21758
+ "CONFIRMATION_REQUIRED",
21759
+ `disable_storage needs confirm set to the project's Storage bucket, ${current.bucket}.`
21760
+ );
21761
+ return { projectId, storage: await client.storage.disable(projectId) };
21762
+ }
21763
+ },
21562
21764
  {
21563
21765
  name: "list_storage_buckets",
21564
21766
  group: "storage",
@@ -21589,7 +21791,7 @@ var STORAGE_TOOLS = [
21589
21791
  },
21590
21792
  untrusted: true,
21591
21793
  untrustedSubject: "the Storage query",
21592
- 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}`,
21794
+ 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}`,
21593
21795
  inputSchema: {
21594
21796
  type: "object",
21595
21797
  properties: { ...PROJECT_ARGS, name: NAME, ...FIELDS },
@@ -21920,6 +22122,18 @@ function envTarget(value) {
21920
22122
  `Invalid target ${JSON.stringify(value)} \u2014 use production, preview, all or an environment name.`
21921
22123
  );
21922
22124
  }
22125
+ var REBUILD_TIMEOUT_MS = 15 * 6e4;
22126
+ var REUPLOAD_TIMEOUT_MS = 5 * 6e4;
22127
+ function promoteTarget(value) {
22128
+ if (value === void 0 || value === null) return void 0;
22129
+ const name = typeof value === "string" ? value.trim() : "";
22130
+ if (name === "" || name === "preview")
22131
+ throw new NaroError(
22132
+ "VALIDATION_FAILED",
22133
+ "target must be production or an environment name."
22134
+ );
22135
+ return name;
22136
+ }
21923
22137
  var REGISTERED = [
21924
22138
  {
21925
22139
  name: "whoami",
@@ -22075,7 +22289,7 @@ var REGISTERED = [
22075
22289
  },
22076
22290
  environment: {
22077
22291
  type: "string",
22078
- description: "Slug of a named environment (list_environments, create_environment) to deploy to instead of preview, such as qa. Not together with production: true \u2014 the control plane refuses both. production and preview are not environment names: use production: true, or omit this for a preview."
22292
+ description: "Slug of a named environment (list_environments, create_environment) to deploy to instead of preview, such as qa. Not together with production: true \u2014 both are refused (VALIDATION_FAILED) before any value is read, asked for or sent and before the deployment is created. production and preview are not environment names: use production: true, or omit this for a preview."
22079
22293
  },
22080
22294
  branch: {
22081
22295
  type: "string",
@@ -22124,8 +22338,9 @@ var REGISTERED = [
22124
22338
  projectId: link?.projectId,
22125
22339
  teamSlug: team.slug,
22126
22340
  production: args.production === true,
22127
- // Sent beside production, not instead of it: the control plane
22128
- // refuses both together and that answer is the one shown.
22341
+ // Passed beside production, not instead of it: deployDirectory
22342
+ // refuses both together before any request (an older control plane
22343
+ // would deploy production), and that answer is the one shown.
22129
22344
  environment,
22130
22345
  // Never a surprise browser window: this server may be headless.
22131
22346
  allowLogin: false,
@@ -22542,7 +22757,7 @@ var REGISTERED = [
22542
22757
  destructiveHint: true,
22543
22758
  idempotentHint: false
22544
22759
  },
22545
- description: `${DESTRUCTIVE}publishes an existing READY deployment (e.g. a preview build) to production. Production traffic switches to it once READY.`,
22760
+ 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.`,
22546
22761
  inputSchema: {
22547
22762
  type: "object",
22548
22763
  properties: {
@@ -22550,6 +22765,10 @@ var REGISTERED = [
22550
22765
  deploymentId: {
22551
22766
  type: "string",
22552
22767
  description: "The READY deployment to promote."
22768
+ },
22769
+ target: {
22770
+ type: "string",
22771
+ 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.`
22553
22772
  }
22554
22773
  },
22555
22774
  required: ["deploymentId"]
@@ -22558,6 +22777,7 @@ var REGISTERED = [
22558
22777
  const deploymentId = str(args.deploymentId);
22559
22778
  if (!deploymentId)
22560
22779
  throw new NaroError("VALIDATION_FAILED", "deploymentId is required.");
22780
+ const target = promoteTarget(args.target);
22561
22781
  const projectId = await deps.resolveProjectId({
22562
22782
  projectId: str(args.projectId),
22563
22783
  directory: str(args.directory)
@@ -22565,17 +22785,20 @@ var REGISTERED = [
22565
22785
  const client = await deps.client();
22566
22786
  const promoted = await client.deployments.promote(
22567
22787
  projectId,
22568
- deploymentId
22788
+ deploymentId,
22789
+ target !== void 0 ? { target } : void 0
22569
22790
  );
22570
22791
  const detail = await client.deployments.wait(promoted.id, {
22571
- timeoutMs: 5 * 6e4
22792
+ timeoutMs: promoted.mode === "rebuild" ? REBUILD_TIMEOUT_MS : REUPLOAD_TIMEOUT_MS
22572
22793
  });
22573
22794
  return {
22574
22795
  deploymentId: promoted.id,
22575
22796
  sourceDeploymentId: deploymentId,
22576
22797
  status: detail.deployment.status,
22577
22798
  url: detail.deployment.url,
22578
- errorCode: detail.deployment.errorCode
22799
+ errorCode: detail.deployment.errorCode,
22800
+ target: target ?? null,
22801
+ mode: promoted.mode
22579
22802
  };
22580
22803
  }
22581
22804
  },
@@ -22622,14 +22845,15 @@ var REGISTERED = [
22622
22845
  }
22623
22846
  const promoted = await client.deployments.promote(projectId, target);
22624
22847
  const detail = await client.deployments.wait(promoted.id, {
22625
- timeoutMs: 5 * 6e4
22848
+ timeoutMs: promoted.mode === "rebuild" ? REBUILD_TIMEOUT_MS : REUPLOAD_TIMEOUT_MS
22626
22849
  });
22627
22850
  return {
22628
22851
  deploymentId: promoted.id,
22629
22852
  rolledBackTo: target,
22630
22853
  status: detail.deployment.status,
22631
22854
  url: detail.deployment.url,
22632
- errorCode: detail.deployment.errorCode
22855
+ errorCode: detail.deployment.errorCode,
22856
+ mode: promoted.mode
22633
22857
  };
22634
22858
  }
22635
22859
  },
@@ -22678,7 +22902,7 @@ var REGISTERED = [
22678
22902
  destructiveHint: false,
22679
22903
  idempotentHint: true
22680
22904
  },
22681
- description: 'Connects the project to a GitHub repository so every push to the default branch deploys automatically. Use after deploy reports gitConnection.state "available" AND the user has agreed. If the response has connected:false with an installUrl, the GitHub App is not installed on that account yet \u2014 give the user the installUrl to open in their own browser, then call this tool again once they confirm. With installUrl null there is nothing to install from here: tell the user to connect from the Naro dashboard. Never open a browser yourself.',
22905
+ description: 'Connects the project to a GitHub repository so every push to the default branch deploys automatically. Use after deploy reports gitConnection.state "available" AND the user has agreed. If the response has connected:false with an installUrl, Naro cannot reach the repository yet \u2014 the installUrl either installs the GitHub App on that account or imports the GitHub access of the user for an account the team already connected. Give it to the user to open in their own browser; if the repository is not among the ones the App was granted on GitHub, they add it there first. Call this tool again once they confirm. With installUrl null there is nothing to install from here: tell the user to connect from the Naro dashboard. Never open a browser yourself.',
22682
22906
  inputSchema: {
22683
22907
  type: "object",
22684
22908
  properties: {
@@ -22719,7 +22943,7 @@ var REGISTERED = [
22719
22943
  // installing from here would not connect anything. "Open the
22720
22944
  // installUrl" sent the agent guessing one (known-issues #42); point
22721
22945
  // at the dashboard, as the CLI does.
22722
- message: result.installUrl ? `Naro cannot reach ${fullName} yet. Ask the user to open the installUrl in their browser and install the GitHub App, then call this tool again.` : `Naro cannot reach ${fullName}, and no install link was offered. Tell the user to connect the repository from the Naro dashboard \u2014 do not make up a link.`
22946
+ message: result.installUrl ? `Naro cannot reach ${fullName} yet. Ask the user to open the installUrl in their browser (and, if the repository is not among the ones the GitHub App was granted on GitHub, add it there first), then call this tool again.` : `Naro cannot reach ${fullName}, and no install link was offered. Tell the user to connect the repository from the Naro dashboard \u2014 do not make up a link.`
22723
22947
  };
22724
22948
  }
22725
22949
  return {
@@ -22786,7 +23010,7 @@ async function openClaims(client, projectId) {
22786
23010
  }
22787
23011
 
22788
23012
  // src/server.ts
22789
- var SERVER_VERSION = "0.18.0";
23013
+ var SERVER_VERSION = "0.19.0";
22790
23014
  function visibleTools(options2) {
22791
23015
  const { features, readOnly } = options2;
22792
23016
  return TOOLS.flatMap((tool) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "naro-mcp",
3
- "version": "0.18.0",
3
+ "version": "0.19.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.0"
21
+ "naro-cli": "0.20.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",