@homie-rocks/studio 0.51.0 → 0.51.2
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 +32 -0
- package/README.md +15 -4
- package/bin/homie-studio.mjs +7 -7
- package/guides/app/SKILL.md +14 -0
- package/guides/game/RULES.md +26 -0
- package/guides/office/SKILL.md +9 -0
- package/guides/studio-setup/SKILL.md +9 -1
- package/lib/app-preset-history.json +840 -0
- package/lib/build.mjs +4 -2
- package/lib/cloudflare.mjs +19 -10
- package/lib/dev.mjs +1 -1
- package/lib/directory.mjs +22 -5
- package/lib/giving-preset.mjs +9 -0
- package/lib/mcp-tools.mjs +1 -1
- package/lib/office.mjs +2 -1
- package/lib/scaffold.mjs +7 -4
- package/lib/shop.mjs +2 -1
- package/lib/template-history.json +90 -0
- package/lib/upgrade-app-presets.mjs +49 -0
- package/lib/upgrade.mjs +2 -1
- package/mcp/ui/build.js +3 -2
- package/package.json +1 -1
- package/rules/correction-path.ts +35 -0
- package/rules/map-index.ts +4 -4
- package/rules/view.ts +196 -28
- package/site/SITE.md +13 -0
- package/tools/TOOLS.md +9 -0
- package/worker/arrival.mjs +2 -1
- package/worker/build-id.mjs +1 -1
- package/worker/directory-choice.mjs +9 -0
- package/worker/index.mjs +18 -9
- package/worker/mcp.mjs +2 -2
- package/worker/pages.mjs +3 -2
- package/worker/records-page.mjs +1 -1
- package/worker/signin-target.mjs +8 -0
- package/worker/site.mjs +2 -0
- package/worker/stats-page.mjs +6 -3
- package/worker/version.mjs +1 -1
package/worker/records-page.mjs
CHANGED
|
@@ -9,5 +9,5 @@ export function recordsPage(meta,url,cat={},shop=null) {
|
|
|
9
9
|
const tags=Object.entries(playerProperties(cat,meta,{origin:url.origin})).map(([name,content])=>`<meta name="${esc(name)}" content="${esc(content)}">`).join('');
|
|
10
10
|
const boot={kind:'app',game:meta.id,screen:url.pathname.endsWith('/tv'),shop};
|
|
11
11
|
const source=`/${meta.id}/__game/?${params}`;
|
|
12
|
-
return new Response(`<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>${esc(meta.name)}</title>${tags}<style>${SHOP_SHELL_CSS}html,body{margin:0;height:100%;background:#10151c;color:white;font:18px system-ui}iframe{border:0;width:100%;height:100%;display:block}[data-arrive]{position:fixed;inset:0;display:grid;place-items:center;background:#10151c}[data-arrive][hidden]{display:none;opacity:0;visibility:hidden}</style></head><body><div data-arrive>Opening ${esc(meta.name)}…</div><iframe class="game" title="${esc(meta.name)}" sandbox="allow-scripts allow-forms allow-modals allow-popups" allow="autoplay *; fullscreen *"></iframe><script>window.__HOMIE_APP=${JSON.stringify(boot).replace(/</g,'\\u003c')};window.__HOMIE_PLAY=window.__HOMIE_APP;window.__shell={room:new URLSearchParams(location.search).get('room')||'main',link:{state:'connecting'},arrival:{phase:'loading'}};</script><script>${APP_SHELL_JS}</script><script>${SHOP_SHELL_JS}</script><script>const frame=document.querySelector('iframe');frame.addEventListener('load',()=>{if(!frame.getAttribute('src'))return;requestAnimationFrame(()=>requestAnimationFrame(()=>{document.querySelector('[data-arrive]').hidden=true;window.__shell.link.state='online';window.__shell.arrival.phase='done';}));});frame.src=${JSON.stringify(source).replace(/</g,'\\u003c')};</script></body></html>`,{headers:{'content-type':'text/html;charset=utf-8','cache-control':'no-store, no-transform','x-frame-options':'SAMEORIGIN'}});
|
|
12
|
+
return new Response(`<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>${esc(meta.name)}</title>${tags}<style>${SHOP_SHELL_CSS}html,body{margin:0;height:100%;background:#10151c;color:white;font:18px system-ui}iframe{border:0;width:100%;height:100%;display:block}[data-arrive]{position:fixed;inset:0;display:grid;place-items:center;background:#10151c}[data-arrive][hidden]{display:none;opacity:0;visibility:hidden}</style></head><body><div data-arrive>Opening ${esc(meta.name)}…</div><iframe class="game" title="${esc(meta.name)}" sandbox="allow-scripts allow-forms allow-modals allow-popups allow-downloads" allow="autoplay *; fullscreen *; web-share *"></iframe><script>window.__HOMIE_APP=${JSON.stringify(boot).replace(/</g,'\\u003c')};window.__HOMIE_PLAY=window.__HOMIE_APP;window.__shell={room:new URLSearchParams(location.search).get('room')||'main',link:{state:'connecting'},arrival:{phase:'loading'}};</script><script>${APP_SHELL_JS}</script><script>${SHOP_SHELL_JS}</script><script>const frame=document.querySelector('iframe');frame.addEventListener('load',()=>{if(!frame.getAttribute('src'))return;requestAnimationFrame(()=>requestAnimationFrame(()=>{document.querySelector('[data-arrive]').hidden=true;window.__shell.link.state='online';window.__shell.arrival.phase='done';}));});frame.src=${JSON.stringify(source).replace(/</g,'\\u003c')};</script></body></html>`,{headers:{'content-type':'text/html;charset=utf-8','cache-control':'no-store, no-transform','x-frame-options':'SAMEORIGIN'}});
|
|
13
13
|
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** Same-site destinations, including app routes and query strings. */
|
|
2
|
+
export function signinTarget(value, fallback = '/_studio/stats') {
|
|
3
|
+
if (typeof value !== 'string' || !value.startsWith('/') || /^\/\//.test(value) || /[\\\u0000-\u0020]/.test(value)) return fallback;
|
|
4
|
+
try {
|
|
5
|
+
const target = new URL(value, 'https://studio.invalid');
|
|
6
|
+
return target.origin === 'https://studio.invalid' ? target.pathname + target.search + target.hash : fallback;
|
|
7
|
+
} catch { return fallback; }
|
|
8
|
+
}
|
package/worker/site.mjs
CHANGED
|
@@ -107,6 +107,8 @@ img,video{display:block;max-width:100%}
|
|
|
107
107
|
.hero-media{position:absolute;inset:0;z-index:-2;overflow:hidden;background:
|
|
108
108
|
radial-gradient(60% 50% at 70% 30%,color-mix(in srgb,var(--glow) 26%,transparent),transparent 70%),
|
|
109
109
|
radial-gradient(55% 45% at 25% 70%,color-mix(in srgb,var(--hot) 24%,transparent),transparent 70%),var(--bg)}
|
|
110
|
+
.hero-media iframe{pointer-events:none}
|
|
111
|
+
.hero-media iframe[data-interactive]{pointer-events:auto}
|
|
110
112
|
.hero-media img,.hero-media video{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;object-position:var(--focus,50% 50%)}
|
|
111
113
|
/* The studio's own colour over the picture, so a bright cover still carries the words (game.json landing.hero.tint, 0 to 80). */
|
|
112
114
|
.hero-media::before{content:"";position:absolute;inset:0;z-index:1;pointer-events:none;background:color-mix(in srgb,var(--bg) var(--tint,16%),transparent)}
|
package/worker/stats-page.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { signinTarget } from './signin-target.mjs';
|
|
1
2
|
/**
|
|
2
3
|
* The studio owner's private stats page (/_studio/stats) and its one-time sign-in (/_studio/signin).
|
|
3
4
|
*
|
|
@@ -14,8 +15,8 @@ import { CODEX_SCRIPT } from './codex-script.mjs';
|
|
|
14
15
|
* Where a sign-in may send the owner next: the stats, one game's codex, the back office, an ask waiting for the
|
|
15
16
|
* owner's tap, or one of the studio's games (a private game's own secret road for the owner's phone).
|
|
16
17
|
*/
|
|
17
|
-
const
|
|
18
|
-
const
|
|
18
|
+
const nextOf = (url) => signinTarget(url.searchParams.get('to'));
|
|
19
|
+
const SIGNIN_SCRIPT = "document.querySelector('form').requestSubmit();";
|
|
19
20
|
const signinWord = (to) => (to.startsWith('/_studio/codex/') ? 'Open the codex' : to === '/_studio/office' ? 'Open the office' : to.startsWith('/_studio/confirm/') ? 'Sign in and see what your AI asks' : to.startsWith('/_studio/stats') ? 'Open the stats' : 'Sign in and play');
|
|
20
21
|
let scriptHash = null;
|
|
21
22
|
async function codexScriptHash() {
|
|
@@ -135,9 +136,11 @@ export async function ownerRoutes(request, env, url, { catalogueOf }) {
|
|
|
135
136
|
const key = url.searchParams.get('k') ?? '';
|
|
136
137
|
if (request.method === 'GET') {
|
|
137
138
|
// A GET spends nothing: a chat app's link preview must not use up the owner's one-time link.
|
|
139
|
+
const bytes = new Uint8Array(await crypto.subtle.digest('SHA-256', new TextEncoder().encode(SIGNIN_SCRIPT)));
|
|
140
|
+
const hash = btoa(String.fromCharCode(...bytes));
|
|
138
141
|
return page('Sign in · stats', `<h1>${esc(cat.studio?.name ?? 'Studio')} stats</h1>
|
|
139
142
|
<p class="dim">Sign this browser in as the studio's owner: the private stats and office, and the owner's tools in your own games. The link works once.</p>
|
|
140
|
-
<form method="post" action="/_studio/signin?k=${esc(encodeURIComponent(key))}${nextOf(url) !== '/_studio/stats' ? `&to=${esc(encodeURIComponent(nextOf(url)))}` : ''}"><button type="submit">${signinWord(nextOf(url))}</button></form
|
|
143
|
+
<form method="post" action="/_studio/signin?k=${esc(encodeURIComponent(key))}${nextOf(url) !== '/_studio/stats' ? `&to=${esc(encodeURIComponent(nextOf(url)))}` : ''}"><button type="submit">${signinWord(nextOf(url))}</button></form><script>${SIGNIN_SCRIPT}</script>`, 200, { 'content-security-policy': PRIVATE_HEADERS['content-security-policy'].replace("default-src 'none';", `default-src 'none'; script-src 'sha256-${hash}';`) });
|
|
141
144
|
}
|
|
142
145
|
if (request.method !== 'POST' || !sameOrigin(request, url)) return page('Not allowed', '<h1>Not allowed</h1>', 403);
|
|
143
146
|
const session = await spendSignin(env, key);
|
package/worker/version.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
/** The @homie-rocks/studio version this Worker code is (package.json's; a test keeps them equal). Cache-busts /_homie/site.js. */
|
|
2
|
-
export const STUDIO_VERSION_TAG = '0.51.
|
|
2
|
+
export const STUDIO_VERSION_TAG = '0.51.2';
|