@homie-rocks/studio 0.51.4 → 0.51.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 +49 -0
- package/README.md +10 -29
- package/apps/APPS.md +16 -1
- package/bin/homie-studio.mjs +28 -30
- package/guides/app/NEEDS.md +160 -0
- package/guides/app/SKILL.md +20 -4
- package/guides/art/SKILL.md +68 -140
- package/guides/art/scripts/art.mjs +6 -79
- package/guides/models/SKILL.md +73 -177
- package/guides/models/references/RIGHTS.md +1 -1
- package/guides/models/scripts/models.mjs +7 -288
- package/guides/music/SKILL.md +74 -183
- package/guides/music/references/COMPOSITION.md +21 -26
- package/guides/music/references/GAME-AUDIO.md +3 -3
- package/guides/music/references/RIGHTS.md +12 -15
- package/guides/music/scripts/music.mjs +11 -202
- package/guides/parts/SKILL.md +1 -1
- package/guides/shop/SKILL.md +68 -77
- package/guides/sound/SKILL.md +70 -1
- package/guides/sound/scripts/player/sound.js +233 -0
- package/guides/studio-setup/SKILL.md +11 -27
- package/guides/studio-setup/references/OFFICIAL-MEDIA.md +66 -0
- package/guides/style/SKILL.md +66 -3
- package/guides/video/SKILL.md +83 -0
- package/guides/video/references/DELIVERY.md +73 -0
- package/guides/video/references/HONESTY.md +40 -0
- package/guides/video/references/METHOD.md +144 -0
- package/guides/video/references/RECORD.md +137 -0
- package/guides/video/references/TRAILER.md +154 -0
- package/guides/video/references/examples/site-walkthrough.json +14 -0
- package/guides/video/references/examples/studio-play-phone.json +15 -0
- package/guides/video/references/examples/studio-play.json +17 -0
- package/guides/video/scripts/capture-game.mjs +175 -0
- package/guides/video/scripts/clock.js +136 -0
- package/guides/video/scripts/drawover/film.js +58 -0
- package/guides/video/scripts/drawover/index.html +9 -0
- package/guides/video/scripts/drawover/look.js +115 -0
- package/guides/video/scripts/drawover/shots.json +12 -0
- package/guides/video/scripts/lib/browser.mjs +87 -0
- package/guides/video/scripts/lib/frames.mjs +170 -0
- package/guides/video/scripts/lib/remix.mjs +159 -0
- package/guides/video/scripts/qa.mjs +127 -0
- package/guides/video/scripts/record-fixed.mjs +403 -0
- package/guides/video/scripts/record-page.mjs +477 -0
- package/guides/video/scripts/tap.js +108 -0
- package/guides/video/scripts/video.mjs +888 -0
- package/lib/app-needs.mjs +86 -18
- package/lib/app-ready.mjs +21 -0
- package/lib/build.mjs +12 -9
- package/lib/check.mjs +16 -7
- package/lib/cloudflare.mjs +6 -11
- package/lib/codex.mjs +7 -5
- package/lib/dev.mjs +1 -2
- package/lib/doctor.mjs +10 -54
- package/lib/mcp-tools.mjs +42 -21
- package/lib/mcp.mjs +1 -1
- package/lib/media-import.mjs +41 -0
- package/lib/media-workflows.mjs +13 -0
- package/lib/needs-headless.mjs +34 -0
- package/lib/official-media.mjs +15 -0
- package/lib/rules-build.mjs +13 -5
- package/lib/scaffold.mjs +2 -2
- package/lib/shop-links.mjs +81 -19
- package/lib/shop-listen.mjs +43 -0
- package/lib/shop-local.mjs +1 -1
- package/lib/shop-mcp.mjs +58 -0
- package/lib/stripe-connect.mjs +36 -15
- package/lib/template-history.json +87 -0
- package/lib/upgrade-media.mjs +13 -0
- package/lib/upgrade.mjs +10 -0
- package/package.json +1 -1
- package/parts/PARTS.md +1 -1
- package/rules/view.ts +4 -0
- package/shop/SHOP.md +110 -34
- package/worker/app-records.mjs +1 -0
- package/worker/build-id.mjs +1 -1
- package/worker/event-resources.mjs +9 -3
- package/worker/index.mjs +5 -1
- package/worker/local-test.mjs +4 -0
- package/worker/mcp-events.mjs +2 -2
- package/worker/mcp-tools.mjs +19 -1
- package/worker/mcp.mjs +1 -1
- package/worker/purchase-reconciliation.mjs +16 -1
- package/worker/shop-links.mjs +1 -1
- package/worker/shop-rules.mjs +3 -0
- package/worker/shop-subscriptions.mjs +2 -0
- package/worker/shop.mjs +58 -8
- package/worker/stripe-ownership.mjs +7 -0
- package/worker/version.mjs +1 -1
- package/guides/models/references/models.json +0 -175
- package/guides/music/scripts/lib/eleven.mjs +0 -260
- package/lib/projects-env.mjs +0 -68
- package/lib/projects.mjs +0 -311
- package/lib/provider-key.mjs +0 -21
package/lib/shop-mcp.mjs
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/** Pure, incremental Stripe plan. The owner's AI supplies public API reads; no Stripe transport here. */
|
|
2
|
+
import {createHash} from 'node:crypto';
|
|
3
|
+
import {existsSync, readFileSync, mkdirSync, writeFileSync} from 'node:fs';
|
|
4
|
+
import {join} from 'node:path';
|
|
5
|
+
import {readShop, SHOP_HOOK_EVENTS} from './shop.mjs';
|
|
6
|
+
import {readStudio, siteUrl} from './studio.mjs';
|
|
7
|
+
import {reconcilePaymentLinks} from './shop-links.mjs';
|
|
8
|
+
import {fullerReason, LINKS_STRIPE_VERSION} from '../worker/shop-links.mjs';
|
|
9
|
+
const hash = x => createHash('sha256').update(JSON.stringify(x)).digest('hex');
|
|
10
|
+
export const readKey = (path, params) => JSON.stringify([path,Object.fromEntries(Object.entries(params).sort(([a],[b])=>a.localeCompare(b)))]);
|
|
11
|
+
export async function shopMcpPlan(root, {observations = [], mode = 'test', url, slots, verify = false} = {}) {
|
|
12
|
+
const checked=readShop(root);
|
|
13
|
+
if(!checked.ok || checked.absent) return {ok:false,why:'Create and check shop.json first.'};
|
|
14
|
+
if(!['test','live'].includes(mode))throw new Error('Choose test or live mode');
|
|
15
|
+
const reason=fullerReason(checked.shop);
|
|
16
|
+
if(reason)return {ok:false,why:reason+' Adjust those settings to use Payment Links; no key is required.'};
|
|
17
|
+
const studio=readStudio(root), address=url??siteUrl(root);
|
|
18
|
+
if(!address)return {ok:false,needs:'site',why:'Supply the local test URL, or deploy the studio before planning its public checkout links.'};
|
|
19
|
+
const site=new URL(address).origin;
|
|
20
|
+
const local=new URL(site).protocol==='http:';
|
|
21
|
+
if(local && (mode!=='test' || !/^(localhost|127\.0\.0\.1|10\.\d+\.\d+\.\d+|192\.168\.\d+\.\d+|172\.(1[6-9]|2\d|3[01])\.\d+\.\d+)$/.test(new URL(site).hostname)))throw new Error('Local checkout requires a private HTTP address and test mode');
|
|
22
|
+
if(!local && new URL(site).protocol!=='https:')throw new Error('Use the studio HTTPS address');
|
|
23
|
+
const bookingSlots=slots??(existsSync(join(root,'.studio','booking-slots.json'))?JSON.parse(readFileSync(join(root,'.studio','booking-slots.json'),'utf8')):[]);
|
|
24
|
+
const slug=local?'local-'+createHash('sha256').update(root).digest('hex').slice(0,16):studio.slug;
|
|
25
|
+
// Reject secrets before persisting observations. Webhook creation is never part of this plan.
|
|
26
|
+
if(/(?:whsec_|[rs]k_(?:test|live)_)/.test(JSON.stringify(observations)))throw new Error('Supply only public catalog reads, never credentials or webhook creation responses');
|
|
27
|
+
const api=async(method,path,params={},idempotency)=>{
|
|
28
|
+
if(method!=='GET')throw {next:{method,path,params,...(idempotency?{idempotency}:{}),retry:'Read fresh before retrying; updates set desired fields and are idempotent.'}};
|
|
29
|
+
const found=observations.find(o=>readKey(o.path,o.params??{})===readKey(path,params));
|
|
30
|
+
if(!found)throw {next:{method,path,params}};
|
|
31
|
+
const body=found.body;
|
|
32
|
+
if(!body || body.error || body.livemode !== undefined && body.livemode !== (mode==='live') || body.data?.some(o=>o.livemode !== undefined && o.livemode !== (mode==='live')))throw new Error('Stripe read has an error or wrong mode');
|
|
33
|
+
return body;
|
|
34
|
+
};
|
|
35
|
+
const base={command:verify?'shop verify':'shop plan',mode,site,apiVersion:LINKS_STRIPE_VERSION,
|
|
36
|
+
webhook:{method:'POST',path:'/v1/webhook_endpoints',private:true,params:{url:`${site}/api/shop/hook`,enabled_events:SHOP_HOOK_EVENTS,api_version:LINKS_STRIPE_VERSION,metadata:{homie:'shop-v1'},description:'Studio shop'}}};
|
|
37
|
+
try {
|
|
38
|
+
const catalog=await reconcilePaymentLinks({api,shop:checked.shop,slug,site,mode,bookingSlots,verifyObjects:true});
|
|
39
|
+
if(checked.shop.items.some(item=>item.interval) && !/^https:\/\/billing\.stripe\.com\//.test(catalog.config.portal??''))throw new Error('Invalid Stripe portal URL');
|
|
40
|
+
const result={...base,ok:true,verified:true,verifiedAt:new Date().toISOString(),...catalog,local, fingerprint:hash({shop:checked.shop,bookingSlots,site,mode,catalog})};
|
|
41
|
+
return result;
|
|
42
|
+
} catch(e) {
|
|
43
|
+
if(!e.next)throw e;
|
|
44
|
+
return {...base,ok:!verify,verified:false,next:e.next,instructions:e.next.method==='GET'?'Read through Stripe MCP; append {path,params,body} to observations, including every pagination page.':'Perform this exact operation through Stripe MCP. Then discard observations and read fresh before planning again. Do not create the webhook through MCP.'};
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
export async function saveMcpVerification(root, options) {
|
|
48
|
+
const result=await shopMcpPlan(root,{...options,verify:true});
|
|
49
|
+
if(result.verified) {
|
|
50
|
+
if(result.local) {
|
|
51
|
+
const {saveLocalStripe}=await import('./shop-listen.mjs');
|
|
52
|
+
saveLocalStripe(root,{STRIPE_CHECKOUT_MODE:'links',STRIPE_SHOP_LINKS:'base64:'+Buffer.from(JSON.stringify(result.config)).toString('base64')});
|
|
53
|
+
}
|
|
54
|
+
mkdirSync(join(root,'.studio'),{recursive:true});
|
|
55
|
+
writeFileSync(join(root,'.studio','stripe-mcp.json'),JSON.stringify({options,result},null,2));
|
|
56
|
+
}
|
|
57
|
+
return result;
|
|
58
|
+
}
|
package/lib/stripe-connect.mjs
CHANGED
|
@@ -72,6 +72,10 @@ export function completionArgs(step) {
|
|
|
72
72
|
}
|
|
73
73
|
|
|
74
74
|
export async function shopConnect(root, options = {}) {
|
|
75
|
+
const fail=(...args)=>{
|
|
76
|
+
const result=failure(...args);
|
|
77
|
+
return options.webhookOnly?{...result,command:'shop webhook',why:result.why.replaceAll('shop connect','shop webhook')}:result;
|
|
78
|
+
};
|
|
75
79
|
if (options.manual || options.fromClipboard) {
|
|
76
80
|
const result = await shopConnectManual(root, options);
|
|
77
81
|
if (result.ok) rmSync(join(receiptDir(root, options.storage ?? receiptHome()), 'test.json'), { force: true });
|
|
@@ -81,19 +85,32 @@ export async function shopConnect(root, options = {}) {
|
|
|
81
85
|
storage = receiptHome(), wrangler = null } = options;
|
|
82
86
|
const mode = live ? 'live' : 'test';
|
|
83
87
|
const checked = readShop(root);
|
|
84
|
-
if (!checked.ok || checked.absent) return
|
|
85
|
-
if (options.managed === true && checked.shop.till !== 'stripe-managed') return
|
|
88
|
+
if (!checked.ok || checked.absent) return fail('shop', 'Create and check shop.json first: homie-studio shop init, then shop check.');
|
|
89
|
+
if (options.managed === true && checked.shop.till !== 'stripe-managed') return fail('shop-settings', 'For Managed Payments, set shop.json till to stripe-managed and deploy that choice before connecting. Stripe’s own eligibility and terms still apply.');
|
|
90
|
+
let catalog, mcpAccount;
|
|
91
|
+
if (options.webhookOnly) {
|
|
92
|
+
try {
|
|
93
|
+
const {saveMcpVerification} = await import('./shop-mcp.mjs');
|
|
94
|
+
const saved=JSON.parse(readFileSync(join(root,'.studio','stripe-mcp.json'),'utf8'));
|
|
95
|
+
if(!saved.result?.verifiedAt || Date.now()-Date.parse(saved.result.verifiedAt)>15*60_000) return fail('verify','Run shop verify with fresh Stripe MCP reads before installing the webhook (verification is valid for 15 minutes).');
|
|
96
|
+
const verified=await saveMcpVerification(root,saved.options);
|
|
97
|
+
if(!verified.verified || verified.local || verified.mode!==mode || verified.site!==new URL(siteUrl(root)).origin) return fail('verify','Run shop verify with fresh Stripe MCP reads for this site and mode first.');
|
|
98
|
+
mcpAccount=saved.options.account;
|
|
99
|
+
if(!/^acct_[A-Za-z0-9]+$/.test(mcpAccount??''))return fail('account','Verify with the Stripe MCP account ID first.');
|
|
100
|
+
catalog={config:verified.config,retire:verified.retire};
|
|
101
|
+
} catch { return fail('verify','Run shop verify with fresh Stripe MCP reads for this site first.'); }
|
|
102
|
+
}
|
|
86
103
|
const id = hash(realpathSync(root)).slice(0, 24);
|
|
87
|
-
const profile = `homie-${id}`;
|
|
104
|
+
const profile = options.profile ?? `homie-${id}`;
|
|
88
105
|
const dir = join(storage, id);
|
|
89
106
|
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
90
107
|
// Reuse the toolkit’s process-aware lock, in the private receipt folder. Dead processes do not strand setup.
|
|
91
108
|
const lock = lockDeploy(dir);
|
|
92
|
-
if (!lock.ok) return
|
|
109
|
+
if (!lock.ok) return fail('busy', 'Stripe connection is already running for this studio. Wait for that command to finish.');
|
|
93
110
|
try {
|
|
94
111
|
const cli = (args) => exec(['--project-name', profile, ...args], { cwd: root });
|
|
95
112
|
const installed = await cli(['--version']);
|
|
96
|
-
if (installed.code !== 0) return
|
|
113
|
+
if (installed.code !== 0) return fail('stripe-cli', 'Install Stripe’s official CLI, then run shop connect again.', { next: ['npm install -g @stripe/cli@latest'] });
|
|
97
114
|
|
|
98
115
|
// A read both checks existing credentials and lets Stripe refresh its OAuth session itself.
|
|
99
116
|
let probe = renew ? { code: 1 } : await cli(['get', '/v1/products', '-d', 'limit=1', ...(live ? ['--live'] : [])]);
|
|
@@ -102,21 +119,25 @@ export async function shopConnect(root, options = {}) {
|
|
|
102
119
|
const login = json(start.stdout);
|
|
103
120
|
const complete = completionArgs(login?.next_step);
|
|
104
121
|
if (start.code !== 0 || !stripeURL(login?.browser_url) || !complete || !/^[a-zA-Z0-9 -]{1,100}$/.test(login?.verification_code ?? '')) {
|
|
105
|
-
return
|
|
122
|
+
return fail('stripe-login', 'Stripe could not start browser approval. Update the official Stripe CLI and try shop connect again. An account administrator may need to enable CLI access in Stripe’s MCP and CLI access settings.' + stripeReason(start, 'login'));
|
|
106
123
|
}
|
|
107
124
|
log(`Approve your studio’s Stripe account in your browser. Pairing code: ${login.verification_code}. ${login.browser_url}`);
|
|
108
125
|
open(login.browser_url);
|
|
109
126
|
const finished = await cli(complete);
|
|
110
|
-
if (finished.code !== 0) return
|
|
127
|
+
if (finished.code !== 0) return fail('stripe-login', 'Stripe approval did not finish. Run shop connect again when ready.' + stripeReason(finished, 'login completion'));
|
|
111
128
|
|
|
112
129
|
probe = await cli(['get', '/v1/products', '-d', 'limit=1', ...(live ? ['--live'] : [])]);
|
|
113
|
-
if (probe.code !== 0 || !Array.isArray(json(probe.stdout)?.data)) return
|
|
130
|
+
if (probe.code !== 0 || !Array.isArray(json(probe.stdout)?.data)) return fail('stripe-access', 'Stripe is signed in but cannot read Products in the requested mode. Review the account, sandbox and permissions in Stripe, then run shop connect --renew again.' + stripeReason(probe, 'GET /v1/products'));
|
|
131
|
+
}
|
|
132
|
+
if (options.webhookOnly) {
|
|
133
|
+
const accountResult=await cli(['get','/v1/account',...(live?['--live']:[])]);
|
|
134
|
+
if(accountResult.code!==0 || json(accountResult.stdout)?.id!==mcpAccount) return fail('account','Stripe CLI and MCP must select the same account. Verify with the MCP account ID first.');
|
|
114
135
|
}
|
|
115
136
|
const site = siteUrl(root);
|
|
116
|
-
if (!site) return
|
|
137
|
+
if (!site) return fail('deploy', 'Stripe is connected on this computer. Deploy the studio with studio_deploy (Cloudflare approval first if needed), then run shop connect again.', { mode, connected: true });
|
|
117
138
|
let target;
|
|
118
|
-
try { target = new URL(site); } catch { return
|
|
119
|
-
if (target.protocol !== 'https:' || target.username || target.password || /^(localhost|127\.|\[::1\])/.test(target.hostname)) return
|
|
139
|
+
try { target = new URL(site); } catch { return fail('deploy', 'Deploy the studio to a public HTTPS address, then run shop connect again.'); }
|
|
140
|
+
if (target.protocol !== 'https:' || target.username || target.password || /^(localhost|127\.|\[::1\])/.test(target.hostname)) return fail('deploy', 'Deploy the studio to a public HTTPS address, then run shop connect again.');
|
|
120
141
|
const api = async (method, path, params, idempotency) => {
|
|
121
142
|
const args = [method.toLowerCase(), path, '--stripe-version', LINKS_STRIPE_VERSION, ...(live ? ['--live'] : []), ...(idempotency ? ['--idempotency', idempotency] : [])];
|
|
122
143
|
for (const [k, v] of formEncode(params)) args.push('-d', `${k}=${v}`);
|
|
@@ -128,20 +149,20 @@ export async function shopConnect(root, options = {}) {
|
|
|
128
149
|
return body;
|
|
129
150
|
};
|
|
130
151
|
const reason = fullerReason(checked.shop);
|
|
131
|
-
if (reason) return
|
|
152
|
+
if (reason) return fail('fuller-checkout', reason, { mode, connected: true });
|
|
132
153
|
const studio = readStudio(root);
|
|
133
154
|
const w = wrangler ?? runner(root, studio.cloudflare?.accountId ? { CLOUDFLARE_ACCOUNT_ID: studio.cloudflare.accountId } : {});
|
|
134
155
|
const receiptFile = join(dir, `${mode}.json`);
|
|
135
156
|
const receipt = existsSync(receiptFile) ? json(readFileSync(receiptFile, 'utf8')) : null;
|
|
136
|
-
const result = await syncPaymentLinks({ root, api, shop: checked.shop, slug: studio.slug, site: target.origin, mode, w, receipt,
|
|
157
|
+
const result = await syncPaymentLinks({ catalog, root, api, shop: checked.shop, slug: studio.slug, site: target.origin, mode, w, receipt,
|
|
137
158
|
bookingSlots: options.bookingSlots ?? (existsSync(join(root,'.studio','booking-slots.json')) ? JSON.parse(readFileSync(join(root,'.studio','booking-slots.json'),'utf8')) : []),
|
|
138
159
|
saveReceipt: (record) => {
|
|
139
160
|
writeFileSync(`${receiptFile}.tmp`, JSON.stringify(record), { mode: 0o600 });
|
|
140
161
|
renameSync(`${receiptFile}.tmp`, receiptFile);
|
|
141
162
|
} });
|
|
142
|
-
return { ...result, profile };
|
|
163
|
+
return { ...result, command:options.webhookOnly?'shop webhook':result.command, profile };
|
|
143
164
|
} catch (e) {
|
|
144
165
|
const said = e?.stripeReason ?? '';
|
|
145
|
-
return
|
|
166
|
+
return fail('stripe-setup', 'Stripe setup could not finish. Check the studio’s Stripe session and Products, Prices, Payment Links and Webhook Endpoints write permissions, and the Cloudflare login, then run shop connect again. No credentials were printed.' + said);
|
|
146
167
|
} finally { lock.release(); }
|
|
147
168
|
}
|
|
@@ -3708,6 +3708,93 @@
|
|
|
3708
3708
|
}
|
|
3709
3709
|
}
|
|
3710
3710
|
}
|
|
3711
|
+
},
|
|
3712
|
+
"0.51.5": {
|
|
3713
|
+
"files": {
|
|
3714
|
+
"CLAUDE.md": "ddf2b2a0376202f6",
|
|
3715
|
+
"README.md": "684b3cca8001673c",
|
|
3716
|
+
"HANDOFF.md": "29337e2ace95b2e7",
|
|
3717
|
+
"games/README.md": "5ad32fe80c61f117",
|
|
3718
|
+
"music/README.md": "a3dc042dadd39df8",
|
|
3719
|
+
"videos/README.md": "16babc3437d5545b",
|
|
3720
|
+
"posts/README.md": "37841aae476bd540",
|
|
3721
|
+
"site/README.md": "1dbdf22c03a1e505",
|
|
3722
|
+
"site/src/worker.mjs": "6cc5c6131eaf6fd0"
|
|
3723
|
+
},
|
|
3724
|
+
"sections": {
|
|
3725
|
+
"AGENTS.md": {
|
|
3726
|
+
"(intro)": "394cbbe934e80722",
|
|
3727
|
+
"## Version-matched authoring guides": "f6261a0e5801b96b",
|
|
3728
|
+
"## Layout": "f3a280b8c96cd068",
|
|
3729
|
+
"## Apps: one screen, roles and parts": "c10aa0441ea89702",
|
|
3730
|
+
"## Commands (all through the pinned CLI in node_modules)": "c20d90de21cde0c7",
|
|
3731
|
+
"## The site": "0cb8dd9a6f8a0096",
|
|
3732
|
+
"## Making games": "bb715272fd1a615b",
|
|
3733
|
+
"## The Game Codex and progress": "0738ef719cef3a4c",
|
|
3734
|
+
"## Art direction and models": "a48fff7bf0a5f2fc",
|
|
3735
|
+
"## Running live games (the back office)": "51ebf2ab756f6e4d",
|
|
3736
|
+
"## Servers and AI seats": "1ccfd549d2369a60",
|
|
3737
|
+
"## Selling things (the shop)": [
|
|
3738
|
+
"0df5a5fe1833c624",
|
|
3739
|
+
"43a3556be93d1235"
|
|
3740
|
+
],
|
|
3741
|
+
"## Continuing a build from the Claude app": "0d68081e924ca419",
|
|
3742
|
+
"## Rules": "b2234d80f6cbe71d",
|
|
3743
|
+
"## Telling Homie": "80b46fdd9e578db8",
|
|
3744
|
+
"## Beta": "9d02638207a26e73"
|
|
3745
|
+
}
|
|
3746
|
+
},
|
|
3747
|
+
"named": {
|
|
3748
|
+
"Homie Arcade": {
|
|
3749
|
+
"sections": {
|
|
3750
|
+
"AGENTS.md": {
|
|
3751
|
+
"## Commands (all through the pinned CLI in node_modules)": "8364e05cebefcba4"
|
|
3752
|
+
}
|
|
3753
|
+
}
|
|
3754
|
+
}
|
|
3755
|
+
}
|
|
3756
|
+
},
|
|
3757
|
+
"0.51.6": {
|
|
3758
|
+
"files": {
|
|
3759
|
+
"CLAUDE.md": "ddf2b2a0376202f6",
|
|
3760
|
+
"README.md": "684b3cca8001673c",
|
|
3761
|
+
"HANDOFF.md": "29337e2ace95b2e7",
|
|
3762
|
+
"games/README.md": "5ad32fe80c61f117",
|
|
3763
|
+
"music/README.md": "a3dc042dadd39df8",
|
|
3764
|
+
"videos/README.md": "16babc3437d5545b",
|
|
3765
|
+
"posts/README.md": "37841aae476bd540",
|
|
3766
|
+
"site/README.md": "1dbdf22c03a1e505",
|
|
3767
|
+
"site/src/worker.mjs": "6cc5c6131eaf6fd0"
|
|
3768
|
+
},
|
|
3769
|
+
"sections": {
|
|
3770
|
+
"AGENTS.md": {
|
|
3771
|
+
"(intro)": "394cbbe934e80722",
|
|
3772
|
+
"## Version-matched authoring guides": "f6261a0e5801b96b",
|
|
3773
|
+
"## Layout": "f3a280b8c96cd068",
|
|
3774
|
+
"## Apps: one screen, roles and parts": "c10aa0441ea89702",
|
|
3775
|
+
"## Commands (all through the pinned CLI in node_modules)": "c20d90de21cde0c7",
|
|
3776
|
+
"## The site": "0cb8dd9a6f8a0096",
|
|
3777
|
+
"## Making games": "bb715272fd1a615b",
|
|
3778
|
+
"## The Game Codex and progress": "0738ef719cef3a4c",
|
|
3779
|
+
"## Art direction and models": "a48fff7bf0a5f2fc",
|
|
3780
|
+
"## Running live games (the back office)": "51ebf2ab756f6e4d",
|
|
3781
|
+
"## Servers and AI seats": "1ccfd549d2369a60",
|
|
3782
|
+
"## Selling things (the shop)": "0df5a5fe1833c624",
|
|
3783
|
+
"## Continuing a build from the Claude app": "0d68081e924ca419",
|
|
3784
|
+
"## Rules": "b2234d80f6cbe71d",
|
|
3785
|
+
"## Telling Homie": "80b46fdd9e578db8",
|
|
3786
|
+
"## Beta": "9d02638207a26e73"
|
|
3787
|
+
}
|
|
3788
|
+
},
|
|
3789
|
+
"named": {
|
|
3790
|
+
"Homie Arcade": {
|
|
3791
|
+
"sections": {
|
|
3792
|
+
"AGENTS.md": {
|
|
3793
|
+
"## Commands (all through the pinned CLI in node_modules)": "8364e05cebefcba4"
|
|
3794
|
+
}
|
|
3795
|
+
}
|
|
3796
|
+
}
|
|
3797
|
+
}
|
|
3711
3798
|
}
|
|
3712
3799
|
}
|
|
3713
3800
|
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** Retire the exact guidance shipped by Homie without opening credential files. */
|
|
2
|
+
export const RETIRED_MEDIA_GUIDANCE = "### Existing provider credentials\n\nThe tools read `FAL_KEY` or `ELEVENLABS_API_KEY` first, then the plain single-line\nkey in `~/.homie/keys/fal-key` or `~/.homie/keys/elevenlabs-key`. They also accept\n`~/.homie/workspace/fal-key` and `~/.homie/workspace/elevenlabs-key` from earlier\ninstalls. Checks name the source, never the key. Keep these files private (mode\n0600); never copy them into a studio. ElevenLabs also accepts its CLI browser\nsign-in and the existing Stripe Projects connection. Run the tool\u2019s `check`\nbefore declaring a provider disconnected.\n";
|
|
3
|
+
export const OFFICIAL_MEDIA_GUIDANCE = `### Official media connections
|
|
4
|
+
|
|
5
|
+
Use the official fal and ElevenLabs plugins in the AI client, with their browser sign-in.
|
|
6
|
+
Homie never reads provider credentials. Cloudflare deploys use Wrangler login; official
|
|
7
|
+
Cloudflare MCP tools inspect logs, Workers Issues and D1. Follow the matching toolkit's
|
|
8
|
+
guides/studio-setup/references/OFFICIAL-MEDIA.md. Retired Homie generation scripts have
|
|
9
|
+
no compatibility path. Keep existing assets and receipts; do not migrate credential files.
|
|
10
|
+
`;
|
|
11
|
+
export function upgradeMediaGuidance(text) {
|
|
12
|
+
return text.replace(RETIRED_MEDIA_GUIDANCE.trimEnd(), OFFICIAL_MEDIA_GUIDANCE.trimEnd());
|
|
13
|
+
}
|
package/lib/upgrade.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { upgradeMediaGuidance } from './upgrade-media.mjs';
|
|
1
2
|
import {appPresetChanges} from './upgrade-app-presets.mjs';
|
|
2
3
|
import {refreshWorkerConfig} from './worker-config.mjs';
|
|
3
4
|
/**
|
|
@@ -199,8 +200,17 @@ export function upgradePlan(root, { history = readHistory() } = {}) {
|
|
|
199
200
|
if (Object.keys(missingScripts).length) changes.push({ file: 'package.json', kind: 'scripts', what: `add scripts: ${Object.keys(missingScripts).join(', ')}`, scripts: missingScripts });
|
|
200
201
|
if (studio.homie?.studio !== STUDIO_VERSION) changes.push({ file: 'studio.json', kind: 'version', what: `homie.studio ${studio.homie?.studio ?? '(none)'} → ${STUDIO_VERSION}`, to: STUDIO_VERSION });
|
|
201
202
|
|
|
203
|
+
const mediaFiles = new Set();
|
|
204
|
+
for (const file of ['AGENTS.md', 'CLAUDE.md', 'README.md']) {
|
|
205
|
+
const from = read(root, file);
|
|
206
|
+
if (from === null) continue;
|
|
207
|
+
const text = upgradeMediaGuidance(from);
|
|
208
|
+
if (text !== from) { changes.push({ file, kind: 'update-file', what: 'replace retired Homie credential guidance with official plugin connections', from, text }); mediaFiles.add(file); }
|
|
209
|
+
}
|
|
210
|
+
|
|
202
211
|
// AGENTS.md, section by section; the other template files whole.
|
|
203
212
|
for (const rel of TEMPLATE_FILES) {
|
|
213
|
+
if (mediaFiles.has(rel)) continue;
|
|
204
214
|
const mine = read(root, rel);
|
|
205
215
|
const want = tmpl[rel];
|
|
206
216
|
if (want === undefined) continue;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@homie-rocks/studio",
|
|
3
|
-
"version": "0.51.
|
|
3
|
+
"version": "0.51.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/parts/PARTS.md
CHANGED
|
@@ -240,7 +240,7 @@ Paid releases use a **separate private R2 bucket and `PURCHASE_MEDIA` binding**.
|
|
|
240
240
|
|
|
241
241
|
Stripe's [MPP guide](https://docs.stripe.com/payments/machine/mpp) initializes the Stripe client with a secret credential. Its [x402 guide](https://docs.stripe.com/payments/machine/x402) separates the on-chain facilitator settlement from authenticated PaymentIntent recording. **x402 itself does not require a Stripe API key**; this supported path records money into the seller's Stripe books as Stripe documents. The keyless Payment Link path uses [signed webhooks](https://docs.stripe.com/webhooks) instead of provider reads. CLI OAuth is not exported as a Worker REST credential; see [the setup research](../../../docs/stripe-connect-research.md).
|
|
242
242
|
|
|
243
|
-
|
|
243
|
+
Owner setup uses Stripe’s official OAuth plugin and Homie’s private webhook installation; read [SHOP.md](../shop/SHOP.md). Runtime-credential machine payment integrations are developer work, not an owner fallback. Never ask an owner to create or copy a key.
|
|
244
244
|
|
|
245
245
|
Turnstile is optional. If the studio configures it, the fallback verifies its hostname and action. The per-address Worker rate limiter remains configurable flood protection. Machine payment proofs are verified by the protocol libraries.
|
|
246
246
|
|
package/rules/view.ts
CHANGED
|
@@ -728,6 +728,10 @@ export function openRoom<R = unknown>(opts: OpenRoomOptions = {}): Room<R> {
|
|
|
728
728
|
while (stepped < t) {
|
|
729
729
|
stepped += 1;
|
|
730
730
|
step(stepped, stepped === t);
|
|
731
|
+
// fire() can read/cache the own entity before this pump advances it.
|
|
732
|
+
// A renderer later in the same callback must see the completed step,
|
|
733
|
+
// not hold that effect's old pose until the next (possibly early) frame.
|
|
734
|
+
presentedEntity = null;
|
|
731
735
|
if (stepped % sendEvery === sendEvery - 1) flush(now, stepped);
|
|
732
736
|
}
|
|
733
737
|
}
|
package/shop/SHOP.md
CHANGED
|
@@ -101,26 +101,68 @@ One order stores every line and its item snapshot. `/api/shop/mine` lists the li
|
|
|
101
101
|
|
|
102
102
|
`homie-studio shop init --supporter` writes an example item and `SELLING.md`, without a policy, cap or refund
|
|
103
103
|
window. `shop check` validates settings; the build uses the same validation.
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
`
|
|
123
|
-
|
|
104
|
+
## Default: Stripe's official plugin
|
|
105
|
+
|
|
106
|
+
Use Stripe's official plugin, authenticated with its browser OAuth approval, for Products,
|
|
107
|
+
Prices, Payment Links, menu links, per-slot booking links, monthly giving and the customer
|
|
108
|
+
portal. Install with `stripe agent setup` if needed; the MCP URL is
|
|
109
|
+
`https://mcp.stripe.com`. Current tools are `stripe_api_search`, `stripe_api_details`,
|
|
110
|
+
`stripe_api_read` and `stripe_api_write`. Discover the operation and its schema before
|
|
111
|
+
calling it; tool argument shapes can change. See https://docs.stripe.com/mcp.
|
|
112
|
+
|
|
113
|
+
1. Create/check `shop.json`. Read the app's published slot records and supply the complete
|
|
114
|
+
slot manifest described below. Select the owner's account and sandbox in Stripe's plugin;
|
|
115
|
+
read `/v1/account` to record its account ID. Live mode only after the owner requests it.
|
|
116
|
+
2. Call Homie's `stripe_shop_plan` with `account`, `mode: "test"`, `slots` and
|
|
117
|
+
`observations: []`. CLI equivalent: `homie-studio shop plan --have plan-input.json`.
|
|
118
|
+
That JSON file contains the same fields. A returned `next` is the exact operation:
|
|
119
|
+
method, API path, parameters, API version and (for creates) idempotency key. Updates set desired fields; read before retrying. Metadata
|
|
120
|
+
scopes objects to this studio and origin; prices and links have content revisions.
|
|
121
|
+
3. Perform each `next` through Stripe's official MCP. For GET, append
|
|
122
|
+
`{ "path": "…", "params": {…}, "body": <Stripe response> }` to `observations`
|
|
123
|
+
and plan again. Follow every pagination request. After a write, discard observations
|
|
124
|
+
and read fresh. Replanning resolves real Stripe IDs; never invent IDs or substitute
|
|
125
|
+
a different price. Reuse the supplied idempotency key where the tool supports it;
|
|
126
|
+
otherwise read before retrying and match the stable product ID/revision metadata.
|
|
127
|
+
4. When no writes remain, use fresh reads with `stripe_shop_plan` `verify: true`
|
|
128
|
+
(CLI `homie-studio shop verify --have plan-input.json`). It checks the catalog,
|
|
129
|
+
actual link fields and line items, menus, slot limits and portal settings against
|
|
130
|
+
shop.json and the slot manifest. A mismatch blocks verification. It saves only public
|
|
131
|
+
configuration to `.studio/stripe-mcp.json`; local test configuration goes to `.dev.vars`.
|
|
132
|
+
5. For a deployed HTTPS studio, run `stripe_webhook` (CLI `homie-studio shop webhook`).
|
|
133
|
+
Follow `studio_job`. This uses Stripe CLI only for private webhook installation,
|
|
134
|
+
checks the same account as the MCP, and installs the signing secret and verified
|
|
135
|
+
links through Wrangler stdin. The CLI needs one separate browser approval if its
|
|
136
|
+
studio profile is not already connected. MCP approval cannot be reused or exported.
|
|
137
|
+
Cloudflare authorization and an existing deployment are prerequisites.
|
|
138
|
+
6. After installation, perform the returned `retire` operations through Stripe MCP to
|
|
139
|
+
archive replaced catalog objects, then verify fresh reads again. Test a sandbox
|
|
140
|
+
purchase and its signed event, order grant, booking capacity or recurring invoice;
|
|
141
|
+
a browser return is never proof. Stripe CLI `trigger` can send a test event, and
|
|
142
|
+
`events resend <event> --webhook-endpoint <endpoint>` retries a delivery. Check the
|
|
143
|
+
endpoint's delivery result and the app's verified records. Do not type a card into
|
|
144
|
+
a page for an automated proof; use Stripe's documented test PaymentMethods/fixtures.
|
|
145
|
+
|
|
146
|
+
Never create a webhook through model-visible Stripe MCP: its response exposes the signing secret.
|
|
147
|
+
|
|
148
|
+
The owner never handles an API key or signing secret. No Homie backend handles payments.
|
|
149
|
+
The Worker needs only public link configuration and the signing secret; buyers pay
|
|
150
|
+
Stripe directly even when the AI and the owner's computer are offline. The old
|
|
151
|
+
`stripe_login` / `shop connect` flow remains an alternative that syncs everything
|
|
152
|
+
through Stripe CLI browser approval. `--renew` renews that approval; `--live` requires
|
|
153
|
+
the owner's go-live request. Never read CLI config or print login-completion output.
|
|
154
|
+
|
|
155
|
+
For local checkout, pass `url: "http://localhost:8787"` (or the private Wi-Fi address)
|
|
156
|
+
and `mode: "test"` to the same plan/verify flow; the official plugin makes the links.
|
|
157
|
+
Run `homie-studio shop listen --profile <sandbox-profile>` in a background terminal.
|
|
158
|
+
Stripe CLI is required for forwarding; one browser approval is enough if needed.
|
|
159
|
+
The command captures and saves the listener secret privately. Start/restart
|
|
160
|
+
`homie-studio dev --lan` once it reports ready and keep the listener running.
|
|
161
|
+
Never copy the listener's raw output into the conversation.
|
|
162
|
+
|
|
163
|
+
For a faster local iteration, `homie-studio build --quick` skips generated rules play
|
|
164
|
+
and the snapshot smoke-run, while retaining compilation, guards and type checks.
|
|
165
|
+
Run a full build before release; quick output is not evidence that play checks passed.
|
|
124
166
|
|
|
125
167
|
At purchase time, the Worker saves an opaque order reference and sends it as the link's
|
|
126
168
|
`client_reference_id`. The buyer confirms quantity or tip amount on Stripe. A signed snapshot
|
|
@@ -142,17 +184,7 @@ items; failed refunds restore the financial booking. A lost dispute revokes good
|
|
|
142
184
|
| Managed Payments | Payment Links support managed_payments.enabled; connect uses it when the studio selects Stripe as seller, subject to Stripe eligibility and terms. |
|
|
143
185
|
| Currency display | Payment Links always enable Stripe Adaptive Pricing. Bookkeeping uses integration currency; presentment details describe what the customer saw. |
|
|
144
186
|
|
|
145
|
-
|
|
146
|
-
The explicit `shop connect --manual` path remains available, including `--live`, but requires the
|
|
147
|
-
owner's existing Dashboard key step; it does not satisfy one approval. Never choose it automatically.
|
|
148
|
-
Running default connect again saves links, removes the API key, and keeps signed-event fulfillment.
|
|
149
|
-
When switching to spending restrictions, the AI must deactivate previously issued Payment Links through
|
|
150
|
-
the approved CLI and resolve in-flight checkouts before relying on those restrictions; switching credentials
|
|
151
|
-
does not cancel old Stripe sessions. The runtime OAuth finding and sources are in `docs/stripe-connect-research.md` in the source checkout.
|
|
152
|
-
|
|
153
|
-
`shop`, `shop orders`, and `/_studio/office/shop` show readiness and sales. Stripe's optional MCP
|
|
154
|
-
is useful for owner-requested refunds and analytics; honor its approval links. Do not create a
|
|
155
|
-
secret-returning webhook through a conversation tool: the private connect process owns that call.
|
|
187
|
+
`shop`, `shop orders`, and `/_studio/office/shop` show readiness and sales. Use Stripe's official MCP for owner-requested refunds and analytics; honor its approval links.
|
|
156
188
|
|
|
157
189
|
## Payment guarantees and schema
|
|
158
190
|
|
|
@@ -220,7 +252,7 @@ For referrals, use Stripe Connect to split payments or a Stripe-based affiliate
|
|
|
220
252
|
|
|
221
253
|
## Keyless menus, paid seats and monthly giving (0.51.0)
|
|
222
254
|
|
|
223
|
-
Use
|
|
255
|
+
Use the official MCP plan/verify flow by default. All objects belong to the owner's Stripe account and
|
|
224
256
|
are created through its approved CLI session. Homie has no platform key, OAuth
|
|
225
257
|
backend, payment intermediary or cut. A desktop is needed to sync, not to take money.
|
|
226
258
|
|
|
@@ -251,7 +283,7 @@ data goes in that reference. A return URL is never proof of payment.
|
|
|
251
283
|
collection with integer `capacity` and a server-managed `bookings` collection with
|
|
252
284
|
`"booking":{"slots":"slots","item":"full-tune"}`. Publish slot records, then have
|
|
253
285
|
the owner's AI write the complete manifest to `.studio/booking-slots.json` and run
|
|
254
|
-
`
|
|
286
|
+
`stripe_shop_plan` with the same `slots` array (the CLI alternative is `stripe_bookings_sync`):
|
|
255
287
|
|
|
256
288
|
```json
|
|
257
289
|
[{"app":"tune","collection":"bookings","slots":"slots","slot":"sat-0900","item":"full-tune","capacity":1}]
|
|
@@ -303,7 +335,7 @@ Signed `subscription.started`, `.renewed`, `.failed`, `.cancelled` events drive
|
|
|
303
335
|
membership and giving. Membership access ends at the paid invoice line's period end;
|
|
304
336
|
failed invoices cannot extend it and cancellation ends access. Unknown Prices grant
|
|
305
337
|
nothing. Keyless portal configuration supports cancellation, payment-method updates
|
|
306
|
-
and invoice history.
|
|
338
|
+
and invoice history. The official MCP plan enables `login_page.enabled` through Stripe’s API
|
|
307
339
|
and stores Stripe's no-code login URL; **no Dashboard toggle is required**. Givers
|
|
308
340
|
verify their email with Stripe to manage/cancel. The buyer-owned `/api/shop/portal`
|
|
309
341
|
returns this URL. Plan changes require deliberate portal configuration and fulfilment
|
|
@@ -339,3 +371,47 @@ dev bindings and disposable studios when finished.
|
|
|
339
371
|
See `apps/APPS.md`, “Real Stripe test checkout on this computer”, for the supported
|
|
340
372
|
`shop connect --local` and Stripe CLI forwarding recipe. Every paid monthly cycle
|
|
341
373
|
creates a paid order and `order.paid` with the subscription’s original app reference.
|
|
374
|
+
|
|
375
|
+
## Preserve a local basket with keyless checkout
|
|
376
|
+
|
|
377
|
+
For a venue that wants the phone's exact items and quantities, opt into a saved
|
|
378
|
+
basket in `shop.json` before syncing Payment Links:
|
|
379
|
+
|
|
380
|
+
```json
|
|
381
|
+
"basket": {
|
|
382
|
+
"name": "Round for your table",
|
|
383
|
+
"description": "Enter the total shown on your pub basket. Any shortfall is settled at the bar."
|
|
384
|
+
}
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
Use Stripe (`till: "stripe"`) and tax-inclusive menu prices (`automaticTax: false`).
|
|
388
|
+
Sync creates one custom-amount Price and Payment Link per app, with one line and
|
|
389
|
+
quantity one. `shop.checkout({reference: "table-7"})` saves the validated basket
|
|
390
|
+
lines, quantities, total and reference in the studio's D1 before opening Stripe.
|
|
391
|
+
Even a basket containing only one item keeps its exact quantity. The opaque order
|
|
392
|
+
ID travels as `client_reference_id`; no Stripe API key or Homie payment relay is
|
|
393
|
+
used by the Worker. Existing menus without `basket` retain their behavior.
|
|
394
|
+
|
|
395
|
+
The link prefills the saved total using Stripe's documented
|
|
396
|
+
[`prefilled_amount`](https://docs.stripe.com/payment-links/customize#customize-checkout-with-url-parameters)
|
|
397
|
+
in minor currency units. The guest can edit it, so a preset is never payment proof.
|
|
398
|
+
[`custom_unit_amount.preset`](https://docs.stripe.com/api/prices/create?query=custom_unit_amount)
|
|
399
|
+
is the Price-level fallback, not a per-buyer setting. Never mutate that shared Price
|
|
400
|
+
at checkout: concurrent baskets would race. Both the Price fallback and the URL
|
|
401
|
+
override were verified in the Homie sandbox. Invalid overrides fall back to the
|
|
402
|
+
stored preset or an empty amount; keep the expected total visible on the phone.
|
|
403
|
+
|
|
404
|
+
After a signed paid session matches the frozen link, revision, mode, currency and
|
|
405
|
+
order reference, `order.paid` contains the **saved basket lines**, `amount` (actual
|
|
406
|
+
payment), `basketTotal` (expected total), and `settlement` (`paid` or `short`).
|
|
407
|
+
Line `amount` is the saved extended amount (unit price × quantity); line `total`
|
|
408
|
+
is its proportionally allocated share of the actual aggregate payment for refunds.
|
|
409
|
+
A short payment emits the event so staff can resolve it, but grants no automatic
|
|
410
|
+
entitlements. Apps must compare the actual payment with `basketTotal`, show a
|
|
411
|
+
shortfall explicitly, and deduplicate by order ID. More than the total is paid.
|
|
412
|
+
Reopening and paying the link creates another order for the same frozen basket;
|
|
413
|
+
repeated delivery of the same session never creates another round.
|
|
414
|
+
|
|
415
|
+
The Stripe heading is the connected Stripe account's business name. A sandbox
|
|
416
|
+
account named “Homie sandbox” shows that name; a real pub's account shows the pub's
|
|
417
|
+
name. The basket's product name and description use the pub's own wording.
|
package/worker/app-records.mjs
CHANGED
|
@@ -47,6 +47,7 @@ export async function appAccess(request, env, meta, url) {
|
|
|
47
47
|
const caller = toolIdentity(request);
|
|
48
48
|
if (caller) {
|
|
49
49
|
if (caller.owner) return { ok: true, role, can: declared.can };
|
|
50
|
+
if(env.HOMIE_LOCAL_TEST_SECRET && caller.localTest && caller.roles?.[meta.id]?.includes(role))return {ok:true,role,can:declared.can};
|
|
50
51
|
if (!caller.id) return { ok: false, status: 401, error: 'sign in to this studio' };
|
|
51
52
|
const grant = await env.DB.prepare("SELECT player FROM app_roles WHERE app IN (?1, '*') AND role = ?2 AND player = ?3").bind(meta.id, role, caller.id).first();
|
|
52
53
|
return grant ? { ok: true, role, can: declared.can } : { ok: false, status: 403, error: 'this account has no role grant' };
|
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.51.
|
|
2
|
+
export const STUDIO_BUILD = Object.freeze({"version":"0.51.6","sha256":"035298d6bd38bab86e0471ea08819786995a53cedb8da961b595506e5403b410"});
|
|
@@ -25,10 +25,16 @@ export async function readableFeed(env,cat,caller,name) {
|
|
|
25
25
|
// Non-record events have no app read capability; they remain owner-only.
|
|
26
26
|
return caller.owner===true;
|
|
27
27
|
}
|
|
28
|
-
export async function feedEvents(env,name,{after=0,through=Number.MAX_SAFE_INTEGER}={}) {
|
|
28
|
+
export async function feedEvents(env,name,{after=0,through=Number.MAX_SAFE_INTEGER,caller}={}) {
|
|
29
29
|
const feed=eventFeeds(env)[name];if(!feed)return [];
|
|
30
|
-
const
|
|
31
|
-
|
|
30
|
+
const scopedApp=env.HOMIE_LOCAL_TEST_SECRET&&caller?.localTest&&caller.testScope&&feed.event==='record.changed'?`${feed.app}:check:${caller.testScope}`:null;
|
|
31
|
+
const rows=(await env.DB.prepare("SELECT seq,id,type,data,at FROM studio_events WHERE type=?1 AND seq>?2 AND seq<=?3 AND (?4 IS NULL OR json_extract(data,'$.app')=?4) ORDER BY seq LIMIT 100").bind(feed.event,after,through,scopedApp).all()).results;
|
|
32
|
+
const events=rows.map(r=>({...r,data:JSON.parse(r.data)}));
|
|
33
|
+
if(env.HOMIE_LOCAL_TEST_SECRET && caller?.localTest && caller.testScope && feed.event==='record.changed') {
|
|
34
|
+
const app=`${feed.app}:check:${caller.testScope}`;
|
|
35
|
+
return events.filter(e=>e.data.app===app).map(e=>({...e,data:{...e.data,app:feed.app}}));
|
|
36
|
+
}
|
|
37
|
+
return events;
|
|
32
38
|
}
|
|
33
39
|
export function matchesFeed(feed,event) {
|
|
34
40
|
return feed.event===event.type && (feed.event!=='record.changed'||(event.data.app===feed.app&&event.data.collection===feed.collection));
|
package/worker/index.mjs
CHANGED
|
@@ -189,7 +189,11 @@ async function availableShellShop(env, cat, game, options) {
|
|
|
189
189
|
// The shell can show a disabled catalog before payment setup is complete.
|
|
190
190
|
// Checkout readiness is enforced by the shop API, separately from visibility.
|
|
191
191
|
const preview = (cat.games ?? []).find(g => g.id === game)?.screen?.shopPreview === true;
|
|
192
|
-
|
|
192
|
+
const shop = shopOf(cat);
|
|
193
|
+
// Free services must retain their bridge in a mixed free/paid catalogue.
|
|
194
|
+
// Paid readiness, policy and atomic booking capacity remain API decisions.
|
|
195
|
+
const free = shop?.open && !shop.broken && shop.items.some(i => i.price === 0 && !i.interval && (!i.game || i.game === game));
|
|
196
|
+
return (preview && !shop?.broken || free || await sellingShop(env, cat)) ? shellShop(cat, game, options) : null;
|
|
193
197
|
}
|
|
194
198
|
|
|
195
199
|
async function postsOf(env, origin) {
|
package/worker/local-test.mjs
CHANGED
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
const local = (request, env) => env.HOMIE_LOCAL_TEST_SECRET && ['localhost','127.0.0.1','[::1]'].includes(new URL(request.url).hostname);
|
|
3
3
|
export async function localCaller(request, env) {
|
|
4
4
|
if (!local(request, env) || request.headers.get('authorization') !== `Bearer ${env.HOMIE_LOCAL_TEST_SECRET}`) return null;
|
|
5
|
+
const session=await localSession(request,env);
|
|
6
|
+
// A supplied but invalid signed session must never fall back to owner.
|
|
7
|
+
if(request.headers.get('cookie')?.includes('homie_test=')&&!session)return null;
|
|
8
|
+
if(session)return {id:`local-test-${session.app}-${session.role}`,owner:session.role==='owner',client:null,localTest:true,roles:{[session.app]:[session.role]},testApp:session.app,testScope:session.scope??null};
|
|
5
9
|
return {id:'local-test-owner',owner:true,client:null};
|
|
6
10
|
}
|
|
7
11
|
export async function localSession(request, env) {
|
package/worker/mcp-events.mjs
CHANGED
|
@@ -41,9 +41,9 @@ export async function pollEvents(env,cat,caller,params,now=Date.now()){
|
|
|
41
41
|
const rows=(await env.DB.prepare(`SELECT seq,id,type,data,at FROM studio_events
|
|
42
42
|
WHERE type=?1 AND seq>?2 AND seq<=?3
|
|
43
43
|
AND (?4 IS NULL OR (json_extract(data,'$.app')=?4 AND json_extract(data,'$.collection')=?5))
|
|
44
|
-
ORDER BY seq LIMIT ?6`).bind(feed.event,start.after,start.through,feed.event==='record.changed'?feed.app:null,feed.collection??null,max+1).all()).results;
|
|
44
|
+
ORDER BY seq LIMIT ?6`).bind(feed.event,start.after,start.through,feed.event==='record.changed'?(env.HOMIE_LOCAL_TEST_SECRET&&caller.localTest&&caller.testScope?`${feed.app}:check:${caller.testScope}`:feed.app):null,feed.collection??null,max+1).all()).results;
|
|
45
45
|
const hasMore=rows.length>max,page=rows.slice(0,max);
|
|
46
|
-
const events=page.map(row=>occurrence(params.name,{...row,data
|
|
46
|
+
const events=page.map(row=>{const data=JSON.parse(row.data);if(env.HOMIE_LOCAL_TEST_SECRET&&caller.localTest&&caller.testScope&&feed.event==='record.changed')data.app=feed.app;return occurrence(params.name,{...row,data},cursorOf(params.name,row.seq));});
|
|
47
47
|
const after=hasMore?page.at(-1).seq:start.through;
|
|
48
48
|
return {events,cursor:cursorOf(params.name,after),truncated:start.truncated,hasMore,nextPollMs:5000};
|
|
49
49
|
}
|