castle-web-sdk 0.4.16 → 0.4.18

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/dist/castle.d.ts CHANGED
@@ -11,7 +11,7 @@ export { Pass } from "./passes";
11
11
  export type { CastlePassApi, PassOfferResult, PassOfferStatus, } from "./passes";
12
12
  export { Portal } from "./portal";
13
13
  export type { CastlePortalApi, PortalOpenResult, PortalOpenStatus, PortalPrefetchResult, PortalPrefetchStatus, } from "./portal";
14
- export { CARD_RATIO, fileUrl, initCard, onBeforeRestart, onFilesChanged, onSaveReloadState, requestReload, setup, takeReloadState, writeFileOnce, } from "./runtime";
14
+ export { CARD_RATIO, deleteFile, fileUrl, initCard, onBeforeRestart, onFilesChanged, onSaveReloadState, openFile, renameFile, requestReload, setup, takeReloadState, writeFileOnce, } from "./runtime";
15
15
  export type { FileChange, FilesChangedEvent } from "./runtime";
16
16
  export { flushSaves, hasPendingSave, onSaveState, writeFile, } from "./saveQueue";
17
17
  export type { SaveState } from "./saveQueue";
package/dist/castle.js CHANGED
@@ -7,7 +7,7 @@ export { Leaderboard } from "./leaderboard";
7
7
  export { Lifecycle } from "./lifecycle";
8
8
  export { Pass } from "./passes";
9
9
  export { Portal } from "./portal";
10
- export { CARD_RATIO, fileUrl, initCard, onBeforeRestart, onFilesChanged, onSaveReloadState, requestReload, setup, takeReloadState, writeFileOnce, } from "./runtime";
10
+ export { CARD_RATIO, deleteFile, fileUrl, initCard, onBeforeRestart, onFilesChanged, onSaveReloadState, openFile, renameFile, requestReload, setup, takeReloadState, writeFileOnce, } from "./runtime";
11
11
  export { flushSaves, hasPendingSave, onSaveState, writeFile, } from "./saveQueue";
12
12
  export { SharedStorage, Storage } from "./storage";
13
13
  export { Time } from "./time";
package/dist/runtime.d.ts CHANGED
@@ -32,6 +32,9 @@ export declare function setup(): void;
32
32
  */
33
33
  export declare function writeFileOnce(path: string, contents: string): Promise<LocalResponse>;
34
34
  export declare function fileUrl(path: string): string;
35
+ export declare function renameFile(from: string, to: string): Promise<void>;
36
+ export declare function deleteFile(path: string): Promise<void>;
37
+ export declare function openFile(path: string): void;
35
38
  export declare function initCard(): HTMLDivElement;
36
39
  export declare function sendLocalCommand<C extends CommandName>(command: C, params: CommandParams[C]): Promise<CommandResponseEnvelope>;
37
40
  /**
package/dist/runtime.js CHANGED
@@ -66,6 +66,41 @@ export function writeFileOnce(path, contents) {
66
66
  export function fileUrl(path) {
67
67
  return `/__castle/files/raw?path=${encodeURIComponent(path)}`;
68
68
  }
69
+ // Move a deck file. `writeFile`'s counterpart for the case where the file's
70
+ // NAME is the thing changing -- an editor renaming a blueprint has to move the
71
+ // file, not write a copy and leave the old one behind. Rejects when the
72
+ // destination exists or the source is gone (the serve's own guards), so a
73
+ // caller can surface the collision instead of silently clobbering.
74
+ export async function renameFile(from, to) {
75
+ await fileOp("rename", { from, to });
76
+ }
77
+ // Delete a deck file. Editor UI only, same as `writeFile` -- a published deck
78
+ // has no serve to ask.
79
+ export async function deleteFile(path) {
80
+ await fileOp("delete", { path });
81
+ }
82
+ async function fileOp(action, body) {
83
+ const res = await fetch(`/__castle/files/${action}`, {
84
+ method: "POST",
85
+ headers: { "content-type": "application/json" },
86
+ body: JSON.stringify(body),
87
+ });
88
+ if (res.ok)
89
+ return;
90
+ const detail = await res.text().catch(() => "");
91
+ throw new Error(`castle: ${action} failed (${res.status}) ${detail}`.trim());
92
+ }
93
+ // Ask the editor shell to open (or focus) an editor for `path`. This is how a
94
+ // kit editor hands the creator off to another file -- the creation modal
95
+ // dropping them into the pixel editor for the sprite it just made. A no-op
96
+ // anywhere there's no shell listening (a published deck, a bare serve), which
97
+ // is why it neither returns nor throws: the handoff is a courtesy, and the
98
+ // file was already written before we asked.
99
+ export function openFile(path) {
100
+ if (typeof window === "undefined" || window.parent === window)
101
+ return;
102
+ window.parent.postMessage({ type: "castle-open-file", path }, "*");
103
+ }
69
104
  export function initCard() {
70
105
  const style = document.createElement("style");
71
106
  style.textContent = `
@@ -441,6 +476,8 @@ function initHostCapture() {
441
476
  // than starting a second capture alongside the one already running.
442
477
  const inFlight = new Set();
443
478
  window.addEventListener("message", (event) => {
479
+ if (!isTrustedShellMessage(event))
480
+ return;
444
481
  const data = event.data;
445
482
  if (!data || data.type !== HOST_CAPTURE_REQUEST)
446
483
  return;
@@ -454,11 +491,17 @@ function initHostCapture() {
454
491
  if (requestId)
455
492
  inFlight.delete(requestId);
456
493
  const target = event.source ?? window.parent;
457
- target.postMessage({ type: HOST_CAPTURE_RESULT, requestId, dataUrl, ok: !!dataUrl }, "*");
494
+ target.postMessage({ type: HOST_CAPTURE_RESULT, requestId, dataUrl, ok: !!dataUrl }, event.origin);
458
495
  };
459
496
  void captureScreenshot().then(reply, () => reply(null));
460
497
  });
461
498
  }
499
+ function isTrustedShellMessage(event) {
500
+ const expectedOrigin = window
501
+ .__castleShellOrigin;
502
+ return (event.source === window.parent &&
503
+ (expectedOrigin ? event.origin === expectedOrigin : event.origin !== "null"));
504
+ }
462
505
  // ─── local dev-server socket ───────────────────────────────────────────────────
463
506
  // Reconnect policy mirrors the shell's terminal client: back off while the tab
464
507
  // is visible, stay quiet while it is hidden or offline, and force a FRESH socket
@@ -490,6 +533,7 @@ let localServeSeen = false;
490
533
  let needsWakeReconnect = false;
491
534
  const intentionallyClosed = new WeakSet();
492
535
  const connectionListeners = new Set();
536
+ let bridgeListening = false;
493
537
  /**
494
538
  * Notified `true` on every fresh dev-server socket and `false` when one is lost.
495
539
  * What lets a queued write know the moment retrying is worth it again.
@@ -539,6 +583,10 @@ function connectLocal() {
539
583
  clearReconnectTimer();
540
584
  if (!canConnectLocal())
541
585
  return;
586
+ if (typeof window !== "undefined" && window.parent !== window) {
587
+ connectBridge();
588
+ return;
589
+ }
542
590
  fetch("/__castle/ws-port")
543
591
  .then((r) => r.json())
544
592
  .then(({ port, path }) => {
@@ -555,6 +603,48 @@ function connectLocal() {
555
603
  scheduleReconnect();
556
604
  });
557
605
  }
606
+ function connectBridge() {
607
+ if (bridgeListening || ws?.readyState === WebSocket.OPEN)
608
+ return;
609
+ bridgeListening = true;
610
+ const onInit = (event) => {
611
+ const data = event.data;
612
+ if (!data ||
613
+ data.type !== "castle-bridge-init" ||
614
+ !isTrustedShellMessage(event) ||
615
+ event.ports.length !== 1) {
616
+ return;
617
+ }
618
+ const port = event.ports[0];
619
+ const socket = {
620
+ readyState: WebSocket.OPEN,
621
+ send: (value) => {
622
+ try {
623
+ port.postMessage(JSON.parse(value));
624
+ }
625
+ catch {
626
+ /* malformed local payload */
627
+ }
628
+ },
629
+ };
630
+ port.onmessage = (messageEvent) => {
631
+ const value = messageEvent.data;
632
+ if (!value || typeof value !== "object")
633
+ return;
634
+ handleLocalMessage(value);
635
+ };
636
+ port.start();
637
+ localServeSeen = true;
638
+ ws = socket;
639
+ for (const msg of logBuffer)
640
+ socket.send(JSON.stringify(msg));
641
+ logBuffer = [];
642
+ bridgeListening = false;
643
+ window.removeEventListener("message", onInit);
644
+ setConnected(true);
645
+ };
646
+ window.addEventListener("message", onInit);
647
+ }
558
648
  function stopHeartbeat() {
559
649
  if (pingTimer !== null) {
560
650
  clearInterval(pingTimer);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "castle-web-sdk",
3
- "version": "0.4.16",
3
+ "version": "0.4.18",
4
4
  "type": "module",
5
5
  "main": "dist/castle.js",
6
6
  "types": "dist/castle.d.ts",