@mediamonks/canopy-cli 0.7.28 → 0.7.30

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 (42) hide show
  1. package/dist/ai-fAvjdjVX.mjs +54 -0
  2. package/dist/{db-DVMwCLlU.mjs → db-KTLn8J1u.mjs} +2 -2
  3. package/dist/{deploy-CTMcwHhI.mjs → deploy-CBf44bvG.mjs} +2 -2
  4. package/dist/dev-DGCu9MjB.mjs +2 -0
  5. package/dist/{dev-BvjbJcse.mjs → dev-JlzMZJow.mjs} +50 -21
  6. package/dist/{dev-worker-BerdPJ_U.mjs → dev-worker-DjJGSUGj.mjs} +1 -1
  7. package/dist/docs/cli-ai.md +43 -0
  8. package/dist/docs/cli-db.md +2 -0
  9. package/dist/docs/cli.md +2 -0
  10. package/dist/docs/index.json +17 -2
  11. package/dist/docs/platform-ai-usage.md +6 -4
  12. package/dist/docs/platform-members-and-access.md +13 -4
  13. package/dist/docs/platform-slack.md +6 -0
  14. package/dist/docs/sdk-ai.md +41 -10
  15. package/dist/docs/sdk-notify.md +140 -0
  16. package/dist/docs/sdk-realtime.md +149 -0
  17. package/dist/docs/sdk-slack.md +3 -0
  18. package/dist/docs/sdk.md +5 -4
  19. package/dist/docs/static-serving.md +19 -0
  20. package/dist/{docs-DMFMPnxG.mjs → docs-Dg0UJugG.mjs} +2 -2
  21. package/dist/{egress-Cn-P46O3.mjs → egress-Cn9zDfDT.mjs} +1 -1
  22. package/dist/{feedback-OMAkwHpR.mjs → feedback-Dvb6CdlP.mjs} +2 -2
  23. package/dist/index.mjs +28 -26
  24. package/dist/{local-laAZEDVJ.mjs → local-BA_TPWpN.mjs} +56 -4
  25. package/dist/{login-C4MDP5NM.mjs → login-MX0xq8UE.mjs} +1 -1
  26. package/dist/{logs-CfDINw7U.mjs → logs-D5RWqsWl.mjs} +1 -1
  27. package/dist/{migrations-B9fXxMDj.mjs → migrations-C8yRPJIX.mjs} +1 -1
  28. package/dist/{open-rS1ARzOy.mjs → open-BJgd0R61.mjs} +1 -1
  29. package/dist/{package-BL6hRqtr.mjs → package-CnS450Yc.mjs} +1 -1
  30. package/dist/{projects-Dmm_976k.mjs → projects-BG7Injpb.mjs} +1 -1
  31. package/dist/{schedule-Buu71RWQ.mjs → schedule-CLuksxSM.mjs} +1 -1
  32. package/dist/{secrets-BD3DaTih.mjs → secrets-Bl3uOx4l.mjs} +1 -1
  33. package/dist/skill/canopy/SKILL.md +3 -0
  34. package/dist/{skill-C83Za5BU.mjs → skill-CuES7kPj.mjs} +2 -2
  35. package/dist/{src-CwSKZwNK.mjs → src-C7zPhbxi.mjs} +238 -1
  36. package/dist/{staging-CKEoDnac.mjs → staging-D06zobnf.mjs} +1 -1
  37. package/dist/{token-Dp2CoIYj.mjs → token-C_PFNqcv.mjs} +1 -1
  38. package/dist/{ui-DR-siu2S.mjs → ui-CeupNHNa.mjs} +4 -0
  39. package/dist/{upgrade-BjQguwG6.mjs → upgrade-C8YdSKKd.mjs} +2 -2
  40. package/dist/{usage-81JSiWIx.mjs → usage-CV7TjTO8.mjs} +1 -1
  41. package/package.json +2 -2
  42. package/dist/dev-BOvMCaML.mjs +0 -2
@@ -581,6 +581,243 @@ function dbRouter(config) {
581
581
  });
582
582
  return db;
583
583
  }
584
+ /** Serialized JSON bytes of one message's `data`. */
585
+ const REALTIME_MAX_DATA_BYTES = 4096;
586
+ /** Reserved prefix: topics only the project's backend may publish to. */
587
+ const WORKER_TOPIC_PREFIX = "worker:";
588
+ /** How often an idle stream sends a comment so proxies keep it open. */
589
+ const REALTIME_KEEPALIVE_MS = 25e3;
590
+ /**
591
+ * A stream whose reader is this far behind is closed rather than buffered
592
+ * without bound (a stalled or deliberately slow client). The SDK reconnects
593
+ * and re-syncs, exactly as after any other close.
594
+ */
595
+ const REALTIME_MAX_BUFFER_BYTES = 256 * 1024;
596
+ /** Every stream closes between these two ages, so auth is re-checked on reconnect. */
597
+ const REALTIME_STREAM_MIN_MS = 13 * 6e4;
598
+ const REALTIME_STREAM_MAX_MS = 15 * 6e4;
599
+ /** 1-64 of [A-Za-z0-9_.-], not dots only (`.`/`..` would be eaten by URL path normalization). */
600
+ const TOPIC_BODY = /^(?!\.+$)[A-Za-z0-9_.-]{1,64}$/;
601
+ function validateTopic(name) {
602
+ const body = name.startsWith("worker:") ? name.slice(7) : name;
603
+ return TOPIC_BODY.test(body);
604
+ }
605
+ const isWorkerTopic = (name) => name.startsWith(WORKER_TOPIC_PREFIX);
606
+ /** A subscription's topics: valid, de-duplicated, 1 to REALTIME_MAX_TOPICS. */
607
+ function parseTopicList(raw) {
608
+ const topics = [...new Set(raw)];
609
+ if (topics.length === 0) return {
610
+ ok: false,
611
+ error: "invalid_topic",
612
+ detail: "subscribe to at least one topic"
613
+ };
614
+ const bad = topics.find((t) => !validateTopic(t));
615
+ if (bad !== void 0) return {
616
+ ok: false,
617
+ error: "invalid_topic",
618
+ detail: topicDetail(bad)
619
+ };
620
+ if (topics.length > 20) return {
621
+ ok: false,
622
+ error: "too_many_topics",
623
+ detail: `one stream carries at most 20 topics`
624
+ };
625
+ return {
626
+ ok: true,
627
+ topics
628
+ };
629
+ }
630
+ function topicDetail(name) {
631
+ return `"${name.slice(0, 80)}" is not a topic name: use 1-64 letters, digits, "-", "_" or ".", optionally after "${WORKER_TOPIC_PREFIX}"`;
632
+ }
633
+ /** Time-ordered, unguessable enough to be a message id, not a credential. */
634
+ function newMessageId(now = Date.now()) {
635
+ const random = crypto.getRandomValues(/* @__PURE__ */ new Uint8Array(8));
636
+ let hex = "";
637
+ for (const byte of random) hex += byte.toString(16).padStart(2, "0");
638
+ return `${now.toString(36).padStart(9, "0")}${hex}`;
639
+ }
640
+ function stampFrom(sender) {
641
+ return sender.kind === "monk" && sender.email ? {
642
+ kind: "monk",
643
+ email: sender.email
644
+ } : { kind: sender.kind };
645
+ }
646
+ function refusalJson(c, refusal) {
647
+ return c.json({
648
+ error: refusal.error,
649
+ detail: refusal.detail
650
+ }, refusal.status);
651
+ }
652
+ function gate(c, ctx) {
653
+ if (!ctx.broker) return c.json({
654
+ error: "not_supported_on_target",
655
+ capability: "realtime",
656
+ detail: "realtime is not available on this Canopy site"
657
+ }, 501);
658
+ if (!ctx.enabled) return c.json({
659
+ error: "realtime_not_enabled",
660
+ detail: "realtime is turned off for this project — a Canopy admin can turn it on for you"
661
+ }, 403);
662
+ }
663
+ const encoder = new TextEncoder();
664
+ const frame = (event, data) => encoder.encode(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`);
665
+ const KEEPALIVE = encoder.encode(": keep-alive\n\n");
666
+ function realtimeRouter(config) {
667
+ const realtime = new Hono();
668
+ const random = config.random ?? Math.random;
669
+ realtime.get("/realtime", async (c) => {
670
+ if (c.req.method !== "GET") return c.json({
671
+ error: "method_not_allowed",
672
+ detail: "subscribe with GET"
673
+ }, 405);
674
+ const ctx = await config.context(c);
675
+ if (ctx instanceof Response) return ctx;
676
+ const refused = gate(c, ctx);
677
+ if (refused) return refused;
678
+ const list = parseTopicList(c.req.queries("topic") ?? []);
679
+ if (!list.ok) return c.json({
680
+ error: list.error,
681
+ detail: list.detail
682
+ }, 400);
683
+ const broker = ctx.broker;
684
+ let controller;
685
+ let handle;
686
+ let ended = false;
687
+ let keepAlive;
688
+ let lifetime;
689
+ const send = (bytes) => {
690
+ if (ended) return;
691
+ try {
692
+ controller.enqueue(bytes);
693
+ } catch {
694
+ end(false);
695
+ return;
696
+ }
697
+ if ((controller.desiredSize ?? 0) < 0) {
698
+ end(false);
699
+ try {
700
+ controller.error(/* @__PURE__ */ new Error("realtime stream reader fell behind"));
701
+ } catch {}
702
+ }
703
+ };
704
+ const end = (closeStream) => {
705
+ if (ended) return;
706
+ ended = true;
707
+ clearInterval(keepAlive);
708
+ clearTimeout(lifetime);
709
+ handle?.close();
710
+ if (closeStream) try {
711
+ controller.close();
712
+ } catch {}
713
+ };
714
+ const stream = new ReadableStream({
715
+ start(ctrl) {
716
+ controller = ctrl;
717
+ },
718
+ cancel() {
719
+ end(false);
720
+ }
721
+ }, new ByteLengthQueuingStrategy({ highWaterMark: REALTIME_MAX_BUFFER_BYTES }));
722
+ const sink = {
723
+ deliver(envelope) {
724
+ const out = ctx.redactEmails && envelope.from.email !== void 0 ? {
725
+ ...envelope,
726
+ from: { kind: envelope.from.kind }
727
+ } : envelope;
728
+ send(frame("message", out));
729
+ },
730
+ close() {
731
+ end(true);
732
+ }
733
+ };
734
+ send(frame("open", { topics: list.topics }));
735
+ const signal = c.req.raw.signal;
736
+ signal?.addEventListener("abort", () => end(false), { once: true });
737
+ let subscribed;
738
+ try {
739
+ subscribed = await broker.subscribe(ctx.scope, list.topics, sink, ctx.owner);
740
+ } catch (error) {
741
+ config.onError?.(c, error);
742
+ ended = true;
743
+ return c.json({
744
+ error: "realtime_unavailable",
745
+ detail: "realtime is briefly unavailable, retry shortly"
746
+ }, 503);
747
+ }
748
+ if ("refused" in subscribed) {
749
+ ended = true;
750
+ return refusalJson(c, subscribed);
751
+ }
752
+ handle = subscribed;
753
+ if (ended || signal?.aborted) {
754
+ end(false);
755
+ handle.close();
756
+ } else {
757
+ keepAlive = setInterval(() => send(KEEPALIVE), REALTIME_KEEPALIVE_MS);
758
+ const span = REALTIME_STREAM_MAX_MS - REALTIME_STREAM_MIN_MS;
759
+ lifetime = setTimeout(() => end(true), REALTIME_STREAM_MIN_MS + Math.floor(random() * span));
760
+ }
761
+ return new Response(stream, {
762
+ status: 200,
763
+ headers: {
764
+ "content-type": "text/event-stream; charset=utf-8",
765
+ "cache-control": "private, no-store, no-transform",
766
+ "x-accel-buffering": "no"
767
+ }
768
+ });
769
+ });
770
+ realtime.post("/realtime/:topic", async (c) => {
771
+ const ctx = await config.context(c);
772
+ if (ctx instanceof Response) return ctx;
773
+ const refused = gate(c, ctx);
774
+ if (refused) return refused;
775
+ const topic = c.req.param("topic");
776
+ if (!validateTopic(topic)) return c.json({
777
+ error: "invalid_topic",
778
+ detail: topicDetail(topic)
779
+ }, 400);
780
+ if (!ctx.isRuntime && isWorkerTopic(topic)) return c.json({
781
+ error: "topic_worker_only",
782
+ detail: `only the project's backend can publish to "${topic}" — pages can subscribe to it, but publish from a worker route instead`
783
+ }, 403);
784
+ const body = await c.req.json().catch(() => void 0);
785
+ if (typeof body !== "object" || body === null || Array.isArray(body) || body.data === void 0) return c.json({
786
+ error: "invalid_message",
787
+ detail: "send a JSON body of the form { \"data\": … }"
788
+ }, 400);
789
+ const data = body.data;
790
+ const bytes = encoder.encode(JSON.stringify(data)).byteLength;
791
+ if (bytes > 4096) return c.json({
792
+ error: "message_too_large",
793
+ detail: `message data is ${bytes} bytes; the limit is ${REALTIME_MAX_DATA_BYTES}. Send an id and load the rest from the database`
794
+ }, 413);
795
+ const envelope = {
796
+ id: newMessageId(),
797
+ topic,
798
+ data,
799
+ from: stampFrom(ctx.sender),
800
+ ts: (/* @__PURE__ */ new Date()).toISOString()
801
+ };
802
+ let refusal;
803
+ try {
804
+ refusal = await ctx.broker.publish(ctx.scope, envelope, {
805
+ bytes,
806
+ dailyQuota: ctx.dailyQuota,
807
+ dailyBytesQuota: ctx.dailyBytesQuota
808
+ });
809
+ } catch (error) {
810
+ config.onError?.(c, error);
811
+ return c.json({
812
+ error: "realtime_unavailable",
813
+ detail: "realtime is briefly unavailable, retry shortly"
814
+ }, 503);
815
+ }
816
+ if (refusal) return refusalJson(c, refusal);
817
+ return c.json({ id: envelope.id }, 202);
818
+ });
819
+ return realtime;
820
+ }
584
821
  //#endregion
585
822
  //#region ../data-plane/src/worker-request.ts
586
823
  /**
@@ -652,4 +889,4 @@ function stampWorkerHeaders(headers, ctx) {
652
889
  return headers;
653
890
  }
654
891
  //#endregion
655
- export { blobRouter as a, META_COLUMNS as i, stampWorkerHeaders as n, dbRouter as r, redactCanopyCredentials as t };
892
+ export { META_COLUMNS as a, dbRouter as i, stampWorkerHeaders as n, blobRouter as o, realtimeRouter as r, redactCanopyCredentials as t };
@@ -1,5 +1,5 @@
1
1
  import { c as resolveProject, l as stagingUrl, o as projectFlagArgs, s as projectUrl } from "./site-DM2J10CB.mjs";
2
- import { a as unwrap, d as withContext, i as requireTty, l as apiRequest, r as reportErrors, t as formatDateTime } from "./ui-DR-siu2S.mjs";
2
+ import { a as unwrap, d as withContext, i as requireTty, l as apiRequest, r as reportErrors, t as formatDateTime } from "./ui-CeupNHNa.mjs";
3
3
  import { defineCommand } from "citty";
4
4
  import * as p from "@clack/prompts";
5
5
  //#region src/staging.ts
@@ -1,5 +1,5 @@
1
1
  import { a as projectDirArgs, c as resolveProject, o as projectFlagArgs } from "./site-DM2J10CB.mjs";
2
- import { l as apiRequest, r as reportErrors, t as formatDateTime } from "./ui-DR-siu2S.mjs";
2
+ import { l as apiRequest, r as reportErrors, t as formatDateTime } from "./ui-CeupNHNa.mjs";
3
3
  import { defineCommand } from "citty";
4
4
  const tokenCommand = defineCommand({
5
5
  meta: {
@@ -38,6 +38,10 @@ const ERROR_COPY = {
38
38
  why: "the project is paused",
39
39
  fix: "Resume it from the dashboard, then retry."
40
40
  },
41
+ project_moved: {
42
+ why: "the project was marked as moved to another URL, so its hostnames only redirect",
43
+ fix: "Deploy to the project's new home instead, or undo the move under Settings in the dashboard and retry."
44
+ },
41
45
  no_database: {
42
46
  why: "the project has no database yet",
43
47
  fix: "Deploy the project once (`canopy deploy`) to provision its database, then retry."
@@ -1,5 +1,5 @@
1
1
  import { t as PACKAGE_NAME } from "./version-check-CcsA1QjI.mjs";
2
- import { r as reportErrors } from "./ui-DR-siu2S.mjs";
2
+ import { r as reportErrors } from "./ui-CeupNHNa.mjs";
3
3
  import { defineCommand } from "citty";
4
4
  import { spawn } from "node:child_process";
5
5
  import { fileURLToPath } from "node:url";
@@ -112,7 +112,7 @@ const upgradeCommand = defineCommand({
112
112
  const spec = `${PACKAGE_NAME}@${args.version ?? "latest"}`;
113
113
  const command = pmCommand(pm);
114
114
  const cmd = installArgs(pm, spec);
115
- const { skillInstalled } = await import("./skill-C83Za5BU.mjs");
115
+ const { skillInstalled } = await import("./skill-CuES7kPj.mjs");
116
116
  const { homedir } = await import("node:os");
117
117
  const refreshGlobal = skillInstalled(homedir());
118
118
  const refreshRepo = process.cwd() !== homedir() && skillInstalled(process.cwd());
@@ -1,5 +1,5 @@
1
1
  import { c as resolveProject, o as projectFlagArgs } from "./site-DM2J10CB.mjs";
2
- import { d as withContext, l as apiRequest, r as reportErrors } from "./ui-DR-siu2S.mjs";
2
+ import { d as withContext, l as apiRequest, r as reportErrors } from "./ui-CeupNHNa.mjs";
3
3
  import { defineCommand } from "citty";
4
4
  //#region src/usage.ts
5
5
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mediamonks/canopy-cli",
3
- "version": "0.7.28",
3
+ "version": "0.7.30",
4
4
  "description": "Canopy CLI — create, develop and deploy Canopy projects.",
5
5
  "bin": {
6
6
  "canopy": "./dist/index.mjs"
@@ -20,7 +20,7 @@
20
20
  "esbuild": "^0.28.1",
21
21
  "hono": "^4.8.0",
22
22
  "vite-server": "npm:vite@^7",
23
- "@mediamonks/canopy": "0.0.7",
23
+ "@mediamonks/canopy": "0.0.10",
24
24
  "@mediamonks/canopy-data-plane": "0.0.4"
25
25
  },
26
26
  "devDependencies": {
@@ -1,2 +0,0 @@
1
- import { t as devCommand } from "./dev-BvjbJcse.mjs";
2
- export { devCommand };