@homie-rocks/studio 0.58.25 → 0.58.26
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 +10 -0
- package/lib/template-history.json +42 -0
- package/package.json +1 -1
- package/worker/build-id.mjs +1 -1
- package/worker/customer-mcp.mjs +4 -4
- package/worker/customer-shop.mjs +1 -1
- package/worker/mcp-result-safety.mjs +32 -0
- package/worker/mcp.mjs +6 -4
- package/worker/version.mjs +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -17,6 +17,16 @@ 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.26] - 2026-10-29
|
|
21
|
+
|
|
22
|
+
**Plugin 0.58.15**
|
|
23
|
+
|
|
24
|
+
MCP tool results never carry session cookies.
|
|
25
|
+
|
|
26
|
+
### Fixed
|
|
27
|
+
|
|
28
|
+
- `studio_checkout`, `studio_cart` and `studio_purchase` no longer return `account.cookies` (the guest `studio_player` session cookie) to the MCP caller. A guest cart still returns `account.player`. Every MCP tool result, protocol result and error detail is now scrubbed of cookies, Set-Cookie values and session tokens. The website and HTTP routes still set their own cookies as before.
|
|
29
|
+
|
|
20
30
|
## [0.58.25] - 2026-10-29
|
|
21
31
|
|
|
22
32
|
**Plugin 0.58.15**
|
|
@@ -5034,6 +5034,48 @@
|
|
|
5034
5034
|
}
|
|
5035
5035
|
}
|
|
5036
5036
|
}
|
|
5037
|
+
},
|
|
5038
|
+
"0.58.26": {
|
|
5039
|
+
"files": {
|
|
5040
|
+
"CLAUDE.md": "ddf2b2a0376202f6",
|
|
5041
|
+
"README.md": "684b3cca8001673c",
|
|
5042
|
+
"HANDOFF.md": "29337e2ace95b2e7",
|
|
5043
|
+
"games/README.md": "5ad32fe80c61f117",
|
|
5044
|
+
"music/README.md": "a3dc042dadd39df8",
|
|
5045
|
+
"videos/README.md": "16babc3437d5545b",
|
|
5046
|
+
"posts/README.md": "37841aae476bd540",
|
|
5047
|
+
"site/README.md": "1dbdf22c03a1e505",
|
|
5048
|
+
"site/src/worker.mjs": "6cc5c6131eaf6fd0"
|
|
5049
|
+
},
|
|
5050
|
+
"sections": {
|
|
5051
|
+
"AGENTS.md": {
|
|
5052
|
+
"(intro)": "af7f2f732505d379",
|
|
5053
|
+
"## Version-matched authoring guides": "f6261a0e5801b96b",
|
|
5054
|
+
"## Layout": "f3a280b8c96cd068",
|
|
5055
|
+
"## Apps: one screen, roles and parts": "2ecf713305fd26f5",
|
|
5056
|
+
"## Commands (all through the pinned CLI in node_modules)": "661cbdf5d6a9dbae",
|
|
5057
|
+
"## The site": "0cb8dd9a6f8a0096",
|
|
5058
|
+
"## Making games": "644c1e7608bb9740",
|
|
5059
|
+
"## The Game Codex and progress": "0738ef719cef3a4c",
|
|
5060
|
+
"## Art direction and models": "a48fff7bf0a5f2fc",
|
|
5061
|
+
"## Running live games (the back office)": "51ebf2ab756f6e4d",
|
|
5062
|
+
"## Servers and AI seats": "1904f45b599a33e4",
|
|
5063
|
+
"## Selling things (the shop)": "e5bd380cb77f2b11",
|
|
5064
|
+
"## Continuing a build from the Claude app": "0d68081e924ca419",
|
|
5065
|
+
"## Rules": "b2234d80f6cbe71d",
|
|
5066
|
+
"## Telling Homie": "80b46fdd9e578db8",
|
|
5067
|
+
"## Beta": "9d02638207a26e73"
|
|
5068
|
+
}
|
|
5069
|
+
},
|
|
5070
|
+
"named": {
|
|
5071
|
+
"Homie Arcade": {
|
|
5072
|
+
"sections": {
|
|
5073
|
+
"AGENTS.md": {
|
|
5074
|
+
"## Commands (all through the pinned CLI in node_modules)": "0a7b2c36414a29a0"
|
|
5075
|
+
}
|
|
5076
|
+
}
|
|
5077
|
+
}
|
|
5078
|
+
}
|
|
5037
5079
|
}
|
|
5038
5080
|
}
|
|
5039
5081
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@homie-rocks/studio",
|
|
3
|
-
"version": "0.58.
|
|
3
|
+
"version": "0.58.26",
|
|
4
4
|
"description": "Open-source tools that let your AI build and run a multiplayer game studio on its own Cloudflare: an MCP server, multiplayer starters, Workers and Durable Objects.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
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.26","sha256":"6c29636ac247b26eeba746b127f164f1266bbec37b624f1e2c2a9e522fd65e2d"});
|
package/worker/customer-mcp.mjs
CHANGED
|
@@ -11,6 +11,7 @@ import {shopRoutes} from './shop.mjs';
|
|
|
11
11
|
import {object} from './mcp-tools.mjs';
|
|
12
12
|
import {withToolIdentity} from './tool-identity.mjs';
|
|
13
13
|
import {digest} from './purchase-crypto.mjs';
|
|
14
|
+
import {withoutSessionMaterial} from './mcp-result-safety.mjs';
|
|
14
15
|
const str={type:'string'};
|
|
15
16
|
const cartSchema=object({lines:{type:'array',minItems:1,items:object({item:str,quantity:{type:'integer',minimum:1},amount:{type:'integer',minimum:0}},['item'])},game:str},['lines']);
|
|
16
17
|
export async function customerShop(env,cat,origin,caller,path,args) {
|
|
@@ -18,14 +19,13 @@ export async function customerShop(env,cat,origin,caller,path,args) {
|
|
|
18
19
|
const request=withToolIdentity(new Request(url,{method:post?'POST':'GET',headers:{origin,'content-type':'application/json'},...(post?{body:JSON.stringify(args)}:{})}),caller);
|
|
19
20
|
const response=await shopRoutes(request,env,{},url,{catalogueOf:async()=>cat});
|
|
20
21
|
if(!response)return {ok:false,error:'Shop unavailable'};
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
return {...data,...(cookies.length?{account:{cookies}}:{})};
|
|
22
|
+
// Any Set-Cookie on the internal response stays here: an MCP caller never receives session cookies.
|
|
23
|
+
return withoutSessionMaterial(await response.json());
|
|
24
24
|
}
|
|
25
25
|
export function customerTools(env,cat,origin) {
|
|
26
26
|
return [
|
|
27
27
|
{name:'studio_catalogue',description:'Browse the public shop, paid parts and app goods. Prices use the studio’s currency minor units.',audience:'public',inputSchema:object(),handler:async(_args,ctx)=>({shop:await customerShop(env,cat,origin,ctx.caller,'/api/shop'),resources:await listOffers(env,origin)})},
|
|
28
|
-
{name:'studio_cart',description:'Quote a cart for agent payment.
|
|
28
|
+
{name:'studio_cart',description:'Quote a cart for agent payment. A guest cart gets its own pre-payment account (account.player); no session cookies are returned. Approve this immutable offerVersion before purchase.',audience:'public',inputSchema:cartSchema,handler:(args,ctx)=>quoteCart(env,cat,origin,ctx.caller,args,ctx.request)},
|
|
29
29
|
{name:'studio_checkout',description:'Open the studio’s Stripe checkout for a person to approve; returns url. For a booking pass the same top-level app, hold (data.id from studio_book) and token as the venue tools: the line (the booking\'s service item × its quantity) is filled in. For goods pass lines from studio_catalogue. Uses the same cart, account, order and refund as the website.',audience:'public',inputSchema:{type:'object',properties:{...cartSchema.properties,reference:str,app:str,hold:{type:'string',pattern:'^(hold_[a-f0-9]{48}|[a-f0-9]{64})$'},token:{type:'string',pattern:'^[a-f0-9]{32,128}$'},booking:object({collection:str,slot:str,hold:str,token:str},['collection','slot'])},additionalProperties:false},handler:async(args,ctx)=>{
|
|
30
30
|
const coded=(code,message,status=400)=>Object.assign(new Error(message),{name:'StudioOperationError',status,code});
|
|
31
31
|
const {app,hold,token,...rest}=args;let body=rest;
|
package/worker/customer-shop.mjs
CHANGED
|
@@ -33,7 +33,7 @@ export async function quoteCart(env,cat,origin,caller,input,request) {
|
|
|
33
33
|
caller = {...caller,id:session.player.id};
|
|
34
34
|
}
|
|
35
35
|
const quote = await customerOffer(env,'cart',{name:lines.map(l=>`${l.item.name} × ${l.quantity}`).join(', '),lines,player:caller.id??null,shop, sale:serviceTerms({scope:'cart',amount,currency:shop.currency,automaticTax:shop.automaticTax===true,taxBehavior:shop.taxBehavior,refundWindowDays:shop.refundDays,refund:Number.isFinite(shop.refundDays)?`Studio refund window: ${shop.refundDays} days.`:undefined})});
|
|
36
|
-
return {...quote,...(session?{account:{player:session.player
|
|
36
|
+
return {...quote,...(session?{account:{player:session.player}}:{})};
|
|
37
37
|
}
|
|
38
38
|
export async function fulfillCart(env,origin,order) {
|
|
39
39
|
const manifest=JSON.parse(order.manifest),shop=manifest.shop;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A tool result goes to whoever called the MCP server, often an anonymous agent. Session material
|
|
3
|
+
* (Set-Cookie headers, cookie strings, session tokens) must never ride along: the browser and HTTP
|
|
4
|
+
* routes set cookies on their own responses, and MCP results carry none of it.
|
|
5
|
+
*/
|
|
6
|
+
const SESSION_KEY=/^(cookies?|set-?cookies?|session_?token|sessionToken)$/i;
|
|
7
|
+
const COOKIE_STRING=/^\s*[A-Za-z0-9_.-]+=[^;]*;\s*(?:[^;]*;\s*)*(?:Path|HttpOnly|Max-Age|Expires|SameSite|Secure|Domain)\b/i;
|
|
8
|
+
export function withoutSessionMaterial(value,depth=0) {
|
|
9
|
+
if(depth>32||value===null||typeof value!=='object')return typeof value==='string'&&COOKIE_STRING.test(value)?undefined:value;
|
|
10
|
+
if(Array.isArray(value))return value.map(v=>withoutSessionMaterial(v,depth+1)).filter(v=>v!==undefined);
|
|
11
|
+
const out={};
|
|
12
|
+
for(const [key,v] of Object.entries(value)){
|
|
13
|
+
if(SESSION_KEY.test(key))continue;
|
|
14
|
+
const kept=withoutSessionMaterial(v,depth+1);
|
|
15
|
+
if(kept!==undefined)out[key]=kept;
|
|
16
|
+
}
|
|
17
|
+
// An account block left with nothing but the stripped cookies is dropped too.
|
|
18
|
+
if(out.account&&typeof out.account==='object'&&!Array.isArray(out.account)&&!Object.keys(out.account).length)delete out.account;
|
|
19
|
+
return out;
|
|
20
|
+
}
|
|
21
|
+
/** MCP content text that is JSON gets the same treatment; other text is left as it is. */
|
|
22
|
+
export function textWithoutSessionMaterial(text) {
|
|
23
|
+
if(typeof text!=='string'||!/cookie|session_?token|sessionToken/i.test(text))return text;
|
|
24
|
+
try{return JSON.stringify(withoutSessionMaterial(JSON.parse(text)));}catch{return text;}
|
|
25
|
+
}
|
|
26
|
+
export function resultWithoutSessionMaterial(result) {
|
|
27
|
+
if(!result||typeof result!=='object')return result;
|
|
28
|
+
const out={...result};
|
|
29
|
+
if(Array.isArray(out.content))out.content=out.content.map(c=>c?.type==='text'?{...c,text:textWithoutSessionMaterial(c.text)}:c);
|
|
30
|
+
if(out.structuredContent!==undefined)out.structuredContent=withoutSessionMaterial(out.structuredContent);
|
|
31
|
+
return out;
|
|
32
|
+
}
|
package/worker/mcp.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {studioOrigin} from './studios-client.mjs';
|
|
2
2
|
import {studioToken,studioCaller,setStudioPolicy,studioRegistration} from './studios-auth.mjs';
|
|
3
|
+
import {withoutSessionMaterial,resultWithoutSessionMaterial} from './mcp-result-safety.mjs';
|
|
3
4
|
import {tokenCaller,toolInScope} from './connector-token.mjs';
|
|
4
5
|
import {eventMethods,eventsCapability} from './mcp-events-wire.mjs';
|
|
5
6
|
import {eventsRequest} from './mcp-events.mjs';
|
|
@@ -154,9 +155,10 @@ export async function remoteMcp(request,env,ctx,{catalogueOf,definitions=[]}) {
|
|
|
154
155
|
if(!toolInScope(fresh,tool.name)||!await allowed(tool.audience,fresh,env)){outcome='denied';throw new Error('Permission denied');}
|
|
155
156
|
try{await rate(fresh);}catch(error){outcome='limited';throw error;}
|
|
156
157
|
const context=services(env,cat,origin,fresh,(tool.namespace??tool.name)+(fresh.testScope?':check:'+fresh.testScope:''),tool);
|
|
157
|
-
if(tool.price) {const result=await selling.paidTool(env,cat,origin,tool,args,context,extra);outcome=result.isError?'error':'ok';return result;}
|
|
158
|
-
if(tool.protocol) {const result=await tool.handler(args,{...context,paymentExtra:extra});outcome=result.isError?'error':'ok';return result;}
|
|
159
|
-
|
|
158
|
+
if(tool.price) {const result=resultWithoutSessionMaterial(await selling.paidTool(env,cat,origin,tool,args,context,extra));outcome=result.isError?'error':'ok';return result;}
|
|
159
|
+
if(tool.protocol) {const result=resultWithoutSessionMaterial(await tool.handler(args,{...context,paymentExtra:extra}));outcome=result.isError?'error':'ok';return result;}
|
|
160
|
+
// No tool result carries cookies or session tokens back to the MCP caller.
|
|
161
|
+
const value=withoutSessionMaterial(await (await import('./app-tools.mjs')).invokeAppTool(tool,args,{...context,request}));
|
|
160
162
|
const text=JSON.stringify({kind:'untrusted-tool-data',tool:tool.name,data:value});
|
|
161
163
|
outcome='ok';return {structuredContent:{kind:'untrusted-tool-data',tool:tool.name,data:value??null},content:[{type:'text',text}]};
|
|
162
164
|
}catch(error){if(error?.code===-32042)throw error;
|
|
@@ -164,7 +166,7 @@ export async function remoteMcp(request,env,ctx,{catalogueOf,definitions=[]}) {
|
|
|
164
166
|
const message=shown?error.message:freshErrorMessage(error,env,caller);
|
|
165
167
|
// Stable, machine-readable refusal: {kind:'studio-error',error:{code,message,status}}. Codes only ever get added.
|
|
166
168
|
const code=typeof error?.code==='string'&&/^[a-z0-9_-]{1,64}$/.test(error.code)?error.code:outcome==='denied'?'forbidden':outcome==='limited'?'rate_limited':error?.message==='Input does not match the tool schema'?'invalid_input':error?.name==='StudioOperationError'?({400:'invalid_request',403:'forbidden',404:'not_found',409:'conflict'}[error.status]??'refused'):'internal_error';
|
|
167
|
-
return {isError:true,structuredContent:{kind:'studio-error',tool:tool?.name??null,error:{code,message,...(Number.isInteger(error?.status)?{status:error.status}:{}),...(shown&&error?.details&&typeof error.details==='object'?{details:error.details}:{})}},content:[{type:'text',text:message}]};}
|
|
169
|
+
return {isError:true,structuredContent:{kind:'studio-error',tool:tool?.name??null,error:{code,message,...(Number.isInteger(error?.status)?{status:error.status}:{}),...(shown&&error?.details&&typeof error.details==='object'?{details:withoutSessionMaterial(error.details)}:{})}},content:[{type:'text',text:message}]};}
|
|
168
170
|
finally{
|
|
169
171
|
try { await audit(env,caller,tool.name,outcome); }
|
|
170
172
|
finally {
|
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.26';
|