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.
- package/README.md +25 -24
- package/dist/index.js +326 -102
- 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
|
|
59
|
-
|
|
|
60
|
-
| `whoami`, `list_teams`
|
|
61
|
-
| `list_projects`, `get_project`, `link_project`
|
|
62
|
-
| `get_team_usage`
|
|
63
|
-
| `deploy`
|
|
64
|
-
| `list_deployments`, `get_deployment`
|
|
65
|
-
| `get_build_logs`, `get_runtime_logs`
|
|
66
|
-
| `list_environment_variables`, `set_environment_variable`, `remove_environment_variable`
|
|
67
|
-
| `list_domains`, `add_domain`, `verify_domain`, `remove_domain`
|
|
68
|
-
| `promote_deployment`, `
|
|
69
|
-
| `
|
|
70
|
-
| `
|
|
71
|
-
| `
|
|
72
|
-
| `
|
|
73
|
-
| `
|
|
74
|
-
| `
|
|
75
|
-
| `
|
|
76
|
-
| `
|
|
77
|
-
| `
|
|
78
|
-
| `
|
|
79
|
-
| `
|
|
80
|
-
| `
|
|
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:#
|
|
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
|
-
*
|
|
15544
|
-
*
|
|
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
|
|
15601
|
+
async promoteCheck(projectId, deploymentId, target) {
|
|
15602
|
+
const query = target !== void 0 ? `?target=${encodeURIComponent(target)}` : "";
|
|
15548
15603
|
const body = await this.client.request(
|
|
15549
|
-
|
|
15550
|
-
{
|
|
15604
|
+
`${this.promotePath(projectId, deploymentId)}${query}`,
|
|
15605
|
+
{ context: "Check promotion" }
|
|
15551
15606
|
);
|
|
15552
|
-
return
|
|
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
|
-
|
|
16699
|
-
|
|
16700
|
-
|
|
16701
|
-
|
|
16702
|
-
|
|
16703
|
-
|
|
16704
|
-
|
|
16705
|
-
|
|
16706
|
-
|
|
16707
|
-
|
|
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
|
-
|
|
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 ${
|
|
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
|
|
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
|
|
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
|
-
//
|
|
22128
|
-
// refuses both together
|
|
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
|
|
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:
|
|
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:
|
|
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
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
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/
|
|
29
|
-
"@next-deploy/
|
|
28
|
+
"@next-deploy/config": "0.1.0",
|
|
29
|
+
"@next-deploy/sdk": "0.1.0"
|
|
30
30
|
},
|
|
31
31
|
"scripts": {
|
|
32
32
|
"build": "tsup",
|