@mediamonks/canopy-cli 0.7.11 → 0.7.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (108) hide show
  1. package/dist/{deploy-BCtEY1is.mjs → deploy-B1WrPq5W.mjs} +8 -1
  2. package/dist/{dev-DtoZrexp.mjs → dev-BnUYGQQm.mjs} +5 -4
  3. package/dist/dev-CySBxW7I.mjs +2 -0
  4. package/dist/{dev-worker-BZgtEM9V.mjs → dev-worker-IWnHvQ1f.mjs} +42 -7
  5. package/dist/{docs-BV9qhLJx.mjs → docs-DxB7NhZs.mjs} +1 -1
  6. package/dist/{feedback-Cb8ccQ_R.mjs → feedback-B-GXUBPs.mjs} +1 -1
  7. package/dist/index.mjs +10 -10
  8. package/dist/{local-C5AhA4GM.mjs → local-BGRd_FMZ.mjs} +6 -469
  9. package/dist/{package-BBpw-n7k.mjs → package-CDLufecy.mjs} +1 -1
  10. package/dist/{skill-DJHlTEhS.mjs → skill-BOWfqwr2.mjs} +1 -1
  11. package/dist/src-Bu4Ze32I.mjs +470 -0
  12. package/dist/{upgrade-B86Ao6T5.mjs → upgrade-fhfrid_O.mjs} +1 -1
  13. package/package.json +3 -3
  14. package/dist/dev-BYxSHUH6.mjs +0 -2
  15. package/dist/docs/ci.md +0 -50
  16. package/dist/docs/cli-create.md +0 -46
  17. package/dist/docs/cli-db.md +0 -86
  18. package/dist/docs/cli-delete.md +0 -44
  19. package/dist/docs/cli-deploy.md +0 -58
  20. package/dist/docs/cli-dev.md +0 -82
  21. package/dist/docs/cli-docs.md +0 -47
  22. package/dist/docs/cli-download.md +0 -42
  23. package/dist/docs/cli-egress.md +0 -37
  24. package/dist/docs/cli-feedback.md +0 -75
  25. package/dist/docs/cli-index.md +0 -61
  26. package/dist/docs/cli-init.md +0 -46
  27. package/dist/docs/cli-login.md +0 -43
  28. package/dist/docs/cli-logout.md +0 -32
  29. package/dist/docs/cli-logs.md +0 -57
  30. package/dist/docs/cli-open.md +0 -41
  31. package/dist/docs/cli-projects.md +0 -43
  32. package/dist/docs/cli-promote.md +0 -44
  33. package/dist/docs/cli-rollback.md +0 -45
  34. package/dist/docs/cli-skill.md +0 -39
  35. package/dist/docs/cli-staging.md +0 -54
  36. package/dist/docs/cli-token.md +0 -47
  37. package/dist/docs/cli-upgrade.md +0 -43
  38. package/dist/docs/cli-usage.md +0 -43
  39. package/dist/docs/cli-whoami.md +0 -46
  40. package/dist/docs/cli.md +0 -86
  41. package/dist/docs/data.md +0 -85
  42. package/dist/docs/deploy.md +0 -81
  43. package/dist/docs/dev.md +0 -72
  44. package/dist/docs/developers-agents-and-ai.md +0 -95
  45. package/dist/docs/developers-architecture.md +0 -106
  46. package/dist/docs/getting-started.md +0 -108
  47. package/dist/docs/index.json +0 -331
  48. package/dist/docs/infrastructure.md +0 -406
  49. package/dist/docs/migrations.md +0 -183
  50. package/dist/docs/overview-faq.md +0 -105
  51. package/dist/docs/overview-how-it-works.md +0 -82
  52. package/dist/docs/overview-use-cases.md +0 -76
  53. package/dist/docs/overview-what-is-canopy.md +0 -75
  54. package/dist/docs/platform-ai-usage.md +0 -65
  55. package/dist/docs/platform-client-access.md +0 -54
  56. package/dist/docs/platform-deploys-and-rollbacks.md +0 -93
  57. package/dist/docs/platform-egress.md +0 -75
  58. package/dist/docs/platform-feedback.md +0 -114
  59. package/dist/docs/platform-members-and-access.md +0 -71
  60. package/dist/docs/platform-project-data.md +0 -76
  61. package/dist/docs/platform-schedules.md +0 -90
  62. package/dist/docs/platform-sites-and-profiles.md +0 -61
  63. package/dist/docs/porting-nextjs.md +0 -233
  64. package/dist/docs/sdk-ai.md +0 -103
  65. package/dist/docs/sdk-auth.md +0 -57
  66. package/dist/docs/sdk-database.md +0 -126
  67. package/dist/docs/sdk-files.md +0 -82
  68. package/dist/docs/sdk-services.md +0 -58
  69. package/dist/docs/sdk.md +0 -126
  70. package/dist/docs/staging.md +0 -106
  71. package/dist/docs/static-serving.md +0 -318
  72. package/dist/docs/troubleshooting.md +0 -102
  73. package/dist/skill/canopy/SKILL.md +0 -160
  74. package/dist/templates/spa/.github/workflows/canopy-deploy.yml +0 -34
  75. package/dist/templates/spa/AGENTS.md +0 -47
  76. package/dist/templates/spa/README.md +0 -14
  77. package/dist/templates/spa/_gitignore +0 -4
  78. package/dist/templates/spa/canopy.json +0 -5
  79. package/dist/templates/spa/index.html +0 -12
  80. package/dist/templates/spa/package.json +0 -21
  81. package/dist/templates/spa/src/app.tsx +0 -25
  82. package/dist/templates/spa/src/canopy.ts +0 -5
  83. package/dist/templates/spa/src/main.tsx +0 -10
  84. package/dist/templates/spa/src/style.css +0 -29
  85. package/dist/templates/spa/tsconfig.json +0 -16
  86. package/dist/templates/spa/vite.config.ts +0 -7
  87. package/dist/templates/static/AGENTS.md +0 -45
  88. package/dist/templates/static/README.md +0 -9
  89. package/dist/templates/static/_gitignore +0 -2
  90. package/dist/templates/static/canopy.json +0 -4
  91. package/dist/templates/static/index.html +0 -21
  92. package/dist/templates/static/main.js +0 -9
  93. package/dist/templates/static/style.css +0 -29
  94. package/dist/templates/worker/.dev.vars.example +0 -9
  95. package/dist/templates/worker/.github/workflows/canopy-deploy.yml +0 -34
  96. package/dist/templates/worker/AGENTS.md +0 -63
  97. package/dist/templates/worker/README.md +0 -17
  98. package/dist/templates/worker/_gitignore +0 -5
  99. package/dist/templates/worker/canopy.json +0 -6
  100. package/dist/templates/worker/index.html +0 -12
  101. package/dist/templates/worker/package.json +0 -22
  102. package/dist/templates/worker/src/app.tsx +0 -25
  103. package/dist/templates/worker/src/canopy.ts +0 -5
  104. package/dist/templates/worker/src/main.tsx +0 -10
  105. package/dist/templates/worker/src/style.css +0 -29
  106. package/dist/templates/worker/tsconfig.json +0 -16
  107. package/dist/templates/worker/vite.config.ts +0 -7
  108. package/dist/templates/worker/worker/index.ts +0 -52
@@ -113,6 +113,12 @@ const deployCommand = defineCommand({
113
113
  type: "boolean",
114
114
  default: false,
115
115
  description: "Deploy to the project's staging channel (created on first use, database cloned from live)"
116
+ },
117
+ worker: {
118
+ type: "boolean",
119
+ default: true,
120
+ description: "Include the backend worker from canopy.json",
121
+ negativeDescription: "Deploy the frontend only — for sites whose target hosts no per-project backends"
116
122
  }
117
123
  },
118
124
  run: reportErrors("deploy", async ({ args }) => {
@@ -145,7 +151,8 @@ const deployCommand = defineCommand({
145
151
  });
146
152
  }
147
153
  let workerBytes = null;
148
- if (config.worker) {
154
+ if (config.worker && !args.worker) p.log.warn(`Skipping the backend (${config.worker}) — deploying the frontend only.`);
155
+ if (config.worker && args.worker) {
149
156
  const spinWorker = p.spinner();
150
157
  spinWorker.start(`Bundling worker ${config.worker}`);
151
158
  try {
@@ -37,7 +37,7 @@ async function workerPlugin(flags, slug) {
37
37
  console.log(`note: canopy.json points "worker" at ${entry}, which does not exist — skipping.`);
38
38
  return;
39
39
  }
40
- const { missingSecretNames, readDevVars, workerDevPlugin } = await import("./dev-worker-BZgtEM9V.mjs");
40
+ const { missingSecretNames, readDevVars, workerDevPlugin } = await import("./dev-worker-IWnHvQ1f.mjs");
41
41
  const secrets = readDevVars(flags.dir);
42
42
  const email = siteConfig().email;
43
43
  const plugin = workerDevPlugin({
@@ -165,7 +165,7 @@ async function devRemote(flags, slug) {
165
165
  async function devLocal(flags, slug) {
166
166
  let dump;
167
167
  if (flags.pull) {
168
- const { fetchProjectDump } = await import("./local-C5AhA4GM.mjs");
168
+ const { fetchProjectDump } = await import("./local-BGRd_FMZ.mjs");
169
169
  console.log(`Pulling a fresh export of ${slug}${flags.staging ? " [staging]" : ""}…`);
170
170
  dump = await fetchProjectDump(slug, flags.staging ? "staging" : "live");
171
171
  }
@@ -180,7 +180,7 @@ async function devLocal(flags, slug) {
180
180
  const reason = err instanceof ApiError ? err.explain().why : String(err);
181
181
  console.log(`Note: ai/services will be unavailable — could not reach the platform (${reason}).`);
182
182
  }
183
- const { createSandbox } = await import("./local-C5AhA4GM.mjs");
183
+ const { createSandbox } = await import("./local-BGRd_FMZ.mjs");
184
184
  const sandbox = await createSandbox({
185
185
  slug,
186
186
  persistDir: join(flags.dir, ".canopy", "local"),
@@ -233,7 +233,8 @@ async function devLocal(flags, slug) {
233
233
  const server = await startVite(flags, { proxy: {
234
234
  ...remoteEntry && {
235
235
  "/_api/data/v1/ai": remoteEntry,
236
- "/_api/data/v1/services": remoteEntry
236
+ "/_api/data/v1/services": remoteEntry,
237
+ "/_api/data/v1/google": remoteEntry
237
238
  },
238
239
  "/_api": {
239
240
  target: `http://127.0.0.1:${apiAddress.port}`,
@@ -0,0 +1,2 @@
1
+ import { t as devCommand } from "./dev-BnUYGQQm.mjs";
2
+ export { devCommand };
@@ -1,5 +1,43 @@
1
+ import "./src-Bu4Ze32I.mjs";
1
2
  import { readFileSync } from "node:fs";
2
3
  import { relative, resolve, sep } from "node:path";
4
+ //#region ../data-plane/src/worker-request.ts
5
+ /**
6
+ * How a project's backend addresses the platform — the one definition.
7
+ *
8
+ * Three hosts run project workers and each builds this request itself: the
9
+ * Workers-for-Platforms shim (apps/api/src/platform/wfp-shim.ts), the Node
10
+ * runtime (apps/project-runtime/src/canopy-fetch.ts), and `canopy dev`
11
+ * (packages/cli/src/dev-worker.ts). They had three copies of the header
12
+ * shaping, and the two that hold a worker token had two copies of the
13
+ * credential rule — which is exactly how the Google project grant came to
14
+ * work on one target and not the other.
15
+ *
16
+ * The shim cannot import this: it ships as a standalone module uploaded
17
+ * beside the user bundle, so it may only import `./bundle.mjs`. It keeps a
18
+ * transcription, and `wfp-shim.test.ts` runs it against the table in
19
+ * `worker-request.cases.ts` alongside this implementation, so the two cannot
20
+ * drift silently.
21
+ */
22
+ /** Gateway-stamped identity, for the worker's own `env.USER` — never relayed. */
23
+ const IDENTITY_STAMP_HEADERS = [
24
+ "x-canopy-user-sub",
25
+ "x-canopy-user-email",
26
+ "x-canopy-user-name"
27
+ ];
28
+ /**
29
+ * Strip what must never travel upstream and stamp who is calling. Common to
30
+ * every host; says nothing about credentials.
31
+ */
32
+ function stampWorkerHeaders(headers, ctx) {
33
+ headers.delete("host");
34
+ for (const stamp of IDENTITY_STAMP_HEADERS) headers.delete(stamp);
35
+ headers.set("x-canopy-project", ctx.projectSlug);
36
+ if (ctx.channel === "staging") headers.set("x-canopy-channel", "staging");
37
+ else headers.delete("x-canopy-channel");
38
+ return headers;
39
+ }
40
+ //#endregion
3
41
  //#region src/dev-worker.ts
4
42
  /**
5
43
  * Paths that belong to the worker rather than the SPA — the gateway's rule
@@ -79,15 +117,12 @@ function rewriteCanopyUrl(raw, base) {
79
117
  function createCanopyFetch(apiBase, slug, channel, fetchImpl = fetch) {
80
118
  return async (input, init) => {
81
119
  const url = rewriteCanopyUrl(typeof input === "string" ? input : input instanceof URL ? input.href : input.url, apiBase());
82
- const headers = new Headers(input instanceof Request ? input.headers : init?.headers);
83
- headers.delete("host");
84
- headers.delete("x-canopy-user-sub");
85
- headers.delete("x-canopy-user-email");
86
- headers.delete("x-canopy-user-name");
120
+ const headers = stampWorkerHeaders(new Headers(input instanceof Request ? input.headers : init?.headers), {
121
+ projectSlug: slug,
122
+ channel
123
+ });
87
124
  headers.delete("authorization");
88
125
  headers.delete("cookie");
89
- headers.set("x-canopy-project", slug);
90
- if (channel === "staging") headers.set("x-canopy-channel", "staging");
91
126
  const method = (input instanceof Request ? input.method : init?.method) ?? "GET";
92
127
  const body = input instanceof Request ? input.body : init?.body;
93
128
  const outbound = {
@@ -1,4 +1,4 @@
1
- import { t as version } from "./package-BBpw-n7k.mjs";
1
+ import { t as version } from "./package-CDLufecy.mjs";
2
2
  import { n as siteConfig } from "./site-BUlQR117.mjs";
3
3
  import { i as latestKnownVersion, r as compareSemver } from "./version-check-CcsA1QjI.mjs";
4
4
  import { n as reportErrors } from "./ui-2mrJcjYC.mjs";
@@ -1,4 +1,4 @@
1
- import { t as version } from "./package-BBpw-n7k.mjs";
1
+ import { t as version } from "./package-CDLufecy.mjs";
2
2
  import { a as projectDirArgs, c as resolveProject, n as siteConfig, u as tryReadCanopyJson } from "./site-BUlQR117.mjs";
3
3
  import { c as apiRequest, l as apiUpload, n as reportErrors, s as apiDownload } from "./ui-2mrJcjYC.mjs";
4
4
  import { defineCommand } from "citty";
package/dist/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { t as version } from "./package-BBpw-n7k.mjs";
2
+ import { t as version } from "./package-CDLufecy.mjs";
3
3
  import { r as siteHeaderLine, t as setInvocationProfile } from "./site-BUlQR117.mjs";
4
4
  import { a as notifyOfUpdate, o as runRefresh } from "./version-check-CcsA1QjI.mjs";
5
5
  import { defineCommand, runMain } from "citty";
@@ -67,21 +67,21 @@ const main = defineCommand({
67
67
  init: () => import("./projects-C6JQjVqg.mjs").then((m) => m.initCommand),
68
68
  projects: () => import("./projects-C6JQjVqg.mjs").then((m) => m.projectsCommand),
69
69
  delete: () => import("./projects-C6JQjVqg.mjs").then((m) => m.deleteCommand),
70
- dev: () => import("./dev-BYxSHUH6.mjs").then((m) => m.devCommand),
71
- deploy: () => import("./deploy-BCtEY1is.mjs").then((m) => m.deployCommand),
72
- promote: () => import("./deploy-BCtEY1is.mjs").then((m) => m.promoteCommand),
70
+ dev: () => import("./dev-CySBxW7I.mjs").then((m) => m.devCommand),
71
+ deploy: () => import("./deploy-B1WrPq5W.mjs").then((m) => m.deployCommand),
72
+ promote: () => import("./deploy-B1WrPq5W.mjs").then((m) => m.promoteCommand),
73
73
  staging: () => import("./staging-Bw1D_J8M.mjs").then((m) => m.stagingCommand),
74
- rollback: () => import("./deploy-BCtEY1is.mjs").then((m) => m.rollbackCommand),
75
- download: () => import("./deploy-BCtEY1is.mjs").then((m) => m.downloadCommand),
74
+ rollback: () => import("./deploy-B1WrPq5W.mjs").then((m) => m.rollbackCommand),
75
+ download: () => import("./deploy-B1WrPq5W.mjs").then((m) => m.downloadCommand),
76
76
  token: () => import("./token-D7zrIF8G.mjs").then((m) => m.tokenCommand),
77
77
  egress: () => import("./egress-CN_axsAN.mjs").then((m) => m.egressCommand),
78
- feedback: () => import("./feedback-Cb8ccQ_R.mjs").then((m) => m.feedbackCommand),
78
+ feedback: () => import("./feedback-B-GXUBPs.mjs").then((m) => m.feedbackCommand),
79
79
  db: () => import("./db-8Etjmzxh.mjs").then((m) => m.dbCommand),
80
80
  usage: () => import("./usage-CX1Izj_l.mjs").then((m) => m.usageCommand),
81
81
  open: () => import("./open-DRPp4QJg.mjs").then((m) => m.openCommand),
82
- docs: () => import("./docs-BV9qhLJx.mjs").then((m) => m.docsCommand),
83
- skill: () => import("./skill-DJHlTEhS.mjs").then((m) => m.skillCommand),
84
- upgrade: () => import("./upgrade-B86Ao6T5.mjs").then((m) => m.upgradeCommand),
82
+ docs: () => import("./docs-DxB7NhZs.mjs").then((m) => m.docsCommand),
83
+ skill: () => import("./skill-BOWfqwr2.mjs").then((m) => m.skillCommand),
84
+ upgrade: () => import("./upgrade-fhfrid_O.mjs").then((m) => m.upgradeCommand),
85
85
  logs: () => import("./logs-DNiZquki.mjs").then((m) => m.logsCommand)
86
86
  }
87
87
  });
@@ -1,477 +1,10 @@
1
1
  import { o as apiDataRequest } from "./ui-2mrJcjYC.mjs";
2
- import { n as rewriteApiPath } from "./dev-DtoZrexp.mjs";
2
+ import { n as META_COLUMNS, r as blobRouter, t as dbRouter } from "./src-Bu4Ze32I.mjs";
3
+ import { n as rewriteApiPath } from "./dev-BnUYGQQm.mjs";
3
4
  import { createRequire } from "node:module";
4
5
  import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
5
6
  import { dirname, join, relative, sep } from "node:path";
6
7
  import { Hono } from "hono";
7
- //#region ../data-plane/src/blob.ts
8
- /**
9
- * /blob — the SDK's file storage over an injectable BlobStore (a bucket
10
- * plus the project's isolated key prefix). Same single-implementation deal as
11
- * db.ts: canopy-api and the CLI's local sandbox mount this exact router.
12
- *
13
- * Authorization is the HOST's job (canopy-api enforces session-or-signature
14
- * ahead of this router; the local sandbox has no auth wall). This router only
15
- * mints download URLs via the injected signer — verification lives with the
16
- * host's auth middleware (see signing.ts).
17
- */
18
- const MAX_BLOB_BYTES = 25 * 1024 * 1024;
19
- const MAX_LIST_KEYS = 1e3;
20
- const URL_TTL_SECONDS = 600;
21
- /** Keys may nest with `/`, but never escape the prefix or surprise a URL. */
22
- function safeBlobKey(raw) {
23
- if (raw.length === 0 || raw.length > 512) return null;
24
- if (raw.includes("\\") || raw.includes("\0")) return null;
25
- if (raw.split("/").some((seg) => seg === "" || seg === "." || seg === "..")) return null;
26
- return raw;
27
- }
28
- function blobRouter(config) {
29
- const blob = new Hono();
30
- const resolve = async (c) => config.store(c);
31
- blob.put("/blob/:key", async (c) => {
32
- const resolved = await resolve(c);
33
- if (resolved instanceof Response) return resolved;
34
- const { store } = resolved;
35
- const key = safeBlobKey(c.req.param("key"));
36
- if (!key) return c.json({ error: "invalid_key" }, 400);
37
- if (Number(c.req.header("content-length") ?? 0) > MAX_BLOB_BYTES) return c.json({
38
- error: "blob_too_large",
39
- maxBytes: MAX_BLOB_BYTES
40
- }, 413);
41
- const body = await c.req.raw.arrayBuffer();
42
- if (body.byteLength > MAX_BLOB_BYTES) return c.json({
43
- error: "blob_too_large",
44
- maxBytes: MAX_BLOB_BYTES
45
- }, 413);
46
- const contentType = c.req.header("content-type") ?? "application/octet-stream";
47
- await store.bucket.put(store.prefix + key, body, { httpMetadata: { contentType } });
48
- return c.json({
49
- key,
50
- size: body.byteLength,
51
- contentType,
52
- uploadedAt: (/* @__PURE__ */ new Date()).toISOString()
53
- }, 201);
54
- });
55
- blob.get("/blob", async (c) => {
56
- const resolved = await resolve(c);
57
- if (resolved instanceof Response) return resolved;
58
- const { store } = resolved;
59
- const prefix = c.req.query("prefix") ?? "";
60
- if (prefix.length > 512 || prefix.includes("\0")) return c.json({ error: "invalid_prefix" }, 400);
61
- const base = store.prefix;
62
- const objects = [];
63
- let cursor;
64
- do {
65
- const page = await store.bucket.list({
66
- prefix: base + prefix,
67
- cursor,
68
- include: ["httpMetadata"]
69
- });
70
- for (const o of page.objects) objects.push({
71
- key: o.key.slice(base.length),
72
- size: o.size,
73
- contentType: o.httpMetadata?.contentType ?? "application/octet-stream",
74
- uploadedAt: o.uploaded.toISOString()
75
- });
76
- cursor = page.truncated && objects.length < MAX_LIST_KEYS ? page.cursor : void 0;
77
- } while (cursor);
78
- return c.json(objects.slice(0, MAX_LIST_KEYS));
79
- });
80
- blob.get("/blob/:key/url", async (c) => {
81
- const resolved = await resolve(c);
82
- if (resolved instanceof Response) return resolved;
83
- const { slug, store } = resolved;
84
- const key = safeBlobKey(c.req.param("key"));
85
- if (!key) return c.json({ error: "invalid_key" }, 400);
86
- if (!await store.bucket.head(store.prefix + key)) return c.json({ error: "not_found" }, 404);
87
- const exp = Math.floor(Date.now() / 1e3) + URL_TTL_SECONDS;
88
- const sig = await config.signDownload(c, slug, key, exp);
89
- return c.json({
90
- url: `/_api/data/v1/blob/${encodeURIComponent(key)}?project=${encodeURIComponent(slug)}&exp=${exp}&sig=${sig}`,
91
- expiresAt: (/* @__PURE__ */ new Date(exp * 1e3)).toISOString()
92
- });
93
- });
94
- blob.get("/blob/:key", async (c) => {
95
- const resolved = await resolve(c);
96
- if (resolved instanceof Response) return resolved;
97
- const { store } = resolved;
98
- const key = safeBlobKey(c.req.param("key"));
99
- if (!key) return c.json({ error: "invalid_key" }, 400);
100
- const object = await store.bucket.get(store.prefix + key);
101
- if (!object) return c.json({ error: "not_found" }, 404);
102
- return new Response(object.body, { headers: {
103
- "content-type": object.httpMetadata?.contentType ?? "application/octet-stream",
104
- "cache-control": "private, no-store"
105
- } });
106
- });
107
- blob.delete("/blob/:key", async (c) => {
108
- const resolved = await resolve(c);
109
- if (resolved instanceof Response) return resolved;
110
- const { store } = resolved;
111
- const key = safeBlobKey(c.req.param("key"));
112
- if (!key) return c.json({ error: "invalid_key" }, 400);
113
- const fullKey = store.prefix + key;
114
- const existed = await store.bucket.head(fullKey) !== null;
115
- await store.bucket.delete(fullKey);
116
- return c.json({ deleted: existed });
117
- });
118
- return blob;
119
- }
120
- //#endregion
121
- //#region ../data-plane/src/dialect.ts
122
- /** Platform-managed fields map to real columns instead of JSON extraction. */
123
- const META_COLUMNS = {
124
- _id: "id",
125
- _createdAt: "created_at",
126
- _updatedAt: "updated_at"
127
- };
128
- //#endregion
129
- //#region ../data-plane/src/schema.ts
130
- /** Platform/system tables never shown to users. */
131
- const RESERVED = /^(_canopy_|_cf_|sqlite_|pg_)/;
132
- /** Single-quoted SQL string literal (introspection args ride inline, not bound). */
133
- const lit = (value) => `'${value.replaceAll("'", "''")}'`;
134
- async function sqliteSchema(store) {
135
- const names = (await store.query(`SELECT name FROM sqlite_master WHERE type = 'table' ORDER BY name`)).results.map((r) => String(r.name)).filter((n) => !RESERVED.test(n));
136
- return Promise.all(names.map(async (name) => {
137
- const [cols, idxList] = await Promise.all([store.query(`SELECT name, type, "notnull" AS not_null, pk FROM pragma_table_info(${lit(name)})`), store.query(`SELECT name, "unique" AS is_unique FROM pragma_index_list(${lit(name)})`)]);
138
- const indexes = await Promise.all(idxList.results.filter((idx) => !RESERVED.test(String(idx.name))).map(async (idx) => ({
139
- name: String(idx.name),
140
- unique: Boolean(Number(idx.is_unique)),
141
- columns: (await store.query(`SELECT name FROM pragma_index_info(${lit(String(idx.name))})`)).results.map((r) => String(r.name))
142
- })));
143
- return {
144
- name,
145
- columns: cols.results.map((col) => ({
146
- name: String(col.name),
147
- type: String(col.type),
148
- nullable: !Number(col.not_null),
149
- pk: Number(col.pk) > 0
150
- })),
151
- indexes
152
- };
153
- }));
154
- }
155
- async function postgresSchema(store) {
156
- const [tables, columns, pks, indexes] = await Promise.all([
157
- store.query(`SELECT table_name AS name FROM information_schema.tables
158
- WHERE table_schema = current_schema() AND table_type = 'BASE TABLE'
159
- ORDER BY table_name`),
160
- store.query(`SELECT table_name, column_name, data_type, is_nullable
161
- FROM information_schema.columns
162
- WHERE table_schema = current_schema()
163
- ORDER BY table_name, ordinal_position`),
164
- store.query(`SELECT tc.table_name, kcu.column_name
165
- FROM information_schema.table_constraints tc
166
- JOIN information_schema.key_column_usage kcu
167
- ON kcu.constraint_name = tc.constraint_name
168
- AND kcu.table_schema = tc.table_schema
169
- WHERE tc.constraint_type = 'PRIMARY KEY' AND tc.table_schema = current_schema()`),
170
- store.query(`SELECT t.relname AS table_name, i.relname AS index_name,
171
- ix.indisunique AS is_unique, a.attname AS column_name
172
- FROM pg_index ix
173
- JOIN pg_class i ON i.oid = ix.indexrelid
174
- JOIN pg_class t ON t.oid = ix.indrelid
175
- JOIN pg_namespace n ON n.oid = t.relnamespace
176
- JOIN pg_attribute a ON a.attrelid = t.oid AND a.attnum = ANY(ix.indkey)
177
- WHERE n.nspname = current_schema() AND NOT ix.indisprimary`)
178
- ]);
179
- const pkSet = new Set(pks.results.map((r) => `${String(r.table_name)}.${String(r.column_name)}`));
180
- const byTable = /* @__PURE__ */ new Map();
181
- for (const row of tables.results) {
182
- const name = String(row.name);
183
- if (!RESERVED.test(name)) byTable.set(name, {
184
- name,
185
- columns: [],
186
- indexes: []
187
- });
188
- }
189
- for (const col of columns.results) {
190
- const table = byTable.get(String(col.table_name));
191
- if (!table) continue;
192
- table.columns.push({
193
- name: String(col.column_name),
194
- type: String(col.data_type),
195
- nullable: col.is_nullable === "YES",
196
- pk: pkSet.has(`${String(col.table_name)}.${String(col.column_name)}`)
197
- });
198
- }
199
- for (const idx of indexes.results) {
200
- const table = byTable.get(String(idx.table_name));
201
- if (!table) continue;
202
- const name = String(idx.index_name);
203
- let index = table.indexes.find((i) => i.name === name);
204
- if (!index) {
205
- index = {
206
- name,
207
- unique: Boolean(idx.is_unique),
208
- columns: []
209
- };
210
- table.indexes.push(index);
211
- }
212
- index.columns.push(String(idx.column_name));
213
- }
214
- return [...byTable.values()];
215
- }
216
- async function introspectSchema(store, dialect) {
217
- const [tables, collections] = await Promise.all([dialect === "postgres" ? postgresSchema(store) : sqliteSchema(store), store.query("SELECT collection, COUNT(*) AS docs FROM _canopy_docs GROUP BY collection ORDER BY collection").then((r) => r.results.map((row) => ({
218
- name: String(row.collection),
219
- docs: Number(row.docs)
220
- }))).catch(() => [])]);
221
- return {
222
- dialect,
223
- collections,
224
- tables
225
- };
226
- }
227
- //#endregion
228
- //#region ../data-plane/src/db.ts
229
- /**
230
- * /db — the SDK's document API + raw-SQL escape hatch, over an injectable
231
- * DocStore. This is THE data-plane implementation: canopy-api mounts it under
232
- * /data/v1 against project D1s (or Postgres on GCP), and `canopy dev --local`
233
- * mounts the very same router against PGlite — one place to change when the
234
- * API surface moves.
235
- *
236
- * Paths carry no project: hosts wrap the router with their own auth/scope
237
- * middleware, resolve the project themselves (gateway-stamped
238
- * X-Canopy-Project or token scope — see canopy-api's session.ts), and hand
239
- * back the store in `config.store` (returning a Response for not-found/paused…).
240
- */
241
- const COLLECTION_RE = /^[a-z][a-z0-9_]{0,63}$/;
242
- const FIELD_RE = /^[A-Za-z_][A-Za-z0-9_]*(\.[A-Za-z_][A-Za-z0-9_]*)*$/;
243
- const MAX_DOC_BYTES = 256 * 1024;
244
- const MAX_LIMIT = 1e3;
245
- /** Row cap for versioned /db/sql responses (the legacy shape is uncapped). */
246
- const MAX_SQL_ROWS = 1e3;
247
- /**
248
- * Statement classifier for read-only /db/sql runs. First keyword must be a
249
- * read; WITH additionally may not smuggle DML in a CTE (impossible on SQLite,
250
- * possible on Postgres — where hosts also enforce `SET TRANSACTION READ ONLY`
251
- * at the engine, making this the belt to that suspender).
252
- */
253
- function readOnlySqlViolation(query) {
254
- const bare = query.replace(/--[^\n]*/g, " ").replace(/\/\*[\s\S]*?\*\//g, " ").replace(/'(?:[^']|'')*'/g, "''").replace(/"(?:[^"]|"")*"/g, "\"\"");
255
- const first = /^\s*([A-Za-z]+)/.exec(bare)?.[1]?.toUpperCase();
256
- if (first === void 0) return "empty statement";
257
- if (![
258
- "SELECT",
259
- "EXPLAIN",
260
- "VALUES",
261
- "WITH"
262
- ].includes(first)) return `${first} is not allowed in read-only mode`;
263
- const dml = /\b(INSERT|UPDATE|DELETE|MERGE|REPLACE|CREATE|ALTER|DROP|TRUNCATE|GRANT|REVOKE|VACUUM|ATTACH|REINDEX)\b/i.exec(bare);
264
- if (first === "WITH" && dml) return `${dml[1].toUpperCase()} is not allowed in read-only mode`;
265
- if (first === "EXPLAIN" && dml && /\bANALYZE\b/i.test(bare)) return "EXPLAIN ANALYZE on a write is not allowed in read-only mode";
266
- return null;
267
- }
268
- function toDoc(row) {
269
- return {
270
- ...JSON.parse(row.data),
271
- _id: row.id,
272
- _createdAt: row.created_at,
273
- _updatedAt: row.updated_at
274
- };
275
- }
276
- function isPlainObject(value) {
277
- return typeof value === "object" && value !== null && !Array.isArray(value);
278
- }
279
- /** User fields only — `_`-prefixed keys are platform-managed. */
280
- function stripMetaKeys(doc) {
281
- return Object.fromEntries(Object.entries(doc).filter(([key]) => !key.startsWith("_")));
282
- }
283
- function isScalar(value) {
284
- return value === null || [
285
- "string",
286
- "number",
287
- "boolean"
288
- ].includes(typeof value);
289
- }
290
- var BadQuery = class extends Error {};
291
- /** Validate a where object: known meta keys / dot-path fields, scalar values. */
292
- function validateWhere(where) {
293
- for (const [field, value] of Object.entries(where)) {
294
- if (!isScalar(value)) throw new BadQuery(`where.${field}: only string/number/boolean/null values are supported`);
295
- if (!META_COLUMNS[field] && !FIELD_RE.test(field)) throw new BadQuery(`where.${field}: invalid field name`);
296
- }
297
- return where;
298
- }
299
- function intParam(raw, name, fallback, max) {
300
- if (raw === void 0) return fallback;
301
- if (!/^\d{1,9}$/.test(raw)) throw new BadQuery(`${name} must be a non-negative integer`);
302
- return Math.min(Number(raw), max);
303
- }
304
- function dbRouter(config) {
305
- const db = new Hono();
306
- const resolve = async (c) => config.store(c);
307
- db.post("/db/sql", async (c) => {
308
- const resolved = await resolve(c);
309
- if (resolved instanceof Response) return resolved;
310
- const { store } = resolved;
311
- const body = await c.req.json().catch(() => ({}));
312
- const params = body.params ?? [];
313
- if (typeof body.query !== "string" || body.query.trim() === "" || !Array.isArray(params) || !params.every(isScalar) || !(body.readonly === void 0 || typeof body.readonly === "boolean")) return c.json({
314
- error: "invalid_query",
315
- detail: "expected { query: string, params?: (string|number|boolean|null)[], readonly?: boolean }"
316
- }, 400);
317
- if (body.readonly === void 0) {
318
- const result = await store.query(body.query, params).catch((err) => err);
319
- if (result instanceof Error) return c.json({
320
- error: "sql_error",
321
- detail: result.message
322
- }, 400);
323
- return c.json(result.results);
324
- }
325
- if (body.readonly) {
326
- const violation = readOnlySqlViolation(body.query);
327
- if (violation) return c.json({
328
- error: "read_only",
329
- detail: violation
330
- }, 400);
331
- }
332
- const result = await store.query(body.query, params, { readonly: body.readonly }).catch((err) => err);
333
- if (result instanceof Error) return c.json({
334
- error: "sql_error",
335
- detail: result.message
336
- }, 400);
337
- if (!body.readonly) await config.onSqlWrite?.(c, body.query);
338
- const truncated = result.results.length > MAX_SQL_ROWS;
339
- const rows = truncated ? result.results.slice(0, MAX_SQL_ROWS) : result.results;
340
- return c.json({
341
- rows,
342
- rowCount: rows.length,
343
- truncated
344
- });
345
- });
346
- db.get("/db/schema", async (c) => {
347
- const resolved = await resolve(c);
348
- if (resolved instanceof Response) return resolved;
349
- const schema = await introspectSchema(resolved.store, resolved.dialect.name).catch((err) => err);
350
- if (schema instanceof Error) return c.json({
351
- error: "sql_error",
352
- detail: schema.message
353
- }, 400);
354
- return c.json(schema);
355
- });
356
- db.post("/db/:collection", async (c) => {
357
- const resolved = await resolve(c);
358
- if (resolved instanceof Response) return resolved;
359
- const { store, dialect } = resolved;
360
- const collection = c.req.param("collection");
361
- if (!COLLECTION_RE.test(collection)) return c.json({ error: "invalid_collection" }, 400);
362
- const body = await c.req.json().catch(() => void 0);
363
- if (!isPlainObject(body)) return c.json({
364
- error: "invalid_document",
365
- detail: "expected a JSON object"
366
- }, 400);
367
- const fields = stripMetaKeys(body);
368
- const data = JSON.stringify(fields);
369
- if (data.length > MAX_DOC_BYTES) return c.json({
370
- error: "document_too_large",
371
- maxBytes: MAX_DOC_BYTES
372
- }, 413);
373
- const id = crypto.randomUUID();
374
- const now = (/* @__PURE__ */ new Date()).toISOString();
375
- const insert = dialect.insert(collection, id, data, now);
376
- await store.query(insert.sql, insert.params);
377
- return c.json({
378
- ...fields,
379
- _id: id,
380
- _createdAt: now,
381
- _updatedAt: now
382
- }, 201);
383
- });
384
- db.get("/db/:collection", async (c) => {
385
- const resolved = await resolve(c);
386
- if (resolved instanceof Response) return resolved;
387
- const { store, dialect } = resolved;
388
- const collection = c.req.param("collection");
389
- if (!COLLECTION_RE.test(collection)) return c.json({ error: "invalid_collection" }, 400);
390
- const q = c.req.query();
391
- try {
392
- const limit = intParam(q.limit, "limit", 100, MAX_LIMIT);
393
- const offset = intParam(q.offset, "offset", 0, 1e9);
394
- let whereRaw = {};
395
- if (q.where !== void 0) {
396
- try {
397
- whereRaw = JSON.parse(q.where);
398
- } catch {
399
- throw new BadQuery("where must be a JSON object");
400
- }
401
- if (!isPlainObject(whereRaw)) throw new BadQuery("where must be a JSON object");
402
- }
403
- const where = validateWhere(whereRaw);
404
- let query;
405
- if (q.search !== void 0) {
406
- query = dialect.search({
407
- collection,
408
- text: q.search,
409
- where,
410
- limit,
411
- offset
412
- });
413
- if (query === null) return c.json([]);
414
- } else {
415
- if (q.orderBy !== void 0 && !META_COLUMNS[q.orderBy] && !FIELD_RE.test(q.orderBy)) throw new BadQuery("orderBy: invalid field name");
416
- if (q.order !== void 0 && q.order !== "asc" && q.order !== "desc") throw new BadQuery("order must be 'asc' or 'desc'");
417
- query = dialect.find({
418
- collection,
419
- where,
420
- orderBy: q.orderBy,
421
- order: q.order === "desc" ? "desc" : "asc",
422
- limit,
423
- offset
424
- });
425
- }
426
- const result = await store.query(query.sql, query.params);
427
- return c.json(result.results.map(toDoc));
428
- } catch (err) {
429
- if (err instanceof BadQuery) return c.json({
430
- error: "invalid_query",
431
- detail: err.message
432
- }, 400);
433
- throw err;
434
- }
435
- });
436
- db.get("/db/:collection/:id", async (c) => {
437
- const resolved = await resolve(c);
438
- if (resolved instanceof Response) return resolved;
439
- const { store, dialect } = resolved;
440
- const collection = c.req.param("collection");
441
- if (!COLLECTION_RE.test(collection)) return c.json({ error: "invalid_collection" }, 400);
442
- const get = dialect.get(collection, c.req.param("id"));
443
- const row = (await store.query(get.sql, get.params)).results[0];
444
- return c.json(row ? toDoc(row) : null);
445
- });
446
- db.patch("/db/:collection/:id", async (c) => {
447
- const resolved = await resolve(c);
448
- if (resolved instanceof Response) return resolved;
449
- const { store, dialect } = resolved;
450
- const collection = c.req.param("collection");
451
- if (!COLLECTION_RE.test(collection)) return c.json({ error: "invalid_collection" }, 400);
452
- const body = await c.req.json().catch(() => void 0);
453
- if (!isPlainObject(body)) return c.json({
454
- error: "invalid_patch",
455
- detail: "expected a JSON object"
456
- }, 400);
457
- const patch = dialect.patch(collection, c.req.param("id"), JSON.stringify(stripMetaKeys(body)), (/* @__PURE__ */ new Date()).toISOString());
458
- const row = (await store.query(patch.sql, patch.params)).results[0];
459
- if (!row) return c.json({ error: "not_found" }, 404);
460
- return c.json(toDoc(row));
461
- });
462
- db.delete("/db/:collection/:id", async (c) => {
463
- const resolved = await resolve(c);
464
- if (resolved instanceof Response) return resolved;
465
- const { store, dialect } = resolved;
466
- const collection = c.req.param("collection");
467
- if (!COLLECTION_RE.test(collection)) return c.json({ error: "invalid_collection" }, 400);
468
- const del = dialect.del(collection, c.req.param("id"));
469
- const result = await store.query(del.sql, del.params);
470
- return c.json({ deleted: (result.meta.changes ?? 0) > 0 });
471
- });
472
- return db;
473
- }
474
- //#endregion
475
8
  //#region ../data-plane/src/dialect-postgres.ts
476
9
  /**
477
10
  * Postgres doc-store dialect (Cloud SQL, PGlite). Same table shape as SQLite
@@ -1270,6 +803,10 @@ async function createSandbox(options) {
1270
803
  error: "not_available_in_local_dev",
1271
804
  detail: "services need the platform — log in (`canopy login`) so --local can proxy them."
1272
805
  }, 501));
806
+ app.all("/data/v1/google/*", (c) => c.json({
807
+ error: "not_available_in_local_dev",
808
+ detail: "google needs the platform — log in (`canopy login`) so --local can proxy it. The account itself is connected in the dashboard, not on your laptop."
809
+ }, 501));
1273
810
  app.notFound((c) => c.json({
1274
811
  error: "not_available_in_local_dev",
1275
812
  detail: "the local sandbox serves the data plane only (db, blob, auth.user, sdk.js) — control-plane calls need `canopy dev` against the platform."
@@ -1,4 +1,4 @@
1
1
  //#region package.json
2
- var version = "0.7.11";
2
+ var version = "0.7.12";
3
3
  //#endregion
4
4
  export { version as t };