@homie-rocks/studio 0.50.4 → 0.50.5

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 CHANGED
@@ -17,6 +17,22 @@ 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.50.5] - 2026-10-14
21
+
22
+ **Plugin 0.50.5** · Homie has no referral system.
23
+
24
+ Your studio no longer tracks sales referrals or manages referral payments. Visit-source statistics remain available in your studio’s own stats.
25
+
26
+ ### Changed
27
+
28
+ - Removed referral terms, sales attribution, referral statements and office books, along with the referral commands and routes. Homie no longer sets an arrival cookie. `?via=` links still count as visit sources, without attributing sales.
29
+ - Referral payment arrangements now belong in your own Stripe Connect setup or Stripe-based affiliate service.
30
+
31
+ ### Upgrade notes
32
+
33
+ - Existing `shop.json` fields `referrals` and `referralNewPlayersOnly`, and `studio.json` `referrals`, are ignored with a one-line build note per field. They do not prevent a build; you can remove them from your configuration.
34
+ - The next deploy applies `0022_remove_referrals.sql`, deleting referral tables, stored referral keys and sales-attribution columns from orders. Orders, payments, refunds and entitlements remain intact. Old referral cookies are ignored, and the removed referral features are no longer available.
35
+
20
36
  ## [0.50.4] - 2026-10-13
21
37
 
22
38
  **Plugin 0.50.4** · Send studio events by signed webhook.
package/README.md CHANGED
@@ -517,8 +517,7 @@ that change does not affect the shop's ordinary restricted API key. [SHOP.md](sh
517
517
  In a game: `createShop()` from `@homie-rocks/studio/shop` (`has`, `entitlements`, `on('change')`, `open`, `used`, `add`, `checkout`, `buy`); a
518
518
  supporter's badge rides on their seat (`peer.badge`). Stripe Checkout (hosted), optional Stripe Tax, or Stripe Managed
519
519
  Payments (Stripe as seller of record, 3.5% more) as one switch; a signed, idempotent webhook; refunds from the
520
- office, per order or line; a dispute never touches the account; referral statements signed per referrer (homie.rocks is one more
521
- referrer). The guide: `shop/SHOP.md`.
520
+ office, per order or line; a dispute never touches the account. The guide: `shop/SHOP.md`.
522
521
 
523
522
  ## Setup status
524
523
 
@@ -192,7 +192,6 @@
192
192
  * homie-studio shop disconnect both secrets gone: the shop closes
193
193
  * homie-studio shop orders the latest orders (never a card or an email)
194
194
  * homie-studio shop refund <order> [--reason …] [--note "<why>"] ASKS the owner (a one-tap link)
195
- * homie-studio shop statements [--period YYYY-MM] [--send] signed referral statements this studio owes
196
195
  * homie-studio agents sit <game> [--server <id>] [--pass hap_…] [--label Claude]
197
196
  * (an AI guide's seat from this terminal: then lines of `do <goal> {args}`,
198
197
  * `say <line> {args}`, `look`, `stand`)
@@ -360,7 +359,7 @@ import { detectLocalAi } from '../lib/local-ai.mjs';
360
359
  import { dev, stopDev } from '../lib/dev.mjs';
361
360
  import { agentsBrain, agentsBrainKey, agentsPass, agentsPasses, agentsRevoke, agentsTry, serversClose, serversLevel, serversLines, serversList, serversMember, serversNew, serversSet } from '../lib/servers.mjs';
362
361
  import { AgentSeat } from '../lib/agent-seat.mjs';
363
- import { shopCheck, shopConnect, shopDisconnect, shopInit, shopLines, shopOrders, shopRefund, shopStatements, shopStatus } from '../lib/shop.mjs';
362
+ import { shopCheck, shopConnect, shopDisconnect, shopInit, shopLines, shopOrders, shopRefund, shopStatus } from '../lib/shop.mjs';
364
363
  import { catalogLines, catalogPlan } from '../lib/shop-catalog.mjs';
365
364
  import { projectsLines, setupViaProjects } from '../lib/projects.mjs';
366
365
  import { projectsCloudflareEnv } from '../lib/projects-env.mjs';
@@ -627,7 +626,6 @@ function print(result) {
627
626
  case 'shop orders':
628
627
  case 'shop check':
629
628
  case 'shop init':
630
- case 'shop statements':
631
629
  case 'shop connect':
632
630
  case 'shop disconnect':
633
631
  lines.push(...shopLines(result));
@@ -842,7 +840,7 @@ const OUTWARD_FLAGS = {
842
840
  'chat remove': ['url', 'all'], 'chat budget': ['url'],
843
841
  'servers close': ['url', 'reopen'], 'servers level': ['url'], 'servers member': ['url', 'role', 'remove'],
844
842
  'parts reissue': ['claim-hash', 'receipt-verified', 'buyer'], 'parts retire': [], 'parts keys': [], 'parts refund': ['game'],
845
- 'shop connect': ['url', 'managed', 'live', 'manual', 'renew', 'from-clipboard'], 'shop disconnect': ['url'], 'shop refund': ['url', 'reason', 'note', 'manual-transaction'], 'shop statements': ['url', 'period', 'send', 'cursor'],
843
+ 'shop connect': ['url', 'managed', 'live', 'manual', 'renew', 'from-clipboard'], 'shop disconnect': ['url'], 'shop refund': ['url', 'reason', 'note', 'manual-transaction'],
846
844
  'agents pass': ['url', 'label', 'server', 'hands', 'role', 'days'], 'agents revoke': ['url'], 'agents brain': ['url', 'budget', 'remove'],
847
845
  'agents sit': ['url', 'server', 'pass', 'label', 'brain'],
848
846
  'events webhook': ['url','after'],
@@ -1126,8 +1124,7 @@ async function main() {
1126
1124
  }
1127
1125
  if (sub === 'orders') return shopOrders(root, { url });
1128
1126
  if (sub === 'refund') return shopRefund(root, positional[2], { url, reason: flags.get('reason'), note: flags.get('note'), manualTransaction: flags.get('manual-transaction') });
1129
- if (sub === 'statements') return shopStatements(root, { url, period: flags.get('period'), cursor: flags.get('cursor') ?? '', send: flags.has('send') });
1130
- return { ok: false, command: 'shop', why: `unknown: shop ${sub} (init, check, connect, disconnect, catalog, orders, refund, statements)` };
1127
+ return { ok: false, command: 'shop', why: `unknown: shop ${sub} (init, check, connect, disconnect, catalog, orders, refund)` };
1131
1128
  }
1132
1129
  if (cmd === 'agents') {
1133
1130
  const url = flags.get('url');
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: shop
3
- description: Sell things in a Homie studio's games with the studio's OWN Stripe - a supporter pack, cosmetics, a season pass, a one-time unlock, a tip - in real money, with Stripe Checkout, Stripe Tax or Stripe Managed Payments, refunds from the office, and optional studio policies with an open default; set up with Stripe's own agent tools (Stripe's MCP server and skills) with one browser approval and Payment Links by default, no human-handled key and no API key in the Worker. Use when someone asks to sell something, add a shop or store, take payments or donations, make money from a game, set up or connect Stripe, make the products in Stripe, asks "how are sales?", about tax or Managed Payments, to add a supporter badge, refund a player, or about chargebacks, referral shares or affiliate links between studios.
3
+ description: Sell things in a Homie studio's games with the studio's OWN Stripe - a supporter pack, cosmetics, a season pass, a one-time unlock, a tip - in real money, with Stripe Checkout, Stripe Tax or Stripe Managed Payments, refunds from the office, and optional studio policies with an open default; set up with Stripe's own agent tools (Stripe's MCP server and skills) with one browser approval and Payment Links by default, no human-handled key and no API key in the Worker. Use when someone asks to sell something, add a shop or store, take payments or donations, make money from a game, set up or connect Stripe, make the products in Stripe, asks "how are sales?", about tax or Managed Payments, to add a supporter badge, refund a player, or about chargebacks.
4
4
  compatibility: The studio's own current toolkit with stripe_login or shop connect --renew, Wrangler, and the official Stripe CLI. Stripe's own agent plugin (MCP and skills) is optional for catalog work and analytics.
5
5
  metadata:
6
6
  providers: stripe
@@ -110,7 +110,6 @@ for approval before retrying. Do not silently replace the payment runtime with M
110
110
  | "Refund that player" | The owner’s official Stripe connection, or link their Stripe Dashboard payment | Honor Stripe’s confirmation link; signed events update the books and revoke fully refunded items. The keyed way still offers office refunds. |
111
111
  | "Someone charged back" | nothing to undo: the owner answers it in Stripe (the office links it) | While open nothing changes; lost: that one item goes; won: it stays. **The account is never locked or deleted over a dispute.** |
112
112
  | "Show the item in the game" / "the supporter badge" | in the game: `createShop()` from `@homie-rocks/studio/shop`; `shop.has('skin:ember')`, `shop.on('change', …)`, `shop.open('ember-skin')` from a button the player pressed, `shop.used(key)` when equipped | The play shell answers for the signed-in player; a badge rides on their seat as `peer.badge` (the Worker sets it, never a hello). |
113
- | "Pay studios that send us players" / "affiliate links" | `shop.json` `referrals` (rate, window, hold), `shop statements [--send]` | A `?via=<host>` link from another site (homie.rocks is one more referrer, on the same terms) counts for a new player's purchases; statements are signed with the studio's key; the referrer invoices the studio; the owner pays and marks it paid (an ASK from you). Nothing moves through Homie. |
114
113
 
115
114
  ## Your shop, your choices
116
115
 
@@ -120,7 +119,7 @@ Write `"policy": { "preset": "protective" }` to retain earlier account and age b
120
119
  `adults-only` is another optional bundle. Neither scans content. SHOP.md lists individual overrides.
121
120
 
122
121
  The studio can set items, quantities, prices, tip ranges, entitlements, sale dates, currency, optional
123
- `automaticTax`, `capPerPlayerMonth`, `refundDays`, referral terms and configurable flood protection.
122
+ `automaticTax`, `capPerPlayerMonth`, `refundDays` and configurable flood protection.
124
123
  No toolkit amount or duration ceiling applies. Stripe currency constraints and safe integer arithmetic apply.
125
124
  A refund window includes used items unless `policy.refundUsedItems` is false. A player cannot refund a tip;
126
125
  the owner can refund through Stripe; keyed shops also offer office refunds. Guest purchases stay owned when the buyer signs in later.
@@ -151,3 +150,5 @@ For a cause, use `shop init --giving --currency <organisation currency>`: one-ti
151
150
  ## App payment references
152
151
 
153
152
  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.
153
+
154
+ For referrals, use Stripe Connect to split payments or a Stripe-based affiliate service; Homie builds none.
package/lib/build.mjs CHANGED
@@ -510,6 +510,7 @@ export function orderGames(games, order, log = () => {}) {
510
510
  export async function build(root, { only = null, log = () => {}, deploy = process.env.WORKERS_CI === '1', maps = false, types = false, beforePublish = async () => {}, longCheck = false } = {}) {
511
511
  const esbuild = await studioEsbuild(root);
512
512
  const studio = readStudio(root);
513
+ if (Object.hasOwn(studio, 'referrals')) log('studio.json referrals: ignored; Homie has no referral system.');
513
514
  if (listGames(root).some(g => isRulesGame(g) && g.room?.host !== 'browser' && g.room?.host !== 'offline')) await (await import('./rules-wiring.mjs')).ensureRulesWiring(root);
514
515
  await (await import('./tools-build.mjs')).buildTools(root, esbuild);
515
516
  await (await import('./functions-build.mjs')).buildFunctions(root, esbuild);
@@ -710,10 +711,8 @@ async function buildInto(dist, { esbuild, studio, shop, live, games, before, typ
710
711
  : studio.homie?.directory === false || studio.homie?.directory === null ? null : String(studio.homie?.directory ?? 'https://homie.rocks'),
711
712
  // A copy of the public template that nobody has named yet shows the name typed in Cloudflare's form.
712
713
  ...(studio.template === true ? { template: true } : {}),
713
- // studio.json "audience": "kids" (a studio audience label; shop policies are optional) or "teens"; and "referrals": false
714
- // (this studio takes no referral statements as a referrer).
714
+ // studio.json audience is a studio label; shop policies are optional.
715
715
  ...(audienceOf(studio) !== 'general' ? { audience: audienceOf(studio) } : {}),
716
- ...(studio.referrals === false ? { referrals: false } : {}),
717
716
  ...(lounge ? { lounge } : {}),
718
717
  ...(studio.mcp ? { mcp: studio.mcp } : {}),
719
718
  build: buildInfo(root),
@@ -1,6 +1,6 @@
1
1
  import { experienceDir, experienceFile } from './studio.mjs';
2
2
  import { createHash } from 'node:crypto';
3
- import { canonicalJson } from '../worker/referrals.mjs';
3
+ import { canonicalJson } from '../worker/canonical-json.mjs';
4
4
  import { publicFiles, saleProblems } from "../worker/parts-sale.mjs";
5
5
  /**
6
6
  * GAME PARTS IN THE BUILD (parts/PARTS.md). Three small things lib/build.mjs and the deploy plan call, so the build
package/lib/parts.mjs CHANGED
@@ -20,7 +20,7 @@ import { experienceDir, experienceFile } from './studio.mjs';
20
20
  * NOTHING HERE IS A PACKAGE MANAGER: packages are npm's (lib/parts-store.mjs hands them to npm), and a part is copied
21
21
  * in and then the studio's own. There are no version ranges, no resolver and no lockfile of Homie's.
22
22
  */
23
- import { canonicalJson } from '../worker/referrals.mjs';
23
+ import { canonicalJson } from '../worker/canonical-json.mjs';
24
24
  import { createRequire } from 'node:module';
25
25
  const require = createRequire(import.meta.url);
26
26
  const parseSpdx = (value) => require('spdx-expression-parse')(value);
@@ -5,7 +5,7 @@ import { createHash } from 'node:crypto';
5
5
  import { existsSync, readFileSync } from 'node:fs';
6
6
  import { join } from 'node:path';
7
7
  import { sharedPartsOf, readPart, packedDir } from './parts.mjs';
8
- import { canonicalJson } from '../worker/referrals.mjs';
8
+ import { canonicalJson } from '../worker/canonical-json.mjs';
9
9
  import { paymentLinkURL } from '../worker/shop-links.mjs';
10
10
  const hash = (v) => createHash('sha256').update(canonicalJson(v)).digest('hex');
11
11
  export async function syncPurchaseLinks({ root, api, slug, site, mode, list, till }) {
package/lib/scaffold.mjs CHANGED
@@ -33,7 +33,7 @@ import { PLAYERS_MIGRATION, PLAYERS_MIGRATION_FILE } from '../worker/players.mjs
33
33
  import { OFFICE_MIGRATION, OFFICE_MIGRATION_FILE } from '../worker/office-schema.mjs';
34
34
  import { SERVERS_MIGRATION, SERVERS_MIGRATION_FILE } from '../worker/servers.mjs';
35
35
  import { CHAT_MIGRATION, CHAT_MIGRATION_FILE } from '../worker/chat-store.mjs';
36
- import { SHOP_MIGRATION, SHOP_MIGRATION_FILE, SHOP_RESERVATIONS, SHOP_RESERVATIONS_FILE, SHOP_STATEMENTS, SHOP_STATEMENTS_FILE, SHOP_LINES, SHOP_LINES_FILE } from '../worker/shop-store.mjs';
36
+ import { SHOP_MIGRATION, SHOP_MIGRATION_FILE, SHOP_RESERVATIONS, SHOP_RESERVATIONS_FILE, SHOP_REMOVE_REFERRALS, SHOP_REMOVE_REFERRALS_FILE, SHOP_LINES, SHOP_LINES_FILE } from '../worker/shop-store.mjs';
37
37
  import { LOUNGE_MIGRATION, LOUNGE_MIGRATION_FILE } from '../worker/lounge-store.mjs';
38
38
  import { themeFile } from './site.mjs';
39
39
  import { rulesIndex } from './studio.mjs';
@@ -521,7 +521,7 @@ Your shop, your choices. The studio sells through its own Stripe account and is
521
521
  where it sells and for Stripe's terms. Homie takes no cut.
522
522
 
523
523
  - \`shop.json\` lists items, wording, kinds, prices, quantities and entitlements. Optional settings include
524
- sale dates, tips, currency, \`automaticTax\`, refund windows, spending caps and referral terms.
524
+ sale dates, tips, currency, \`automaticTax\`, refund windows, spending caps.
525
525
  - The default is open: guests can buy repeatedly, without age questions, from any page. Signing in keeps
526
526
  guest purchases. \`shop init --supporter\` writes an example item without a policy, cap or refund window.
527
527
  - Choose \`policy.preset: "protective"\` or \`"adults-only"\` to enable an account and age policy. Omitted
@@ -545,7 +545,7 @@ where it sells and for Stripe's terms. Homie takes no cut.
545
545
  Stripe's approval links belong to the owner. \`checkoutMinutes\` is optional (Stripe: 30 to 1440 minutes,
546
546
  default 1440; values below 31 use 31 for transport margin); cancelling a named checkout, or replacing one at least a minute old, expires the open session on Stripe's confirmation.
547
547
  - \`shop orders\` and \`/_studio/office/shop\` show sales. \`shop refund <order>\` asks the owner to confirm.
548
- \`shop statements\` records optional referral shares; studios pay their referrers directly.
548
+ For referrals, use Stripe Connect to split payments or a Stripe-based affiliate service; Homie builds none.
549
549
 
550
550
  The guide is \`node_modules/@homie-rocks/studio/shop/SHOP.md\`.
551
551
 
@@ -817,8 +817,8 @@ export { default, Table, Lobby, Gate, Concentrator } from '@homie-rocks/studio/w
817
817
  [`site/migrations/${PURCHASE_MIGRATION_FILE}`]: PURCHASE_MIGRATION,
818
818
  [`site/migrations/${PURCHASE_STATE_FILE}`]: PURCHASE_STATE,
819
819
  [`site/migrations/${SHOP_RESERVATIONS_FILE}`]: SHOP_RESERVATIONS,
820
- [`site/migrations/${SHOP_STATEMENTS_FILE}`]: SHOP_STATEMENTS,
821
820
  [`site/migrations/${SHOP_LINES_FILE}`]: SHOP_LINES,
821
+ [`site/migrations/${SHOP_REMOVE_REFERRALS_FILE}`]: SHOP_REMOVE_REFERRALS,
822
822
  [`site/migrations/${APPS_MIGRATION_FILE}`]: APPS_MIGRATION,
823
823
  [`site/migrations/${MCP_MIGRATION_FILE}`]: MCP_MIGRATION,
824
824
  [`site/migrations/${FUNCTIONS_MIGRATION_FILE}`]: FUNCTIONS_MIGRATION,
@@ -920,7 +920,7 @@ export function ensureServersMigration(root) {
920
920
 
921
921
  /**
922
922
  * A studio made before 0.24.0 has no shop migration (worker/shop-store.mjs: orders, entitlements, the age band,
923
- * referral lines): add it, so the next `d1 migrations apply` makes its tables. A studio with no shop.json keeps
923
+ * refunds): add it, so the next `d1 migrations apply` makes its tables. A studio with no shop.json keeps
924
924
  * them empty. Returns the file it wrote, or null when it was there.
925
925
  */
926
926
  export function ensureShopMigration(root) {
@@ -939,12 +939,12 @@ function ensureShopReservations(root) {
939
939
  return `site/migrations/${SHOP_RESERVATIONS_FILE}`;
940
940
  }
941
941
 
942
- function ensureShopStatements(root) {
943
- const file = join(root, 'site', 'migrations', SHOP_STATEMENTS_FILE);
942
+ function ensureShopRemoveReferrals(root) {
943
+ const file = join(root, 'site', 'migrations', SHOP_REMOVE_REFERRALS_FILE);
944
944
  if (existsSync(file)) return null;
945
945
  mkdirSync(dirname(file), { recursive: true });
946
- writeFileSync(file, SHOP_STATEMENTS);
947
- return `site/migrations/${SHOP_STATEMENTS_FILE}`;
946
+ writeFileSync(file, SHOP_REMOVE_REFERRALS);
947
+ return `site/migrations/${SHOP_REMOVE_REFERRALS_FILE}`;
948
948
  }
949
949
 
950
950
  function ensureShopLines(root) {
@@ -1022,12 +1022,12 @@ function ensureMcpMigration(root) {
1022
1022
  mkdirSync(dirname(file),{recursive:true});writeFileSync(file,MCP_MIGRATION);return `site/migrations/${MCP_MIGRATION_FILE}`;
1023
1023
  }
1024
1024
  export function ensureMigrations(root) {
1025
- return [ensureStatsMigration(root), ensurePlayersMigration(root), ensureOfficeMigration(root), ensureServersMigration(root), ensureChatMigration(root), ensureShopMigration(root), ensureLoungeMigration(root), ensureShopReservations(root), ensureShopStatements(root), ensureShopLines(root), ensureAppsMigration(root), ensurePartsMigration(root), ensurePartsStateMigration(root), ensureMcpMigration(root), ensureFunctionsMigration(root), ensureFunctionCursors(root), ensureFunctionMeta(root), ensureFunctionReference(root)].filter(Boolean);
1025
+ return [ensureStatsMigration(root), ensurePlayersMigration(root), ensureOfficeMigration(root), ensureServersMigration(root), ensureChatMigration(root), ensureShopMigration(root), ensureLoungeMigration(root), ensureShopReservations(root), ensureShopLines(root), ensureAppsMigration(root), ensurePartsMigration(root), ensurePartsStateMigration(root), ensureMcpMigration(root), ensureFunctionsMigration(root), ensureFunctionCursors(root), ensureFunctionMeta(root), ensureFunctionReference(root), ensureShopRemoveReferrals(root)].filter(Boolean);
1026
1026
  }
1027
1027
 
1028
1028
  /** What a migration file the template added is for, in a few words (deploy and dev say it). */
1029
1029
  export function migrationWord(file) {
1030
- return /function_meta/.test(file) ? 'refund events alongside plain-text studio settings' : /studio_mcp/.test(file) ? 'AI connections and tool call history' : /studio_apps/.test(file) ? 'apps: lasting records and signed-in role grants' : /lounge/.test(file) ? 'the Lounge and kept chat: play nights, moderators, and what was said only where an owner turns history on' : /shop/.test(file) ? 'the shop: orders, what players own, refunds and referral books (paid sales wait until shop.json and the studio\'s Stripe connection are ready)' : /players/.test(file) ? 'player accounts and cloud saves' : /chat/.test(file) ? 'room chat: the owner\'s chat rules and players\' reports (never the chat itself)' : /servers/.test(file) ? 'servers and agent seats: room pools with their own rules, AI passes' : /office/.test(file) ? 'the back office: launch states, invites, the owner\'s controls' : 'the studio\'s own stats: counts, never tracks';
1030
+ return /remove_referrals/.test(file) ? 'remove referral tables, keys and sales attribution' : /function_meta/.test(file) ? 'refund events alongside plain-text studio settings' : /studio_mcp/.test(file) ? 'AI connections and tool call history' : /studio_apps/.test(file) ? 'apps: lasting records and signed-in role grants' : /lounge/.test(file) ? 'the Lounge and kept chat: play nights, moderators, and what was said only where an owner turns history on' : /shop/.test(file) ? 'the shop: orders, what players own, refunds (paid sales wait until shop.json and the studio\'s Stripe connection are ready)' : /players/.test(file) ? 'player accounts and cloud saves' : /chat/.test(file) ? 'room chat: the owner\'s chat rules and players\' reports (never the chat itself)' : /servers/.test(file) ? 'servers and agent seats: room pools with their own rules, AI passes' : /office/.test(file) ? 'the back office: launch states, invites, the owner\'s controls' : 'the studio\'s own stats: counts, never tracks';
1031
1031
  }
1032
1032
 
1033
1033
  /**
package/lib/shop.mjs CHANGED
@@ -22,7 +22,6 @@ import { paidReleases } from './parts-upload.mjs';
22
22
  * homie-studio shop orders the latest orders (never a card or an email)
23
23
  * homie-studio shop refund <order> [--reason requested_by_customer|duplicate|fraudulent] [--note "<why>"]
24
24
  * ASKS the owner (a one-tap link): the AI never refunds by itself
25
- * homie-studio shop statements [--period YYYY-MM] [--send] the signed referral statements this studio owes
26
25
  *
27
26
  * The proof of ownership is the studio's own Cloudflare login, as for the office.
28
27
  */
@@ -139,7 +138,7 @@ export async function shopStatus(root, { url } = {}) {
139
138
  export async function shopOrders(root, { url } = {}) {
140
139
  const r = await withKey(root, url, (call) => call('/_studio/api/shop'));
141
140
  if (!r.ok) return { ok: false, command: 'shop orders', why: r.message ?? r.why ?? 'the studio did not answer' };
142
- return { ok: true, command: 'shop orders', orders: (r.orders ?? []).map((o) => ({ id: o.id, at: new Date(o.createdAt).toISOString(), item: o.item, name: o.name, shown: o.shown, status: o.status, mode: o.mode, player: o.player?.name ?? null, via: o.via, stripe: o.stripe, manualRefund: o.manualRefund ?? null })) };
141
+ return { ok: true, command: 'shop orders', orders: (r.orders ?? []).map((o) => ({ id: o.id, at: new Date(o.createdAt).toISOString(), item: o.item, name: o.name, shown: o.shown, status: o.status, mode: o.mode, player: o.player?.name ?? null, stripe: o.stripe, manualRefund: o.manualRefund ?? null })) };
143
142
  }
144
143
 
145
144
  /** The AI proposes a refund: an ask the owner confirms with one tap (an office key can never refund by itself). */
@@ -149,28 +148,6 @@ export async function shopRefund(root, order, { url, reason, note, manualTransac
149
148
  return askedFor(root, url, r, 'shop refund');
150
149
  }
151
150
 
152
- export async function shopStatements(root, { url, period, cursor = '', send = false } = {}) {
153
- if (!send) {
154
- const r = await withKey(root, url, (call) => call(`/_studio/api/shop/statements?cursor=${encodeURIComponent(cursor)}${period ? `&period=${encodeURIComponent(period)}` : ''}`));
155
- return r.ok ? { ok: true, command: 'shop statements', period: r.period, nextCursor: r.nextCursor, statements: r.statements } : { ok: false, command: 'shop statements', why: r.message ?? r.why ?? 'the studio did not answer' };
156
- }
157
- const sent = [];
158
- console.error('Sending statements. Keep this command open; run it again after closing to resume safely.');
159
- return withKey(root, url, async (call) => {
160
- do {
161
- const page = await call('/_studio/api/shop/statements/send', { cursor, ...(period ? { period } : {}) });
162
- if (!page.ok) return { ok: false, command: 'shop statements', sent, why: page.message ?? page.why ?? 'the studio did not answer' };
163
- for (const failure of page.failures ?? []) if (!sent.some((x) => !x.ok && x.via === failure.via)) { sent.push(failure); console.error(`${failure.via}: failed: ${failure.why ?? failure.status}`); }
164
- sent.push(...page.sent);
165
- for (const result of page.sent) console.error(`${result.via}: ${result.ok ? 'page sent' : 'failed: ' + (result.why ?? result.status)}`);
166
- period = page.period;
167
- cursor = page.nextCursor;
168
- if (page.retryAfter) await new Promise((resolve) => setTimeout(resolve, page.retryAfter * 1000));
169
- } while (cursor !== null);
170
- return { ok: true, command: 'shop statements', period, nextCursor: null, sent };
171
- });
172
- }
173
-
174
151
  /* ------------------------------------------------------------------ the key, from a page on this computer */
175
152
 
176
153
  const esc = (v) => String(v ?? '').replace(/[&<>"']/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' })[c]);
@@ -439,7 +416,7 @@ export function shopLines(r) {
439
416
  lines.push(`Webhook endpoint for Stripe: ${r.hook}`, `Payouts: ${r.stripe?.payouts}`);
440
417
  if (r.local && !r.local.ok) lines.push(`shop.json here: ${r.local.errors.map((e) => e.message).join('; ')}`);
441
418
  } else if (r.command === 'shop orders') {
442
- for (const o of r.orders) lines.push(`${o.id} ${o.at.slice(0, 16)} ${o.name} ${o.shown} ${o.status}${o.mode === 'test' ? ' (test)' : ''} ${o.player ?? '(deleted account)'}${o.via ? ` via ${o.via}` : ''}`);
419
+ for (const o of r.orders) lines.push(`${o.id} ${o.at.slice(0, 16)} ${o.name} ${o.shown} ${o.status}${o.mode === 'test' ? ' (test)' : ''} ${o.player ?? '(deleted account)'}`);
443
420
  if (!r.orders.length) lines.push('No orders yet.');
444
421
  } else if (r.command === 'shop check') {
445
422
  if (r.absent) lines.push(r.message);
@@ -451,11 +428,6 @@ export function shopLines(r) {
451
428
  }
452
429
  } else if (r.command === 'shop init') {
453
430
  lines.push(`Wrote ${r.wrote.join(', ')} (till: ${r.till}${r.items.length ? `; ${r.items.join(', ')}` : ''}). Next:`, ...r.next.map((n) => ` ${n}`));
454
- } else if (r.command === 'shop statements') {
455
- if (r.nextCursor) lines.push(`More rows: shop statements --period ${r.period} --cursor '${r.nextCursor.replaceAll("'", "'\\''")}'${r.sent ? ' --send' : ''}`);
456
- if (r.sent) for (const s of r.sent) lines.push(`${s.via}: ${s.ok ? 'sent' : `not sent (${s.why ?? s.status})`}`);
457
- else for (const s of r.statements ?? []) lines.push(`${s.statement.referrer}: due ${s.statement.totals ? money(s.statement.totals.due, s.statement.currency) : 'see first page'} (${s.statement.lines.length} line(s)), signed`);
458
- if (!(r.sent ?? r.statements ?? []).length) lines.push(`No referral statements for ${r.period}.`);
459
431
  } else { lines.push(r.message ?? r.why ?? (r.alreadyConnected ? 'Stripe test credentials are already installed.' : '')); for (const step of r.next ?? []) lines.push(step); if (r.fallback) lines.push(`Optional fallback, only if chosen: ${r.fallback}`); }
460
432
  return lines;
461
433
  }
@@ -3263,7 +3263,10 @@
3263
3263
  "## Art direction and models": "a48fff7bf0a5f2fc",
3264
3264
  "## Running live games (the back office)": "51ebf2ab756f6e4d",
3265
3265
  "## Servers and AI seats": "1ccfd549d2369a60",
3266
- "## Selling things (the shop)": "9e020d69dd6a954e",
3266
+ "## Selling things (the shop)": [
3267
+ "43a3556be93d1235",
3268
+ "9e020d69dd6a954e"
3269
+ ],
3267
3270
  "## Continuing a build from the Claude app": "0d68081e924ca419",
3268
3271
  "## Rules": "b2234d80f6cbe71d",
3269
3272
  "## Telling Homie": "80b46fdd9e578db8",
@@ -3363,6 +3366,48 @@
3363
3366
  }
3364
3367
  }
3365
3368
  }
3369
+ },
3370
+ "0.50.5": {
3371
+ "files": {
3372
+ "CLAUDE.md": "ddf2b2a0376202f6",
3373
+ "README.md": "684b3cca8001673c",
3374
+ "HANDOFF.md": "29337e2ace95b2e7",
3375
+ "games/README.md": "5ad32fe80c61f117",
3376
+ "music/README.md": "a3dc042dadd39df8",
3377
+ "videos/README.md": "16babc3437d5545b",
3378
+ "posts/README.md": "37841aae476bd540",
3379
+ "site/README.md": "1dbdf22c03a1e505",
3380
+ "site/src/worker.mjs": "6cc5c6131eaf6fd0"
3381
+ },
3382
+ "sections": {
3383
+ "AGENTS.md": {
3384
+ "(intro)": "394cbbe934e80722",
3385
+ "## Version-matched authoring guides": "f6261a0e5801b96b",
3386
+ "## Layout": "f3a280b8c96cd068",
3387
+ "## Apps: one screen, roles and parts": "c10aa0441ea89702",
3388
+ "## Commands (all through the pinned CLI in node_modules)": "5222e08ca55e326e",
3389
+ "## The site": "0cb8dd9a6f8a0096",
3390
+ "## Making games": "bb715272fd1a615b",
3391
+ "## The Game Codex and progress": "0738ef719cef3a4c",
3392
+ "## Art direction and models": "a48fff7bf0a5f2fc",
3393
+ "## Running live games (the back office)": "51ebf2ab756f6e4d",
3394
+ "## Servers and AI seats": "1ccfd549d2369a60",
3395
+ "## Selling things (the shop)": "43a3556be93d1235",
3396
+ "## Continuing a build from the Claude app": "0d68081e924ca419",
3397
+ "## Rules": "b2234d80f6cbe71d",
3398
+ "## Telling Homie": "80b46fdd9e578db8",
3399
+ "## Beta": "9d02638207a26e73"
3400
+ }
3401
+ },
3402
+ "named": {
3403
+ "Homie Arcade": {
3404
+ "sections": {
3405
+ "AGENTS.md": {
3406
+ "## Commands (all through the pinned CLI in node_modules)": "611b437f5b3303f0"
3407
+ }
3408
+ }
3409
+ }
3410
+ }
3366
3411
  }
3367
3412
  }
3368
3413
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@homie-rocks/studio",
3
- "version": "0.50.4",
3
+ "version": "0.50.5",
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/SELLING.md CHANGED
@@ -4,7 +4,7 @@ You sell with your own Stripe account. Money goes directly to you; Homie takes n
4
4
  The studio is the seller and is responsible for the law where it sells and for Stripe's terms.
5
5
 
6
6
  Your `shop.json` chooses items, wording, prices, currency, tax integration, sale dates, entitlements,
7
- referral terms, optional spending caps and optional refund windows. Nothing requires an account or age
7
+ optional spending caps and optional refund windows. Nothing requires an account or age
8
8
  question by default. Guests can buy repeatedly and keep their purchases when signing in later.
9
9
 
10
10
  You can choose `policy.preset: "protective"` or `"adults-only"`, or set individual options.
package/shop/SHOP.md CHANGED
@@ -34,20 +34,16 @@ A kind is a label, not a recurring-billing, wallet, random-reward or fulfillment
34
34
  Its reservation releases only when Stripe confirms expiry or another terminal outcome.
35
35
  - `refundDays`: optional self-service refund window for items, including used items by default.
36
36
  A tip is never refunded by the player. The studio can refund any paid order or line from its office; disputed charges wait for Stripe to resolve the dispute.
37
- - `referrals`: optional `rate`, `windowDays`, `capPerPlayer`, `holdDays`, `minimumInvoice`,
38
- `billingEmail` and `accept`. `rate` defaults to zero until written;
39
- omitted limits do not apply. `referralNewPlayersOnly: true` limits attribution to new players.
40
37
  - `catalog`: modes (`test`, `live`) populated by `homie-studio shop catalog` for named Stripe Products.
41
38
  Prices always come from your file, even when a Stripe Product exists.
42
39
  - Optional flood protection: `purchaseAttemptsPerMinute`,
43
40
  `purchaseAttemptsPerAddressPerMinute`, `guestBuyersPerAddressPerHour`.
44
41
  Omitted or null means no limit. Explicit positive integers apply per Worker instance.
45
42
  An explicitly set `PLAYER_LIMIT_DAILY` also applies to new shop buyers.
46
- Referral delivery uses `REFERRAL_STATEMENTS_PER_MINUTE`, `REFERRAL_STATEMENTS_GLOBAL_PER_MINUTE`
47
- (both 30), and `REFERRAL_STATEMENT_BYTES` (65536). `requestBytes` overrides the catalog-sized shop request
43
+ `requestBytes` overrides the catalog-sized shop request
48
44
  allowance; `SHOP_WEBHOOK_BYTES` (524288) controls the webhook byte allowance.
49
45
 
50
- No toolkit ceiling applies to prices, quantities, tips, durations, refund windows, caps or referral terms.
46
+ No toolkit ceiling applies to prices, quantities, tips, durations, refund windows or caps.
51
47
  Money is integer Stripe currency units, with checked safe arithmetic. Stripe's currency rules and
52
48
  [Checkout's maximum of 100 lines](https://docs.stripe.com/api/checkout/sessions/create#line_items) apply.
53
49
  Free and paid lines can share a cart. An entirely free cart grants locally without a Stripe call. Product text limits and currency-unit requirements come from Stripe;
@@ -161,22 +157,23 @@ secret-returning webhook through a conversation tool: the private connect proces
161
157
 
162
158
  ## Payment guarantees and schema
163
159
 
164
- The schema step `0012_shop_lines.sql` follows the reservation and statement steps. Deploy applies it through
160
+ The schema step `0012_shop_lines.sql` follows the reservation step. Deploy applies it through
165
161
  the existing migration mechanism. It adds tables and columns without changing any existing primary key or
166
162
  column meaning. Released code keeps working after the step, including through rollback. The new Worker
167
163
  without the step still reads owned items and refunds from the office; new sales wait for the named step.
168
164
 
169
- A verified payment atomically grants all line snapshots and records the referral share on the order total.
165
+ `0022_remove_referrals.sql` removes the old referral tables, keys and sales-attribution columns; orders and entitlements remain intact. Old referral settings are ignored with a build note.
166
+
167
+ A verified payment atomically records the payment and grants all line snapshots.
170
168
  Duplicate events, reconciliation and late payments grant exactly once, including after a missing mark,
171
- owner release, expiry or failure. Refunds revoke only their own lines, retain other purchases of the same key,
172
- and adjust the referral share with exact integer allocation. Stripe tax is read per line when applicable.
169
+ owner release, expiry or failure. Refunds revoke only their own lines, and retain other purchases of the same key. Stripe tax is read per line when applicable.
173
170
  Snapshots preserve delivery after catalog edits; the schema step represents older orders as one line.
174
171
  An unpaid older order has no historical item snapshot and needs its catalog definition to complete delivery.
175
172
  A missing definition gives a named retryable response and office note; the office can refund the verified
176
173
  payment. Older paid orders missing a grant are repaired once. An unknown older item is refunded by the office,
177
174
  since its original kind cannot be recovered safely for player self-service.
178
175
  A whole-order refund uses one Stripe refund for the remaining charge. Dashboard partial refunds reduce cap
179
- usage and referral shares by their pre-tax portion, without assigning money to an unnamed line.
176
+ usage by their pre-tax portion, without assigning money to an unnamed line.
180
177
  Refunds are rebuilt from [Stripe’s refunds list](https://docs.stripe.com/api/refunds/list). Only
181
178
  [succeeded refunds](https://docs.stripe.com/api/refunds/object#refund_object-status) revoke items;
182
179
  pending refunds keep them and failed refunds restore them. A named line loses only its own items;
@@ -219,3 +216,5 @@ These calls open the same shop in games and apps. Inside a player card, purchase
219
216
  ## App payment references
220
217
 
221
218
  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.
219
+
220
+ For referrals, use Stripe Connect to split payments or a Stripe-based affiliate service; Homie builds none.
@@ -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.50.4","sha256":"d3f8cab6830919a815fcb6f995ea98a43fa1f246d240a2d1afe22905f38aae08"});
2
+ export const STUDIO_BUILD = Object.freeze({"version":"0.50.5","sha256":"11a7b1a55c4f2499f8a04e3f9808ed38f4dde1725f991fffdcfab47dc7a28ea0"});
@@ -0,0 +1,6 @@
1
+ /** JSON with sorted keys: what a signature covers, byte for byte, on both sides. */
2
+ export function canonicalJson(v) {
3
+ if (Array.isArray(v)) return `[${v.map(canonicalJson).join(',')}]`;
4
+ if (v && typeof v === 'object') return `{${Object.keys(v).sort().filter((k) => v[k] !== undefined).map((k) => `${JSON.stringify(k)}:${canonicalJson(v[k])}`).join(',')}}`;
5
+ return JSON.stringify(v ?? null);
6
+ }
@@ -1,5 +1,5 @@
1
1
  import {byClaim} from './purchase-store.mjs';
2
- import {canonicalJson} from './referrals.mjs';
2
+ import {canonicalJson} from './canonical-json.mjs';
3
3
  import {purchaseResponse} from './purchase-response.mjs';
4
4
  import {machineResource} from './purchase-machine.mjs';
5
5
  import {purchaseSchema} from './purchase-mcp.mjs';
@@ -4,7 +4,7 @@ import {registerResourceKind} from './resource-kinds.mjs';
4
4
  import {quoteHash,legalTerms} from './parts-sale.mjs';
5
5
  import {digest,manifestHash} from './purchase-crypto.mjs';
6
6
  import {offerVersion} from './purchase-core.mjs';
7
- import {canonicalJson} from './referrals.mjs';
7
+ import {canonicalJson} from './canonical-json.mjs';
8
8
  import {serviceTerms} from './service-terms.mjs';
9
9
  export {serviceTerms} from './service-terms.mjs';
10
10
  export async function pruneOffers(env,now=Date.now()) {
package/worker/index.mjs CHANGED
@@ -66,7 +66,6 @@ import { appRole, openPath } from './app-format.mjs';
66
66
  * /shop/, /api/shop/... the studio's shop (worker/shop.mjs, shop/SHOP.md): its items in real money, the studio's
67
67
  * OWN Stripe Checkout, the signed webhook, what a player owns (/api/player/owns), refunds;
68
68
  * closed until the owner's restricted key is in; never on a kids server or a television
69
- * /api/referrals/statement another studio's signed referral statement to this one (worker/referrals.mjs)
70
69
  *
71
70
  * A page in the studio's site/pages wins over the generated one at the same address. Every HTML answer is
72
71
  * `no-transform` (an edge in front of a custom domain injects nothing) and is never framed by another site.
@@ -130,7 +129,6 @@ import { STUDIO_VERSION_TAG } from './version.mjs';
130
129
  import { reconcileOrders, readiness, roomBadge, shellShop, shopOf, shopRoutes } from './shop.mjs';
131
130
  import { DISCOVERY_FILES, discoveryResponse, llmsTxt, robotsTxt, sitemapXml } from './discover.mjs';
132
131
  import { ldScript, studioNode } from './schema.mjs';
133
- import { arrivalCookie, manifestReferrals } from './referrals.mjs';
134
132
  import { licenseOf } from './license.mjs';
135
133
  // GAME PARTS (parts/PARTS.md section 4): three call-outs below, everything else is worker/parts.mjs.
136
134
  import { PART_FILES, isPartsPath, partsRoutes, withPartsBand } from './parts.mjs';
@@ -647,15 +645,6 @@ export default {
647
645
  }
648
646
  }
649
647
  if (appLobby || appRecords) return appCors(request, res);
650
- // A referral's arrival (worker/referrals.mjs): only a person's page load with ?via= of another site, on a studio
651
- // that pays referrals. The page is answered first; a cookie is added to it only then.
652
- if (url.searchParams.has('via') && res && res.status === 200 && /text\/html/i.test(res.headers.get('content-type') ?? '')) {
653
- try {
654
- const cat = await catalogue(env, url.origin);
655
- const cookie = await arrivalCookie(request, env, url, shopOf(cat));
656
- if (cookie) { const out = new Response(res.body, res); out.headers.append('set-cookie', cookie); return out; }
657
- } catch { /* an arrival is never worth a failed page */ }
658
- }
659
648
  return res;
660
649
  },
661
650
  };
@@ -770,10 +759,8 @@ async function route(request, env, ctx) {
770
759
  ...(cat.studio?.lounge ? { lounge: { name: cat.studio.lounge.name, page: `${url.origin}/lounge/`, now: `${url.origin}/lounge/api/now` } } : {}),
771
760
  // Shared only when studio.json says `stats.share`: two numbers for the whole studio, for the hub.
772
761
  ...(played ? { played } : {}),
773
- // The shop (0.24.0): whether it sells and its till, never a key or a sale; and referrals: this studio takes
774
- // signed statements as a referrer, with the public half of its statement key and, if it sells, its terms.
762
+ // The shop: whether it sells and its till, never a key or a sale.
775
763
  ...(shopOf(cat)?.open ? { shop: { open: true, till: shopOf(cat).till, currency: shopOf(cat).currency, page: `${url.origin}/shop/` } } : {}),
776
- ...(await manifestReferrals(env, url.origin, shopOf(cat), cat.studio).then((r) => (r ? { referrals: r } : {})).catch(() => ({}))),
777
764
  }, 200, { 'access-control-allow-origin': '*' });
778
765
  }
779
766
  if (path === '/api/games') {
@@ -126,9 +126,9 @@ export function builtins(env,cat,origin) {
126
126
  for(const [name,path,description] of [
127
127
  ['room_announce','announce','Announce to a room.'],['room_kick','kick','Ask the owner to remove a player.'],['room_mute','mute','Ask the owner to mute a player.'],['room_close','close','Ask the owner to close a room.'],['studio_game','game','Ask the owner to change game access or room size.'],['shop_refund','shop/refund','Ask the owner to refund an order.'],
128
128
  ]) tools.push(tool(name,description,'owner',object({action:{type:'object'}},['action']),({action},ctx)=>ctx.office('/_studio/api/'+path,action)));
129
- const reads={rooms:'office',servers:'servers',members:'servers/members',chat:'chat',lounge:'lounge',shop:'shop',orders:'shop/orders.csv',lines:'shop/lines',received:'shop/received',statements:'shop/statements',invites:'invites'};
129
+ const reads={rooms:'office',servers:'servers',members:'servers/members',chat:'chat',lounge:'lounge',shop:'shop',orders:'shop/orders.csv',invites:'invites'};
130
130
  tools.push(tool('studio_office_read','Read an office view; use query for its existing filters and pagination.','owner',object({view:{enum:Object.keys(reads)},query:{type:'object',additionalProperties:{type:'string'}}},['view']),({view,query},ctx)=>ctx.office('/_studio/api/'+reads[view],undefined,query)));
131
- const actions=['announce','kick','mute','close','game','servers','servers/set','servers/close','servers/member','room-level','agents/brain','chat/rules','chat/remove','chat/report','chat/budget','lounge/rules','lounge/night','lounge/mod','lounge/remove','lounge/hold','shop/release','shop/refund','shop/settle','invites','invites/revoke'];
131
+ const actions=['announce','kick','mute','close','game','servers','servers/set','servers/close','servers/member','room-level','agents/brain','chat/rules','chat/remove','chat/report','chat/budget','lounge/rules','lounge/night','lounge/mod','lounge/remove','lounge/hold','shop/release','shop/refund','invites','invites/revoke'];
132
132
  tools.push(tool('studio_office_action','Use an existing office control. Destructive changes return a browser confirmation for the owner.','owner',object({operation:{enum:actions},action:{type:'object'}},['operation','action']),({operation,action},ctx)=>ctx.office('/_studio/api/'+operation,action)));
133
133
  for (const op of ['sit','look','act','speak','stand']) tools.push(tool('agent_'+op, 'A marked AI guide: '+op+'. Room policy and declared vocabulary apply.', 'signed-in', object({game:str,role:str,server:str,room:str,goal:str,line:str,args:{type:'object'}},['game']), async (args,ctx)=>(await import('./mcp-room.mjs')).remoteSeat(env,cat,ctx.caller,op,args)));
134
134
  tools.push(...selling.customerTools(env,cat,origin));
@@ -978,7 +978,7 @@ export function lockedPage(cat, { what = 'office', ask = null } = {}) {
978
978
  return new Response(shell(cat, `${what === 'confirm' ? 'Confirm' : 'Office'} · private`, body), { status: 401, headers: { ...PRIVATE, 'content-security-policy': NO_SCRIPT_CSP } });
979
979
  }
980
980
 
981
- const VERB = { refund: 'Refund it', 'shop-settle': 'Mark it paid', kick: 'Kick', mute: 'Mute', close: 'Close the room', announce: 'Announce', game: 'Change it', 'server-set': 'Change it', 'server-close': 'Close it', member: 'Remove', 'agents-brain': 'Let them talk' };
981
+ const VERB = { refund: 'Refund it', kick: 'Kick', mute: 'Mute', close: 'Close the room', announce: 'Announce', game: 'Change it', 'server-set': 'Change it', 'server-close': 'Close it', member: 'Remove', 'agents-brain': 'Let them talk' };
982
982
 
983
983
  /** The owner's one tap for what their AI asked, and what came of it. */
984
984
  export function confirmPage(cat, ask, { missing = false } = {}) {