@homie-rocks/studio 0.39.0 → 0.42.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 (83) hide show
  1. package/CHANGELOG.md +55 -1
  2. package/README.md +11 -0
  3. package/bin/homie-studio.mjs +8 -0
  4. package/functions/FUNCTIONS.md +99 -0
  5. package/functions/functions.ts +13 -0
  6. package/lib/build.mjs +2 -0
  7. package/lib/cloudflare.mjs +9 -9
  8. package/lib/dev.mjs +2 -1
  9. package/lib/functions-build.mjs +36 -0
  10. package/lib/functions-cli.mjs +23 -0
  11. package/lib/port-check.mjs +6 -3
  12. package/lib/purchase-links.mjs +8 -0
  13. package/lib/rules-build.mjs +8 -1
  14. package/lib/rules-check-worker.mjs +3 -3
  15. package/lib/rules-check.mjs +3 -1
  16. package/lib/scaffold.mjs +16 -3
  17. package/lib/shop.mjs +1 -1
  18. package/lib/template-history.json +85 -0
  19. package/lib/tools-build.mjs +2 -0
  20. package/lib/upgrade.mjs +2 -0
  21. package/lib/worker-build.mjs +8 -20
  22. package/lib/worker-config.mjs +28 -0
  23. package/netplay/NETPLAY.md +74 -2
  24. package/package.json +7 -2
  25. package/rules/collision.ts +115 -0
  26. package/rules/core.ts +65 -25
  27. package/rules/host.ts +5 -1
  28. package/rules/math.ts +15 -5
  29. package/rules/rules.ts +33 -10
  30. package/rules/types.ts +2 -2
  31. package/rules/view.ts +126 -32
  32. package/shop/SHOP.md +4 -3
  33. package/starters/ember-vale/game.json +16 -4
  34. package/starters/ember-vale/map/main.json +66 -0
  35. package/starters/ember-vale/src/move.ts +31 -0
  36. package/starters/ember-vale/src/rules.ts +244 -0
  37. package/starters/ember-vale/src/{main.ts → view.ts} +89 -574
  38. package/starters/ember-vale/tunables.json +100 -11
  39. package/starters/gem-rush/src/move.ts +9 -1
  40. package/starters/gem-rush/src/view.ts +2 -1
  41. package/starters/gem-rush-3d/game.json +23 -5
  42. package/starters/gem-rush-3d/map/main.json +153 -0
  43. package/starters/gem-rush-3d/src/move.ts +37 -0
  44. package/starters/gem-rush-3d/src/rules.ts +133 -0
  45. package/starters/gem-rush-3d/src/{main.ts → view.ts} +63 -671
  46. package/starters/gem-rush-3d/tunables.json +98 -11
  47. package/starters/hero-rush-3d/game.json +23 -5
  48. package/starters/hero-rush-3d/map/main.json +153 -0
  49. package/starters/hero-rush-3d/src/move.ts +49 -0
  50. package/starters/hero-rush-3d/src/rules.ts +200 -0
  51. package/starters/hero-rush-3d/src/{main.ts → view.ts} +83 -976
  52. package/starters/hero-rush-3d/tunables.json +260 -30
  53. package/tools/TOOLS.md +42 -2
  54. package/tools/tools.ts +3 -0
  55. package/worker/customer-mcp.mjs +77 -0
  56. package/worker/customer-resources.mjs +53 -0
  57. package/worker/customer-shop.mjs +54 -0
  58. package/worker/extensions.mjs +1 -0
  59. package/worker/functions-schema.mjs +55 -0
  60. package/worker/functions.mjs +93 -0
  61. package/worker/index.mjs +37 -2
  62. package/worker/machine-payments.mjs +1 -1
  63. package/worker/mcp-store.mjs +3 -2
  64. package/worker/mcp-tools.mjs +16 -4
  65. package/worker/mcp.mjs +31 -15
  66. package/worker/office-page.mjs +1 -1
  67. package/worker/players.mjs +5 -2
  68. package/worker/purchase-delivery.mjs +1 -0
  69. package/worker/purchase-hosted-readiness.mjs +0 -1
  70. package/worker/purchase-links.mjs +1 -1
  71. package/worker/purchase-machine.mjs +1 -0
  72. package/worker/purchase-office.mjs +1 -1
  73. package/worker/purchase-owner.mjs +0 -2
  74. package/worker/purchase-reconciliation.mjs +1 -0
  75. package/worker/purchase-routes.mjs +11 -17
  76. package/worker/purchase-store.mjs +1 -0
  77. package/worker/selling/runtime.mjs +4 -1
  78. package/worker/service-run.mjs +25 -0
  79. package/worker/service-terms.mjs +2 -0
  80. package/worker/shop-rules.mjs +9 -8
  81. package/worker/shop.mjs +22 -9
  82. package/worker/tool-webhook.mjs +1 -0
  83. package/worker/version.mjs +1 -1
@@ -0,0 +1,2 @@
1
+ /** Shared immutable terms for priced tools and their Stripe Payment Links. */
2
+ export const serviceTerms = (price) => ({billing:'one-time',scope:'service',updates:'none',source:false,commercialUse:true,transferable:false,onRefund:'terminate',onExpiry:'terminate',refund:'Ask the studio for a refund.',...price});
@@ -128,14 +128,15 @@ export function checkShop(raw, { games = null, audience = 'general', studioName
128
128
  const requestBytes = optionalNumber(raw.requestBytes, 'requestBytes');
129
129
  const refundDays = optionalNumber(raw.refundDays, 'refundDays');
130
130
  const cap = optionalNumber(raw.capPerPlayerMonth, 'capPerPlayerMonth');
131
- // 600 new buyers per hour allows a school or venue to arrive together without disabling flood protection.
132
- const guestBuyersPerAddressPerHour = numeric(raw.guestBuyersPerAddressPerHour ?? 600);
133
- if (!Number.isSafeInteger(guestBuyersPerAddressPerHour) || guestBuyersPerAddressPerHour < 1) err('guestBuyersPerAddressPerHour', 'use a positive whole number for new guest flood protection');
134
- const purchaseAttemptsPerMinute = numeric(raw.purchaseAttemptsPerMinute ?? 6);
135
- // A school or venue can share an address; its aggregate bucket must allow many independent accounts.
136
- const purchaseAttemptsPerAddressPerMinute = numeric(raw.purchaseAttemptsPerAddressPerMinute ?? 600);
137
- if (!Number.isSafeInteger(purchaseAttemptsPerAddressPerMinute) || purchaseAttemptsPerAddressPerMinute < 1) err('purchaseAttemptsPerAddressPerMinute', 'use a positive whole number for address flood protection');
138
- if (!Number.isSafeInteger(purchaseAttemptsPerMinute) || purchaseAttemptsPerMinute < 1) err('purchaseAttemptsPerMinute', 'use a positive whole number for the studio account rate limit');
131
+ const ratePolicy = key => {
132
+ if(raw[key] === undefined || raw[key] === null)return null;
133
+ const value=numeric(raw[key]);
134
+ if(!Number.isSafeInteger(value)||value<1)err(key,'use a positive whole number, or omit the rate policy');
135
+ return value;
136
+ };
137
+ const guestBuyersPerAddressPerHour = ratePolicy('guestBuyersPerAddressPerHour');
138
+ const purchaseAttemptsPerMinute = ratePolicy('purchaseAttemptsPerMinute');
139
+ const purchaseAttemptsPerAddressPerMinute = ratePolicy('purchaseAttemptsPerAddressPerMinute');
139
140
 
140
141
  for (const key of ['automaticTax', 'referralNewPlayersOnly']) if (raw[key] !== undefined && typeof raw[key] !== 'boolean') err(key, 'use a boolean');
141
142
  const items = [];
package/worker/shop.mjs CHANGED
@@ -556,8 +556,8 @@ const WAY_WORDS = Object.freeze({
556
556
  });
557
557
 
558
558
  function purchaseLimited(request, shop, player, kind) {
559
- const account = player && limited(`${kind}:account:${player}`, shop.purchaseAttemptsPerMinute, 60_000);
560
- const address = limited(`${kind}:address:${addressOf(request)}`, shop.purchaseAttemptsPerAddressPerMinute, 60_000);
559
+ const account = player && shop.purchaseAttemptsPerMinute !== null && limited(`${kind}:account:${player}`, shop.purchaseAttemptsPerMinute, 60_000);
560
+ const address = shop.purchaseAttemptsPerAddressPerMinute !== null && limited(`${kind}:address:${addressOf(request)}`, shop.purchaseAttemptsPerAddressPerMinute, 60_000);
561
561
  return account || address;
562
562
  }
563
563
 
@@ -707,6 +707,18 @@ function refundableNow(order, owns, shop, now = Date.now()) {
707
707
  return shop.policy.refundUsedItems || !owns.some((o) => o.item === order.item && o.used);
708
708
  }
709
709
 
710
+ /** Shared authorization for web and claim refunds, using persisted grants and line snapshots. */
711
+ export async function refundableShopOrder(env, order, shop, lines = null) {
712
+ if (!order || !shop) return false;
713
+ lines ??= await orderLines(env, order.id);
714
+ if (!lines.length) return false;
715
+ for (const line of lines) {
716
+ const used = await lineWasUsed(env, order.id, line);
717
+ if (!refundableNow({ ...lineView(line), paidAt: order.paid_at }, used ? [{ item: line.item, used: true }] : [], shop)) return false;
718
+ }
719
+ return true;
720
+ }
721
+
710
722
  /** A player's own refund follows the studio window and used-item setting, excluding tips. */
711
723
  async function selfRefundRoute(request, env, shop, ready) {
712
724
  const b = await bodyOf(request, requestSize(shop));
@@ -717,10 +729,7 @@ async function selfRefundRoute(request, env, shop, ready) {
717
729
  if (!o || o.player !== p.id) return fail(404, 'order', 'no such order on this account');
718
730
  const lines = (await orderLines(env, o.id)).filter((l) => !b.body.line || l.id === b.body.line);
719
731
  if (!lines.length) return fail(404, 'line', 'no such line on this order');
720
- for (const line of lines) {
721
- const used = await lineWasUsed(env, o.id, line);
722
- if (!refundableNow({ ...lineView(line), paidAt: o.paid_at }, used ? [{ item: line.item, used: true }] : [], shop)) return fail(403, 'not-refundable', 'Ask the studio for this refund. Tips are not refundable by the player; items follow the studio refund settings.');
723
- }
732
+ if (!await refundableShopOrder(env, o, shop, lines)) return fail(403, 'not-refundable', 'Ask the studio for this refund. Tips are not refundable by the player; items follow the studio refund settings.');
724
733
  if (Number(o.amount) !== 0 && !hasStripeKey(env)) return fail(409, 'ask-studio', 'Ask the studio to refund this payment in Stripe.');
725
734
  const r = await refundOrder(env, o, { reason: 'requested_by_customer', by: 'player', line: b.body.line });
726
735
  if (r.held) return json({ ...r, error: r.error === 'pending' ? 'pending' : 'held', message: r.error === 'pending' ? r.message : 'The studio approves this refund in Stripe first. Your money comes back once they do, and the item leaves your account then.' }, 202);
@@ -743,7 +752,11 @@ async function hook(request, env, cat, shop, ready) {
743
752
  if (selling.enabled) {
744
753
  if (typeof ev.livemode === 'boolean' && ev.livemode !== (shopMode(env) === 'live')) return json({ ok: true, ignored: 'mode' });
745
754
  const purchase = await selling.purchasePaymentEvent(env, ev);
746
- if (purchase !== null) { await env.DB.prepare('INSERT INTO shop_events (id, type, at) VALUES (?1, ?2, ?3) ON CONFLICT(id) DO NOTHING').bind(ev.id, ev.type, Date.now()).run(); return json({ ok: true, did: purchase }); }
755
+ if (purchase !== null) {
756
+ // Resource carts share the web shop's grants and refund bookkeeping.
757
+ const payment = typeof obj.payment_intent === 'string' ? obj.payment_intent : obj.payment_intent?.id;
758
+ if (payment) await syncRefunds(env, payment);
759
+ await env.DB.prepare('INSERT INTO shop_events (id, type, at) VALUES (?1, ?2, ?3) ON CONFLICT(id) DO NOTHING').bind(ev.id, ev.type, Date.now()).run(); return json({ ok: true, did: purchase }); }
747
760
  }
748
761
  if (!needed.includes(ev.type)) return json({ ok: true, did: 'ignored' });
749
762
  if (typeof ev.livemode === 'boolean' && (hasStripeKey(env) || linkConfig(env)) && ev.livemode !== (shopMode(env) === 'live')) return json({ ok: true, ignored: 'mode' });
@@ -864,7 +877,7 @@ async function bindLinkSession(env, session, mode) {
864
877
  }
865
878
 
866
879
  /** A paid checkout: the order is paid, its entitlements granted, a referral line written. Idempotent. */
867
- async function paid(env, shop, session, paidAt = Date.now()) {
880
+ export async function paid(env, shop, session, paidAt = Date.now()) {
868
881
  const o = await orderBySession(env, session.id);
869
882
  if (!o) return 'unknown-order';
870
883
  // The session must be the one this order opened, for this player and this price.
@@ -950,7 +963,7 @@ async function paid(env, shop, session, paidAt = Date.now()) {
950
963
  * https://docs.stripe.com/api/refunds/object#refund_object-status
951
964
  * https://docs.stripe.com/api/refunds/list
952
965
  */
953
- async function syncRefunds(env, payment, freeOrder = null, freeLines = []) {
966
+ export async function syncRefunds(env, payment, freeOrder = null, freeLines = []) {
954
967
  const o = freeOrder ?? await orderByPayment(env, payment);
955
968
  if (!o) return 'unknown-order';
956
969
  const all = await orderLines(env, o.id);
@@ -6,6 +6,7 @@ export async function toolWebhook(request,env,{cat,definitions}) {
6
6
  const name=new URL(request.url).pathname.split('/').pop(),tool=definitions.find(t=>t.name===name&&t.webhook);
7
7
  const fail=(status)=>Response.json({ok:false},{status});
8
8
  if(!tool||request.method!=='POST')return fail(404);
9
+ if(tool.price)return Response.json({ok:false,error:'Use /mcp to approve and pay for this tool'},{status:402});
9
10
  const timestamp=request.headers.get('x-studio-timestamp')??'',delivery=request.headers.get('x-studio-delivery')??'',signature=request.headers.get('x-studio-signature')??'';
10
11
  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
12
  const secret=env[tool.webhook.secret];if(typeof secret!=='string'||secret.length<32)return fail(503);
@@ -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.39.0';
2
+ export const STUDIO_VERSION_TAG = '0.42.0';