@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.
@@ -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)}
@@ -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 NEXT = /^\/(?:_studio\/(?:stats|office|codex\/[a-z0-9][a-z0-9-]{0,39}\/|confirm\/ask_[a-f0-9]{16})|[a-z0-9][a-z0-9-]{0,39}\/(?:play)?)$/;
18
- const nextOf = (url) => { const to = url.searchParams.get('to') ?? ''; return NEXT.test(to) ? to : '/_studio/stats'; };
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' ? `&amp;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' ? `&amp;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);
@@ -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.0';
2
+ export const STUDIO_VERSION_TAG = '0.51.2';