@joystack/cli 0.1.52 → 0.1.54
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/CHANGELOG.md +21 -0
- package/bin/joystack.mjs +2 -2
- package/lib/inject.mjs +55 -10
- package/lib/joystack-node-doors.mjs +1 -0
- package/lib/publish-checks.mjs +16 -0
- package/lib/publish.mjs +3 -3
- package/lib/replay-store.mjs +2 -2
- package/lib/static-server.mjs +59 -10
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -14,6 +14,27 @@ Versions: [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
14
14
|
|
|
15
15
|
## [Unreleased]
|
|
16
16
|
|
|
17
|
+
## [0.1.54] - 2026-10-21
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
|
|
21
|
+
- Every page of yours `joystack dev` serves gets the dev tools, replays
|
|
22
|
+
(`?replay=`) and staged entities, whatever its address, including the
|
|
23
|
+
maker's live link; a tester's share link gets none of them.
|
|
24
|
+
- `joystack publish --dev-tools` says the review build has the dev tools on,
|
|
25
|
+
and an old recording's sweep message points at **Keep** in Sessions, in
|
|
26
|
+
the dev tools.
|
|
27
|
+
|
|
28
|
+
## [0.1.53] - 2026-10-20
|
|
29
|
+
|
|
30
|
+
### Added
|
|
31
|
+
|
|
32
|
+
- `joystack dev` finds your game's own dev tools, the files
|
|
33
|
+
`tools/<name>.tool.js`, runs them in the page, and serves the
|
|
34
|
+
`joystack/tools` door they import.
|
|
35
|
+
- `joystack publish` refuses game code that imports `joystack/tools`: only a
|
|
36
|
+
tool file may, and a published page has no such door.
|
|
37
|
+
|
|
17
38
|
## [0.1.52] - 2026-10-14
|
|
18
39
|
|
|
19
40
|
### Added
|
package/bin/joystack.mjs
CHANGED
|
@@ -53,7 +53,7 @@ Usage:
|
|
|
53
53
|
Publish this game to <slug>.joystack.games, open to anyone with a
|
|
54
54
|
Joystack account (--slug for an A/B copy, open only to invited
|
|
55
55
|
accounts unless --open; --dev-tools makes that copy a review build
|
|
56
|
-
with
|
|
56
|
+
with the dev tools on)
|
|
57
57
|
joystack build-id The id of this game's code as it is now, with its git commit
|
|
58
58
|
joystack replay list Every recorded session: when, how long, on what device, which build
|
|
59
59
|
joystack replay export [<id>] Turn a recorded session into tables you can query (Parquet)
|
|
@@ -266,7 +266,7 @@ try {
|
|
|
266
266
|
case 'replay': {
|
|
267
267
|
// `replay` is the accessor for a recorded session; `export` is its first
|
|
268
268
|
// verb. Watching one back needs no command at all — it is `?replay=<id>`
|
|
269
|
-
// on the game's URL, or the
|
|
269
|
+
// on the game's URL, or Sessions in the dev tools.
|
|
270
270
|
const { REPLAY_HELP, replayExport, replayProfile, replayList, parseReplayArgs } = await import('../lib/replay-cmd.mjs');
|
|
271
271
|
// Parsed in the lib so the argv split is under test — a flag's value must
|
|
272
272
|
// never slide into the session slot, and --help must be answered before
|
package/lib/inject.mjs
CHANGED
|
@@ -17,18 +17,34 @@ import { LEGACY_PAGE_GLOBALS, withLegacyDoors } from './legacy-names.mjs';
|
|
|
17
17
|
export const DEV_IDENTITY = { userId: 'dev-local-user', name: 'Dev', color: '#38bdf8' };
|
|
18
18
|
|
|
19
19
|
export const PLATFORM_URL = '/shared/platform/platform.js';
|
|
20
|
-
// The dev toolbox
|
|
21
|
-
// (
|
|
20
|
+
// The old dev toolbox, which an engine from before the dev tools runtime
|
|
21
|
+
// carries (0.2.46 and earlier) — injected ONLY here, never by the production
|
|
22
|
+
// serve path (backend/api/src/lib/inject.js). That separation is the gate
|
|
22
23
|
// that keeps a dev surface off published pages: the production injector never
|
|
23
24
|
// names this module, so there is nothing to strip and nothing to trust.
|
|
24
25
|
export const TOOLBOX_URL = '/shared/ui/toolbox/toolbox.js';
|
|
26
|
+
// The dev tools (shared/ui/devtools/): the runtime that runs a game's tools
|
|
27
|
+
// and talks to whatever draws them, the maker around the game or the
|
|
28
|
+
// overlay over it. Dev-only. The overlay is named here and
|
|
29
|
+
// imported only when the runtime finds no page around the game to draw.
|
|
30
|
+
export const DEVTOOLS_URL = '/shared/ui/devtools/runtime.js';
|
|
31
|
+
// The engine's list of its own dev tools: { tools: [{ url, devServer }] }.
|
|
32
|
+
export const ENGINE_TOOLS_URL = '/shared/ui/devtools/engine-tools.json';
|
|
33
|
+
export const DEVTOOLS_OVERLAY_URL = '/shared/ui/devtools/overlay/overlay.js';
|
|
34
|
+
// Point (shared/ui/pointing/), which the runtime loads when a host turns it
|
|
35
|
+
// on; named only when the engine has it.
|
|
36
|
+
export const POINT_URL = '/shared/ui/pointing/pointing.js';
|
|
37
|
+
// What a game's own tool files (`tools/<name>.tool.js`) import as
|
|
38
|
+
// `joystack/tools`. Dev pages only: a published page never loads a tool, so
|
|
39
|
+
// the production import map (V2_BARE_SPECIFIERS) has no such door.
|
|
40
|
+
export const DEVTOOLS_DOOR = { 'joystack/tools': '/shared/ui/devtools/devtools.js' };
|
|
25
41
|
// The laptop note (shared/ui/note/): the conversation listener a page carries
|
|
26
|
-
// when the dev server was started by a coding-agent session. Dev-only,
|
|
27
|
-
//
|
|
42
|
+
// when the dev server was started by a coding-agent session. Dev-only, and on
|
|
43
|
+
// every page the creator opens, not only the game.
|
|
28
44
|
export const NOTE_URL = '/shared/ui/note/note.js';
|
|
29
45
|
|
|
30
46
|
// The note's tag, last in <head> so it comes up after the platform layer and
|
|
31
|
-
// the
|
|
47
|
+
// the dev tools; a page without a <head> gets it first.
|
|
32
48
|
export function injectNoteTag(html, noteUrl = NOTE_URL) {
|
|
33
49
|
const tag = `<script type="module">import { installNote } from "${noteUrl}"; installNote();</script>`;
|
|
34
50
|
const end = /<\/head>/i.exec(html);
|
|
@@ -77,23 +93,50 @@ function setGlobal(scope, name, value) {
|
|
|
77
93
|
return `<script>${scope}.${name} = ${old ? `${scope}.${old} = ` : ''}${jsJson(value)};</script>`;
|
|
78
94
|
}
|
|
79
95
|
|
|
80
|
-
|
|
96
|
+
// The page's import map: the framework's doors for a v2 game, and
|
|
97
|
+
// `joystack/tools` when the game has tool files, which import it. A v1 page
|
|
98
|
+
// with tools gets a map of that door alone; a page with neither, none.
|
|
99
|
+
function importMap(v2, tools) {
|
|
100
|
+
if (!v2 && !tools) return '';
|
|
101
|
+
const imports = { ...(v2 ? withLegacyDoors(V2_IMPORTS) : {}), ...(tools ? DEVTOOLS_DOOR : {}) };
|
|
102
|
+
return `<script type="importmap">${jsJson({ imports })}</script>`;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// The runtime's tag: the overlay's and Point's doors as lazy imports, and
|
|
106
|
+
// the tool files. `point`: the engine has Point. `shared`: the page is
|
|
107
|
+
// reached through a share link (the maker's live link), where Point is the
|
|
108
|
+
// maker's only: opened alone, nobody hears a pick.
|
|
109
|
+
function devtoolsTag(engineTools, gameTools, { point = false, shared = false } = {}) {
|
|
110
|
+
const pointing = point ? `, point: () => import("${POINT_URL}")${shared ? ', pointAlone: false' : ''}` : '';
|
|
111
|
+
const files = `${engineTools.length ? `, engineTools: ${jsJson(engineTools)}` : ''}${gameTools.length ? `, gameTools: ${jsJson(gameTools)}` : ''}`;
|
|
112
|
+
return `<script type="module">import { startDevtools } from "${DEVTOOLS_URL}"; startDevtools({ overlay: () => import("${DEVTOOLS_OVERLAY_URL}")${pointing}${files} });</script>`;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const devFlag = (replay) => `<script>window.JOYSTACK_DEV = ${jsJson({ dev: true, tools: true, replay })};</script>`;
|
|
116
|
+
|
|
117
|
+
export function injectHeadTags(html, slug, { identity = DEV_IDENTITY, platformUrl = PLATFORM_URL, toolbox = false, toolboxUrl = TOOLBOX_URL, devtools = false, engineTools = [], gameTools = [], point = false, skeleton = null, shareHost = null, replay } = {}) {
|
|
81
118
|
const parts = [];
|
|
119
|
+
const tools = devtools ? gameTools : [];
|
|
82
120
|
// A framework-v2 game resolves `joystack`/`joystack/client` through an import map and
|
|
83
121
|
// reads its lowered graph from globalThis.__JOYSTACK_SKELETON__. BOTH must precede
|
|
84
122
|
// any module script (the platform tag AND game.js): a browser ignores an import
|
|
85
123
|
// map once a module has begun executing, and startGame() reads the skeleton at
|
|
86
124
|
// module-eval time. So they go FIRST.
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
}
|
|
125
|
+
const map = importMap(!!skeleton, tools.length > 0);
|
|
126
|
+
if (map) parts.push(map);
|
|
127
|
+
if (skeleton) parts.push(setGlobal('globalThis', '__JOYSTACK_SKELETON__', skeleton));
|
|
91
128
|
// A page served through `joystack dev --share`'s tunnel: the hostname it was
|
|
92
129
|
// shared on, which the engine's isSharedDevPage compares with the page's own
|
|
93
130
|
// before treating a public name as a dev page. Ahead of every module script,
|
|
94
131
|
// like the skeleton, because the room transport reads it as the game boots.
|
|
95
132
|
if (shareHost) parts.push(`<script>window.JOYSTACK_DEV_SHARE = ${jsJson(shareHost)};</script>`);
|
|
96
133
|
if (slug) parts.push(`<script>window.GAME_SLUG = ${jsJson(slug)};</script>`);
|
|
134
|
+
// Every page this server serves is a dev page carrying the dev tools: the
|
|
135
|
+
// engine's one page-side dev answer (`devPage`, shared/ui/room/lib/dev-host.js)
|
|
136
|
+
// reads it, ahead of every module. `replay`: the page is the creator's own,
|
|
137
|
+
// and may play a recording back or stage entities; a tester's share page
|
|
138
|
+
// may not.
|
|
139
|
+
parts.push(devFlag(replay !== false));
|
|
97
140
|
parts.push(setGlobal('window', 'JOYSTACK_IDENTITY', identity));
|
|
98
141
|
parts.push(
|
|
99
142
|
`<script type="module">import { installPlatform } from "${platformUrl}"; installPlatform();</script>`,
|
|
@@ -107,6 +150,8 @@ export function injectHeadTags(html, slug, { identity = DEV_IDENTITY, platformUr
|
|
|
107
150
|
`<script type="module">import { ensureToolbox } from "${toolboxUrl}"; ensureToolbox();</script>`,
|
|
108
151
|
);
|
|
109
152
|
}
|
|
153
|
+
// Inject-if-present: only an engine that carries the runtime gets its tag.
|
|
154
|
+
if (devtools) parts.push(devtoolsTag(engineTools, tools, { point, shared: !!shareHost }));
|
|
110
155
|
const inject = '\n ' + parts.join('\n ');
|
|
111
156
|
const m = /<head[^>]*>/i.exec(html);
|
|
112
157
|
if (m) return html.slice(0, m.index + m[0].length) + inject + html.slice(m.index + m[0].length);
|
package/lib/publish-checks.mjs
CHANGED
|
@@ -17,6 +17,9 @@ import { LEGACY_ROOT_DOOR_FILE, withLegacyDoors } from './legacy-names.mjs';
|
|
|
17
17
|
const SHARED_REF_RE = /["'(=]\s*\/(shared\/[A-Za-z0-9_@./-]+)/g;
|
|
18
18
|
const CODE_FILE_RE = /\.(js|mjs|html|css)$/i;
|
|
19
19
|
const ROOT_DOOR = 'joystack';
|
|
20
|
+
const TOOLS_DOOR = 'joystack/tools';
|
|
21
|
+
// What `joystack dev` runs as a tool: a `.tool.js` file directly in `tools/`.
|
|
22
|
+
const TOOL_FILE_RE = /^tools\/[^/]+\.tool\.js$/;
|
|
20
23
|
|
|
21
24
|
function decodeText(bytes) {
|
|
22
25
|
try {
|
|
@@ -72,4 +75,17 @@ export function checkGameCode(files, engine) {
|
|
|
72
75
|
for (const door of used) {
|
|
73
76
|
if (!doors[door]) throw new Error(`engine ${engine.version} does not ship the \`${door}\` import`);
|
|
74
77
|
}
|
|
78
|
+
checkToolsDoor(texts);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// A game's dev tools are the one place `joystack/tools` resolves, and only on
|
|
82
|
+
// the pages `joystack dev` serves: game code importing it would load there
|
|
83
|
+
// and fail in the published page.
|
|
84
|
+
function checkToolsDoor(texts) {
|
|
85
|
+
for (const [path, text] of texts) {
|
|
86
|
+
if (!text || TOOL_FILE_RE.test(path) || !CODE_FILE_RE.test(path)) continue;
|
|
87
|
+
if (text.includes(`'${TOOLS_DOOR}'`) || text.includes(`"${TOOLS_DOOR}"`)) {
|
|
88
|
+
throw new Error(`${path} imports ${TOOLS_DOOR}, which only a dev tool file (tools/<name>.tool.js) may import: a published page has no such door`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
75
91
|
}
|
package/lib/publish.mjs
CHANGED
|
@@ -47,7 +47,7 @@ export async function publish({
|
|
|
47
47
|
const project = findProject(cwd);
|
|
48
48
|
|
|
49
49
|
// `--dev-tools` publishes a REVIEW BUILD: the published page carries the dev
|
|
50
|
-
//
|
|
50
|
+
// tools, so whoever opens the link can use the game's tools. That belongs on
|
|
51
51
|
// a copy made for reviewing (a pull request's link, a lane's), never on the
|
|
52
52
|
// game players open — so it needs its own --slug, and the game's own slug is
|
|
53
53
|
// refused outright, and so is the slug of the game a lane was opened from.
|
|
@@ -55,7 +55,7 @@ export async function publish({
|
|
|
55
55
|
if (devTools && (!slug || slug === project.slug || slug === laneParentSlug(project.dir))) {
|
|
56
56
|
throw new Error(
|
|
57
57
|
`--dev-tools publishes a review build, and a review build never replaces the live game.\n\n` +
|
|
58
|
-
` npx joystack publish --dev-tools --slug ${project.slug}-review # a review copy with
|
|
58
|
+
` npx joystack publish --dev-tools --slug ${project.slug}-review # a review copy with dev tools\n` +
|
|
59
59
|
` npx joystack publish # the live game, without dev tools`,
|
|
60
60
|
);
|
|
61
61
|
}
|
|
@@ -183,7 +183,7 @@ export async function publish({
|
|
|
183
183
|
out = await publishInOneRequest({ apiBase, token, files, meta });
|
|
184
184
|
}
|
|
185
185
|
const url = playUrlFor(out.slug);
|
|
186
|
-
log(devTools ? `✅ Live (review build,
|
|
186
|
+
log(devTools ? `✅ Live (review build, dev tools on): ${url}` : `✅ Live: ${url}`);
|
|
187
187
|
return { slug: out.slug, url };
|
|
188
188
|
}
|
|
189
189
|
|
package/lib/replay-store.mjs
CHANGED
|
@@ -30,7 +30,7 @@ export const DEV_SEGMENT_PATH = '/__joystack/segments';
|
|
|
30
30
|
|
|
31
31
|
// Where the dev page asks what recordings exist, deletes one, or pins one
|
|
32
32
|
// against the prune — the read half of the same lane. Spelled out in the
|
|
33
|
-
// engine's Sessions tool too (shared/ui/sessions/sessions.js), for the same
|
|
33
|
+
// engine's Sessions tool too (shared/ui/sessions/sessions.tool.js), for the same
|
|
34
34
|
// reason as above: two ends of one wire, neither able to drift silently.
|
|
35
35
|
export const DEV_REPLAYS_PATH = '/__joystack/replays';
|
|
36
36
|
|
|
@@ -713,7 +713,7 @@ export function pruneOnStart({ projectDir, log = () => {}, caps } = {}) {
|
|
|
713
713
|
// went for a reason that was not the reason.
|
|
714
714
|
log(`→ replays swept ${result.removed.length} old recording(s) — keeping the newest `
|
|
715
715
|
+ `${result.caps.maxCount} and up to ${sizeLabel(result.caps.maxBytes)} `
|
|
716
|
-
+
|
|
716
|
+
+ '(Keep one in Sessions, in the dev tools, to hold on to it)');
|
|
717
717
|
}
|
|
718
718
|
return result;
|
|
719
719
|
}
|
package/lib/static-server.mjs
CHANGED
|
@@ -13,11 +13,11 @@ import http from 'node:http';
|
|
|
13
13
|
import { connect as connectTcp } from 'node:net';
|
|
14
14
|
import { randomBytes } from 'node:crypto';
|
|
15
15
|
import { EventEmitter } from 'node:events';
|
|
16
|
-
import { readFileSync, statSync, existsSync, realpathSync } from 'node:fs';
|
|
16
|
+
import { readFileSync, readdirSync, statSync, existsSync, realpathSync } from 'node:fs';
|
|
17
17
|
import { extname, join, normalize, relative, sep } from 'node:path';
|
|
18
18
|
import { fileURLToPath } from 'node:url';
|
|
19
19
|
import { gunzipSync } from 'node:zlib';
|
|
20
|
-
import { injectHeadTags, injectNoteTag, rewriteHtmlScripts, rewriteJsImports, NOTE_URL, PLATFORM_URL, TOOLBOX_URL } from './inject.mjs';
|
|
20
|
+
import { injectHeadTags, injectNoteTag, rewriteHtmlScripts, rewriteJsImports, NOTE_URL, PLATFORM_URL, TOOLBOX_URL, DEVTOOLS_URL, ENGINE_TOOLS_URL, POINT_URL } from './inject.mjs';
|
|
21
21
|
import { createRelay, RELAY_PATH } from './relay/relay.mjs';
|
|
22
22
|
import { WHOAMI_PATH } from './ports.mjs';
|
|
23
23
|
import {
|
|
@@ -232,6 +232,16 @@ function sameOrigin(req, { requireOrigin = false, shareHost = null } = {}) {
|
|
|
232
232
|
return !!shareHost && isShareHost(req.headers.host, shareHost) && origin === `https://${req.headers.host}`;
|
|
233
233
|
}
|
|
234
234
|
|
|
235
|
+
// Whether a game page gets the dev tools' runtime: when the engine carries
|
|
236
|
+
// it, and the page is served on this machine, or through a share link that
|
|
237
|
+
// belongs to the creator (the maker's live link, `shareIsOwner`), where the
|
|
238
|
+
// page around the game draws the tools. On the maker that is everyone the
|
|
239
|
+
// maker lets onto the live link, not only the game's owner. Never for a
|
|
240
|
+
// tester on `joystack dev --share`.
|
|
241
|
+
function devtoolsRide({ shared, shareIsOwner, carried }) {
|
|
242
|
+
return carried && (!shared || shareIsOwner);
|
|
243
|
+
}
|
|
244
|
+
|
|
235
245
|
// Whether a Host header names the shared tunnel hostname (with or without a port).
|
|
236
246
|
function isShareHost(header, shareHost) {
|
|
237
247
|
return !!shareHost && String(header ?? '').replace(/:\d+$/, '').toLowerCase() === shareHost;
|
|
@@ -389,6 +399,44 @@ export function createStaticServer({
|
|
|
389
399
|
return relative(realDir, real).split(sep).some((seg) => STATE_DIR_NAMES.includes(seg.toLowerCase()));
|
|
390
400
|
}
|
|
391
401
|
|
|
402
|
+
// The game's own dev tools: each `<name>.tool.js` directly in its `tools/`
|
|
403
|
+
// folder, as the page reaches them, in name order. Read on every page load,
|
|
404
|
+
// so a tool written since shows on the next reload.
|
|
405
|
+
function toolFiles(urlDir) {
|
|
406
|
+
const dir = resolveUrl(urlDir);
|
|
407
|
+
if (!dir) return [];
|
|
408
|
+
try {
|
|
409
|
+
return readdirSync(dir, { withFileTypes: true })
|
|
410
|
+
.filter((e) => e.isFile() && e.name.endsWith('.tool.js'))
|
|
411
|
+
.map((e) => e.name).sort()
|
|
412
|
+
.map((name) => `${urlDir}/${encodeURIComponent(name)}`);
|
|
413
|
+
} catch {
|
|
414
|
+
return [];
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// The runtime's tag and the tool files it is handed, for a game's page.
|
|
419
|
+
function devtoolsFor(shared, slug) {
|
|
420
|
+
const devtools = devtoolsRide({ shared, shareIsOwner, carried: !!resolveUrl(DEVTOOLS_URL) });
|
|
421
|
+
if (!devtools) return { devtools };
|
|
422
|
+
return { devtools, engineTools: engineToolFiles(), gameTools: toolFiles(`/games/${slug}/tools`), point: !!resolveUrl(POINT_URL) };
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// The engine's own dev tools, as its list names them, each only when the
|
|
426
|
+
// engine serving the page has its file.
|
|
427
|
+
function engineToolFiles() {
|
|
428
|
+
const list = resolveUrl(ENGINE_TOOLS_URL);
|
|
429
|
+
if (!list) return [];
|
|
430
|
+
try {
|
|
431
|
+
const tools = JSON.parse(readFileSync(list, 'utf8')).tools;
|
|
432
|
+
return (Array.isArray(tools) ? tools : [])
|
|
433
|
+
.map((t) => t?.url)
|
|
434
|
+
.filter((url) => typeof url === 'string' && url.startsWith('/shared/') && !!resolveUrl(url));
|
|
435
|
+
} catch {
|
|
436
|
+
return [];
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
392
440
|
const reload = new EventEmitter();
|
|
393
441
|
reload.setMaxListeners(0);
|
|
394
442
|
// The build a new session is stamped with: the version's, or, serving the
|
|
@@ -438,15 +486,16 @@ export function createStaticServer({
|
|
|
438
486
|
const tabIdentity = as
|
|
439
487
|
? { userId: `dev-local-${as}`, name: as, color: '#f472b6' }
|
|
440
488
|
: (shared ? shareGuestIdentity() : identity);
|
|
441
|
-
// The toolbox rides only when the resolved engine carries it
|
|
442
|
-
//
|
|
443
|
-
//
|
|
489
|
+
// The old toolbox rides only when the resolved engine carries it (0.2.46
|
|
490
|
+
// and earlier); a newer engine has the dev tools runtime instead.
|
|
491
|
+
// Per-request, so a bundle swap mid-session is honoured.
|
|
444
492
|
const toolbox = !shared && !!resolveUrl(TOOLBOX_URL);
|
|
445
493
|
html = injectHeadTags(html, m[1], {
|
|
446
494
|
...(tabIdentity ? { identity: tabIdentity } : {}),
|
|
447
495
|
toolbox,
|
|
496
|
+
...devtoolsFor(shared, m[1]),
|
|
448
497
|
skeleton: currentSkeleton,
|
|
449
|
-
...(shared ? { shareHost } : {}),
|
|
498
|
+
...(shared ? { shareHost, replay: shareIsOwner } : {}),
|
|
450
499
|
});
|
|
451
500
|
}
|
|
452
501
|
if (wantsNote(req)) html = injectNoteTag(html);
|
|
@@ -480,10 +529,10 @@ export function createStaticServer({
|
|
|
480
529
|
'Content-Length': buf.length,
|
|
481
530
|
'Cache-Control': 'no-store, must-revalidate',
|
|
482
531
|
'Access-Control-Allow-Origin': '*',
|
|
483
|
-
// Only a page of this server's own may frame one of its pages. The
|
|
484
|
-
//
|
|
485
|
-
//
|
|
486
|
-
//
|
|
532
|
+
// Only a page of this server's own may frame one of its pages. The dev
|
|
533
|
+
// tools runtime (shared/ui/devtools/runtime.js) takes the page that
|
|
534
|
+
// frames it as its host: a page on another site that framed it could
|
|
535
|
+
// write into the game's world and receive every pick. The maker frames
|
|
487
536
|
// live links from its own origin, which its Worker names in place of
|
|
488
537
|
// this on every page it passes on.
|
|
489
538
|
...(type === MIME['.html'] ? { 'Content-Security-Policy': "frame-ancestors 'self'" } : {}),
|
package/package.json
CHANGED