@homie-rocks/studio 0.38.0 → 0.39.0
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 +29 -0
- package/bin/homie-studio.mjs +13 -1
- package/lib/build.mjs +3 -0
- package/lib/cloudflare.mjs +2 -2
- package/lib/dev.mjs +12 -1
- package/lib/mcp-tools.mjs +10 -0
- package/lib/port-check.mjs +27 -4
- package/lib/scaffold.mjs +19 -6
- package/lib/studio.mjs +1 -1
- package/lib/template-history.json +41 -0
- package/lib/tools-build.mjs +55 -0
- package/lib/tools-cli.mjs +55 -0
- package/lib/worker-build.mjs +35 -0
- package/package.json +17 -6
- package/tools/TOOLS.md +113 -0
- package/tools/tools.ts +28 -0
- package/worker/app-records.mjs +8 -0
- package/worker/index.mjs +26 -0
- package/worker/mcp-room.mjs +61 -0
- package/worker/mcp-store.mjs +45 -0
- package/worker/mcp-tools.mjs +105 -0
- package/worker/mcp.mjs +159 -0
- package/worker/office-page.mjs +1 -1
- package/worker/office.mjs +2 -1
- package/worker/purchase-mcp.mjs +3 -3
- package/worker/tool-events.mjs +14 -0
- package/worker/tool-identity.mjs +4 -0
- package/worker/tool-webhook.mjs +28 -0
- package/worker/version.mjs +1 -1
package/worker/office.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { selling } from './extensions.mjs';
|
|
2
2
|
import { purchaseOrderIfPresent } from './shop.mjs';
|
|
3
|
+
import { toolIdentity } from './tool-identity.mjs';
|
|
3
4
|
/**
|
|
4
5
|
* THE STUDIO'S BACK OFFICE (@homie-rocks/studio 0.13.0): the studio's owner, and the owner's AI, see and run the
|
|
5
6
|
* studio's live games. Everything lives in the studio's own Worker and D1; homie.rocks stores none of it.
|
|
@@ -1196,7 +1197,7 @@ async function invitesApi(request, env, url, cat, body, who) {
|
|
|
1196
1197
|
async function api(request, env, url, cat) {
|
|
1197
1198
|
const path = url.pathname;
|
|
1198
1199
|
// An office key, the owner's signed-in browser, or the owner's own player account (a passkey).
|
|
1199
|
-
let who = await ownerAllowed(request, env, { kinds: ['office', 'session'] });
|
|
1200
|
+
let who = toolIdentity(request)?.owner ? 'office' : await ownerAllowed(request, env, { kinds: ['office', 'session'] });
|
|
1200
1201
|
if (!who && (await isOwner(request, env))) who = 'session';
|
|
1201
1202
|
if (!who) {
|
|
1202
1203
|
return json({ ok: false, error: 'owner-only', message: 'The back office is the studio owner\'s. In the studio folder, `npx --no-install homie-studio office key` gives the owner\'s AI a key; `npx --no-install homie-studio office link` signs the owner\'s browser in.' }, 401, { 'www-authenticate': 'Bearer' });
|
package/worker/purchase-mcp.mjs
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { purchaseResponse } from './purchase-response.mjs';
|
|
2
2
|
import { machineCapabilities } from './payment-capabilities.mjs';
|
|
3
3
|
/** The SDK owns protocol negotiation, JSON-RPC errors and stateless HTTP transport. */
|
|
4
|
-
import { Server } from '
|
|
5
|
-
import { WebStandardStreamableHTTPServerTransport } from '
|
|
6
|
-
import { CallToolRequestSchema, ListToolsRequestSchema } from '
|
|
4
|
+
import { Server } from 'mcp-purchase-sdk/server/index.js';
|
|
5
|
+
import { WebStandardStreamableHTTPServerTransport } from 'mcp-purchase-sdk/server/webStandardStreamableHttp.js';
|
|
6
|
+
import { CallToolRequestSchema, ListToolsRequestSchema } from 'mcp-purchase-sdk/types.js';
|
|
7
7
|
import { machineResource } from './purchase-machine.mjs';
|
|
8
8
|
export const purchaseSchema = {
|
|
9
9
|
type: 'object', required: ['kind', 'resource', 'version', 'buyer', 'claim', 'offerVersion'],
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import {CfWorkerJsonSchemaValidator} from '@modelcontextprotocol/server/validators/cf-worker';
|
|
2
|
+
import {signControl} from './office.mjs';
|
|
3
|
+
import {allowed} from './mcp-tools.mjs';
|
|
4
|
+
/** A tool must declare each external event and its schema; it cannot impersonate player input. */
|
|
5
|
+
export async function sendToolEvent(env,caller,definition,{game,room,event,data}) {
|
|
6
|
+
const declaration=definition.events?.[event];
|
|
7
|
+
if(!declaration||declaration.game!==game||!/^external:[a-z][a-z0-9_-]{0,40}$/.test(event)||!/^[A-Za-z0-9_-]{1,32}$/.test(room))throw new Error('event not declared');
|
|
8
|
+
if(!await allowed(declaration.audience??'owner',caller,env))throw new Error('event permission denied');
|
|
9
|
+
if(!new CfWorkerJsonSchemaValidator().getValidator(declaration.schema)(data).valid)throw new Error('event input invalid');
|
|
10
|
+
if(JSON.stringify(data).length>8192)throw new Error('event too large');
|
|
11
|
+
const ctl=await signControl(env,{op:'mcp-event',game,room,args:{event,data,person:caller.id}});
|
|
12
|
+
const response=await env.TABLE.get(env.TABLE.idFromName(`${game}/${room}`)).fetch(`https://table/__mcp-event?game=${game}&room=${room}`,{method:'POST',body:JSON.stringify(ctl)});
|
|
13
|
+
const result=await response.json();if(!response.ok)throw new Error('room refused event');return result;
|
|
14
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
/** Internal request authority. HTTP headers cannot manufacture this identity. */
|
|
2
|
+
const identities = new WeakMap();
|
|
3
|
+
export function withToolIdentity(request, identity) { identities.set(request, identity); return request; }
|
|
4
|
+
export function toolIdentity(request) { return identities.get(request) ?? null; }
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import {allowed,services} from './mcp-tools.mjs';
|
|
2
|
+
import {rateLimit,audit,callsPerMinute} from './mcp-store.mjs';
|
|
3
|
+
import {CfWorkerJsonSchemaValidator} from '@modelcontextprotocol/server/validators/cf-worker';
|
|
4
|
+
/** HMAC-SHA256(timestamp + '.' + delivery id + '.' + exact request body). */
|
|
5
|
+
export async function toolWebhook(request,env,{cat,definitions}) {
|
|
6
|
+
const name=new URL(request.url).pathname.split('/').pop(),tool=definitions.find(t=>t.name===name&&t.webhook);
|
|
7
|
+
const fail=(status)=>Response.json({ok:false},{status});
|
|
8
|
+
if(!tool||request.method!=='POST')return fail(404);
|
|
9
|
+
const timestamp=request.headers.get('x-studio-timestamp')??'',delivery=request.headers.get('x-studio-delivery')??'',signature=request.headers.get('x-studio-signature')??'';
|
|
10
|
+
if(!/^\d{10}$/.test(timestamp)||Math.abs(Date.now()/1000-Number(timestamp))>300||!/^[a-zA-Z0-9_-]{8,100}$/.test(delivery)||!/^[a-f0-9]{64}$/.test(signature))return fail(401);
|
|
11
|
+
const secret=env[tool.webhook.secret];if(typeof secret!=='string'||secret.length<32)return fail(503);
|
|
12
|
+
const body=await request.text();if(body.length>65536)return fail(413);
|
|
13
|
+
const key=await crypto.subtle.importKey('raw',new TextEncoder().encode(secret),{name:'HMAC',hash:'SHA-256'},false,['verify']);
|
|
14
|
+
const bytes=Uint8Array.from(signature.match(/../g),b=>parseInt(b,16));
|
|
15
|
+
if(!await crypto.subtle.verify('HMAC',key,bytes,new TextEncoder().encode(`${timestamp}.${delivery}.${body}`)))return fail(401);
|
|
16
|
+
const person=await env.DB.prepare('SELECT id,name,owner,guest FROM players WHERE id=?1').bind(tool.webhook.person).first();
|
|
17
|
+
const caller={id:person?.id,owner:person?.owner===1,client:'webhook:'+name};let outcome='denied';
|
|
18
|
+
try{
|
|
19
|
+
if(!person||person.guest||!await allowed(tool.audience,caller,env))return fail(403);
|
|
20
|
+
const limit=callsPerMinute(cat);
|
|
21
|
+
if(!await rateLimit(env,caller.client,limit)){outcome='limited';return fail(429);}
|
|
22
|
+
const value=JSON.parse(body),validate=new CfWorkerJsonSchemaValidator().getValidator(tool.inputSchema);
|
|
23
|
+
if(!validate(value).valid){outcome='invalid';return fail(400);}
|
|
24
|
+
const result=await env.DB.prepare('INSERT OR IGNORE INTO mcp_kv(key,value,expires) VALUES(?1,?2,?3)').bind(`delivery:${name}:${delivery}`,'true',Math.floor(Date.now()/1000)+600).run();
|
|
25
|
+
if(!result.meta.changes){outcome='replay';return fail(409);}
|
|
26
|
+
await tool.handler(value,services(env,cat,new URL(request.url).origin,caller,tool.namespace??tool.name,tool));outcome='ok';return Response.json({ok:true});
|
|
27
|
+
}catch{outcome='error';return fail(400);}finally{await audit(env,caller,name,outcome);}
|
|
28
|
+
}
|
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.
|
|
2
|
+
export const STUDIO_VERSION_TAG = '0.39.0';
|