naro-mcp 0.18.1 → 0.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -24
- package/dist/index.js +377 -63
- 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
|
|
@@ -15321,6 +15334,7 @@ var NaroClient = class {
|
|
|
15321
15334
|
db;
|
|
15322
15335
|
backend;
|
|
15323
15336
|
storage;
|
|
15337
|
+
realtime;
|
|
15324
15338
|
insights;
|
|
15325
15339
|
analytics;
|
|
15326
15340
|
protection;
|
|
@@ -15340,6 +15354,7 @@ var NaroClient = class {
|
|
|
15340
15354
|
this.db = new DbApi(this);
|
|
15341
15355
|
this.backend = new BackendApi(this);
|
|
15342
15356
|
this.storage = new StorageApi(this);
|
|
15357
|
+
this.realtime = new RealtimeApi(this);
|
|
15343
15358
|
this.insights = new InsightsApi(this);
|
|
15344
15359
|
this.analytics = new AnalyticsApi(this);
|
|
15345
15360
|
this.protection = new ProtectionApi(this);
|
|
@@ -15540,16 +15555,65 @@ var DeploymentsApi = class {
|
|
|
15540
15555
|
return { current, target: production.slice(1).find(differs) ?? null };
|
|
15541
15556
|
}
|
|
15542
15557
|
/**
|
|
15543
|
-
*
|
|
15544
|
-
*
|
|
15545
|
-
* (older production build → production).
|
|
15558
|
+
* Promotes an existing READY deployment: with no `target` (or
|
|
15559
|
+
* `production`) it goes to production — `naro promote` (preview →
|
|
15560
|
+
* production) and `naro rollback` (older production build → production).
|
|
15561
|
+
* A deployment from an environment is rebuilt from its commit with
|
|
15562
|
+
* production's variables (`mode: "rebuild"`); any other is published as
|
|
15563
|
+
* stored (`mode: "reupload"`). With an environment `target` the control
|
|
15564
|
+
* plane likewise rebuilds the deployment's commit with that environment's
|
|
15565
|
+
* variables or publishes the stored build.
|
|
15566
|
+
*
|
|
15567
|
+
* No `target` sends no body, exactly as clients before targets did. A
|
|
15568
|
+
* control plane from before targets ignores `target` and promotes to
|
|
15569
|
+
* production, so an environment target is first checked with
|
|
15570
|
+
* `promoteCheck`: a 405 there (no such route) stops with
|
|
15571
|
+
* `PROMOTE_TARGET_UNSUPPORTED` before anything is promoted. The target is
|
|
15572
|
+
* trimmed once, as the control plane trims it, so `" production"` is
|
|
15573
|
+
* production and asks no preview.
|
|
15574
|
+
*/
|
|
15575
|
+
async promote(projectId, deploymentId, options2 = {}) {
|
|
15576
|
+
const target = options2.target?.trim();
|
|
15577
|
+
if (target !== void 0 && target !== "production") {
|
|
15578
|
+
try {
|
|
15579
|
+
await this.promoteCheck(projectId, deploymentId, target);
|
|
15580
|
+
} catch (error51) {
|
|
15581
|
+
if (isNaroError(error51) && error51.status === 405) {
|
|
15582
|
+
throw promoteTargetUnsupported(error51);
|
|
15583
|
+
}
|
|
15584
|
+
throw error51;
|
|
15585
|
+
}
|
|
15586
|
+
}
|
|
15587
|
+
const body = await this.client.request(this.promotePath(projectId, deploymentId), {
|
|
15588
|
+
method: "POST",
|
|
15589
|
+
...target !== void 0 ? { body: { target } } : {},
|
|
15590
|
+
context: "Promote deployment"
|
|
15591
|
+
});
|
|
15592
|
+
return {
|
|
15593
|
+
...deploymentInfo(body.deployment),
|
|
15594
|
+
mode: body.mode ?? "reupload"
|
|
15595
|
+
};
|
|
15596
|
+
}
|
|
15597
|
+
/**
|
|
15598
|
+
* What promoting to `target` would do, changing nothing: how it gets its
|
|
15599
|
+
* build, the migrations the target's database has not applied (null when
|
|
15600
|
+
* they cannot be compared) and the warnings to show first. Answers 405 from
|
|
15601
|
+
* a control plane that predates promote targets.
|
|
15546
15602
|
*/
|
|
15547
|
-
async
|
|
15603
|
+
async promoteCheck(projectId, deploymentId, target) {
|
|
15604
|
+
const query = target !== void 0 ? `?target=${encodeURIComponent(target)}` : "";
|
|
15548
15605
|
const body = await this.client.request(
|
|
15549
|
-
|
|
15550
|
-
{
|
|
15606
|
+
`${this.promotePath(projectId, deploymentId)}${query}`,
|
|
15607
|
+
{ context: "Check promotion" }
|
|
15551
15608
|
);
|
|
15552
|
-
return
|
|
15609
|
+
return {
|
|
15610
|
+
mode: body.mode ?? "reupload",
|
|
15611
|
+
pendingMigrations: body.pendingMigrations ?? null,
|
|
15612
|
+
warnings: body.warnings ?? []
|
|
15613
|
+
};
|
|
15614
|
+
}
|
|
15615
|
+
promotePath(projectId, deploymentId) {
|
|
15616
|
+
return `/api/projects/${encodeURIComponent(projectId)}/deployments/${encodeURIComponent(deploymentId)}/promote`;
|
|
15553
15617
|
}
|
|
15554
15618
|
/**
|
|
15555
15619
|
* Stops an in-flight build (QUEUED through BUILDING) and marks the row
|
|
@@ -16304,6 +16368,7 @@ function storageInfo(body) {
|
|
|
16304
16368
|
return {
|
|
16305
16369
|
enabled: body.enabled,
|
|
16306
16370
|
bucket: body.bucket ?? null,
|
|
16371
|
+
disabledAt: body.disabledAt ?? null,
|
|
16307
16372
|
limits: {
|
|
16308
16373
|
bytes: body.limits.bytes,
|
|
16309
16374
|
objectBytes: body.limits.objectBytes,
|
|
@@ -16315,6 +16380,18 @@ function storageInfo(body) {
|
|
|
16315
16380
|
buckets: (body.buckets ?? []).map(storageBucket)
|
|
16316
16381
|
};
|
|
16317
16382
|
}
|
|
16383
|
+
function provisionFailure(error51, feature) {
|
|
16384
|
+
const details = isNaroError(error51) ? error51.details : void 0;
|
|
16385
|
+
if (isNaroError(error51) && error51.code === "BACKEND_PROVISION_FAILED" && isRecord(details?.[feature]) && isRecord(details.backend))
|
|
16386
|
+
return new NaroError(error51.code, error51.message, {
|
|
16387
|
+
...error51.status !== void 0 ? { status: error51.status } : {},
|
|
16388
|
+
details: {
|
|
16389
|
+
...details,
|
|
16390
|
+
backend: backendInfo(details.backend)
|
|
16391
|
+
}
|
|
16392
|
+
});
|
|
16393
|
+
return error51;
|
|
16394
|
+
}
|
|
16318
16395
|
function bucketFields(fields, into = {}) {
|
|
16319
16396
|
for (const key of ["public", "fileSizeLimit", "allowedMimeTypes"])
|
|
16320
16397
|
if (fields[key] !== void 0) into[key] = fields[key];
|
|
@@ -16743,16 +16820,45 @@ var StorageApi = class {
|
|
|
16743
16820
|
}
|
|
16744
16821
|
);
|
|
16745
16822
|
} catch (error51) {
|
|
16746
|
-
|
|
16747
|
-
|
|
16748
|
-
|
|
16749
|
-
|
|
16750
|
-
|
|
16751
|
-
|
|
16752
|
-
|
|
16753
|
-
|
|
16754
|
-
|
|
16755
|
-
|
|
16823
|
+
throw provisionFailure(error51, "storage");
|
|
16824
|
+
}
|
|
16825
|
+
return storageInfo(body.storage);
|
|
16826
|
+
}
|
|
16827
|
+
/**
|
|
16828
|
+
* Turns Storage off and keeps its files (Storage 3-1k): the Naro worker is
|
|
16829
|
+
* uploaded again without the bucket, so the app's Storage calls and file
|
|
16830
|
+
* URLs answer `404 STORAGE_NOT_ENABLED` until `enable` binds the same
|
|
16831
|
+
* bucket again. The bucket, its files and their storage cost stay; while
|
|
16832
|
+
* it is off, bucket writes and the object routes answer `409
|
|
16833
|
+
* STORAGE_OFF`, and reads go on. Safe to repeat: Storage never on, or
|
|
16834
|
+
* already off, comes back as it is, and a turn-off that stopped is
|
|
16835
|
+
* finished. Two minutes, like `enable`.
|
|
16836
|
+
*
|
|
16837
|
+
* Refusals, in the order they are checked: `BACKEND_DISABLED`,
|
|
16838
|
+
* `FORBIDDEN` (`storage.manage`), `DATABASE_NOT_ENABLED`,
|
|
16839
|
+
* `BACKEND_NOT_READY` (no backend), `BACKEND_OFF`, `BACKEND_BUSY` (another
|
|
16840
|
+
* change holds the backend — or turning Storage on again took this
|
|
16841
|
+
* turn-off over, and Storage is on), `STORAGE_DISABLE_FAILED` (502: the
|
|
16842
|
+
* turn-off could not be recorded; nothing changed), and
|
|
16843
|
+
* `BACKEND_PROVISION_FAILED` (502) when it is
|
|
16844
|
+
* recorded but the worker's upload — or a step after it — stopped:
|
|
16845
|
+
* `details` holds `{ storage: { applied: false, enabled: false }, backend
|
|
16846
|
+
* }`, and `disable` again finishes. Never `STORAGE_DISABLED`: the control
|
|
16847
|
+
* plane's Storage switch does not hold a removal back.
|
|
16848
|
+
*/
|
|
16849
|
+
async disable(projectId) {
|
|
16850
|
+
let body;
|
|
16851
|
+
try {
|
|
16852
|
+
body = await this.client.request(
|
|
16853
|
+
this.path(projectId),
|
|
16854
|
+
{
|
|
16855
|
+
method: "DELETE",
|
|
16856
|
+
context: "Turn off Storage",
|
|
16857
|
+
timeoutMs: PROVISION_TIMEOUT_MS
|
|
16858
|
+
}
|
|
16859
|
+
);
|
|
16860
|
+
} catch (error51) {
|
|
16861
|
+
throw provisionFailure(error51, "storage");
|
|
16756
16862
|
}
|
|
16757
16863
|
return storageInfo(body.storage);
|
|
16758
16864
|
}
|
|
@@ -16911,6 +17017,114 @@ var StorageApi = class {
|
|
|
16911
17017
|
return body.objects;
|
|
16912
17018
|
}
|
|
16913
17019
|
};
|
|
17020
|
+
var RealtimeApi = class {
|
|
17021
|
+
constructor(client) {
|
|
17022
|
+
this.client = client;
|
|
17023
|
+
}
|
|
17024
|
+
path(projectId, suffix = "") {
|
|
17025
|
+
return `/api/projects/${encodeURIComponent(projectId)}/backend/realtime${suffix}`;
|
|
17026
|
+
}
|
|
17027
|
+
/**
|
|
17028
|
+
* Like `StorageApi.bucketPath`: URL parsing folds `.` and `..`, an empty
|
|
17029
|
+
* name is the collection, and `encodeURIComponent` throws on a lone
|
|
17030
|
+
* surrogate — so a name no URL can carry is refused here, before a
|
|
17031
|
+
* request. SQLite does make tables named `.` and `..`, and one with the
|
|
17032
|
+
* empty name (`CREATE TABLE "" (a)`); such a table cannot be published
|
|
17033
|
+
* or unpublished through this API.
|
|
17034
|
+
*/
|
|
17035
|
+
tablePath(projectId, table) {
|
|
17036
|
+
if (table === "" || table === "." || table === ".." || LONE_SURROGATE.test(table))
|
|
17037
|
+
throw new NaroError("INVALID_PATH", "The table name is not valid.");
|
|
17038
|
+
return this.path(projectId, `/tables/${encodeURIComponent(table)}`);
|
|
17039
|
+
}
|
|
17040
|
+
/**
|
|
17041
|
+
* Whether Realtime is on, the limits the hub enforces, the published
|
|
17042
|
+
* tables and the hub's own numbers. A project that never turned it on
|
|
17043
|
+
* answers `enabled: false` with no tables.
|
|
17044
|
+
*/
|
|
17045
|
+
async info(projectId) {
|
|
17046
|
+
const body = await this.client.request(
|
|
17047
|
+
this.path(projectId),
|
|
17048
|
+
{ context: "Read realtime" }
|
|
17049
|
+
);
|
|
17050
|
+
return body.realtime;
|
|
17051
|
+
}
|
|
17052
|
+
/**
|
|
17053
|
+
* Turns Realtime on: the request and the plan's limits are recorded, and
|
|
17054
|
+
* the Naro worker is uploaded again with its hub (a Durable Object). Safe
|
|
17055
|
+
* to repeat, and a repair — also after a plan change. The project must have
|
|
17056
|
+
* a backend, turned on, with its database. A backend in `error` or with a
|
|
17057
|
+
* run that stalled is not refused: its provisioning is resumed on the way.
|
|
17058
|
+
* Two minutes, like `backend.enable`.
|
|
17059
|
+
*
|
|
17060
|
+
* Refusals, in the order they are checked: `BACKEND_DISABLED` (the control
|
|
17061
|
+
* plane's backend switch), `FORBIDDEN` (`database.write`),
|
|
17062
|
+
* `REALTIME_DISABLED` (the control plane's Realtime switch),
|
|
17063
|
+
* `DATABASE_NOT_ENABLED` (the project's database is external),
|
|
17064
|
+
* `BACKEND_NOT_READY` (409, only when the project has no backend at all),
|
|
17065
|
+
* `BACKEND_OFF`, `BACKEND_BUSY`, `REALTIME_ENABLE_FAILED` (502: the request
|
|
17066
|
+
* could not be recorded; nothing changed), and `BACKEND_PROVISION_FAILED`
|
|
17067
|
+
* (502) when Realtime is recorded as on but the worker's upload stopped.
|
|
17068
|
+
* The last one's `details` hold a `RealtimeProvisionFailure` — `{ realtime:
|
|
17069
|
+
* { applied: false, enabled: true }, backend }`, the backend read like
|
|
17070
|
+
* `backend.info` — and `enable` again finishes.
|
|
17071
|
+
*/
|
|
17072
|
+
async enable(projectId) {
|
|
17073
|
+
let body;
|
|
17074
|
+
try {
|
|
17075
|
+
body = await this.client.request(
|
|
17076
|
+
this.path(projectId),
|
|
17077
|
+
{
|
|
17078
|
+
method: "POST",
|
|
17079
|
+
context: "Turn on Realtime",
|
|
17080
|
+
timeoutMs: PROVISION_TIMEOUT_MS
|
|
17081
|
+
}
|
|
17082
|
+
);
|
|
17083
|
+
} catch (error51) {
|
|
17084
|
+
throw provisionFailure(error51, "realtime");
|
|
17085
|
+
}
|
|
17086
|
+
return body.realtime;
|
|
17087
|
+
}
|
|
17088
|
+
/**
|
|
17089
|
+
* The tables whose Data API writes are published, in the order they were
|
|
17090
|
+
* published. `exists: false` is a published name whose table was dropped.
|
|
17091
|
+
* `REALTIME_NOT_ENABLED` (409) before `enable`; it answers while the
|
|
17092
|
+
* backend is off, since the list is in the project's database.
|
|
17093
|
+
*/
|
|
17094
|
+
async listTables(projectId) {
|
|
17095
|
+
const body = await this.client.request(
|
|
17096
|
+
this.path(projectId, "/tables"),
|
|
17097
|
+
{ context: "List realtime tables" }
|
|
17098
|
+
);
|
|
17099
|
+
return body.tables;
|
|
17100
|
+
}
|
|
17101
|
+
/**
|
|
17102
|
+
* Publishes a table's changes to the subscribers of its channel. Safe to
|
|
17103
|
+
* repeat. Only changes made through the Data API are published — writes
|
|
17104
|
+
* the app makes with `env.DB` are not (`warnings` says so).
|
|
17105
|
+
* `TABLE_NOT_FOUND` (404) for a table that does not exist;
|
|
17106
|
+
* `VALIDATION_FAILED` (400) for a Naro system table, a virtual table, a
|
|
17107
|
+
* name over 256 characters, or the 100-table cap; `REALTIME_NOT_ENABLED`
|
|
17108
|
+
* (409) before `enable`.
|
|
17109
|
+
*/
|
|
17110
|
+
async publishTable(projectId, table) {
|
|
17111
|
+
return this.client.request(
|
|
17112
|
+
this.tablePath(projectId, table),
|
|
17113
|
+
{ method: "PUT", context: "Publish a realtime table" }
|
|
17114
|
+
);
|
|
17115
|
+
}
|
|
17116
|
+
/**
|
|
17117
|
+
* Stops publishing a table's changes; a table dropped since can still be
|
|
17118
|
+
* unpublished. `TABLE_NOT_PUBLISHED` (404) for a table that does not
|
|
17119
|
+
* publish, `REALTIME_NOT_ENABLED` (409) before `enable`.
|
|
17120
|
+
*/
|
|
17121
|
+
async unpublishTable(projectId, table) {
|
|
17122
|
+
return this.client.request(
|
|
17123
|
+
this.tablePath(projectId, table),
|
|
17124
|
+
{ method: "DELETE", context: "Stop publishing a realtime table" }
|
|
17125
|
+
);
|
|
17126
|
+
}
|
|
17127
|
+
};
|
|
16914
17128
|
|
|
16915
17129
|
// ../../packages/sdk/src/database.ts
|
|
16916
17130
|
var BOOKMARK_RE = /^[0-9a-f]{8}-[0-9a-f]{8}-[0-9a-f]{8}-[0-9a-f]{32}$/;
|
|
@@ -16937,6 +17151,39 @@ var MAX_FILE_BYTES2 = 1024 * 1024;
|
|
|
16937
17151
|
import { existsSync as existsSync3, readFileSync as readFileSync4 } from "fs";
|
|
16938
17152
|
import { join as join4 } from "path";
|
|
16939
17153
|
|
|
17154
|
+
// ../../packages/data-core/src/system-tables.ts
|
|
17155
|
+
var SYSTEM_TABLE_PREFIXES = [
|
|
17156
|
+
"_naro_",
|
|
17157
|
+
"_cf_",
|
|
17158
|
+
"sqlite_"
|
|
17159
|
+
];
|
|
17160
|
+
var SYSTEM_TABLE_NAMES = ["d1_migrations"];
|
|
17161
|
+
function asciiLower(text) {
|
|
17162
|
+
return text.replace(/[A-Z]+/g, (run) => run.toLowerCase());
|
|
17163
|
+
}
|
|
17164
|
+
function isSystemTable(name) {
|
|
17165
|
+
const lower = asciiLower(name);
|
|
17166
|
+
return SYSTEM_TABLE_PREFIXES.some((prefix) => lower.startsWith(prefix)) || SYSTEM_TABLE_NAMES.includes(lower);
|
|
17167
|
+
}
|
|
17168
|
+
function likeLiteral(text) {
|
|
17169
|
+
return text.replace(/[\\%_]/g, (char) => `\\${char}`).replace(/'/g, "''");
|
|
17170
|
+
}
|
|
17171
|
+
var COLUMN_REFERENCE = /^[A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*)?$/;
|
|
17172
|
+
function systemTablePredicate(column = "name") {
|
|
17173
|
+
if (!COLUMN_REFERENCE.test(column))
|
|
17174
|
+
throw new Error(`Not a column reference: ${column}`);
|
|
17175
|
+
return [
|
|
17176
|
+
...SYSTEM_TABLE_PREFIXES.map(
|
|
17177
|
+
(prefix) => `${column} NOT LIKE '${likeLiteral(prefix)}%' ESCAPE '\\'`
|
|
17178
|
+
),
|
|
17179
|
+
...SYSTEM_TABLE_NAMES.map(
|
|
17180
|
+
(name) => `${column} NOT LIKE '${likeLiteral(name)}' ESCAPE '\\'`
|
|
17181
|
+
),
|
|
17182
|
+
`${column} NOT IN (SELECT name FROM pragma_table_list WHERE schema = 'main' AND type = 'shadow')`
|
|
17183
|
+
].join(" AND ");
|
|
17184
|
+
}
|
|
17185
|
+
var SYSTEM_TABLE_SQL_PREDICATE = systemTablePredicate("name");
|
|
17186
|
+
|
|
16940
17187
|
// ../../packages/data-core/src/storage-path.ts
|
|
16941
17188
|
var STORAGE_RESERVED_WORDS = [
|
|
16942
17189
|
"authenticated",
|
|
@@ -17014,39 +17261,6 @@ var STORAGE_BUCKET_DELETING_COLUMN = {
|
|
|
17014
17261
|
definition: "TEXT"
|
|
17015
17262
|
};
|
|
17016
17263
|
|
|
17017
|
-
// ../../packages/data-core/src/system-tables.ts
|
|
17018
|
-
var SYSTEM_TABLE_PREFIXES = [
|
|
17019
|
-
"_naro_",
|
|
17020
|
-
"_cf_",
|
|
17021
|
-
"sqlite_"
|
|
17022
|
-
];
|
|
17023
|
-
var SYSTEM_TABLE_NAMES = ["d1_migrations"];
|
|
17024
|
-
function asciiLower(text) {
|
|
17025
|
-
return text.replace(/[A-Z]+/g, (run) => run.toLowerCase());
|
|
17026
|
-
}
|
|
17027
|
-
function isSystemTable(name) {
|
|
17028
|
-
const lower = asciiLower(name);
|
|
17029
|
-
return SYSTEM_TABLE_PREFIXES.some((prefix) => lower.startsWith(prefix)) || SYSTEM_TABLE_NAMES.includes(lower);
|
|
17030
|
-
}
|
|
17031
|
-
function likeLiteral(text) {
|
|
17032
|
-
return text.replace(/[\\%_]/g, (char) => `\\${char}`).replace(/'/g, "''");
|
|
17033
|
-
}
|
|
17034
|
-
var COLUMN_REFERENCE = /^[A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*)?$/;
|
|
17035
|
-
function systemTablePredicate(column = "name") {
|
|
17036
|
-
if (!COLUMN_REFERENCE.test(column))
|
|
17037
|
-
throw new Error(`Not a column reference: ${column}`);
|
|
17038
|
-
return [
|
|
17039
|
-
...SYSTEM_TABLE_PREFIXES.map(
|
|
17040
|
-
(prefix) => `${column} NOT LIKE '${likeLiteral(prefix)}%' ESCAPE '\\'`
|
|
17041
|
-
),
|
|
17042
|
-
...SYSTEM_TABLE_NAMES.map(
|
|
17043
|
-
(name) => `${column} NOT LIKE '${likeLiteral(name)}' ESCAPE '\\'`
|
|
17044
|
-
),
|
|
17045
|
-
`${column} NOT IN (SELECT name FROM pragma_table_list WHERE schema = 'main' AND type = 'shadow')`
|
|
17046
|
-
].join(" AND ");
|
|
17047
|
-
}
|
|
17048
|
-
var SYSTEM_TABLE_SQL_PREDICATE = systemTablePredicate("name");
|
|
17049
|
-
|
|
17050
17264
|
// ../../packages/data-core/src/errors.ts
|
|
17051
17265
|
var STORAGE_MIGRATION_PENDING = new RegExp(
|
|
17052
17266
|
`^no such column: (?:b\\.|${STORAGE_BUCKET_DELETING_COLUMN.table}\\.)?${STORAGE_BUCKET_DELETING_COLUMN.column}\\b`
|
|
@@ -19292,8 +19506,34 @@ var MAX_STATEMENT_BYTES = 1e5;
|
|
|
19292
19506
|
// ../../packages/data-core/src/policy-index.ts
|
|
19293
19507
|
var INDEX_SQL = `SELECT t.name AS table_name, il.name AS index_name, il."partial" AS partial, ix.name AS column_name, ix.coll AS collation FROM pragma_table_list AS t JOIN pragma_index_list(t.name, t.schema) AS il JOIN pragma_index_xinfo(il.name, t.schema) AS ix WHERE t.schema = 'main' AND t.type = 'table' AND ${systemTablePredicate("t.name")} AND ix.seqno = 0`;
|
|
19294
19508
|
|
|
19509
|
+
// ../../packages/data-core/src/realtime-filter.ts
|
|
19510
|
+
var FILTER_REASONS = {
|
|
19511
|
+
bytes: "must be a string of up to 1024 bytes in UTF-8",
|
|
19512
|
+
control: "must have no control character",
|
|
19513
|
+
syntax: "supports eq, neq, lt, lte, gt, gte and in, one condition",
|
|
19514
|
+
list: "needs in.(a,b) with 1 to 100 values"
|
|
19515
|
+
};
|
|
19516
|
+
|
|
19295
19517
|
// ../../packages/data-core/src/realtime-protocol.ts
|
|
19296
19518
|
var REALTIME_PROTOCOL_VERSION = "1.0.0";
|
|
19519
|
+
var MAX_CHANGE_CHANNELS_PER_TABLE = 4;
|
|
19520
|
+
var JOIN_CONFIG_REASONS = {
|
|
19521
|
+
object: "must be an object",
|
|
19522
|
+
unsupported: "is not supported",
|
|
19523
|
+
boolean: "must be true or false",
|
|
19524
|
+
presenceKey: "must be 1 to 128 characters",
|
|
19525
|
+
changes: "must be an array of up to 16 subscriptions",
|
|
19526
|
+
changeEvent: "must be *, INSERT, UPDATE or DELETE",
|
|
19527
|
+
schema: "must be public",
|
|
19528
|
+
table: "must be a table name of 1 to 128 characters, or *",
|
|
19529
|
+
filter: FILTER_REASONS.bytes,
|
|
19530
|
+
filterControl: FILTER_REASONS.control,
|
|
19531
|
+
filterSyntax: FILTER_REASONS.syntax,
|
|
19532
|
+
filterList: FILTER_REASONS.list,
|
|
19533
|
+
filterTable: "needs a table: a filter cannot go with *",
|
|
19534
|
+
/** The hub's, after `config.postgres_changes[<i>].table` (3-2c-1): `MAX_CHANGE_CHANNELS_PER_TABLE` against the connection's joins. */
|
|
19535
|
+
changeChannels: `would put more than ${String(MAX_CHANGE_CHANNELS_PER_TABLE)} channels of this connection on one table's changes (* counts for every table): add the subscription to a channel it already has`
|
|
19536
|
+
};
|
|
19297
19537
|
var REALTIME_CLOSE = {
|
|
19298
19538
|
UNSUPPORTED_PROTOCOL: {
|
|
19299
19539
|
code: 4e3,
|
|
@@ -19382,6 +19622,12 @@ var LIVE_BUCKET = `${STORAGE_BUCKET_DELETING_COLUMN.column} IS NULL`;
|
|
|
19382
19622
|
var STORAGE_PROBE_NONCE = "f".repeat(STORAGE_NONCE_MAX_DIGITS);
|
|
19383
19623
|
var REALTIME_PROBE_NONCE = "f".repeat(64);
|
|
19384
19624
|
|
|
19625
|
+
// ../../packages/data-core/src/realtime-publication.ts
|
|
19626
|
+
var REALTIME_TABLES = "_naro_realtime_tables";
|
|
19627
|
+
var REALTIME_TABLES_SQL = `CREATE TABLE IF NOT EXISTS ${REALTIME_TABLES} (table_name TEXT NOT NULL PRIMARY KEY COLLATE NOCASE, created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')))`;
|
|
19628
|
+
var MAX_PUBLISHED_TABLES = 100;
|
|
19629
|
+
var PUBLISHED_TABLES_SQL = `SELECT table_name FROM ${REALTIME_TABLES} ORDER BY rowid LIMIT ${String(MAX_PUBLISHED_TABLES)}`;
|
|
19630
|
+
|
|
19385
19631
|
// ../../packages/data-core/src/system-sql.ts
|
|
19386
19632
|
async function convertLegacyPolicies(db, found) {
|
|
19387
19633
|
const rows = rowsOf(await db.prepare(POLICY_SQL.list).all());
|
|
@@ -19415,6 +19661,7 @@ var MIGRATIONS_TABLE = "CREATE TABLE IF NOT EXISTS _naro_migrations (version INT
|
|
|
19415
19661
|
var STORAGE_SUBQUERIES_VERSION = 7;
|
|
19416
19662
|
var STORAGE_SUBQUERIES_NAME = "storage_subquery_policies";
|
|
19417
19663
|
var STORAGE_SUBQUERIES_SQL = `SELECT version FROM _naro_migrations WHERE version = ${String(STORAGE_SUBQUERIES_VERSION)} AND name = '${STORAGE_SUBQUERIES_NAME}'`;
|
|
19664
|
+
var REALTIME_TABLES_VERSION = 8;
|
|
19418
19665
|
var SYSTEM_MIGRATIONS = [
|
|
19419
19666
|
{
|
|
19420
19667
|
version: 1,
|
|
@@ -19503,6 +19750,14 @@ var SYSTEM_MIGRATIONS = [
|
|
|
19503
19750
|
version: STORAGE_SUBQUERIES_VERSION,
|
|
19504
19751
|
name: STORAGE_SUBQUERIES_NAME,
|
|
19505
19752
|
statements: []
|
|
19753
|
+
},
|
|
19754
|
+
{
|
|
19755
|
+
// Realtime 3-2b (original spec §9.1, design §7.1): the tables whose
|
|
19756
|
+
// Data API writes reach the hub. Empty until the publish API (3-2c)
|
|
19757
|
+
// writes it. A worker from before it never reads it.
|
|
19758
|
+
version: REALTIME_TABLES_VERSION,
|
|
19759
|
+
name: "realtime",
|
|
19760
|
+
statements: [REALTIME_TABLES_SQL]
|
|
19506
19761
|
}
|
|
19507
19762
|
];
|
|
19508
19763
|
|
|
@@ -19544,6 +19799,10 @@ var ENTERPRISE_REALTIME_LIMITS = Object.freeze({
|
|
|
19544
19799
|
restRequestsPerSecond: 100
|
|
19545
19800
|
});
|
|
19546
19801
|
|
|
19802
|
+
// ../../packages/data-core/src/realtime-changes.ts
|
|
19803
|
+
var CHANGE_TYPES = Object.freeze({ insert: "INSERT", update: "UPDATE", delete: "DELETE" });
|
|
19804
|
+
var CHANGE_EVENT_CODES = Object.freeze({ "*": 0, INSERT: 1, UPDATE: 2, DELETE: 3 });
|
|
19805
|
+
|
|
19547
19806
|
// ../../packages/data-core/src/storage-multipart.ts
|
|
19548
19807
|
var MiB = 1024 * 1024;
|
|
19549
19808
|
var MULTIPART_PART_SIZES = [16 * MiB, 32 * MiB, 64 * MiB];
|
|
@@ -20630,7 +20889,7 @@ var DATABASE_TOOLS = [
|
|
|
20630
20889
|
destructiveHint: true,
|
|
20631
20890
|
idempotentHint: false
|
|
20632
20891
|
},
|
|
20633
|
-
description: `${DESTRUCTIVE}executes raw SQL in the D1 database \u2014 it can change or delete data. Use \`apply_migration\` instead for DDL operations. ${UNTRUSTED} Pass values through params (bound to ? placeholders) instead of building them into the SQL text; params work with a single statement only. Several statements in one call run as one transaction: if one fails, none of them is applied. CREATE/DROP POLICY work here too (see \`apply_migration\`; they take no params), and result.warnings, when present, says what a policy needs. ${SHARED_DATABASE} ${LIMITS} ${SINGLE_THREADED}`,
|
|
20892
|
+
description: `${DESTRUCTIVE}executes raw SQL in the D1 database \u2014 it can change or delete data. Use \`apply_migration\` instead for DDL operations. ${UNTRUSTED} Pass values through params (bound to ? placeholders) instead of building them into the SQL text; params work with a single statement only. Several statements in one call run as one transaction: if one fails, none of them is applied. CREATE/DROP POLICY work here too (see \`apply_migration\`; they take no params), and result.warnings, when present, says what a policy needs. An ALTER TABLE that adds a generated column computes it for the rows already there, as in \`apply_migration\`, and takes no params either. ${SHARED_DATABASE} ${LIMITS} ${SINGLE_THREADED}`,
|
|
20634
20893
|
readOnlyVariant: {
|
|
20635
20894
|
description: `Executes a single read-only SQL statement in the D1 database. This server runs with --read-only: writes, DDL and multi-statement batches are refused with DATABASE_READ_ONLY. ${UNTRUSTED} Pass values through params (bound to ? placeholders) instead of building them into the SQL text. ${SHARED_DATABASE} ${LIMITS} ${SINGLE_THREADED}`,
|
|
20636
20895
|
annotations: {
|
|
@@ -20691,7 +20950,7 @@ var DATABASE_TOOLS = [
|
|
|
20691
20950
|
destructiveHint: true,
|
|
20692
20951
|
idempotentHint: false
|
|
20693
20952
|
},
|
|
20694
|
-
description: `${DESTRUCTIVE}applies a migration (DDL) to the D1 database and records it in the d1_migrations ledger \u2014 the same ledger wrangler uses. \`name\` is a short snake_case title; naro numbers it (NNNN_name.sql) and, when the project directory is known, writes the file to ./migrations so \`naro db push\` sees it as applied. All-or-nothing: if a statement fails, the whole migration is rolled back and not recorded. Write CREATE POLICY in the same migration as the table, as in Supabase: it is applied with the table, all or nothing. A table without a policy refuses every anon and signed-in request to the Data API. DROP POLICY [IF EXISTS], COMMENT ON POLICY p ON t IS 'naro:owner <column>' and ALTER TABLE t ENABLE ROW LEVEL SECURITY are accepted too; DISABLE ROW LEVEL SECURITY, GRANT, REVOKE and ALTER POLICY are refused before anything runs. Dropping a table drops its policies, so a SQLite table rebuild (create __new_x, copy, drop x, rename) must create the policies again after the rename: warnings lists the ones it removed, with their SQL. ALTER TABLE a RENAME TO b moves a's policies to b and rewrites the policies of other tables that read a, as in Postgres: do not create them again on b, where one of the same name is refused. ALTER TABLE t RENAME COLUMN c TO d rewrites every policy that reads t.c, an owner rule's column included, to read t.d; a rename such a policy cannot follow is refused before anything runs. ${UNTRUSTED} ${SHARED_DATABASE} ${LIMITS}`,
|
|
20953
|
+
description: `${DESTRUCTIVE}applies a migration (DDL) to the D1 database and records it in the d1_migrations ledger \u2014 the same ledger wrangler uses. \`name\` is a short snake_case title; naro numbers it (NNNN_name.sql) and, when the project directory is known, writes the file to ./migrations so \`naro db push\` sees it as applied. All-or-nothing: if a statement fails, the whole migration is rolled back and not recorded. Write CREATE POLICY in the same migration as the table, as in Supabase: it is applied with the table, all or nothing. A table without a policy refuses every anon and signed-in request to the Data API. DROP POLICY [IF EXISTS], COMMENT ON POLICY p ON t IS 'naro:owner <column>' and ALTER TABLE t ENABLE ROW LEVEL SECURITY are accepted too; DISABLE ROW LEVEL SECURITY, GRANT, REVOKE and ALTER POLICY are refused before anything runs. Dropping a table drops its policies, so a SQLite table rebuild (create __new_x, copy, drop x, rename) must create the policies again after the rename: warnings lists the ones it removed, with their SQL. ALTER TABLE a RENAME TO b moves a's policies to b and rewrites the policies of other tables that read a, as in Postgres: do not create them again on b, where one of the same name is refused. ALTER TABLE t RENAME COLUMN c TO d rewrites every policy that reads t.c, an owner rule's column included, to read t.d; a rename such a policy cannot follow is refused before anything runs. ALTER TABLE t ADD COLUMN c \u2026 AS (\u2026) computes the generated column c for the rows already in t, as Postgres does: a row it cannot be computed for (json_extract over text that is not JSON) fails the whole migration with SQLite's error (malformed JSON) and nothing is applied, so fix those rows in an earlier statement. ${UNTRUSTED} ${SHARED_DATABASE} ${LIMITS}`,
|
|
20695
20954
|
inputSchema: {
|
|
20696
20955
|
type: "object",
|
|
20697
20956
|
properties: {
|
|
@@ -21593,7 +21852,7 @@ var STORAGE_TOOLS = [
|
|
|
21593
21852
|
},
|
|
21594
21853
|
untrusted: true,
|
|
21595
21854
|
untrustedSubject: "the Storage query",
|
|
21596
|
-
description: `Reads the project's Naro Storage: whether it is on, its R2 bucket, the limits its backend enforces (bytes in all, bytes per object, 50 MiB per request; larger files go in parts through naro-js, and through naro storage cp under 4 GiB), what is stored now (usage is null while the backend is not ready), ${STORAGE_METER}, and every bucket with its objects, bytes, public flag and limits. Who may read and write objects is decided by CREATE POLICY \u2026 ON storage.objects in a migration (apply_migration), as in Supabase. ${UNTRUSTED_BUCKETS}`,
|
|
21855
|
+
description: `Reads the project's Naro Storage: whether it is on, its R2 bucket, the limits its backend enforces (bytes in all, bytes per object, 50 MiB per request; larger files go in parts through naro-js, and through naro storage cp under 4 GiB), what is stored now (usage is null while the backend is not ready), disabledAt \u2014 when Storage was turned off with its files kept (then enabled is false, and enable_storage turns it on again only when the user asks) \u2014 ${STORAGE_METER}, and every bucket with its objects, bytes, public flag and limits. Who may read and write objects is decided by CREATE POLICY \u2026 ON storage.objects in a migration (apply_migration), as in Supabase. ${UNTRUSTED_BUCKETS}`,
|
|
21597
21856
|
inputSchema: { type: "object", properties: { ...PROJECT_ARGS } },
|
|
21598
21857
|
handler: async (args, deps) => {
|
|
21599
21858
|
const projectId = await projectOf(args, deps);
|
|
@@ -21613,7 +21872,7 @@ var STORAGE_TOOLS = [
|
|
|
21613
21872
|
},
|
|
21614
21873
|
untrusted: true,
|
|
21615
21874
|
untrustedSubject: "the Storage query",
|
|
21616
|
-
description: `Turns Naro Storage on for the project: makes its R2 bucket and uploads the backend again with it, which can take up to 2 minutes. The project's backend must be on (BACKEND_NOT_READY or BACKEND_OFF otherwise). Safe to call again: it resumes a run that stopped and applies a plan change's limits. STORAGE_DISABLED means Storage is not switched on for the control plane yet. It answers the Storage state as get_storage does, with ${STORAGE_METER}. ${UNTRUSTED_BUCKETS}`,
|
|
21875
|
+
description: `Turns Naro Storage on for the project: makes its R2 bucket and uploads the backend again with it, which can take up to 2 minutes. The project's backend must be on (BACKEND_NOT_READY or BACKEND_OFF otherwise). Turned off with disable_storage, it binds the same bucket again and the files come back. Safe to call again: it resumes a run that stopped and applies a plan change's limits. Storage that is off (disabledAt) was turned off on purpose: turn it on again only when the user asks. STORAGE_DISABLED means Storage is not switched on for the control plane yet. It answers the Storage state as get_storage does, with ${STORAGE_METER}. ${UNTRUSTED_BUCKETS}`,
|
|
21617
21876
|
inputSchema: { type: "object", properties: { ...PROJECT_ARGS } },
|
|
21618
21877
|
handler: async (args, deps) => {
|
|
21619
21878
|
const projectId = await projectOf(args, deps);
|
|
@@ -21623,6 +21882,40 @@ var STORAGE_TOOLS = [
|
|
|
21623
21882
|
};
|
|
21624
21883
|
}
|
|
21625
21884
|
},
|
|
21885
|
+
{
|
|
21886
|
+
name: "disable_storage",
|
|
21887
|
+
group: "storage",
|
|
21888
|
+
annotations: {
|
|
21889
|
+
readOnlyHint: false,
|
|
21890
|
+
destructiveHint: true,
|
|
21891
|
+
idempotentHint: true
|
|
21892
|
+
},
|
|
21893
|
+
untrusted: true,
|
|
21894
|
+
untrustedSubject: "the Storage query",
|
|
21895
|
+
description: `${DESTRUCTIVE}turns Naro Storage off for the project and keeps its files: the backend is uploaded again without its R2 bucket, which can take up to 2 minutes, and from then on the app's Storage calls and file URLs answer 404 until enable_storage turns it on again with the same bucket. The buckets and files stay, and so does their storage cost; while it is off, bucket writes and the file tools answer STORAGE_OFF and get_storage shows disabledAt. To delete the files instead, call delete_storage_bucket with empty: true while Storage is still on. Needs confirm set to the project's Storage bucket as get_storage shows it (naro-storage-<projectId>) \u2014 confirm with the user first. Storage that was never turned on is answered as it is; calling it on Storage that is already off finishes a turn-off that stopped. The project's backend must be on (BACKEND_OFF otherwise). It answers the Storage state as get_storage does. ${UNTRUSTED_BUCKETS}`,
|
|
21896
|
+
inputSchema: {
|
|
21897
|
+
type: "object",
|
|
21898
|
+
properties: {
|
|
21899
|
+
...PROJECT_ARGS,
|
|
21900
|
+
confirm: {
|
|
21901
|
+
type: "string",
|
|
21902
|
+
description: "The project's Storage bucket, as get_storage shows it (naro-storage-<projectId>). Needed while Storage is on."
|
|
21903
|
+
}
|
|
21904
|
+
}
|
|
21905
|
+
},
|
|
21906
|
+
handler: async (args, deps) => {
|
|
21907
|
+
const projectId = await projectOf(args, deps);
|
|
21908
|
+
const client = await deps.client();
|
|
21909
|
+
const current = await client.storage.info(projectId);
|
|
21910
|
+
if (current.bucket === null) return { projectId, storage: current };
|
|
21911
|
+
if (current.disabledAt === null && args.confirm !== current.bucket)
|
|
21912
|
+
throw new NaroError(
|
|
21913
|
+
"CONFIRMATION_REQUIRED",
|
|
21914
|
+
`disable_storage needs confirm set to the project's Storage bucket, ${current.bucket}.`
|
|
21915
|
+
);
|
|
21916
|
+
return { projectId, storage: await client.storage.disable(projectId) };
|
|
21917
|
+
}
|
|
21918
|
+
},
|
|
21626
21919
|
{
|
|
21627
21920
|
name: "list_storage_buckets",
|
|
21628
21921
|
group: "storage",
|
|
@@ -21653,7 +21946,7 @@ var STORAGE_TOOLS = [
|
|
|
21653
21946
|
},
|
|
21654
21947
|
untrusted: true,
|
|
21655
21948
|
untrustedSubject: "the Storage query",
|
|
21656
|
-
description: `Creates a Storage bucket, optionally public, with a largest object and the types it takes. The name cannot change later and cannot be authenticated, copy, info, list, move, public, render, sign or upload; a project has up to 100 buckets. STORAGE_NOT_ENABLED means Storage
|
|
21949
|
+
description: `Creates a Storage bucket, optionally public, with a largest object and the types it takes. The name cannot change later and cannot be authenticated, copy, info, list, move, public, render, sign or upload; a project has up to 100 buckets. STORAGE_NOT_ENABLED means Storage was never turned on: call enable_storage first. STORAGE_OFF means the user turned Storage off on purpose (disable_storage): tell the user, and call enable_storage only if they ask to turn it back on. Objects go in with naro-js or naro storage cp, under the storage.objects policies. It answers the bucket as stored. ${UNTRUSTED_BUCKETS}`,
|
|
21657
21950
|
inputSchema: {
|
|
21658
21951
|
type: "object",
|
|
21659
21952
|
properties: { ...PROJECT_ARGS, name: NAME, ...FIELDS },
|
|
@@ -21984,6 +22277,18 @@ function envTarget(value) {
|
|
|
21984
22277
|
`Invalid target ${JSON.stringify(value)} \u2014 use production, preview, all or an environment name.`
|
|
21985
22278
|
);
|
|
21986
22279
|
}
|
|
22280
|
+
var REBUILD_TIMEOUT_MS = 15 * 6e4;
|
|
22281
|
+
var REUPLOAD_TIMEOUT_MS = 5 * 6e4;
|
|
22282
|
+
function promoteTarget(value) {
|
|
22283
|
+
if (value === void 0 || value === null) return void 0;
|
|
22284
|
+
const name = typeof value === "string" ? value.trim() : "";
|
|
22285
|
+
if (name === "" || name === "preview")
|
|
22286
|
+
throw new NaroError(
|
|
22287
|
+
"VALIDATION_FAILED",
|
|
22288
|
+
"target must be production or an environment name."
|
|
22289
|
+
);
|
|
22290
|
+
return name;
|
|
22291
|
+
}
|
|
21987
22292
|
var REGISTERED = [
|
|
21988
22293
|
{
|
|
21989
22294
|
name: "whoami",
|
|
@@ -22607,7 +22912,7 @@ var REGISTERED = [
|
|
|
22607
22912
|
destructiveHint: true,
|
|
22608
22913
|
idempotentHint: false
|
|
22609
22914
|
},
|
|
22610
|
-
description: `${DESTRUCTIVE}publishes an existing READY deployment (e.g. a preview build) to production
|
|
22915
|
+
description: `${DESTRUCTIVE}publishes an existing READY deployment (e.g. a preview build). Without target it goes to production, and production traffic switches to it once READY; with target set to an environment's name (list_environments) it goes to that environment instead. The result's mode says how it got its build: reupload publishes the stored build again, rebuild builds the same commit again with the target's variables (a promotion between surfaces when one of them is an environment, such as preview \u2192 qa or qa \u2192 production) and only works for a deployment built from Git (PROMOTE_NEEDS_GIT_SOURCE otherwise). target in the result is null when it was left out. If the call times out on the client side, the promotion keeps going: look it up with list_deployments or get_deployment. A control plane that predates promote targets answers PROMOTE_TARGET_UNSUPPORTED and promotes nothing: ask the user to deploy the latest web first.`,
|
|
22611
22916
|
inputSchema: {
|
|
22612
22917
|
type: "object",
|
|
22613
22918
|
properties: {
|
|
@@ -22615,6 +22920,10 @@ var REGISTERED = [
|
|
|
22615
22920
|
deploymentId: {
|
|
22616
22921
|
type: "string",
|
|
22617
22922
|
description: "The READY deployment to promote."
|
|
22923
|
+
},
|
|
22924
|
+
target: {
|
|
22925
|
+
type: "string",
|
|
22926
|
+
description: `Where to promote: production (the default when omitted; switches production traffic to the deployment), or an environment name such as qa (list_environments); preview is not a target. Promoting between surfaces when one of them is an environment (preview \u2192 qa, qa \u2192 production) rebuilds the same commit with the target surface's variables; a rebuild takes up to 15 minutes.`
|
|
22618
22927
|
}
|
|
22619
22928
|
},
|
|
22620
22929
|
required: ["deploymentId"]
|
|
@@ -22623,6 +22932,7 @@ var REGISTERED = [
|
|
|
22623
22932
|
const deploymentId = str(args.deploymentId);
|
|
22624
22933
|
if (!deploymentId)
|
|
22625
22934
|
throw new NaroError("VALIDATION_FAILED", "deploymentId is required.");
|
|
22935
|
+
const target = promoteTarget(args.target);
|
|
22626
22936
|
const projectId = await deps.resolveProjectId({
|
|
22627
22937
|
projectId: str(args.projectId),
|
|
22628
22938
|
directory: str(args.directory)
|
|
@@ -22630,17 +22940,20 @@ var REGISTERED = [
|
|
|
22630
22940
|
const client = await deps.client();
|
|
22631
22941
|
const promoted = await client.deployments.promote(
|
|
22632
22942
|
projectId,
|
|
22633
|
-
deploymentId
|
|
22943
|
+
deploymentId,
|
|
22944
|
+
target !== void 0 ? { target } : void 0
|
|
22634
22945
|
);
|
|
22635
22946
|
const detail = await client.deployments.wait(promoted.id, {
|
|
22636
|
-
timeoutMs:
|
|
22947
|
+
timeoutMs: promoted.mode === "rebuild" ? REBUILD_TIMEOUT_MS : REUPLOAD_TIMEOUT_MS
|
|
22637
22948
|
});
|
|
22638
22949
|
return {
|
|
22639
22950
|
deploymentId: promoted.id,
|
|
22640
22951
|
sourceDeploymentId: deploymentId,
|
|
22641
22952
|
status: detail.deployment.status,
|
|
22642
22953
|
url: detail.deployment.url,
|
|
22643
|
-
errorCode: detail.deployment.errorCode
|
|
22954
|
+
errorCode: detail.deployment.errorCode,
|
|
22955
|
+
target: target ?? null,
|
|
22956
|
+
mode: promoted.mode
|
|
22644
22957
|
};
|
|
22645
22958
|
}
|
|
22646
22959
|
},
|
|
@@ -22687,14 +23000,15 @@ var REGISTERED = [
|
|
|
22687
23000
|
}
|
|
22688
23001
|
const promoted = await client.deployments.promote(projectId, target);
|
|
22689
23002
|
const detail = await client.deployments.wait(promoted.id, {
|
|
22690
|
-
timeoutMs:
|
|
23003
|
+
timeoutMs: promoted.mode === "rebuild" ? REBUILD_TIMEOUT_MS : REUPLOAD_TIMEOUT_MS
|
|
22691
23004
|
});
|
|
22692
23005
|
return {
|
|
22693
23006
|
deploymentId: promoted.id,
|
|
22694
23007
|
rolledBackTo: target,
|
|
22695
23008
|
status: detail.deployment.status,
|
|
22696
23009
|
url: detail.deployment.url,
|
|
22697
|
-
errorCode: detail.deployment.errorCode
|
|
23010
|
+
errorCode: detail.deployment.errorCode,
|
|
23011
|
+
mode: promoted.mode
|
|
22698
23012
|
};
|
|
22699
23013
|
}
|
|
22700
23014
|
},
|
|
@@ -22851,7 +23165,7 @@ async function openClaims(client, projectId) {
|
|
|
22851
23165
|
}
|
|
22852
23166
|
|
|
22853
23167
|
// src/server.ts
|
|
22854
|
-
var SERVER_VERSION = "0.
|
|
23168
|
+
var SERVER_VERSION = "0.20.0";
|
|
22855
23169
|
function visibleTools(options2) {
|
|
22856
23170
|
const { features, readOnly } = options2;
|
|
22857
23171
|
return TOOLS.flatMap((tool) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "naro-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.20.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"naro-mcp": "./dist/index.js"
|
|
@@ -18,15 +18,15 @@
|
|
|
18
18
|
"dependencies": {
|
|
19
19
|
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
20
20
|
"@supabase/supabase-js": "2.112.3",
|
|
21
|
-
"naro-cli": "0.
|
|
21
|
+
"naro-cli": "0.21.0"
|
|
22
22
|
},
|
|
23
23
|
"devDependencies": {
|
|
24
24
|
"tsup": "8.5.0",
|
|
25
25
|
"tsx": "4.23.12",
|
|
26
26
|
"typescript": "7.0.2",
|
|
27
27
|
"vitest": "3.2.7",
|
|
28
|
-
"@next-deploy/
|
|
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",
|