@homie-rocks/studio 0.49.0 → 0.50.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.
Files changed (59) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/bin/homie-studio.mjs +5 -5
  3. package/functions/FUNCTIONS.md +1 -1
  4. package/guides/app/SKILL.md +107 -1
  5. package/guides/art/SKILL.md +154 -0
  6. package/guides/art/references/RIGHTS.md +20 -0
  7. package/guides/art/scripts/art.mjs +308 -0
  8. package/guides/models/SKILL.md +198 -0
  9. package/guides/models/references/RIGHTS.md +46 -0
  10. package/guides/models/references/models.json +175 -0
  11. package/guides/models/scripts/models.mjs +483 -0
  12. package/guides/music/SKILL.md +197 -0
  13. package/guides/music/references/COMPOSITION.md +59 -0
  14. package/guides/music/references/GAME-AUDIO.md +50 -0
  15. package/guides/music/references/RIGHTS.md +43 -0
  16. package/guides/music/scripts/lib/audio.mjs +258 -0
  17. package/guides/music/scripts/lib/eleven.mjs +260 -0
  18. package/guides/music/scripts/lib/studio.mjs +147 -0
  19. package/guides/music/scripts/music.mjs +644 -0
  20. package/guides/plan/SKILL.md +126 -0
  21. package/guides/plan/references/CODEX.md +105 -0
  22. package/guides/plan/references/INTERVIEW.md +86 -0
  23. package/guides/playtest/SKILL.md +205 -0
  24. package/guides/playtest/references/METHOD.md +124 -0
  25. package/guides/playtest/references/REVIEWER.md +78 -0
  26. package/guides/playtest/scripts/lib/exit.mjs +31 -0
  27. package/guides/playtest/scripts/lib/judge.mjs +502 -0
  28. package/guides/playtest/scripts/lib/pixels.mjs +128 -0
  29. package/guides/playtest/scripts/lib/preflight.mjs +69 -0
  30. package/guides/playtest/scripts/playtest.mjs +870 -0
  31. package/guides/shop/SKILL.md +4 -0
  32. package/guides/sound/SKILL.md +156 -0
  33. package/guides/sound/references/GAME-SOUND.md +100 -0
  34. package/guides/sound/references/SCORE.md +131 -0
  35. package/guides/sound/references/TRAPS.md +93 -0
  36. package/guides/sound/scripts/lib/measure.mjs +252 -0
  37. package/guides/sound/scripts/lib/score.mjs +415 -0
  38. package/guides/sound/scripts/lib/sfx.mjs +222 -0
  39. package/guides/sound/scripts/lib/synth.mjs +509 -0
  40. package/guides/sound/scripts/sound.mjs +520 -0
  41. package/guides/style/SKILL.md +105 -0
  42. package/lib/app-needs.mjs +63 -0
  43. package/lib/check.mjs +10 -4
  44. package/lib/codex.mjs +95 -18
  45. package/lib/decisions.mjs +9 -7
  46. package/lib/handoff.mjs +1 -1
  47. package/lib/mcp-tools.mjs +10 -9
  48. package/lib/mcp.mjs +1 -1
  49. package/lib/scaffold.mjs +13 -5
  50. package/lib/studio.mjs +3 -3
  51. package/lib/template-history.json +42 -0
  52. package/package.json +1 -1
  53. package/shop/SHOP.md +4 -0
  54. package/shop/shop.ts +4 -4
  55. package/worker/build-id.mjs +1 -1
  56. package/worker/functions-schema.mjs +15 -0
  57. package/worker/shop-page.mjs +1 -1
  58. package/worker/shop.mjs +6 -2
  59. package/worker/version.mjs +1 -1
@@ -3153,6 +3153,48 @@
3153
3153
  }
3154
3154
  }
3155
3155
  }
3156
+ },
3157
+ "0.50.0": {
3158
+ "files": {
3159
+ "CLAUDE.md": "ddf2b2a0376202f6",
3160
+ "README.md": "684b3cca8001673c",
3161
+ "HANDOFF.md": "29337e2ace95b2e7",
3162
+ "games/README.md": "5ad32fe80c61f117",
3163
+ "music/README.md": "a3dc042dadd39df8",
3164
+ "videos/README.md": "16babc3437d5545b",
3165
+ "posts/README.md": "37841aae476bd540",
3166
+ "site/README.md": "1dbdf22c03a1e505",
3167
+ "site/src/worker.mjs": "6cc5c6131eaf6fd0"
3168
+ },
3169
+ "sections": {
3170
+ "AGENTS.md": {
3171
+ "(intro)": "394cbbe934e80722",
3172
+ "## Version-matched authoring guides": "f6261a0e5801b96b",
3173
+ "## Layout": "f3a280b8c96cd068",
3174
+ "## Apps: one screen, roles and parts": "c10aa0441ea89702",
3175
+ "## Commands (all through the pinned CLI in node_modules)": "5222e08ca55e326e",
3176
+ "## The site": "0cb8dd9a6f8a0096",
3177
+ "## Making games": "bb715272fd1a615b",
3178
+ "## The Game Codex and progress": "0738ef719cef3a4c",
3179
+ "## Art direction and models": "a48fff7bf0a5f2fc",
3180
+ "## Running live games (the back office)": "51ebf2ab756f6e4d",
3181
+ "## Servers and AI seats": "1ccfd549d2369a60",
3182
+ "## Selling things (the shop)": "9e020d69dd6a954e",
3183
+ "## Continuing a build from the Claude app": "0d68081e924ca419",
3184
+ "## Rules": "b2234d80f6cbe71d",
3185
+ "## Telling Homie": "80b46fdd9e578db8",
3186
+ "## Beta": "9d02638207a26e73"
3187
+ }
3188
+ },
3189
+ "named": {
3190
+ "Homie Arcade": {
3191
+ "sections": {
3192
+ "AGENTS.md": {
3193
+ "## Commands (all through the pinned CLI in node_modules)": "611b437f5b3303f0"
3194
+ }
3195
+ }
3196
+ }
3197
+ }
3156
3198
  }
3157
3199
  }
3158
3200
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@homie-rocks/studio",
3
- "version": "0.49.0",
3
+ "version": "0.50.0",
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/shop/SHOP.md CHANGED
@@ -215,3 +215,7 @@ itemButton.onclick = () => shop.open('supporter');
215
215
  ```
216
216
 
217
217
  These calls open the same shop in games and apps. Inside a player card, purchases leave for the studio's shop; ordinary play/open pages start Stripe checkout. The studio's TV and kids policies still apply.
218
+
219
+ ## App payment references
220
+
221
+ Create the booking/ticket/gift intention first. `shop.buy('full-tune', 1, undefined, { reference: booking.id })` or `shop.checkout({ reference: booking.id })` sends the app's own reference through checkout. Existing boolean returns are unchanged: checkout opened is not paid. The verified durable `order.paid` event carries `data.order`, `data.reference` (null if omitted) and `data.app`. Reopened Payment Links retain the reference. A reference is buyer-supplied correlation, not authorization: verify the record, app, amount and currency, deduplicate by event/order ID, then fulfil it. Keep personal data out of references. Before Stripe is connected, local prices and unpaid requests can be demonstrated; actual payment and recurring collection remain unverified. Test events must be labelled simulations.
package/shop/shop.ts CHANGED
@@ -61,8 +61,8 @@ export interface Shop {
61
61
  add(item: string, quantity?: number, amount?: number): void;
62
62
  cart(): CartLine[];
63
63
  clear(): void;
64
- checkout(): Promise<boolean>;
65
- buy(item: string, quantity?: number, amount?: number): Promise<boolean>;
64
+ checkout(options?: { reference?: string }): Promise<boolean>;
65
+ buy(item: string, quantity?: number, amount?: number, options?: { reference?: string }): Promise<boolean>;
66
66
  /** Mark an item used; the studio's refundUsedItems policy decides whether self-service refunds still apply. */
67
67
  used(key: string): Promise<boolean>;
68
68
  on(ev: 'change', fn: (owns: string[]) => void): () => void;
@@ -129,8 +129,8 @@ export function createShop(opts: ShopOptions = {}): Shop {
129
129
  add: (item, quantity = 1, amount) => { cart.push({ item, quantity, ...(amount === undefined ? {} : { amount }) }); },
130
130
  cart: () => cart.map((line) => ({ ...line })),
131
131
  clear: () => { cart = []; },
132
- checkout: async () => { const ok = Boolean((await send({ op: 'checkout', lines: cart })).ok); if (ok) cart = []; return ok; },
133
- buy: async (item, quantity = 1, amount) => Boolean((await send({ op: 'checkout', lines: [{ item, quantity, ...(amount === undefined ? {} : { amount }) }] })).ok),
132
+ checkout: async (options = {}) => { const ok = Boolean((await send({ op: 'checkout', lines: cart, reference: options.reference })).ok); if (ok) cart = []; return ok; },
133
+ buy: async (item, quantity = 1, amount, options = {}) => Boolean((await send({ op: 'checkout', reference: options.reference, lines: [{ item, quantity, ...(amount === undefined ? {} : { amount }) }] })).ok),
134
134
  close: () => { void send({ op: 'close' }); },
135
135
  used: async (key) => (KEY.test(String(key)) && owns.includes(String(key)) ? Boolean((await send({ op: 'used', key })).ok) : false),
136
136
  on: ((ev: ShopEvent, fn: (x: unknown) => void) => {
@@ -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.49.0","sha256":"26095bfdf585bf237d209c64a8ee5e2f99cbb901814dcf972d5872d753127413"});
2
+ export const STUDIO_BUILD = Object.freeze({"version":"0.50.0","sha256":"817f88f291b3f1dc92e51bb54784034debeabc202a269a2466a1efc2ef07d292"});
@@ -69,3 +69,18 @@ ELSE 0 END BEGIN
69
69
  SELECT 'refund:' || json_extract(NEW.value,'$.id'),'payment.refunded',
70
70
  json_object('refund',json_extract(NEW.value,'$.id'),'payment',json_extract(NEW.value,'$.payment_intent'),'amount',json_extract(NEW.value,'$.amount'),'currency',json_extract(NEW.value,'$.currency')),json_extract(NEW.value,'$.at')*1000;
71
71
  END;`).join('\n');
72
+
73
+ /** Application correlation is saved before payment; only the paid transition emits it. */
74
+ export const FUNCTIONS_REFERENCE_MIGRATION_FILE = '0021_app_payment_reference.sql';
75
+ export const FUNCTIONS_REFERENCE_MIGRATION = `
76
+ DROP TRIGGER IF EXISTS shop_orders_function_paid;
77
+ CREATE TRIGGER shop_orders_function_paid AFTER UPDATE ON shop_orders
78
+ WHEN EXISTS (SELECT 1 FROM function_subscriptions WHERE type='order.paid')
79
+ AND OLD.paid_at IS NULL AND NEW.paid_at IS NOT NULL
80
+ AND NOT EXISTS (SELECT 1 FROM purchase_orders WHERE id=NEW.id) BEGIN
81
+ INSERT OR IGNORE INTO studio_events(id,type,data,at)
82
+ VALUES('shop_orders:' || NEW.id || ':paid','order.paid',
83
+ json_object('order',NEW.id,'source','shop_orders','amount',NEW.amount,'currency',NEW.currency,'mode',NEW.mode,
84
+ 'app',NEW.game,'reference',(SELECT value FROM meta WHERE key='shop-reference:' || NEW.id)),NEW.paid_at);
85
+ END;
86
+ `;
@@ -491,7 +491,7 @@ export const SHOP_SHELL_JS = String.raw`(function () {
491
491
  if (m.op === 'checkout') {
492
492
  if (!cfg || kids || screen) return reply({ ok: false, error: 'policy' });
493
493
  var checkoutTab = window.open('', '_blank');
494
- return api('POST', '/api/shop/buy', { lines: m.lines, game: GAME, server: SERVER }).then(function (r) {
494
+ return api('POST', '/api/shop/buy', { lines: m.lines, reference: m.reference, game: GAME, server: SERVER }).then(function (r) {
495
495
  if (r.ok && r.url) { leaveForCheckout(checkoutTab, r); watch(); } else if (checkoutTab) checkoutTab.close();
496
496
  reply(r);
497
497
  }).catch(function () { if (checkoutTab) checkoutTab.close(); reply({ ok: false }); });
package/worker/shop.mjs CHANGED
@@ -461,7 +461,7 @@ export async function reconcileOrders(env, shop, player = null, { expireOpen = f
461
461
  }
462
462
 
463
463
  /** Make the order row, then Stripe's Checkout Session. Returns { order, url } or a refusal Response. */
464
- async function startCheckout(env, url, cat, shop, item, { player, game, via, amount, parent = false, lines = null, requestAt = Date.now() }) {
464
+ async function startCheckout(env, url, cat, shop, item, { player, game, via, amount, parent = false, lines = null, reference = null, requestAt = Date.now() }) {
465
465
  lines ??= [{ item, amount, quantity: 1 }];
466
466
  const keyless = amount > 0 && !hasStripeKey(env);
467
467
  if (keyless && (lines.length !== 1 || fullerReason(shop))) return { error: fail(409, 'one-item', fullerReason(shop) ?? 'Buy one item at a time. A combined cart needs the fuller checkout connection.') };
@@ -480,6 +480,7 @@ async function startCheckout(env, url, cat, shop, item, { player, game, via, amo
480
480
  lines = lines.map((line, i) => ({ ...line, id: `${order.id}_${i}` }));
481
481
  const reserve = async () => {
482
482
  const results = await env.DB.batch([reservation(),
483
+ ...(reference === null ? [] : [env.DB.prepare("INSERT INTO meta(key,value) SELECT ?1,?2 WHERE EXISTS (SELECT 1 FROM shop_orders WHERE id=?3)").bind(`shop-reference:${order.id}`, reference, order.id)]),
483
484
  ...lines.map((line, i) => env.DB.prepare("INSERT INTO shop_order_lines (id, order_id, position, item, quantity, unit_amount, amount, snapshot) SELECT ?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8 WHERE EXISTS (SELECT 1 FROM shop_orders WHERE id = ?2)")
484
485
  .bind(line.id, order.id, i, line.item.id, line.quantity, line.amount, line.amount * line.quantity, JSON.stringify(line.item))),
485
486
  env.DB.prepare('UPDATE shop_orders SET referral_terms = ?2, checkout_player = player WHERE id = ?1').bind(order.id, shop.referrals ? JSON.stringify(shop.referrals) : null),
@@ -564,6 +565,8 @@ function purchaseLimited(request, shop, player, kind) {
564
565
  async function buyRoute(request, env, url, cat, shop, ready, ctx, requestAt) {
565
566
  const b = await bodyOf(request, requestSize(shop));
566
567
  if (b.error) return b.error;
568
+ const reference = b.body.reference ?? null;
569
+ if (reference !== null && (typeof reference !== 'string' || !reference.trim() || /[\u0000-\u001f\u007f]/.test(reference))) return fail(400, 'reference', 'reference must be a nonempty string without control characters');
567
570
  const freeOnly = (b.body.lines ?? [{ item: b.body.item }]).every?.((l) => shop.items.some((i) => i.id === l?.item && i.price === 0));
568
571
  if (!ready.ready && !(freeOnly && ready.missing.every((m) => ['payment-links', 'webhook-secret', 'fuller-checkout'].includes(m)))) return fail(503, 'closed', 'The shop is not open yet.', { missing: ready.missing, migration: ready.migration });
569
572
  const requested = b.body.lines ?? [{ item: b.body.item, quantity: b.body.quantity ?? 1, amount: b.body.amount }];
@@ -603,7 +606,7 @@ async function buyRoute(request, env, url, cat, shop, ready, ctx, requestAt) {
603
606
  cookies = guest.session.cookies;
604
607
  }
605
608
  const via = await viaFor(request, env, url, shop, player);
606
- const r = await startCheckout(env, url, cat, shop, item, { player, game, via, amount, lines, requestAt });
609
+ const r = await startCheckout(env, url, cat, shop, item, { player, game, via, amount, lines, reference, requestAt });
607
610
  const response = r.error ?? json({ ok: true, order: r.order.id, url: r.url });
608
611
  if (!r.error && !r.reconciled && ctx?.waitUntil) ctx.waitUntil(reconcileOrders(env, shop, player.id).catch((error) => logOrderError(env, r.order.id, error)));
609
612
  for (const cookie of cookies) response.headers.append('set-cookie', cookie);
@@ -864,6 +867,7 @@ async function bindLinkSession(env, session, mode) {
864
867
  const id = `ord_${(await sha256Hex(session.id)).slice(0, 20)}`;
865
868
  await env.DB.batch([
866
869
  env.DB.prepare("INSERT INTO shop_orders (id, player, checkout_player, item, game, amount, currency, till, mode, status, parent, via, created_at, updated_at, session, referral_terms) SELECT ?2, player, checkout_player, item, game, ?4, currency, till, mode, 'started', parent, via, ?5, ?5, ?3, referral_terms FROM shop_orders WHERE id = ?1 ON CONFLICT DO NOTHING").bind(source.id, id, session.id, amount, Date.now()),
870
+ env.DB.prepare("INSERT OR IGNORE INTO meta(key,value) SELECT ?1,value FROM meta WHERE key=?2").bind(`shop-reference:${id}`, `shop-reference:${source.id}`),
867
871
  env.DB.prepare('INSERT INTO shop_order_lines (id, order_id, position, item, quantity, unit_amount, amount, snapshot) VALUES (?1, ?2, 0, ?3, ?4, ?5, ?6, ?7) ON CONFLICT DO NOTHING').bind(`${id}_0`, id, item.id, quantity, amount / quantity, amount, lines[0].snapshot),
868
872
  ]);
869
873
  order = await orderBySession(env, session.id);
@@ -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.49.0';
2
+ export const STUDIO_VERSION_TAG = '0.50.0';