@homie-rocks/studio 0.58.4 → 0.58.6
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 +31 -0
- package/lib/template-history.json +84 -0
- package/package.json +1 -1
- package/worker/app-streams.mjs +12 -2
- package/worker/build-id.mjs +1 -1
- package/worker/office.mjs +3 -2
- package/worker/routes.mjs +4 -1
- package/worker/version.mjs +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -17,6 +17,37 @@ To bring a studio up to date, tell Claude: "Upgrade my studio to the newest Homi
|
|
|
17
17
|
`npx -y @homie-rocks/studio@latest upgrade`, which shows what's new since the version your studio pins (from this
|
|
18
18
|
file) and what the upgrade would change, and changes nothing until you agree.
|
|
19
19
|
|
|
20
|
+
## [0.58.6] - 2026-10-18
|
|
21
|
+
|
|
22
|
+
**Plugin 0.58.2**
|
|
23
|
+
|
|
24
|
+
A page that no longer has access stops. It does not keep waking the records object.
|
|
25
|
+
|
|
26
|
+
### Fixed
|
|
27
|
+
|
|
28
|
+
- A live upgrade that is not allowed closes with 4403, and the page stops. A bare refusal was not a socket, so the page tried again about every two minutes and the records object stayed awake.
|
|
29
|
+
- When the role itself is refused, that close happens before the records object is woken.
|
|
30
|
+
|
|
31
|
+
### Upgrade notes
|
|
32
|
+
|
|
33
|
+
- Upgrade the studio to pick this up.
|
|
34
|
+
|
|
35
|
+
## [0.58.5] - 2026-10-18
|
|
36
|
+
|
|
37
|
+
**Plugin 0.58.2**
|
|
38
|
+
|
|
39
|
+
A signed-in app gets its live socket. Leaving that page open no longer wakes the records object.
|
|
40
|
+
|
|
41
|
+
### Fixed
|
|
42
|
+
|
|
43
|
+
- The live upgrade is returned as the records object sent it. Writing the role cookie onto that response threw, so the browser never received the socket and tried again about every two minutes.
|
|
44
|
+
- When the stored role cookie no longer works, the socket closes and the page opens a new one with the cookie it renewed. The page does not give up.
|
|
45
|
+
- Someone who opens the app is counted with the people whose sockets are already asleep.
|
|
46
|
+
|
|
47
|
+
### Upgrade notes
|
|
48
|
+
|
|
49
|
+
- Upgrade the studio to pick this up.
|
|
50
|
+
|
|
20
51
|
## [0.58.4] - 2026-10-18
|
|
21
52
|
|
|
22
53
|
**Plugin 0.58.2**
|
|
@@ -4344,6 +4344,90 @@
|
|
|
4344
4344
|
}
|
|
4345
4345
|
}
|
|
4346
4346
|
}
|
|
4347
|
+
},
|
|
4348
|
+
"0.58.5": {
|
|
4349
|
+
"files": {
|
|
4350
|
+
"CLAUDE.md": "ddf2b2a0376202f6",
|
|
4351
|
+
"README.md": "684b3cca8001673c",
|
|
4352
|
+
"HANDOFF.md": "29337e2ace95b2e7",
|
|
4353
|
+
"games/README.md": "5ad32fe80c61f117",
|
|
4354
|
+
"music/README.md": "a3dc042dadd39df8",
|
|
4355
|
+
"videos/README.md": "16babc3437d5545b",
|
|
4356
|
+
"posts/README.md": "37841aae476bd540",
|
|
4357
|
+
"site/README.md": "1dbdf22c03a1e505",
|
|
4358
|
+
"site/src/worker.mjs": "6cc5c6131eaf6fd0"
|
|
4359
|
+
},
|
|
4360
|
+
"sections": {
|
|
4361
|
+
"AGENTS.md": {
|
|
4362
|
+
"(intro)": "af7f2f732505d379",
|
|
4363
|
+
"## Version-matched authoring guides": "f6261a0e5801b96b",
|
|
4364
|
+
"## Layout": "f3a280b8c96cd068",
|
|
4365
|
+
"## Apps: one screen, roles and parts": "c10aa0441ea89702",
|
|
4366
|
+
"## Commands (all through the pinned CLI in node_modules)": "c20d90de21cde0c7",
|
|
4367
|
+
"## The site": "0cb8dd9a6f8a0096",
|
|
4368
|
+
"## Making games": "bb715272fd1a615b",
|
|
4369
|
+
"## The Game Codex and progress": "0738ef719cef3a4c",
|
|
4370
|
+
"## Art direction and models": "a48fff7bf0a5f2fc",
|
|
4371
|
+
"## Running live games (the back office)": "51ebf2ab756f6e4d",
|
|
4372
|
+
"## Servers and AI seats": "1ccfd549d2369a60",
|
|
4373
|
+
"## Selling things (the shop)": "0df5a5fe1833c624",
|
|
4374
|
+
"## Continuing a build from the Claude app": "0d68081e924ca419",
|
|
4375
|
+
"## Rules": "b2234d80f6cbe71d",
|
|
4376
|
+
"## Telling Homie": "80b46fdd9e578db8",
|
|
4377
|
+
"## Beta": "9d02638207a26e73"
|
|
4378
|
+
}
|
|
4379
|
+
},
|
|
4380
|
+
"named": {
|
|
4381
|
+
"Homie Arcade": {
|
|
4382
|
+
"sections": {
|
|
4383
|
+
"AGENTS.md": {
|
|
4384
|
+
"## Commands (all through the pinned CLI in node_modules)": "8364e05cebefcba4"
|
|
4385
|
+
}
|
|
4386
|
+
}
|
|
4387
|
+
}
|
|
4388
|
+
}
|
|
4389
|
+
},
|
|
4390
|
+
"0.58.6": {
|
|
4391
|
+
"files": {
|
|
4392
|
+
"CLAUDE.md": "ddf2b2a0376202f6",
|
|
4393
|
+
"README.md": "684b3cca8001673c",
|
|
4394
|
+
"HANDOFF.md": "29337e2ace95b2e7",
|
|
4395
|
+
"games/README.md": "5ad32fe80c61f117",
|
|
4396
|
+
"music/README.md": "a3dc042dadd39df8",
|
|
4397
|
+
"videos/README.md": "16babc3437d5545b",
|
|
4398
|
+
"posts/README.md": "37841aae476bd540",
|
|
4399
|
+
"site/README.md": "1dbdf22c03a1e505",
|
|
4400
|
+
"site/src/worker.mjs": "6cc5c6131eaf6fd0"
|
|
4401
|
+
},
|
|
4402
|
+
"sections": {
|
|
4403
|
+
"AGENTS.md": {
|
|
4404
|
+
"(intro)": "af7f2f732505d379",
|
|
4405
|
+
"## Version-matched authoring guides": "f6261a0e5801b96b",
|
|
4406
|
+
"## Layout": "f3a280b8c96cd068",
|
|
4407
|
+
"## Apps: one screen, roles and parts": "c10aa0441ea89702",
|
|
4408
|
+
"## Commands (all through the pinned CLI in node_modules)": "c20d90de21cde0c7",
|
|
4409
|
+
"## The site": "0cb8dd9a6f8a0096",
|
|
4410
|
+
"## Making games": "bb715272fd1a615b",
|
|
4411
|
+
"## The Game Codex and progress": "0738ef719cef3a4c",
|
|
4412
|
+
"## Art direction and models": "a48fff7bf0a5f2fc",
|
|
4413
|
+
"## Running live games (the back office)": "51ebf2ab756f6e4d",
|
|
4414
|
+
"## Servers and AI seats": "1ccfd549d2369a60",
|
|
4415
|
+
"## Selling things (the shop)": "0df5a5fe1833c624",
|
|
4416
|
+
"## Continuing a build from the Claude app": "0d68081e924ca419",
|
|
4417
|
+
"## Rules": "b2234d80f6cbe71d",
|
|
4418
|
+
"## Telling Homie": "80b46fdd9e578db8",
|
|
4419
|
+
"## Beta": "9d02638207a26e73"
|
|
4420
|
+
}
|
|
4421
|
+
},
|
|
4422
|
+
"named": {
|
|
4423
|
+
"Homie Arcade": {
|
|
4424
|
+
"sections": {
|
|
4425
|
+
"AGENTS.md": {
|
|
4426
|
+
"## Commands (all through the pinned CLI in node_modules)": "8364e05cebefcba4"
|
|
4427
|
+
}
|
|
4428
|
+
}
|
|
4429
|
+
}
|
|
4430
|
+
}
|
|
4347
4431
|
}
|
|
4348
4432
|
}
|
|
4349
4433
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@homie-rocks/studio",
|
|
3
|
-
"version": "0.58.
|
|
3
|
+
"version": "0.58.6",
|
|
4
4
|
"description": "A studio for games, music and video on its own Cloudflare: scaffold, multiplayer starters on the netplay contract, a port toolkit that makes existing single-player web games multiplayer, the site Worker with public rooms (Table/Lobby Durable Objects and D1, all on the free Workers plan, no payment method), a site with the hub's sections (Home, Games, Music, Videos, Rooms, Posts with feeds) in the studio's own look, an epic landing page for every game, big songs and videos in the studio's own R2 once it has storage (uploaded, checked by SHA-256, served at the same addresses), deploy, and the homie.rocks directory.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
package/worker/app-streams.mjs
CHANGED
|
@@ -11,6 +11,14 @@ END;`;}).join('\n')}
|
|
|
11
11
|
`;
|
|
12
12
|
const frame=value=>new TextEncoder().encode('data: '+JSON.stringify(value)+'\n\n');
|
|
13
13
|
export const appHub=(env,app)=>{const ns=env.APP_RECORDS??env.LOBBY;return ns.get(ns.idFromName('@app-records/'+app));};
|
|
14
|
+
/** A refused upgrade must be a socket the page can see close with 4403. A bare 403 never becomes one, so the page retries inside the eviction window and the records object stays awake. accept() plus an immediate close ends with the response. acceptWebSocket would keep a hibernating socket whose close the browser does not see. */
|
|
15
|
+
export function deniedLiveSocket(){
|
|
16
|
+
if(typeof WebSocketPair==='undefined')return new Response('role cannot read collections',{status:403});
|
|
17
|
+
const pair=new WebSocketPair(),client=pair[0]??Object.values(pair)[0],server=pair[1]??Object.values(pair)[1];
|
|
18
|
+
server.accept();
|
|
19
|
+
try{server.close(4403,'closed');}catch{/* already closed */}
|
|
20
|
+
return new Response(null,{status:101,webSocket:client,headers:{'cache-control':'no-store'}});
|
|
21
|
+
}
|
|
14
22
|
const pushedRevisions=new WeakMap();
|
|
15
23
|
export async function pushAppRecords(env,cat){
|
|
16
24
|
const apps=(cat.games??[]).filter(g=>g.kind==='app');
|
|
@@ -58,13 +66,14 @@ export class AppRecordStreams {
|
|
|
58
66
|
async open({meta,url,headers,collections=[]}){
|
|
59
67
|
if(!Array.isArray(collections)||collections.some(c=>typeof c!=='string'||!Object.hasOwn(meta.records?.collections??{},c)))return new Response('unknown collection',{status:400});
|
|
60
68
|
const s={id:crypto.randomUUID(),meta,url:new URL(url),request:new Request(url,{headers}),collections,seen:new Map()};
|
|
61
|
-
if(!await this.authorized(s))return
|
|
69
|
+
if(!await this.authorized(s))return deniedLiveSocket();
|
|
62
70
|
const session=await localSession(s.request,this.env);
|
|
63
71
|
s.app=session?.app===meta.id&&session.scope?`${meta.id}:check:${session.scope}`:meta.id;
|
|
64
72
|
s.room=s.url.searchParams.get('room')??'main';
|
|
65
73
|
for(const row of await this.revisions(s))s.seen.set(row.collection,row.revision);
|
|
66
74
|
const cookie=headers?.cookie??headers?.Cookie;
|
|
67
75
|
if(this.ctx.storage?.put)await this.ctx.storage.put('app-live:'+s.id,{app:s.app,room:s.room,collections:s.collections,seen:[...s.seen],url:String(s.url),headers:cookie?{cookie}:{},meta});
|
|
76
|
+
await this.attachHibernated();
|
|
68
77
|
const hello=()=>{this.streams.add(s);this.send(s,{collections:s.collections,presence:this.count(s),reset:true});for(const other of this.streams)if(other!==s)this.send(other,{presence:this.count(other)});};
|
|
69
78
|
// A hibernating socket sleeps while the page is open. The browser renews the short role cookie on its own request, not from this object.
|
|
70
79
|
if(typeof WebSocketPair!=='undefined'&&this.ctx.acceptWebSocket){
|
|
@@ -82,7 +91,8 @@ export class AppRecordStreams {
|
|
|
82
91
|
const changed=new Set();
|
|
83
92
|
for(const row of await this.revisions(s)){if(s.seen.get(row.collection)!==row.revision){if(row.collection==='*')for(const c of s.collections)changed.add(c);else if(s.collections.includes(row.collection))changed.add(row.collection);}s.seen.set(row.collection,row.revision);}
|
|
84
93
|
if(!changed.size&&!recheck)return;
|
|
85
|
-
|
|
94
|
+
// 1000, not 4403. The stored handoff expires; the page opens a new socket with the cookie it just renewed.
|
|
95
|
+
if(!await this.authorized(s)){this.close(s);return;}
|
|
86
96
|
this.send(s,{collections:[...changed],presence:this.count(s)});
|
|
87
97
|
}catch{this.close(s);}finally{s.busy=false;if(s.dirty){s.dirty=false;await this.pump(s);}}
|
|
88
98
|
}
|
package/worker/build-id.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// Generated by lib/build-identity.mjs; identifies the bundled runtime sources.
|
|
2
|
-
export const STUDIO_BUILD = Object.freeze({"version":"0.58.
|
|
2
|
+
export const STUDIO_BUILD = Object.freeze({"version":"0.58.6","sha256":"ffc278d1a25f0d09dfaa8c76a38f4d325971dec38e75e08b6b7fb4d7c6aad504"});
|
package/worker/office.mjs
CHANGED
|
@@ -1471,8 +1471,9 @@ export async function appRoleSession(response,request,env,meta,launch) {
|
|
|
1471
1471
|
if(ticket) {
|
|
1472
1472
|
const secure=new URL(request.url).protocol==='https:'?'; Secure':'';
|
|
1473
1473
|
const cookie=`studio_app_${meta.id}_${role}=${ticket}; Path=/${meta.id}/; Max-Age=300; HttpOnly${secure}; SameSite=Strict`;
|
|
1474
|
-
//
|
|
1475
|
-
|
|
1474
|
+
// The upgrade response's headers are immutable, and appending throws before the browser receives the socket.
|
|
1475
|
+
// The document and the renew request already set this cookie. Return the socket unchanged.
|
|
1476
|
+
if(response.status===101&&response.webSocket)return response;
|
|
1476
1477
|
response=new Response(response.body,{status:response.status,statusText:response.statusText,headers:response.headers});
|
|
1477
1478
|
response.headers.append('set-cookie',cookie);
|
|
1478
1479
|
}
|
package/worker/routes.mjs
CHANGED
|
@@ -327,9 +327,12 @@ export default async function route(request, env, ctx) {
|
|
|
327
327
|
if(request.method!=='GET')return new Response('GET required',{status:405});
|
|
328
328
|
// Cookie renewal is a Worker response. It must not wake the records object.
|
|
329
329
|
if(url.searchParams.get('renew')==='1')return apiSession(new Response(null,{status:204,headers:{'cache-control':'no-store'}}));
|
|
330
|
-
const {appHub}=await import('./app-streams.mjs');
|
|
330
|
+
const {appHub,deniedLiveSocket}=await import('./app-streams.mjs');
|
|
331
331
|
if(typeof WebSocketPair!=='undefined'){
|
|
332
332
|
if(request.headers.get('upgrade')?.toLowerCase()!=='websocket')return new Response('websocket required',{status:426,headers:{'cache-control':'no-store'}});
|
|
333
|
+
// Check before the records object. A refused upgrade is one Worker socket, closed 4403, and the page stops.
|
|
334
|
+
const access=await appAccess(request,env,meta,url);
|
|
335
|
+
if(!access.ok)return deniedLiveSocket();
|
|
333
336
|
// The object has to answer the upgrade itself. A JSON subrequest cannot carry the socket back.
|
|
334
337
|
const headers=new Headers(request.headers);
|
|
335
338
|
headers.set('x-homie-live-page',request.url);
|
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.58.
|
|
2
|
+
export const STUDIO_VERSION_TAG = '0.58.6';
|