castle-web-cli 0.4.165 → 0.4.167
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/agent-prompts.js +1 -1
- package/dist/agent.d.ts +1 -2
- package/dist/agent.js +22 -13
- package/dist/api.js +16 -1
- package/dist/imports.js +19 -4
- package/dist/localPaths.d.ts +2 -0
- package/dist/localPaths.js +6 -0
- package/dist/serve.js +5 -1
- package/dist/serveSecurity.d.ts +12 -0
- package/dist/serveSecurity.js +42 -0
- package/dist/shell/assets/{index-CG9TrvCm.js → index-C8M-p00y.js} +36 -36
- package/dist/shell/index.html +1 -1
- package/kits/base/castle.json +64 -2
- package/kits/base/editors/ImageViewer.js +259 -0
- package/kits/base/editors/MediaPlayer.js +90 -0
- package/kits/base/editors/imageViewer.css +95 -0
- package/kits/base/editors/mediaPlayer.css +58 -0
- package/kits/multiplayer-2d/castle.json +1 -1
- package/kits/multiplayer-2d/package-lock.json +4 -1
- package/kits/multiplayer-3d/castle.json +1 -1
- package/kits/multiplayer-3d/package-lock.json +1 -1
- package/kits/physics-2d/behaviors/Joints.jsx +11 -5
- package/kits/physics-2d/behaviors/Sprite.jsx +6 -21
- package/kits/physics-2d/castle.json +1 -61
- package/kits/physics-2d/editors/BlueprintLibrary.jsx +8 -68
- package/kits/physics-2d/editors/SingleEditor.jsx +3 -10
- package/kits/physics-2d/editors/editorRegistry.jsx +36 -2
- package/kits/physics-2d/engine/art.js +25 -1
- package/kits/physics-2d/engine/artThumbnail.jsx +92 -0
- package/kits/physics-2d/engine/files.js +4 -15
- package/kits/physics-2d/engine/spriteField.jsx +176 -0
- package/kits/physics-2d/engine/ui.module.css +136 -0
- package/kits/physics-2d/package-lock.json +5 -0
- package/kits/physics-2d/pnpm-lock.yaml +96 -3
- package/kits/physics-3d/castle.json +1 -1
- package/kits/physics-3d/package-lock.json +1 -1
- package/kits/physics-3d/pnpm-lock.yaml +88 -3
- package/kits/real-time/castle.json +1 -1
- package/kits/real-time/package.json +3 -0
- package/kits/turn-based/castle.json +1 -1
- package/kits/turn-based/package-lock.json +1 -1
- package/package.json +1 -1
- package/kits/base/node_modules/.package-lock.json +0 -28
- package/kits/base/node_modules/castle-web-fonts/README.md +0 -114
- package/kits/base/node_modules/castle-web-fonts/dist/catalog.d.ts +0 -18
- package/kits/base/node_modules/castle-web-fonts/dist/catalog.js +0 -142
- package/kits/base/node_modules/castle-web-fonts/dist/face.d.ts +0 -1
- package/kits/base/node_modules/castle-web-fonts/dist/face.js +0 -52
- package/kits/base/node_modules/castle-web-fonts/dist/faces/Bore.d.ts +0 -3
- package/kits/base/node_modules/castle-web-fonts/dist/faces/Bore.js +0 -10
- package/kits/base/node_modules/castle-web-fonts/dist/faces/CourierPrime.d.ts +0 -3
- package/kits/base/node_modules/castle-web-fonts/dist/faces/CourierPrime.js +0 -10
- package/kits/base/node_modules/castle-web-fonts/dist/faces/DMSans.d.ts +0 -3
- package/kits/base/node_modules/castle-web-fonts/dist/faces/DMSans.js +0 -10
- package/kits/base/node_modules/castle-web-fonts/dist/faces/Glacier.d.ts +0 -3
- package/kits/base/node_modules/castle-web-fonts/dist/faces/Glacier.js +0 -10
- package/kits/base/node_modules/castle-web-fonts/dist/faces/HelicoCentrica.d.ts +0 -3
- package/kits/base/node_modules/castle-web-fonts/dist/faces/HelicoCentrica.js +0 -10
- package/kits/base/node_modules/castle-web-fonts/dist/faces/Piazzolla.d.ts +0 -3
- package/kits/base/node_modules/castle-web-fonts/dist/faces/Piazzolla.js +0 -10
- package/kits/base/node_modules/castle-web-fonts/dist/faces/Synco.d.ts +0 -3
- package/kits/base/node_modules/castle-web-fonts/dist/faces/Synco.js +0 -10
- package/kits/base/node_modules/castle-web-fonts/dist/faces/Tektur.d.ts +0 -3
- package/kits/base/node_modules/castle-web-fonts/dist/faces/Tektur.js +0 -10
- package/kits/base/node_modules/castle-web-fonts/dist/faces/YatraOne.d.ts +0 -3
- package/kits/base/node_modules/castle-web-fonts/dist/faces/YatraOne.js +0 -10
- package/kits/base/node_modules/castle-web-fonts/dist/index.d.ts +0 -16
- package/kits/base/node_modules/castle-web-fonts/dist/index.js +0 -15
- package/kits/base/node_modules/castle-web-fonts/licenses/CC-BY-ND-4.0.txt +0 -393
- package/kits/base/node_modules/castle-web-fonts/licenses/FONTS.md +0 -64
- package/kits/base/node_modules/castle-web-fonts/licenses/OFL-1.1.txt +0 -95
- package/kits/base/node_modules/castle-web-fonts/package.json +0 -47
- package/kits/base/node_modules/castle-web-fonts/woff2/Bore.woff2 +0 -0
- package/kits/base/node_modules/castle-web-fonts/woff2/CourierPrime.woff2 +0 -0
- package/kits/base/node_modules/castle-web-fonts/woff2/DMSans.woff2 +0 -0
- package/kits/base/node_modules/castle-web-fonts/woff2/Glacier.woff2 +0 -0
- package/kits/base/node_modules/castle-web-fonts/woff2/HelicoCentrica.woff2 +0 -0
- package/kits/base/node_modules/castle-web-fonts/woff2/Piazzolla.woff2 +0 -0
- package/kits/base/node_modules/castle-web-fonts/woff2/SOURCE.txt +0 -2
- package/kits/base/node_modules/castle-web-fonts/woff2/Synco.woff2 +0 -0
- package/kits/base/node_modules/castle-web-fonts/woff2/Tektur.woff2 +0 -0
- package/kits/base/node_modules/castle-web-fonts/woff2/YatraOne.woff2 +0 -0
- package/kits/blank/node_modules/.package-lock.json +0 -13
- package/kits/blank/node_modules/castle-web-fonts/README.md +0 -114
- package/kits/blank/node_modules/castle-web-fonts/dist/catalog.d.ts +0 -18
- package/kits/blank/node_modules/castle-web-fonts/dist/catalog.js +0 -142
- package/kits/blank/node_modules/castle-web-fonts/dist/face.d.ts +0 -1
- package/kits/blank/node_modules/castle-web-fonts/dist/face.js +0 -52
- package/kits/blank/node_modules/castle-web-fonts/dist/faces/Bore.d.ts +0 -3
- package/kits/blank/node_modules/castle-web-fonts/dist/faces/Bore.js +0 -10
- package/kits/blank/node_modules/castle-web-fonts/dist/faces/CourierPrime.d.ts +0 -3
- package/kits/blank/node_modules/castle-web-fonts/dist/faces/CourierPrime.js +0 -10
- package/kits/blank/node_modules/castle-web-fonts/dist/faces/DMSans.d.ts +0 -3
- package/kits/blank/node_modules/castle-web-fonts/dist/faces/DMSans.js +0 -10
- package/kits/blank/node_modules/castle-web-fonts/dist/faces/Glacier.d.ts +0 -3
- package/kits/blank/node_modules/castle-web-fonts/dist/faces/Glacier.js +0 -10
- package/kits/blank/node_modules/castle-web-fonts/dist/faces/HelicoCentrica.d.ts +0 -3
- package/kits/blank/node_modules/castle-web-fonts/dist/faces/HelicoCentrica.js +0 -10
- package/kits/blank/node_modules/castle-web-fonts/dist/faces/Piazzolla.d.ts +0 -3
- package/kits/blank/node_modules/castle-web-fonts/dist/faces/Piazzolla.js +0 -10
- package/kits/blank/node_modules/castle-web-fonts/dist/faces/Synco.d.ts +0 -3
- package/kits/blank/node_modules/castle-web-fonts/dist/faces/Synco.js +0 -10
- package/kits/blank/node_modules/castle-web-fonts/dist/faces/Tektur.d.ts +0 -3
- package/kits/blank/node_modules/castle-web-fonts/dist/faces/Tektur.js +0 -10
- package/kits/blank/node_modules/castle-web-fonts/dist/faces/YatraOne.d.ts +0 -3
- package/kits/blank/node_modules/castle-web-fonts/dist/faces/YatraOne.js +0 -10
- package/kits/blank/node_modules/castle-web-fonts/dist/index.d.ts +0 -16
- package/kits/blank/node_modules/castle-web-fonts/dist/index.js +0 -15
- package/kits/blank/node_modules/castle-web-fonts/licenses/CC-BY-ND-4.0.txt +0 -393
- package/kits/blank/node_modules/castle-web-fonts/licenses/FONTS.md +0 -64
- package/kits/blank/node_modules/castle-web-fonts/licenses/OFL-1.1.txt +0 -95
- package/kits/blank/node_modules/castle-web-fonts/package.json +0 -47
- package/kits/blank/node_modules/castle-web-fonts/woff2/Bore.woff2 +0 -0
- package/kits/blank/node_modules/castle-web-fonts/woff2/CourierPrime.woff2 +0 -0
- package/kits/blank/node_modules/castle-web-fonts/woff2/DMSans.woff2 +0 -0
- package/kits/blank/node_modules/castle-web-fonts/woff2/Glacier.woff2 +0 -0
- package/kits/blank/node_modules/castle-web-fonts/woff2/HelicoCentrica.woff2 +0 -0
- package/kits/blank/node_modules/castle-web-fonts/woff2/Piazzolla.woff2 +0 -0
- package/kits/blank/node_modules/castle-web-fonts/woff2/SOURCE.txt +0 -2
- package/kits/blank/node_modules/castle-web-fonts/woff2/Synco.woff2 +0 -0
- package/kits/blank/node_modules/castle-web-fonts/woff2/Tektur.woff2 +0 -0
- package/kits/blank/node_modules/castle-web-fonts/woff2/YatraOne.woff2 +0 -0
- package/kits/physics-2d/editors/ImageViewer.jsx +0 -206
- package/kits/physics-2d/editors/MediaPlayer.jsx +0 -57
- package/kits/physics-2d/editors/mediaFile.js +0 -31
- package/kits/physics-2d/editors/mediaViewer.module.css +0 -128
package/dist/agent-prompts.js
CHANGED
|
@@ -964,7 +964,7 @@ phase: wiring the paddle to touch
|
|
|
964
964
|
|
|
965
965
|
- \`avatar\` -- what you're doing RIGHT NOW, exactly one of: \`thinking\` (figuring out the plan), \`reading\` (looking through files), \`building\` (writing game logic), \`painting\` (making or editing art/visuals), \`playing\` (trying it in the game).
|
|
966
966
|
- \`phase\` -- a SHORT, plain-language line for this moment ("wiring the paddle to touch", "drawing the flag"). No file or code names. Update \`avatar\` and \`phase\` together each time your stage changes -- a few times across the task, not every turn. (You may also include \`progress: NN\` here, but the progress file above is the primary progress channel.)
|
|
967
|
-
- Before finishing, write ${opts.notesPath}: a tiny test guide for the PLAYER -- AT MOST 2-3 bullets (markdown \`- \` lines), each ONE short phrase: what to try and what should happen. NO code, NO API or "integration contract" detail,
|
|
967
|
+
- Before finishing, write ${opts.notesPath}: a tiny test guide for the PLAYER -- AT MOST 2-3 bullets (markdown \`- \` lines), each ONE short phrase: what to try and what should happen. NO code, NO API or "integration contract" detail, and no tour of how you built it -- that part is for you, not them. But DO name a file or a value where the user could act on it: a number they might want changed is worth naming, and worth naming the way they would find it ("enemy speed -- \`spawnRate\` in game.js"). Occasionally one bullet may run a little longer, but default to terse. Mention a blocker only if you hit one. The user reads this verbatim to check your work.${wrapUp}
|
|
968
968
|
- If you are truly blocked, write the blocker to the notes file and stop rather than guessing wildly.
|
|
969
969
|
- Never touch files under .castle/ other than ${opts.plan ? "those three paths" : "those two paths"}. (The version store lives there too, but you never edit it by hand -- \`castle-web save-version\` writes it for you.)`;
|
|
970
970
|
}
|
package/dist/agent.d.ts
CHANGED
|
@@ -3,8 +3,6 @@ import { Duplex } from 'stream';
|
|
|
3
3
|
import type { ORReasoningEffort, ORRoutingMode } from './native/openrouter.js';
|
|
4
4
|
import { type PlaytestToolResult } from './native/playtest.js';
|
|
5
5
|
export declare const AGENT_WS_PATH = "/__castle/agent";
|
|
6
|
-
export declare const AGENT_ATTACHMENT_PREFIX = "/__castle/agent/attachments/";
|
|
7
|
-
export declare const AGENT_PLAYTEST_PREFIX = "/__castle/agent/playtest/";
|
|
8
6
|
export declare const AGENT_MODEL_CAPS_PREFIX = "/__castle/agent/model-caps";
|
|
9
7
|
export type AgentBackend = 'cursor' | 'claude' | 'smith';
|
|
10
8
|
export type ClaudeModel = 'sonnet' | 'opus' | 'fable' | 'openrouter';
|
|
@@ -90,6 +88,7 @@ export declare function createAgentServer(opts: {
|
|
|
90
88
|
port?: number;
|
|
91
89
|
playtestServeUrl?: string;
|
|
92
90
|
browserContentUrl?: () => string | null;
|
|
91
|
+
signPath?: (reqPath: string) => string;
|
|
93
92
|
restart?: () => void;
|
|
94
93
|
authorizeUpgrade?: (req: http.IncomingMessage, endpoint: 'agent') => boolean;
|
|
95
94
|
}): AgentServer;
|
package/dist/agent.js
CHANGED
|
@@ -24,7 +24,7 @@ import * as path from 'path';
|
|
|
24
24
|
import { nanoid } from 'nanoid';
|
|
25
25
|
import { WebSocketServer } from 'ws';
|
|
26
26
|
import { rawDataToString } from './rawData.js';
|
|
27
|
-
import { PLAN_FILE } from './localPaths.js';
|
|
27
|
+
import { AGENT_ATTACHMENT_PREFIX, AGENT_PLAYTEST_PREFIX, PLAN_FILE } from './localPaths.js';
|
|
28
28
|
import { applyPlanOps, buildRouterPrompt, buildTaskPrompt, parsePlanOps, planOpenQuestionLines, truncateToBytes, userTurnInstruction, CLAUDE_TASK_SYSTEM_REMINDER, } from './agent-prompts.js';
|
|
29
29
|
import { readCastleJson } from './castleJson.js';
|
|
30
30
|
import { checkOpenrouterKey, checkOpenrouterModel, openrouterCatalogEntry, primeOpenrouterCatalog, } from './openrouter-catalog.js';
|
|
@@ -39,10 +39,6 @@ import { createPlaywrightPlaytestExecutor } from './native/playtest-executor.js'
|
|
|
39
39
|
import { createRemotePlaytestExecutor } from './native/playtest-remote.js';
|
|
40
40
|
import { runPlaytest, } from './native/playtest.js';
|
|
41
41
|
export const AGENT_WS_PATH = '/__castle/agent';
|
|
42
|
-
export const AGENT_ATTACHMENT_PREFIX = '/__castle/agent/attachments/';
|
|
43
|
-
// Playtest frame PNGs (tasks/<id>/playtest/<file>.png), served for the
|
|
44
|
-
// finished-task card's thumbnails -- see makePlaytestFrameHandler.
|
|
45
|
-
export const AGENT_PLAYTEST_PREFIX = '/__castle/agent/playtest/';
|
|
46
42
|
// Same-origin proxy for OpenRouter model capabilities (avoids browser CORS
|
|
47
43
|
// against openrouter.ai). GET ?model=<slug> -> ModelCaps JSON. Powers the
|
|
48
44
|
// settings popover's dynamic reasoning-effort / provider-tier pickers.
|
|
@@ -3077,7 +3073,7 @@ function asPromptTask(task) {
|
|
|
3077
3073
|
blockedBy: task.status === 'blocked' ? task.blockedBy : undefined,
|
|
3078
3074
|
};
|
|
3079
3075
|
}
|
|
3080
|
-
function asClientTask(task) {
|
|
3076
|
+
function asClientTask(task, signPath) {
|
|
3081
3077
|
return {
|
|
3082
3078
|
id: task.id,
|
|
3083
3079
|
title: task.title,
|
|
@@ -3094,7 +3090,20 @@ function asClientTask(task) {
|
|
|
3094
3090
|
rejected: task.rejected,
|
|
3095
3091
|
errorCopy: task.errorCopy,
|
|
3096
3092
|
errorDetail: task.errorDetail,
|
|
3097
|
-
playtestFrames: (task.playtestFrames ?? []).map((rel) => `${AGENT_PLAYTEST_PREFIX}${task.id}/${path.basename(rel)}
|
|
3093
|
+
playtestFrames: (task.playtestFrames ?? []).map((rel) => clientUrl(`${AGENT_PLAYTEST_PREFIX}${task.id}/${path.basename(rel)}`, signPath)),
|
|
3094
|
+
};
|
|
3095
|
+
}
|
|
3096
|
+
function clientUrl(reqPath, signPath) {
|
|
3097
|
+
return signPath ? `${reqPath}?t=${signPath(reqPath)}` : reqPath;
|
|
3098
|
+
}
|
|
3099
|
+
// A sent message renders its pasted images as `<img src>`, which cannot carry
|
|
3100
|
+
// the serve token any more than a playtest frame can -- see ServeSecurity.signPath.
|
|
3101
|
+
function asClientMessage(message, signPath) {
|
|
3102
|
+
if (!message.attachments?.length)
|
|
3103
|
+
return message;
|
|
3104
|
+
return {
|
|
3105
|
+
...message,
|
|
3106
|
+
attachments: message.attachments.map((name) => clientUrl(`${AGENT_ATTACHMENT_PREFIX}${name}`, signPath)),
|
|
3098
3107
|
};
|
|
3099
3108
|
}
|
|
3100
3109
|
function createTaskFeeds(broadcast) {
|
|
@@ -3133,7 +3142,7 @@ function createMessageThinking(broadcast) {
|
|
|
3133
3142
|
}
|
|
3134
3143
|
return { append, snapshot };
|
|
3135
3144
|
}
|
|
3136
|
-
function createMessageLog(messagesPath, broadcast, welcomeMessage) {
|
|
3145
|
+
function createMessageLog(messagesPath, broadcast, welcomeMessage, forClient) {
|
|
3137
3146
|
const loaded = readJsonFile(messagesPath) ?? [];
|
|
3138
3147
|
const messages = loaded
|
|
3139
3148
|
.filter((m) => m.text.trim() !== '' || m.role === 'user')
|
|
@@ -3154,7 +3163,7 @@ function createMessageLog(messagesPath, broadcast, welcomeMessage) {
|
|
|
3154
3163
|
function add(message) {
|
|
3155
3164
|
messages.push(message);
|
|
3156
3165
|
persist();
|
|
3157
|
-
broadcast({ type: 'message-add', message });
|
|
3166
|
+
broadcast({ type: 'message-add', message: forClient(message) });
|
|
3158
3167
|
}
|
|
3159
3168
|
function addLog(text) {
|
|
3160
3169
|
add({ id: nanoid(8), role: 'log', text, at: nowIso(), status: 'done' });
|
|
@@ -4215,7 +4224,7 @@ export function createAgentServer(opts) {
|
|
|
4215
4224
|
socket.send(payload);
|
|
4216
4225
|
}
|
|
4217
4226
|
}
|
|
4218
|
-
const log = createMessageLog(messagesPath, broadcast, welcomeMessage);
|
|
4227
|
+
const log = createMessageLog(messagesPath, broadcast, welcomeMessage, (message) => asClientMessage(message, opts.signPath));
|
|
4219
4228
|
const messages = log.messages;
|
|
4220
4229
|
const addLog = (text) => log.addLog(text);
|
|
4221
4230
|
// Which CLI backs the router and the task agents -- independently
|
|
@@ -4265,7 +4274,7 @@ export function createAgentServer(opts) {
|
|
|
4265
4274
|
playtest,
|
|
4266
4275
|
restart: opts.restart,
|
|
4267
4276
|
// Task lifecycle stays on the board only -- log lines for it were spam.
|
|
4268
|
-
onUpdate: (task) => broadcast({ type: 'task-update', task: asClientTask(task) }),
|
|
4277
|
+
onUpdate: (task) => broadcast({ type: 'task-update', task: asClientTask(task, opts.signPath) }),
|
|
4269
4278
|
onStarted: () => undefined,
|
|
4270
4279
|
onRetry: (task, attempt) => addLog(`agent died, retrying (${attempt}/${MAX_TASK_ATTEMPTS}): ${task.title}`),
|
|
4271
4280
|
onFinished: (task) => taskFeeds.map.delete(task.id),
|
|
@@ -4345,8 +4354,8 @@ export function createAgentServer(opts) {
|
|
|
4345
4354
|
clients.add(socket);
|
|
4346
4355
|
const hello = {
|
|
4347
4356
|
type: 'hello',
|
|
4348
|
-
messages,
|
|
4349
|
-
tasks: taskStore.sorted().map(asClientTask),
|
|
4357
|
+
messages: messages.map((message) => asClientMessage(message, opts.signPath)),
|
|
4358
|
+
tasks: taskStore.sorted().map((task) => asClientTask(task, opts.signPath)),
|
|
4350
4359
|
settings,
|
|
4351
4360
|
feeds: Object.fromEntries(taskFeeds.map),
|
|
4352
4361
|
thinking: messageThinking.snapshot(),
|
package/dist/api.js
CHANGED
|
@@ -23,7 +23,22 @@ export async function graphql(query, variables, options = {}) {
|
|
|
23
23
|
body: JSON.stringify(variables ? { query, variables } : { query }),
|
|
24
24
|
signal,
|
|
25
25
|
});
|
|
26
|
-
|
|
26
|
+
const body = await res.text();
|
|
27
|
+
try {
|
|
28
|
+
return JSON.parse(body);
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
// A body that isn't JSON never reached the GraphQL layer at all -- a sandbox
|
|
32
|
+
// proxy refusing the operation, a gateway error page. Parsing it blind reports
|
|
33
|
+
// that as `Unexpected token 'G'`, which hid a broken publish for days.
|
|
34
|
+
throw new Error(`Castle API ${res.status}: ${firstLine(body)}`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function firstLine(body) {
|
|
38
|
+
const line = body.split('\n', 1)[0].trim();
|
|
39
|
+
if (!line)
|
|
40
|
+
return '(empty response body)';
|
|
41
|
+
return line.length > 200 ? `${line.slice(0, 200)}...` : line;
|
|
27
42
|
}
|
|
28
43
|
export async function runBrowserJob(contentGrant, operation, signal) {
|
|
29
44
|
const data = await graphql(`mutation($contentGrant: String!, $operation: Json!) {
|
package/dist/imports.js
CHANGED
|
@@ -664,11 +664,22 @@ function importOptsIntoAutoUpdate(deckDir, alias) {
|
|
|
664
664
|
// publish (the framework kits -- the same account `init` qualifies their aliases
|
|
665
665
|
// with).
|
|
666
666
|
//
|
|
667
|
-
// Deliberately not a setting: nothing
|
|
668
|
-
// would be the leash undoing itself.
|
|
669
|
-
//
|
|
670
|
-
//
|
|
667
|
+
// Deliberately not a deck setting: nothing in castle.json a person can flip,
|
|
668
|
+
// since that would be the leash undoing itself. Process env is the escape
|
|
669
|
+
// hatch for local kit work and tests:
|
|
670
|
+
// - CASTLE_NO_AUTO_UPDATE=1 -- skip serve-start auto-update entirely (keep
|
|
671
|
+
// whatever is already under imports/, including hand-synced kits from this
|
|
672
|
+
// checkout).
|
|
673
|
+
// - CASTLE_AUTO_UPDATE_TRUSTED_ACCOUNT -- which username counts as trusted
|
|
674
|
+
// (default `castle`). Tests set it to exercise both branches; setting it to
|
|
675
|
+
// a non-matching name also disables updates as a side effect.
|
|
671
676
|
const AUTO_UPDATE_ACCOUNT = process.env.CASTLE_AUTO_UPDATE_TRUSTED_ACCOUNT ?? 'castle';
|
|
677
|
+
function autoUpdateDisabled() {
|
|
678
|
+
const raw = process.env.CASTLE_NO_AUTO_UPDATE;
|
|
679
|
+
if (raw == null || raw === '')
|
|
680
|
+
return false;
|
|
681
|
+
return raw !== '0' && raw.toLowerCase() !== 'false' && raw.toLowerCase() !== 'no';
|
|
682
|
+
}
|
|
672
683
|
// Who published a deck. Readable without auth for the same decks webDeckSource
|
|
673
684
|
// is, so this answers in a sandbox with no login. A lookup that fails answers
|
|
674
685
|
// "not trusted": the failure direction has to be the one that leaves an import
|
|
@@ -689,6 +700,10 @@ async function importIsOnTrustedAccount(deckId) {
|
|
|
689
700
|
// checked (offline, no source on the server, a bad archive) stays exactly as it
|
|
690
701
|
// is pinned, and the serve starts anyway.
|
|
691
702
|
export async function autoUpdateImports(deckDir) {
|
|
703
|
+
if (autoUpdateDisabled()) {
|
|
704
|
+
console.log('auto-update: skipped (CASTLE_NO_AUTO_UPDATE)');
|
|
705
|
+
return [];
|
|
706
|
+
}
|
|
692
707
|
const pins = tryReadCastleJson(deckDir)?.imports ?? {};
|
|
693
708
|
const updated = [];
|
|
694
709
|
for (const [alias, pin] of Object.entries(pins)) {
|
package/dist/localPaths.d.ts
CHANGED
|
@@ -5,4 +5,6 @@ export declare function getSdkPackagePath(): string;
|
|
|
5
5
|
export declare function getKitsDir(): string;
|
|
6
6
|
export declare const COVER_FILE = "preview.png";
|
|
7
7
|
export declare const PLAN_FILE = "plan.md";
|
|
8
|
+
export declare const AGENT_PLAYTEST_PREFIX = "/__castle/agent/playtest/";
|
|
9
|
+
export declare const AGENT_ATTACHMENT_PREFIX = "/__castle/agent/attachments/";
|
|
8
10
|
export declare function toPosixPath(filepath: string): string;
|
package/dist/localPaths.js
CHANGED
|
@@ -41,6 +41,12 @@ export const COVER_FILE = 'preview.png';
|
|
|
41
41
|
// instead. Named here because the serve's file listing has to know it too, to
|
|
42
42
|
// keep it out of a deck's path curation.
|
|
43
43
|
export const PLAN_FILE = 'plan.md';
|
|
44
|
+
// Request-path prefixes the agent server serves a task's playtest frame PNGs
|
|
45
|
+
// and a chat message's pasted images under. Named here because ServeSecurity
|
|
46
|
+
// has to know them too -- they are the shell prefixes a signed path may open --
|
|
47
|
+
// and agent.ts already imports that.
|
|
48
|
+
export const AGENT_PLAYTEST_PREFIX = '/__castle/agent/playtest/';
|
|
49
|
+
export const AGENT_ATTACHMENT_PREFIX = '/__castle/agent/attachments/';
|
|
44
50
|
export function toPosixPath(filepath) {
|
|
45
51
|
return filepath.split(path.sep).join('/');
|
|
46
52
|
}
|
package/dist/serve.js
CHANGED
|
@@ -404,7 +404,10 @@ window.__castleShellOrigin=(function(){
|
|
|
404
404
|
const publicShellPath = reqPath === '/' ||
|
|
405
405
|
reqPath.startsWith('/__castle/ide/') ||
|
|
406
406
|
FAVICON_FILES.some((name) => reqPath === `/${name}`);
|
|
407
|
-
if (reqPath.startsWith('/__castle/') &&
|
|
407
|
+
if (reqPath.startsWith('/__castle/') &&
|
|
408
|
+
!publicShellPath &&
|
|
409
|
+
!security.authorizeHttp(req) &&
|
|
410
|
+
!security.authorizeSignedPath(req, reqPath)) {
|
|
408
411
|
security.rejectHttpAuth(res);
|
|
409
412
|
return;
|
|
410
413
|
}
|
|
@@ -778,6 +781,7 @@ export async function serve(dir, options = {}) {
|
|
|
778
781
|
deckLabel: path.basename(projectDir),
|
|
779
782
|
port,
|
|
780
783
|
playtestServeUrl: security.localContentOrigin('play'),
|
|
784
|
+
signPath: (reqPath) => security.signPath(reqPath),
|
|
781
785
|
browserContentUrl: process.env.CASTLE_SANDBOX_ID
|
|
782
786
|
? () => security.browserContentOrigin('play')
|
|
783
787
|
: undefined,
|
package/dist/serveSecurity.d.ts
CHANGED
|
@@ -18,6 +18,7 @@ export declare class ServeSecurity {
|
|
|
18
18
|
private readonly contentProxyToken;
|
|
19
19
|
private readonly signingKey;
|
|
20
20
|
private readonly serveNonce;
|
|
21
|
+
private readonly pathKey;
|
|
21
22
|
private readonly tickets;
|
|
22
23
|
private localContentPorts;
|
|
23
24
|
private localContentHost;
|
|
@@ -26,6 +27,17 @@ export declare class ServeSecurity {
|
|
|
26
27
|
setLocalContentHost(host: string): void;
|
|
27
28
|
authorizeHttp(req: http.IncomingMessage): boolean;
|
|
28
29
|
rejectHttpAuth(res: http.ServerResponse): void;
|
|
30
|
+
/**
|
|
31
|
+
* Sign one shell path so a browser can load it WITHOUT the serve token. The
|
|
32
|
+
* bootstrap supplies that token by patching `window.fetch`, and an `<img
|
|
33
|
+
* src>` or a `target="_blank"` link never goes through fetch -- so a URL the
|
|
34
|
+
* page hands to the browser instead of fetching itself has to carry its own
|
|
35
|
+
* capability. Bound to the exact path, and only a SIGNABLE_PATH_PREFIXES
|
|
36
|
+
* path: a signature minted for one frame opens nothing else under
|
|
37
|
+
* `/__castle/`, and a path outside that set cannot be signed at all.
|
|
38
|
+
*/
|
|
39
|
+
signPath(reqPath: string): string;
|
|
40
|
+
authorizeSignedPath(req: http.IncomingMessage, reqPath: string): boolean;
|
|
29
41
|
issueTicket(endpoint: SocketEndpoint): string;
|
|
30
42
|
/** One-use admission for an SDK that predates the shell's MessagePort bridge. */
|
|
31
43
|
issueLegacyContentTicket(role: ContentRole): string;
|
package/dist/serveSecurity.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createHmac, randomBytes, timingSafeEqual } from 'crypto';
|
|
2
|
+
import { AGENT_ATTACHMENT_PREFIX, AGENT_PLAYTEST_PREFIX } from './localPaths.js';
|
|
2
3
|
export const SERVE_AUTH_FAILURE_HEADER = 'x-castle-serve-auth';
|
|
3
4
|
export const SERVE_AUTH_FAILURE_VALUE = 'invalid';
|
|
4
5
|
const CROCKFORD = '0123456789abcdefghjkmnpqrstvwxyz';
|
|
@@ -7,6 +8,13 @@ const MAX_GRANT_SECONDS = 12 * 60 * 60;
|
|
|
7
8
|
const GRANT_CLOCK_SKEW_SECONDS = 60;
|
|
8
9
|
const BROWSER_GRANT_SECONDS = 3 * 60;
|
|
9
10
|
export const MOBILE_HOST_PROTOCOL = '2';
|
|
11
|
+
// A signature covers the request path and nothing else, so every path it can
|
|
12
|
+
// open must be a static file the path alone names. A query-driven endpoint
|
|
13
|
+
// here (`/__castle/files/read?path=`) would be opened wholesale by one signature.
|
|
14
|
+
const SIGNABLE_PATH_PREFIXES = [AGENT_PLAYTEST_PREFIX, AGENT_ATTACHMENT_PREFIX];
|
|
15
|
+
function isSignablePath(reqPath) {
|
|
16
|
+
return SIGNABLE_PATH_PREFIXES.some((prefix) => reqPath.startsWith(prefix));
|
|
17
|
+
}
|
|
10
18
|
function base32Lower(bytes) {
|
|
11
19
|
let bits = 0;
|
|
12
20
|
let value = 0;
|
|
@@ -57,6 +65,7 @@ export class ServeSecurity {
|
|
|
57
65
|
contentProxyToken = randomBytes(32).toString('base64url');
|
|
58
66
|
signingKey = process.env.CASTLE_AGENT_TOKEN || randomBytes(32).toString('base64url');
|
|
59
67
|
serveNonce = base32Lower(randomBytes(8));
|
|
68
|
+
pathKey = randomBytes(32).toString('base64url');
|
|
60
69
|
tickets = new Map();
|
|
61
70
|
localContentPorts;
|
|
62
71
|
localContentHost = '127.0.0.1';
|
|
@@ -87,6 +96,39 @@ export class ServeSecurity {
|
|
|
87
96
|
})
|
|
88
97
|
.end();
|
|
89
98
|
}
|
|
99
|
+
/**
|
|
100
|
+
* Sign one shell path so a browser can load it WITHOUT the serve token. The
|
|
101
|
+
* bootstrap supplies that token by patching `window.fetch`, and an `<img
|
|
102
|
+
* src>` or a `target="_blank"` link never goes through fetch -- so a URL the
|
|
103
|
+
* page hands to the browser instead of fetching itself has to carry its own
|
|
104
|
+
* capability. Bound to the exact path, and only a SIGNABLE_PATH_PREFIXES
|
|
105
|
+
* path: a signature minted for one frame opens nothing else under
|
|
106
|
+
* `/__castle/`, and a path outside that set cannot be signed at all.
|
|
107
|
+
*/
|
|
108
|
+
signPath(reqPath) {
|
|
109
|
+
if (!isSignablePath(reqPath)) {
|
|
110
|
+
throw new Error(`refusing to sign a shell path outside the signable set: ${reqPath}`);
|
|
111
|
+
}
|
|
112
|
+
return base32Lower(createHmac('sha256', this.pathKey)
|
|
113
|
+
.update(`castle-path-v1\n${reqPath}`)
|
|
114
|
+
.digest()
|
|
115
|
+
.subarray(0, 12));
|
|
116
|
+
}
|
|
117
|
+
authorizeSignedPath(req, reqPath) {
|
|
118
|
+
const method = req.method?.toUpperCase();
|
|
119
|
+
if (method !== 'GET' && method !== 'HEAD')
|
|
120
|
+
return false;
|
|
121
|
+
if (!isSignablePath(reqPath))
|
|
122
|
+
return false;
|
|
123
|
+
let signature = '';
|
|
124
|
+
try {
|
|
125
|
+
signature = new URL(req.url || '/', 'http://localhost').searchParams.get('t') || '';
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
return signature !== '' && safeEqual(this.signPath(reqPath), signature);
|
|
131
|
+
}
|
|
90
132
|
issueTicket(endpoint) {
|
|
91
133
|
this.pruneTickets();
|
|
92
134
|
const ticket = randomBytes(16).toString('base64url');
|