castle-web-sdk 0.4.1 → 0.4.3
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/auth.d.ts +8 -0
- package/dist/auth.js +52 -0
- package/dist/castle.d.ts +11 -0
- package/dist/castle.js +8 -0
- package/dist/context.d.ts +30 -0
- package/dist/context.js +86 -0
- package/dist/errors.d.ts +22 -0
- package/dist/errors.js +16 -0
- package/dist/graphql.d.ts +15 -0
- package/dist/graphql.js +120 -0
- package/dist/leaderboard.d.ts +20 -0
- package/dist/leaderboard.js +296 -0
- package/dist/runtime.d.ts +12 -0
- package/dist/runtime.js +288 -0
- package/dist/storage.d.ts +17 -0
- package/dist/storage.js +405 -0
- package/dist/time.d.ts +17 -0
- package/dist/time.js +131 -0
- package/dist/types.d.ts +3 -0
- package/dist/types.js +1 -0
- package/dist/user.d.ts +9 -0
- package/dist/user.js +58 -0
- package/package.json +30 -3
- package/src/auth.ts +64 -0
- package/src/castle.ts +19 -0
- package/src/context.ts +124 -0
- package/src/errors.ts +32 -0
- package/src/graphql.ts +182 -0
- package/src/leaderboard.ts +456 -0
- package/src/runtime.ts +345 -0
- package/src/storage.ts +636 -0
- package/src/time.ts +226 -0
- package/src/types.ts +7 -0
- package/src/user.ts +91 -0
- package/AGENTS.md +0 -27
- package/castle.js +0 -130
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export declare const CARD_RATIO: number;
|
|
2
|
+
interface LocalResponse {
|
|
3
|
+
type: string;
|
|
4
|
+
requestId?: string;
|
|
5
|
+
ok?: boolean;
|
|
6
|
+
error?: string;
|
|
7
|
+
[key: string]: unknown;
|
|
8
|
+
}
|
|
9
|
+
export declare function setup(): void;
|
|
10
|
+
export declare function writeFile(path: string, contents: string): Promise<LocalResponse>;
|
|
11
|
+
export declare function initCard(): HTMLDivElement;
|
|
12
|
+
export {};
|
package/dist/runtime.js
ADDED
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
import { getCastleEmbed, isEdit } from "./context";
|
|
2
|
+
export const CARD_RATIO = 5 / 7;
|
|
3
|
+
let ws = null;
|
|
4
|
+
let logBuffer = [];
|
|
5
|
+
let nextRequestId = 1;
|
|
6
|
+
const pendingRequests = new Map();
|
|
7
|
+
const origLog = console.log;
|
|
8
|
+
const origWarn = console.warn;
|
|
9
|
+
const origError = console.error;
|
|
10
|
+
// Indirected dynamic import so deck bundlers don't statically rewrite it.
|
|
11
|
+
// eslint-disable-next-line @typescript-eslint/no-implied-eval
|
|
12
|
+
const dynamicImport = new Function("u", "return import(u)");
|
|
13
|
+
export function setup() {
|
|
14
|
+
interceptConsole();
|
|
15
|
+
connectLocal();
|
|
16
|
+
initPlayCard();
|
|
17
|
+
}
|
|
18
|
+
export function writeFile(path, contents) {
|
|
19
|
+
return sendLocalRequest({
|
|
20
|
+
type: "write_file",
|
|
21
|
+
path,
|
|
22
|
+
contents,
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
export function initCard() {
|
|
26
|
+
const style = document.createElement("style");
|
|
27
|
+
style.textContent = `
|
|
28
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
29
|
+
html, body { width: 100%; height: 100%; background: #000; overflow: hidden; }
|
|
30
|
+
body { display: flex; align-items: center; justify-content: center; }
|
|
31
|
+
#castle-card {
|
|
32
|
+
position: relative;
|
|
33
|
+
border-radius: 4% / calc(4% * (5 / 7));
|
|
34
|
+
overflow: hidden;
|
|
35
|
+
background: #121213;
|
|
36
|
+
}
|
|
37
|
+
`;
|
|
38
|
+
document.head.appendChild(style);
|
|
39
|
+
const card = document.createElement("div");
|
|
40
|
+
card.id = "castle-card";
|
|
41
|
+
document.body.appendChild(card);
|
|
42
|
+
function resize() {
|
|
43
|
+
if (getCastleEmbed()?.feed === true) {
|
|
44
|
+
card.style.width = "100vw";
|
|
45
|
+
card.style.height = "100vh";
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
const { w, h } = computeCardSize();
|
|
49
|
+
card.style.width = w + "px";
|
|
50
|
+
card.style.height = h + "px";
|
|
51
|
+
}
|
|
52
|
+
resize();
|
|
53
|
+
window.addEventListener("resize", resize);
|
|
54
|
+
return card;
|
|
55
|
+
}
|
|
56
|
+
// Constrains whatever the deck renders into #root to a centered 5:7 card.
|
|
57
|
+
// The mobile feed host card-sizes its WebView itself, and the editor needs the
|
|
58
|
+
// full viewport, so the card shell applies only to standalone play.
|
|
59
|
+
function initPlayCard() {
|
|
60
|
+
if (isEdit())
|
|
61
|
+
return;
|
|
62
|
+
if (getCastleEmbed()?.feed === true)
|
|
63
|
+
return;
|
|
64
|
+
const style = document.createElement("style");
|
|
65
|
+
style.textContent = `
|
|
66
|
+
html, body { background: #000; }
|
|
67
|
+
#root > * {
|
|
68
|
+
position: fixed !important;
|
|
69
|
+
inset: auto !important;
|
|
70
|
+
left: 50% !important;
|
|
71
|
+
top: 50% !important;
|
|
72
|
+
transform: translate(-50%, -50%) !important;
|
|
73
|
+
width: var(--castle-card-w, 100vw) !important;
|
|
74
|
+
height: var(--castle-card-h, 100vh) !important;
|
|
75
|
+
border-radius: 4% / calc(4% * (5 / 7)) !important;
|
|
76
|
+
overflow: hidden !important;
|
|
77
|
+
}
|
|
78
|
+
`;
|
|
79
|
+
document.head.appendChild(style);
|
|
80
|
+
function resize() {
|
|
81
|
+
const { w, h } = computeCardSize();
|
|
82
|
+
document.documentElement.style.setProperty("--castle-card-w", w + "px");
|
|
83
|
+
document.documentElement.style.setProperty("--castle-card-h", h + "px");
|
|
84
|
+
}
|
|
85
|
+
resize();
|
|
86
|
+
window.addEventListener("resize", resize);
|
|
87
|
+
}
|
|
88
|
+
function computeCardSize() {
|
|
89
|
+
const maxW = 450;
|
|
90
|
+
const maxH = 630;
|
|
91
|
+
const pad = 20;
|
|
92
|
+
const aw = window.innerWidth - pad * 2;
|
|
93
|
+
const ah = window.innerHeight - pad * 2;
|
|
94
|
+
let w;
|
|
95
|
+
let h;
|
|
96
|
+
if (aw / ah < CARD_RATIO) {
|
|
97
|
+
w = Math.min(aw, maxW);
|
|
98
|
+
h = w / CARD_RATIO;
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
h = Math.min(ah, maxH);
|
|
102
|
+
w = h * CARD_RATIO;
|
|
103
|
+
}
|
|
104
|
+
return { w, h };
|
|
105
|
+
}
|
|
106
|
+
function sendMsg(msg) {
|
|
107
|
+
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
108
|
+
ws.send(JSON.stringify(msg));
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
logBuffer.push(msg);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
function sendLocalRequest(msg) {
|
|
115
|
+
if (!ws || ws.readyState !== WebSocket.OPEN) {
|
|
116
|
+
return Promise.reject(new Error("Castle local CLI is not connected."));
|
|
117
|
+
}
|
|
118
|
+
const requestId = `req_${nextRequestId++}`;
|
|
119
|
+
const request = { ...msg, requestId };
|
|
120
|
+
return new Promise((resolve, reject) => {
|
|
121
|
+
const timeout = setTimeout(() => {
|
|
122
|
+
pendingRequests.delete(requestId);
|
|
123
|
+
reject(new Error(`Timed out waiting for ${msg.type}.`));
|
|
124
|
+
}, 10000);
|
|
125
|
+
pendingRequests.set(requestId, { resolve, reject, timeout });
|
|
126
|
+
ws.send(JSON.stringify(request));
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
function resolveLocalRequest(msg) {
|
|
130
|
+
if (!msg.requestId)
|
|
131
|
+
return false;
|
|
132
|
+
const pending = pendingRequests.get(msg.requestId);
|
|
133
|
+
if (!pending)
|
|
134
|
+
return false;
|
|
135
|
+
clearTimeout(pending.timeout);
|
|
136
|
+
pendingRequests.delete(msg.requestId);
|
|
137
|
+
if (msg.ok) {
|
|
138
|
+
pending.resolve(msg);
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
pending.reject(new Error(msg.error || "Castle local request failed."));
|
|
142
|
+
}
|
|
143
|
+
return true;
|
|
144
|
+
}
|
|
145
|
+
function interceptConsole() {
|
|
146
|
+
console.log = (...args) => {
|
|
147
|
+
origLog(...args);
|
|
148
|
+
sendMsg({ type: "log", level: "log", msg: formatConsoleArgs(args) });
|
|
149
|
+
};
|
|
150
|
+
console.warn = (...args) => {
|
|
151
|
+
origWarn(...args);
|
|
152
|
+
sendMsg({ type: "log", level: "warn", msg: formatConsoleArgs(args) });
|
|
153
|
+
};
|
|
154
|
+
console.error = (...args) => {
|
|
155
|
+
origError(...args);
|
|
156
|
+
sendMsg({ type: "log", level: "error", msg: formatConsoleArgs(args) });
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
function formatConsoleArgs(args) {
|
|
160
|
+
return args
|
|
161
|
+
.map((arg) => {
|
|
162
|
+
if (typeof arg === "string")
|
|
163
|
+
return arg;
|
|
164
|
+
if (arg instanceof Error)
|
|
165
|
+
return arg.stack || arg.message;
|
|
166
|
+
try {
|
|
167
|
+
const json = JSON.stringify(arg);
|
|
168
|
+
return json ?? String(arg);
|
|
169
|
+
}
|
|
170
|
+
catch {
|
|
171
|
+
return String(arg);
|
|
172
|
+
}
|
|
173
|
+
})
|
|
174
|
+
.join(" ");
|
|
175
|
+
}
|
|
176
|
+
async function captureWithHtml2Canvas(target) {
|
|
177
|
+
try {
|
|
178
|
+
const mod = (await dynamicImport("https://esm.sh/html2canvas"));
|
|
179
|
+
const c = await mod.default(target, {
|
|
180
|
+
backgroundColor: null,
|
|
181
|
+
scale: devicePixelRatio,
|
|
182
|
+
useCORS: true,
|
|
183
|
+
});
|
|
184
|
+
return c.toDataURL("image/png");
|
|
185
|
+
}
|
|
186
|
+
catch {
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
async function captureScreenshot() {
|
|
191
|
+
const card = document.getElementById("castle-card");
|
|
192
|
+
const canvas = document.querySelector("canvas");
|
|
193
|
+
if (document.body?.dataset.castleScreenshotTarget === "viewport") {
|
|
194
|
+
const viewportCapture = await captureWithHtml2Canvas(document.body);
|
|
195
|
+
if (viewportCapture)
|
|
196
|
+
return viewportCapture;
|
|
197
|
+
}
|
|
198
|
+
if (card && canvas) {
|
|
199
|
+
const cardRect = card.getBoundingClientRect();
|
|
200
|
+
const c = document.createElement("canvas");
|
|
201
|
+
c.width = cardRect.width * devicePixelRatio;
|
|
202
|
+
c.height = cardRect.height * devicePixelRatio;
|
|
203
|
+
const ctx = c.getContext("2d");
|
|
204
|
+
const canvasRect = canvas.getBoundingClientRect();
|
|
205
|
+
const dx = (canvasRect.left - cardRect.left) * devicePixelRatio;
|
|
206
|
+
const dy = (canvasRect.top - cardRect.top) * devicePixelRatio;
|
|
207
|
+
ctx.drawImage(canvas, dx, dy, canvasRect.width * devicePixelRatio, canvasRect.height * devicePixelRatio);
|
|
208
|
+
return c.toDataURL("image/png");
|
|
209
|
+
}
|
|
210
|
+
if (canvas)
|
|
211
|
+
return canvas.toDataURL("image/png");
|
|
212
|
+
return captureWithHtml2Canvas(card || document.body);
|
|
213
|
+
}
|
|
214
|
+
function connectLocal() {
|
|
215
|
+
fetch("/__castle/ws-port")
|
|
216
|
+
.then((r) => r.json())
|
|
217
|
+
.then(({ port, path }) => {
|
|
218
|
+
if (!port && !path)
|
|
219
|
+
return;
|
|
220
|
+
const socket = new WebSocket(path ? localWsUrl(path) : `ws://localhost:${port}`);
|
|
221
|
+
socket.onopen = () => {
|
|
222
|
+
ws = socket;
|
|
223
|
+
for (const msg of logBuffer)
|
|
224
|
+
ws.send(JSON.stringify(msg));
|
|
225
|
+
logBuffer = [];
|
|
226
|
+
};
|
|
227
|
+
socket.onmessage = (evt) => {
|
|
228
|
+
try {
|
|
229
|
+
const msg = JSON.parse(evt.data);
|
|
230
|
+
handleLocalMessage(msg);
|
|
231
|
+
}
|
|
232
|
+
catch {
|
|
233
|
+
// ignore malformed messages
|
|
234
|
+
}
|
|
235
|
+
};
|
|
236
|
+
socket.onclose = () => {
|
|
237
|
+
ws = null;
|
|
238
|
+
for (const [requestId, pending] of pendingRequests) {
|
|
239
|
+
clearTimeout(pending.timeout);
|
|
240
|
+
pending.reject(new Error("Castle local CLI disconnected."));
|
|
241
|
+
pendingRequests.delete(requestId);
|
|
242
|
+
}
|
|
243
|
+
setTimeout(connectLocal, 2000);
|
|
244
|
+
};
|
|
245
|
+
socket.onerror = () => socket.close();
|
|
246
|
+
})
|
|
247
|
+
.catch(() => { });
|
|
248
|
+
}
|
|
249
|
+
function handleLocalMessage(msg) {
|
|
250
|
+
if (msg.type === "screenshot_request") {
|
|
251
|
+
void captureScreenshot()
|
|
252
|
+
.then((data) => {
|
|
253
|
+
if (data) {
|
|
254
|
+
sendMsg({
|
|
255
|
+
type: "screenshot_response",
|
|
256
|
+
requestId: msg.requestId,
|
|
257
|
+
data,
|
|
258
|
+
});
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
sendMsg({
|
|
262
|
+
type: "screenshot_response",
|
|
263
|
+
requestId: msg.requestId,
|
|
264
|
+
ok: false,
|
|
265
|
+
error: "Could not capture screenshot.",
|
|
266
|
+
});
|
|
267
|
+
})
|
|
268
|
+
.catch((error) => {
|
|
269
|
+
sendMsg({
|
|
270
|
+
type: "screenshot_response",
|
|
271
|
+
requestId: msg.requestId,
|
|
272
|
+
ok: false,
|
|
273
|
+
error: error instanceof Error ? error.message : "Could not capture screenshot.",
|
|
274
|
+
});
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
else if (msg.type === "restart") {
|
|
278
|
+
location.reload();
|
|
279
|
+
}
|
|
280
|
+
else if (msg.type === "write_file_response") {
|
|
281
|
+
resolveLocalRequest(msg);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
function localWsUrl(path) {
|
|
285
|
+
const url = new URL(path, location.href);
|
|
286
|
+
url.protocol = location.protocol === "https:" ? "wss:" : "ws:";
|
|
287
|
+
return url.href;
|
|
288
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { Json } from "./types";
|
|
2
|
+
type SharedScope = "deck" | "user";
|
|
3
|
+
export interface StorageApi {
|
|
4
|
+
get<T extends Json = Json>(key: string): Promise<T | null>;
|
|
5
|
+
set(key: string, value: Json): void;
|
|
6
|
+
remove(key: string): void;
|
|
7
|
+
}
|
|
8
|
+
export interface SharedStorageApi {
|
|
9
|
+
get<T extends Json = Json>(scope: "deck", key: string): Promise<T | null>;
|
|
10
|
+
get<T extends Json = Json>(scope: "user", key: string): Promise<T | null>;
|
|
11
|
+
get<T extends Json = Json>(scope: "user", userId: string, key: string): Promise<T | null>;
|
|
12
|
+
set(scope: SharedScope, key: string, value: Json): void;
|
|
13
|
+
remove(scope: SharedScope, key: string): void;
|
|
14
|
+
}
|
|
15
|
+
export declare const Storage: StorageApi;
|
|
16
|
+
export declare const SharedStorage: SharedStorageApi;
|
|
17
|
+
export {};
|