@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 +16 -0
- package/README.md +1 -2
- package/bin/homie-studio.mjs +3 -6
- package/guides/shop/SKILL.md +4 -3
- package/lib/build.mjs +2 -3
- package/lib/parts-build.mjs +1 -1
- package/lib/parts.mjs +1 -1
- package/lib/purchase-links.mjs +1 -1
- package/lib/scaffold.mjs +11 -11
- package/lib/shop.mjs +2 -30
- package/lib/template-history.json +46 -1
- package/package.json +1 -1
- package/shop/SELLING.md +1 -1
- package/shop/SHOP.md +10 -11
- package/worker/build-id.mjs +1 -1
- package/worker/canonical-json.mjs +6 -0
- package/worker/customer-mcp.mjs +1 -1
- package/worker/customer-resources.mjs +1 -1
- package/worker/index.mjs +1 -14
- package/worker/mcp-tools.mjs +2 -2
- package/worker/office-page.mjs +1 -1
- package/worker/office.mjs +7 -14
- package/worker/parts-sale.mjs +1 -1
- package/worker/purchase-core.mjs +1 -1
- package/worker/purchase-crypto.mjs +1 -1
- package/worker/purchase-keys.mjs +1 -1
- package/worker/purchase-links.mjs +1 -1
- package/worker/purchase-owner.mjs +1 -1
- package/worker/purchase-routes.mjs +1 -1
- package/worker/purchase-store.mjs +1 -1
- package/worker/shop-page.mjs +5 -69
- package/worker/shop-rules.mjs +5 -33
- package/worker/shop-store.mjs +76 -56
- package/worker/shop.mjs +22 -78
- package/worker/version.mjs +1 -1
- package/worker/referrals.mjs +0 -460
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
|
|
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
|
|
package/bin/homie-studio.mjs
CHANGED
|
@@ -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,
|
|
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'],
|
|
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
|
-
|
|
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');
|
package/guides/shop/SKILL.md
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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),
|
package/lib/parts-build.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { experienceDir, experienceFile } from './studio.mjs';
|
|
2
2
|
import { createHash } from 'node:crypto';
|
|
3
|
-
import { canonicalJson } from '../worker/
|
|
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/
|
|
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);
|
package/lib/purchase-links.mjs
CHANGED
|
@@ -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/
|
|
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,
|
|
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
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
|
943
|
-
const file = join(root, 'site', 'migrations',
|
|
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,
|
|
947
|
-
return `site/migrations/${
|
|
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),
|
|
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
|
|
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,
|
|
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) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[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)'}
|
|
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)":
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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.
|
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.50.
|
|
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
|
+
}
|
package/worker/customer-mcp.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import {byClaim} from './purchase-store.mjs';
|
|
2
|
-
import {canonicalJson} from './
|
|
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 './
|
|
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
|
|
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') {
|
package/worker/mcp-tools.mjs
CHANGED
|
@@ -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',
|
|
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','
|
|
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));
|
package/worker/office-page.mjs
CHANGED
|
@@ -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',
|
|
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 } = {}) {
|