@homie-rocks/studio 0.37.1 → 0.38.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +60 -0
- package/bin/homie-studio.mjs +7 -6
- package/lib/cloudflare.mjs +28 -3
- package/lib/dev.mjs +1 -1
- package/lib/parts-build.mjs +23 -4
- package/lib/parts-cli.mjs +54 -3
- package/lib/parts-purchase.mjs +174 -0
- package/lib/parts-store.mjs +50 -19
- package/lib/parts-tools.mjs +15 -12
- package/lib/parts-upload.mjs +43 -0
- package/lib/parts.mjs +49 -5
- package/lib/perf.mjs +17 -10
- package/lib/port-check.mjs +77 -18
- package/lib/purchase-links.mjs +42 -0
- package/lib/purchase-wallet.mjs +114 -0
- package/lib/rules-check-worker.mjs +15 -2
- package/lib/scaffold.mjs +25 -5
- package/lib/shoot.mjs +12 -6
- package/lib/shop-links.mjs +15 -6
- package/lib/shop.mjs +91 -21
- package/lib/stripe-connect.mjs +2 -2
- package/lib/template-history.json +85 -0
- package/netplay/NETPLAY.md +20 -6
- package/netplay/netplay.ts +3 -1
- package/package.json +12 -2
- package/parts/COMMERCIAL-LICENCE.md +21 -0
- package/parts/PARTS.md +238 -10
- package/port/probe.ts +2 -1
- package/rules/core.ts +3 -1
- package/rules/host.ts +3 -1
- package/rules/pack.ts +30 -3
- package/rules/view.ts +232 -40
- package/starters/coin-dash/lab.json +17 -0
- package/starters/coin-dash/src/rules.ts +3 -3
- package/starters/gem-rush/game.json +19 -4
- package/starters/gem-rush/map/main.json +50 -0
- package/starters/gem-rush/src/move.ts +23 -0
- package/starters/gem-rush/src/rules.ts +118 -0
- package/starters/gem-rush/src/view.ts +429 -0
- package/starters/gem-rush/tunables.json +98 -11
- package/worker/extensions.mjs +13 -0
- package/worker/index.mjs +3 -2
- package/worker/machine-payments.mjs +72 -0
- package/worker/office.mjs +4 -1
- package/worker/parts-protected.mjs +111 -0
- package/worker/parts-purchase-catalogue.mjs +34 -0
- package/worker/parts-resource.mjs +32 -0
- package/worker/parts-sale.mjs +59 -0
- package/worker/parts.mjs +24 -11
- package/worker/payment-capabilities.mjs +70 -0
- package/worker/payment-dry-exchange.mjs +30 -0
- package/worker/payment-manual-refund.mjs +40 -0
- package/worker/payment-policy.mjs +40 -0
- package/worker/payment-recovery.mjs +163 -0
- package/worker/payment-test-exchange.mjs +19 -0
- package/worker/purchase-core.mjs +140 -0
- package/worker/purchase-crypto.mjs +17 -0
- package/worker/purchase-delivery.mjs +108 -0
- package/worker/purchase-discovery.mjs +26 -0
- package/worker/purchase-hosted-readiness.mjs +16 -0
- package/worker/purchase-keys.mjs +20 -0
- package/worker/purchase-links.mjs +9 -0
- package/worker/purchase-machine.mjs +279 -0
- package/worker/purchase-mcp.mjs +42 -0
- package/worker/purchase-office-routes.mjs +17 -0
- package/worker/purchase-office-script.mjs +38 -0
- package/worker/purchase-office.mjs +35 -0
- package/worker/purchase-owner.mjs +243 -0
- package/worker/purchase-pricing.mjs +8 -0
- package/worker/purchase-problem.mjs +4 -0
- package/worker/purchase-reconciliation.mjs +582 -0
- package/worker/purchase-response.mjs +82 -0
- package/worker/purchase-routes.mjs +611 -0
- package/worker/purchase-schema.mjs +50 -0
- package/worker/purchase-store.mjs +148 -0
- package/worker/purchases.mjs +6 -0
- package/worker/resource-kinds.mjs +20 -0
- package/worker/selling/index.mjs +8 -0
- package/worker/selling/runtime.mjs +42 -0
- package/worker/settlement-journal.mjs +138 -0
- package/worker/shop-page.mjs +7 -4
- package/worker/shop-refunds.mjs +22 -0
- package/worker/shop-rules.mjs +1 -1
- package/worker/shop.mjs +18 -18
- package/worker/stripe.mjs +36 -3
- package/worker/version.mjs +1 -1
- package/starters/gem-rush/src/main.ts +0 -1087
package/lib/shop.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { paidReleases } from './parts-upload.mjs';
|
|
1
2
|
/**
|
|
2
3
|
* `homie-studio shop` — a studio's shop from the studio folder (@homie-rocks/studio 0.24.0; worker/shop.mjs has the
|
|
3
4
|
* rules, shop/SHOP.md the guide, shop/SELLING.md the owner's plain words).
|
|
@@ -25,9 +26,10 @@
|
|
|
25
26
|
*
|
|
26
27
|
* The proof of ownership is the studio's own Cloudflare login, as for the office.
|
|
27
28
|
*/
|
|
29
|
+
import { execFileSync } from 'node:child_process';
|
|
28
30
|
import { createServer } from 'node:http';
|
|
29
|
-
import { randomBytes } from 'node:crypto';
|
|
30
|
-
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
31
|
+
import { randomBytes, createHash } from 'node:crypto';
|
|
32
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
31
33
|
import { dirname, join } from 'node:path';
|
|
32
34
|
import { fileURLToPath } from 'node:url';
|
|
33
35
|
import { askedFor, withKey } from './office.mjs';
|
|
@@ -36,7 +38,7 @@ import { listExperiences as listGames, readStudio, siteUrl } from './studio.mjs'
|
|
|
36
38
|
import { SHOP_FILE, audienceOf, checkShop, defaultTaxCode, money } from '../worker/shop-rules.mjs';
|
|
37
39
|
import {
|
|
38
40
|
KEY_SHAPE, STRIPE_VERSION, StripeError, WEBHOOK_SECRET_SHAPE, createWebhookEndpoint, expireCheckoutSession, isPermissionError, listWebhookEndpoints, modeOf,
|
|
39
|
-
stripeCall, updateWebhookEndpoint,
|
|
41
|
+
stripeCall, updateWebhookEndpoint, redactStripe,
|
|
40
42
|
} from '../worker/stripe.mjs';
|
|
41
43
|
|
|
42
44
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
@@ -44,10 +46,29 @@ const SELLING = join(HERE, '..', 'shop', 'SELLING.md');
|
|
|
44
46
|
/** The events the webhook endpoint listens to (the owner ticks these in Stripe). */
|
|
45
47
|
export const HOOK_EVENTS = Object.freeze([
|
|
46
48
|
'checkout.session.completed', 'checkout.session.async_payment_succeeded', 'checkout.session.async_payment_failed', 'checkout.session.expired',
|
|
47
|
-
'charge.refunded', 'refund.created', 'refund.updated', 'refund.failed', 'charge.dispute.created', 'charge.dispute.closed',
|
|
49
|
+
'payment_intent.succeeded', 'invoice.paid', 'invoice_payment.paid', 'invoice.payment_failed', 'customer.subscription.updated', 'customer.subscription.deleted', 'charge.refunded', 'refund.created', 'refund.updated', 'refund.failed', 'charge.dispute.created', 'charge.dispute.closed',
|
|
48
50
|
]);
|
|
49
51
|
/** The restricted key's permissions, as Stripe's key page names them. Nothing else. */
|
|
50
52
|
export const KEY_PERMISSIONS = Object.freeze([
|
|
53
|
+
['Checkout Sessions', 'Write', 'open Stripe\'s checkout page for one item'],
|
|
54
|
+
['Charges', 'Write', 'refunds (Stripe keeps refunds under Charges)'],
|
|
55
|
+
['PaymentIntents', 'Write', 'verify, create and cancel machine payments as well as read purchase receipts'],
|
|
56
|
+
['Business Profiles', 'Read', 'discover the Stripe profile used by shared payment tokens'],
|
|
57
|
+
['Payment method configurations', 'Read', 'check whether Stripe has enabled crypto payments'],
|
|
58
|
+
['Crypto Deposit Addresses', 'Read', 'read this account’s existing stablecoin deposit addresses'],
|
|
59
|
+
['Subscriptions', 'Write', 'verify paid periods and cancel part subscriptions when the studio retires a part or its refund policy requires cancellation'],
|
|
60
|
+
['Invoices', 'Read', 'verify part renewal payments'],
|
|
61
|
+
['Customer portal', 'Write', 'let part buyers manage or cancel their subscription'],
|
|
62
|
+
['Disputes', 'Read', 'see a dispute; you answer it in Stripe'],
|
|
63
|
+
['Webhook Endpoints', 'Write', 'only so this page makes the webhook for you; you can set it back to None afterwards'],
|
|
64
|
+
]);
|
|
65
|
+
|
|
66
|
+
export const SHOP_HOOK_EVENTS = Object.freeze([
|
|
67
|
+
'checkout.session.completed', 'checkout.session.async_payment_succeeded', 'checkout.session.async_payment_failed', 'checkout.session.expired',
|
|
68
|
+
'charge.refunded', 'refund.created', 'refund.updated', 'refund.failed', 'charge.dispute.created', 'charge.dispute.closed',
|
|
69
|
+
]);
|
|
70
|
+
/** The restricted key's permissions, as Stripe's key page names them. Nothing else. */
|
|
71
|
+
export const SHOP_KEY_PERMISSIONS = Object.freeze([
|
|
51
72
|
['Checkout Sessions', 'Write', 'open Stripe\'s checkout page for one item'],
|
|
52
73
|
['Charges', 'Write', 'refunds (Stripe keeps refunds under Charges)'],
|
|
53
74
|
['PaymentIntents', 'Read', 'which payment an order was'],
|
|
@@ -110,19 +131,19 @@ export async function shopStatus(root, { url } = {}) {
|
|
|
110
131
|
const { stripeConnectionInfo } = await import('./stripe-connect.mjs');
|
|
111
132
|
const connection = r.mode === 'test' ? stripeConnectionInfo(root) : null;
|
|
112
133
|
const missing = [...(r.missing ?? []), ...(connection?.expired ? [{ id: 'stripe-expired', words: 'Stripe test credentials have expired. Run homie-studio shop connect --renew and approve Stripe again.' }] : [])];
|
|
113
|
-
return { ok: true, command: 'shop', ready: r.ready && !connection?.expired, mode: r.mode, missing, connection, totals: r.totals, stripe: r.stripe, hook: r.hook, till: r.shop?.till ?? null, items: r.shop?.items ?? [], local: { ok: local.ok, absent: Boolean(local.absent), errors: local.errors } };
|
|
134
|
+
return { ok: true, command: 'shop', ready: r.ready && !connection?.expired, mode: r.mode, missing, connection, purchases: r.purchases, totals: r.totals, stripe: r.stripe, hook: r.hook, till: r.shop?.till ?? null, items: r.shop?.items ?? [], local: { ok: local.ok, absent: Boolean(local.absent), errors: local.errors } };
|
|
114
135
|
}
|
|
115
136
|
|
|
116
137
|
export async function shopOrders(root, { url } = {}) {
|
|
117
138
|
const r = await withKey(root, url, (call) => call('/_studio/api/shop'));
|
|
118
139
|
if (!r.ok) return { ok: false, command: 'shop orders', why: r.message ?? r.why ?? 'the studio did not answer' };
|
|
119
|
-
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 })) };
|
|
140
|
+
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 })) };
|
|
120
141
|
}
|
|
121
142
|
|
|
122
143
|
/** The AI proposes a refund: an ask the owner confirms with one tap (an office key can never refund by itself). */
|
|
123
|
-
export async function shopRefund(root, order, { url, reason, note } = {}) {
|
|
144
|
+
export async function shopRefund(root, order, { url, reason, note, manualTransaction } = {}) {
|
|
124
145
|
if (!/^ord_[A-Za-z0-9]{20}$/.test(String(order ?? ''))) return { ok: false, command: 'shop refund', why: 'name the order: homie-studio shop refund ord_… (homie-studio shop orders lists them)' };
|
|
125
|
-
const r = await withKey(root, url, (call) => call('/_studio/api/shop/refund', { order, ...(reason ? { reason } : {}), ...(note ? { note } : {}) }));
|
|
146
|
+
const r = await withKey(root, url, (call) => call('/_studio/api/shop/refund', { order, ...(manualTransaction ? {manualTransaction} : {}), ...(reason ? { reason } : {}), ...(note ? { note } : {}) }));
|
|
126
147
|
return askedFor(root, url, r, 'shop refund');
|
|
127
148
|
}
|
|
128
149
|
|
|
@@ -157,10 +178,10 @@ export const hookUrlOf = (site) => `${String(site).replace(/\/+$/, '')}/api/shop
|
|
|
157
178
|
const HOOK_MARK = 'shop-v1';
|
|
158
179
|
|
|
159
180
|
/** The one page the owner uses: what to make in Stripe, what Managed Payments costs and does, one field, one button. */
|
|
160
|
-
export function connectPage({ nonce, site, studio, till = 'stripe', test = true }) {
|
|
181
|
+
export function connectPage({ nonce, site, studio, till = 'stripe', test = true, paidParts = false }) {
|
|
161
182
|
const dash = `https://dashboard.stripe.com${test ? '/test' : ''}`;
|
|
162
|
-
const perms = KEY_PERMISSIONS.map(([n, level, why]) => `<li><b>${esc(n)}</b>: ${esc(level)} <small>(${esc(why)})</small></li>`).join('');
|
|
163
|
-
const events = HOOK_EVENTS.map((e) => `<code>${esc(e)}</code>`).join(', ');
|
|
183
|
+
const perms = (paidParts ? KEY_PERMISSIONS : SHOP_KEY_PERMISSIONS).map(([n, level, why]) => `<li><b>${esc(n)}</b>: ${esc(level)} <small>(${esc(why)})</small></li>`).join('');
|
|
184
|
+
const events = (paidParts ? HOOK_EVENTS : SHOP_HOOK_EVENTS).map((e) => `<code>${esc(e)}</code>`).join(', ');
|
|
164
185
|
return `<!doctype html><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta name="referrer" content="no-referrer"><title>Connect your shop to Stripe</title>
|
|
165
186
|
<style>body{font:16px/1.5 system-ui,sans-serif;max-width:40rem;margin:2.5rem auto;padding:0 1rem;color:#1b1b1b}h1{font-size:1.6rem}h2{font-size:1.1rem;margin-top:1.8rem}input[type=password]{width:100%;box-sizing:border-box;font:inherit;padding:.6rem;border:1px solid #999;border-radius:8px}label{display:block;margin:.8rem 0 .3rem;font-weight:600}fieldset{border:1px solid #ccc;border-radius:10px;padding:.6rem 1rem}fieldset label{font-weight:400;display:flex;gap:.6rem;align-items:flex-start}button{margin-top:1.2rem;font:inherit;font-weight:700;padding:.7rem 1.3rem;border:0;border-radius:8px;background:#1b1b1b;color:#fff}small{color:#555}code{font-size:.85em;background:#f2f2f2;padding:0 .25em;border-radius:4px}ol li,ul li{margin:.25rem 0}.box{background:#f6f6f2;border-radius:10px;padding:.8rem 1rem}details{margin-top:1rem}summary{cursor:pointer}</style>
|
|
166
187
|
<h1>Connect ${esc(studio)}'s shop to your Stripe</h1>
|
|
@@ -172,7 +193,7 @@ export function connectPage({ nonce, site, studio, till = 'stripe', test = true
|
|
|
172
193
|
<li>Name it "${esc(studio)} shop" and give it exactly these permissions (leave everything else at None):<ul>${perms}</ul></li>
|
|
173
194
|
<li>Create it and copy the key (it starts with <code>rk_</code>).</li></ol>
|
|
174
195
|
<h2>2. The webhook: this page makes it</h2>
|
|
175
|
-
<p>With your key, this page makes the endpoint where Stripe tells your shop about payments (<code>${esc(hookUrlOf(site))}</code>, for ${HOOK_EVENTS.length} events), and its signing secret goes straight to your Worker. Nobody sees it. An older one this page made for the same address is turned off, never deleted.</p>
|
|
196
|
+
<p>With your key, this page makes the endpoint where Stripe tells your shop about payments (<code>${esc(hookUrlOf(site))}</code>, for ${(paidParts ? HOOK_EVENTS : SHOP_HOOK_EVENTS).length} events), and its signing secret goes straight to your Worker. Nobody sees it. An older one this page made for the same address is turned off, never deleted.</p>
|
|
176
197
|
<form method="post" action="/key"><input type="hidden" name="n" value="${esc(nonce)}">
|
|
177
198
|
<h2>3. Paste the key here</h2>
|
|
178
199
|
<label for="key">Stripe key</label><input id="key" name="key" type="password" autocomplete="off" placeholder="rk_test_…" required>
|
|
@@ -186,6 +207,15 @@ export function connectPage({ nonce, site, studio, till = 'stripe', test = true
|
|
|
186
207
|
<label><input type="radio" name="till" value="stripe"${till !== 'stripe-managed' ? ' checked' : ''}> <span><b>You are</b> (standard Stripe). Stripe takes its usual card fee (in the US 2.9% + 30¢ a sale; in Canada 2.9% + CA$0.30). Set automaticTax: true in shop.json to enable Stripe Tax: it works out and collects sales tax and VAT where you have told Stripe you are registered (0.5% a sale there). Registering and filing are yours, and some countries (the EU, the UK) expect a foreign seller to register from the first sale.</span></label>
|
|
187
208
|
<label><input type="radio" name="till" value="stripe-managed"${till === 'stripe-managed' ? ' checked' : ''}> <span><b>Stripe is</b> (Stripe Managed Payments). <b>3.5% more</b> a sale, on top of the card fee. Stripe becomes the seller of record: it registers for, collects, files and pays sales tax and VAT in 80+ countries, runs fraud checks, answers card disputes for you and handles buyers' payment questions. Statements read <code>LINK.COM*</code>. You still cover the money of a lost dispute, and Stripe may refund a buyer within 60 days. Turn it on in Stripe first (<a href="${dash}/settings/managed-payments" target="_blank" rel="noopener">Managed Payments</a>, after Stripe's eligibility review; Canada and the US are among the countries it serves).${test ? ' In test mode this page tries one test checkout with Managed Payments (expired at once) and says whether Stripe takes it.' : ''}</span></label>
|
|
188
209
|
</fieldset>
|
|
210
|
+
${paidParts ? `<h2>5. Paid resource checkout and optional Base payments</h2>
|
|
211
|
+
<p>For hosted Checkout, create a Turnstile widget for this studio hostname and enter both keys. Blank fields preserve existing secrets.</p>
|
|
212
|
+
<label>TURNSTILE_SITE_KEY<input name="TURNSTILE_SITE_KEY" autocomplete="off"></label>
|
|
213
|
+
<label>TURNSTILE_SECRET<input name="TURNSTILE_SECRET" type="password" autocomplete="off"></label>
|
|
214
|
+
<p>Live Base payments also require a Coinbase CDP API key.</p>
|
|
215
|
+
<label>CDP_API_KEY_ID<input name="CDP_API_KEY_ID" type="password" autocomplete="off"></label>
|
|
216
|
+
<label>CDP_API_KEY_SECRET<input name="CDP_API_KEY_SECRET" type="password" autocomplete="off"></label>
|
|
217
|
+
<label><input type="checkbox" name="base" value="yes"> Enable Base stablecoin payments. I can refund the original payer directly on Base if Stripe cannot record a settled transfer; I accept this refund responsibility. Live mode requires both CDP keys here.</label>` : ''}
|
|
218
|
+
<p>Tempo is unavailable until complete payment tests pass in both modes.</p>
|
|
189
219
|
<button>Save to my Worker</button></form>
|
|
190
220
|
<p><small>This page works once and closes in ten minutes. You can change the seller later in shop.json ("till"). This is not legal or tax advice: an accountant can tell you where you must register.</small></p>`;
|
|
191
221
|
}
|
|
@@ -198,7 +228,7 @@ const said = (status, text) => ({ status, text });
|
|
|
198
228
|
* for the same address are turned off (not deleted) once the new secret is saved. { ok, id, secret, older } or
|
|
199
229
|
* { ok: false, page } (what the page tells the owner).
|
|
200
230
|
*/
|
|
201
|
-
async function makeWebhook(env, site, { fetcher }) {
|
|
231
|
+
async function makeWebhook(env, site, { fetcher, paidParts = false }) {
|
|
202
232
|
const url = hookUrlOf(site);
|
|
203
233
|
let older = [];
|
|
204
234
|
try {
|
|
@@ -211,7 +241,7 @@ async function makeWebhook(env, site, { fetcher }) {
|
|
|
211
241
|
let made = null;
|
|
212
242
|
try {
|
|
213
243
|
made = await createWebhookEndpoint(env, {
|
|
214
|
-
url, enabled_events: [...HOOK_EVENTS], api_version: STRIPE_VERSION,
|
|
244
|
+
url, enabled_events: [...(paidParts ? HOOK_EVENTS : SHOP_HOOK_EVENTS)], api_version: STRIPE_VERSION,
|
|
215
245
|
description: 'Homie shop (made by homie-studio shop connect)', metadata: { homie: HOOK_MARK },
|
|
216
246
|
}, { fetcher });
|
|
217
247
|
} catch (error) {
|
|
@@ -248,7 +278,7 @@ async function probeManaged(env, shop, { fetcher }) {
|
|
|
248
278
|
* read, then the webhook), and to `wrangler secret put` on its standard input; so does the webhook's signing secret,
|
|
249
279
|
* which Stripe hands this process when it makes the endpoint. Nothing is printed or returned but ids and words.
|
|
250
280
|
*/
|
|
251
|
-
export async function shopConnectManual(root, { managed = null, live = false, log = () => {}, port = 0, wait = 10 * 60_000, verify = true, fetcher = fetch } = {}) {
|
|
281
|
+
export async function shopConnectManual(root, { fromClipboard = false, clipboard = null, managed = null, live = false, log = () => {}, port = 0, wait = 10 * 60_000, verify = true, fetcher = fetch } = {}) {
|
|
252
282
|
const studio = readStudio(root);
|
|
253
283
|
const site = siteUrl(root);
|
|
254
284
|
if (!site) return { ok: false, command: 'shop connect', needs: 'deploy', why: 'the studio has no live address yet, and Stripe needs one to send payments to (the webhook): run npm run deploy first, then this' };
|
|
@@ -257,7 +287,8 @@ export async function shopConnectManual(root, { managed = null, live = false, lo
|
|
|
257
287
|
const shopNow = readShop(root);
|
|
258
288
|
const till = managed === true ? 'stripe-managed' : managed === false ? 'stripe' : shopNow.shop?.till ?? 'stripe';
|
|
259
289
|
const nonce = randomBytes(16).toString('hex');
|
|
260
|
-
const
|
|
290
|
+
const paidParts = paidReleases(root).length > 0;
|
|
291
|
+
const page = connectPage({ paidParts, nonce, site, studio: studio.name ?? 'Your studio', till, test: !live });
|
|
261
292
|
return await new Promise((done) => {
|
|
262
293
|
let finished = false;
|
|
263
294
|
let busy = false;
|
|
@@ -278,6 +309,8 @@ export async function shopConnectManual(root, { managed = null, live = false, lo
|
|
|
278
309
|
// Test mode unless the owner's AI ran it with --live on purpose: a live key on the test page is refused.
|
|
279
310
|
if (modeOf(key) !== (live ? 'live' : 'test')) { say(said(400, live ? 'This page takes a LIVE Stripe key (rk_live_… or sk_live_…).' : 'This page is for TEST mode: paste a test key (rk_test_… or sk_test_…). Live keys go in only when the shop is ready to sell for real (shop connect --manual --live).')); return; }
|
|
280
311
|
if (hook && !WEBHOOK_SECRET_SHAPE.test(hook)) { say(said(400, 'That does not look like a webhook signing secret (whsec_…). Go back and try again, or leave it empty and this page makes the webhook.')); return; }
|
|
312
|
+
if (Boolean(form.get('TURNSTILE_SITE_KEY')) !== Boolean(form.get('TURNSTILE_SECRET')) || Boolean(form.get('CDP_API_KEY_ID')) !== Boolean(form.get('CDP_API_KEY_SECRET'))) { say(said(400, 'Supply both keys in each pair.')); return; }
|
|
313
|
+
if (live && form.get('base') === 'yes' && !form.get('CDP_API_KEY_SECRET')) { say(said(400, 'Enabling live Base requires both CDP keys.')); return; }
|
|
281
314
|
busy = true;
|
|
282
315
|
try {
|
|
283
316
|
const sk = { STRIPE_KEY: key, STRIPE_API_BASE: process.env.STRIPE_API_BASE };
|
|
@@ -292,16 +325,41 @@ export async function shopConnectManual(root, { managed = null, live = false, lo
|
|
|
292
325
|
}
|
|
293
326
|
let webhook = { made: false };
|
|
294
327
|
if (!hook) {
|
|
295
|
-
const m = await makeWebhook(sk, site, { fetcher });
|
|
328
|
+
const m = await makeWebhook(sk, site, { fetcher, paidParts });
|
|
296
329
|
if (!m.ok) { log(m.page.text); say(m.page); return; }
|
|
297
330
|
hook = m.secret;
|
|
298
331
|
webhook = { made: true, id: m.id, older: m.older };
|
|
299
332
|
}
|
|
333
|
+
let recurringReady = false;
|
|
334
|
+
try {
|
|
335
|
+
if (paidParts) {
|
|
336
|
+
await stripeCall(sk, 'GET', '/v1/subscriptions', { limit: 1 }, { fetcher });
|
|
337
|
+
await stripeCall(sk, 'GET', '/v1/invoices', { limit: 1 }, { fetcher });
|
|
338
|
+
await stripeCall(sk, 'GET', '/v1/invoice_payments', { limit: 1 }, { fetcher });
|
|
339
|
+
const endpoints = webhook.made ? null : await listWebhookEndpoints(sk, { fetcher });
|
|
340
|
+
recurringReady = webhook.made || endpoints?.data?.some((e) => e.url === hookUrlOf(site) && e.status === 'enabled' && HOOK_EVENTS.every((event) => e.enabled_events.includes(event) || e.enabled_events.includes('*')));
|
|
341
|
+
}
|
|
342
|
+
} catch { recurringReady = false; }
|
|
343
|
+
const machine = paidParts ? await (await import('../worker/payment-capabilities.mjs')).probeMachinePayments(sk, { fetcher }) : { machine: {}, configuration: {}, reasons: {} };
|
|
344
|
+
const machineNote = ` Machine payments: ${Object.entries(machine.machine).map(([name, enabled]) => `${name}: ${enabled ? 'permission check passed' : machine.reasons[name] ?? 'unavailable'}`).join('; ')}.`;
|
|
300
345
|
finished = true;
|
|
301
346
|
const a = w(['secret', 'put', 'STRIPE_KEY'], { input: `${key}\n` });
|
|
302
347
|
const b = a.code === 0 ? w(['secret', 'put', 'STRIPE_WEBHOOK_SECRET'], { input: `${hook}\n` }) : { code: 1 };
|
|
303
348
|
hook = '';
|
|
304
|
-
const
|
|
349
|
+
const readiness = { ...machine, key: createHash('sha256').update(key).digest('hex'), mode: modeOf(key), recurring: recurringReady };
|
|
350
|
+
const c = !paidParts ? b : b.code === 0 ? w(['secret', 'put', 'PURCHASE_PAYMENT_CAPABILITIES'], { input: JSON.stringify(readiness) }) : { code: 1 };
|
|
351
|
+
const settingsPath = join(root, '.homie', 'purchase-settings.json');
|
|
352
|
+
const prior = existsSync(settingsPath) ? JSON.parse(readFileSync(settingsPath, 'utf8')) : {};
|
|
353
|
+
const next = { ...prior, [modeOf(key)]: { ...prior[modeOf(key)], ...machine.configuration } };
|
|
354
|
+
delete next[modeOf(key)].tempo;
|
|
355
|
+
if (form.get('base') === 'yes' && machine.machine.base) next[modeOf(key)].manualRefunds = true;
|
|
356
|
+
else { delete next[modeOf(key)].base; next[modeOf(key)].manualRefunds = false; }
|
|
357
|
+
const extras = ['TURNSTILE_SITE_KEY','TURNSTILE_SECRET','CDP_API_KEY_ID','CDP_API_KEY_SECRET'];
|
|
358
|
+
let settingsOkay = c.code === 0;
|
|
359
|
+
for (const setting of extras) if (paidParts && settingsOkay && form.get(setting)) settingsOkay = w(['secret','put',setting], { input: form.get(setting).trim() }).code === 0;
|
|
360
|
+
const d = !paidParts ? c : settingsOkay ? w(['secret', 'put', 'PURCHASE_MACHINE_PAYMENTS'], { input: JSON.stringify(next) }) : { code: 1 };
|
|
361
|
+
if (paidParts && d.code === 0) { mkdirSync(dirname(settingsPath), { recursive: true }); writeFileSync(settingsPath, JSON.stringify(next, null, 2) + '\n'); }
|
|
362
|
+
const okay = a.code === 0 && b.code === 0 && c.code === 0 && d.code === 0;
|
|
305
363
|
// The new secret is saved: the older endpoints this kit made for the same address stop sending (turned off,
|
|
306
364
|
// not deleted: the owner can turn one back on in Stripe).
|
|
307
365
|
let disabled = 0;
|
|
@@ -322,12 +380,13 @@ export async function shopConnectManual(root, { managed = null, live = false, lo
|
|
|
322
380
|
} catch { tillNote = ''; }
|
|
323
381
|
}
|
|
324
382
|
const narrow = webhook.made ? ' Recommended, 20 seconds: in Stripe, open this key (… → Edit key) and set Webhook Endpoints back to None. The shop never needs it again.' : '';
|
|
325
|
-
say(said(okay ? 200 : 500, okay ? `Saved to your Worker (${modeOf(key)} mode).${webhook.made ? ' Stripe made the webhook; its secret went straight to your Worker.' : ''}${check?.ok ? ` ${check.words}` : ''}${warnings.length ? ` ${warnings.join(' ')}` : ''}${narrow} You can close this page.` : 'Wrangler could not save it (is this computer signed in to Cloudflare? npx wrangler login). Nothing was kept.'));
|
|
383
|
+
say(said(okay ? 200 : 500, okay ? `Saved to your Worker (${modeOf(key)} mode).${webhook.made ? ' Stripe made the webhook; its secret went straight to your Worker.' : ''}${check?.ok ? ` ${check.words}` : ''}${warnings.length ? ` ${warnings.join(' ')}` : ''}${narrow}${machineNote} You can close this page.` : 'Wrangler could not save it (is this computer signed in to Cloudflare? npx wrangler login). Nothing was kept.'));
|
|
326
384
|
server.close();
|
|
327
385
|
done(okay
|
|
328
386
|
? {
|
|
329
387
|
ok: true, command: 'shop connect', saved: true, mode: modeOf(key), till: chosen,
|
|
330
388
|
webhook: webhook.made ? { made: true, id: webhook.id, url: hookUrlOf(site), turnedOff: disabled } : { made: false, url: hookUrlOf(site) },
|
|
389
|
+
machinePayments: { methods: machine.machine, reasons: machine.reasons },
|
|
331
390
|
...(check ? { managedPayments: check } : {}),
|
|
332
391
|
...(warnings.length ? { warnings } : {}),
|
|
333
392
|
message: `Saved as the Worker secrets STRIPE_KEY and STRIPE_WEBHOOK_SECRET (never shown), ${modeOf(key)} mode, seller: ${chosen === 'stripe-managed' ? 'Stripe (Managed Payments)' : 'the studio'}.${webhook.made ? ` Stripe made the webhook ${webhook.id} for ${hookUrlOf(site)}${disabled ? ` and ${disabled} older one(s) were turned off` : ''}.` : ''}${tillNote}${narrow} homie-studio shop says whether anything else is missing.`,
|
|
@@ -339,9 +398,20 @@ export async function shopConnectManual(root, { managed = null, live = false, lo
|
|
|
339
398
|
}
|
|
340
399
|
res.writeHead(404); res.end();
|
|
341
400
|
});
|
|
342
|
-
server.listen(port, '127.0.0.1', () => {
|
|
401
|
+
server.listen(port, '127.0.0.1', async () => {
|
|
343
402
|
const link = `http://127.0.0.1:${server.address().port}/${nonce}`;
|
|
344
|
-
|
|
403
|
+
if (fromClipboard) {
|
|
404
|
+
try {
|
|
405
|
+
// The owner's agent clicks Copy in Stripe. The key never enters a tool response or argv.
|
|
406
|
+
let key = clipboard ? await clipboard() : process.platform === 'darwin' ? execFileSync('pbpaste', {encoding:'utf8'}).trim() : process.platform === 'win32' ? execFileSync('powershell',['-NoProfile','-Command','Get-Clipboard'],{encoding:'utf8'}).trim() : execFileSync('wl-paste',['--no-newline'],{encoding:'utf8'}).trim();
|
|
407
|
+
if (!KEY_SHAPE.test(key)) throw new Error('Clipboard does not contain a Stripe credential');
|
|
408
|
+
const result = await fetch(`http://127.0.0.1:${server.address().port}/key`, {method:'POST',body:new URLSearchParams({key,n:nonce,till})});
|
|
409
|
+
key = '';
|
|
410
|
+
if (!result.ok) { finished=true;server.close();done({ok:false,command:'shop connect',why:redactStripe(await result.text()).slice(0,800) || 'Credential setup failed. Check the provider permissions and payment mode; no credential was printed.'}); }
|
|
411
|
+
} catch { finished=true;server.close();done({ok:false,command:'shop connect',why:'The agent must copy the approved Stripe key in the browser first. macOS, Windows or Wayland clipboard tooling is required; no credential was printed.'}); } finally {
|
|
412
|
+
if (!clipboard) { try { if(process.platform==='darwin') execFileSync('pbcopy',[],{input:''}); else if(process.platform==='win32') execFileSync('powershell',['-NoProfile','-Command','Set-Clipboard -Value ""']); else execFileSync('wl-copy',['--clear']); } catch {} }
|
|
413
|
+
}
|
|
414
|
+
} else log(`Open this page on this computer; the owner pastes the key there (never in a chat): ${link}`);
|
|
345
415
|
});
|
|
346
416
|
setTimeout(() => { if (!finished) { finished = true; server.close(); done({ ok: false, command: 'shop connect', why: 'nothing was saved in ten minutes; run it again when ready' }); } }, wait).unref?.();
|
|
347
417
|
});
|
package/lib/stripe-connect.mjs
CHANGED
|
@@ -72,7 +72,7 @@ export function completionArgs(step) {
|
|
|
72
72
|
}
|
|
73
73
|
|
|
74
74
|
export async function shopConnect(root, options = {}) {
|
|
75
|
-
if (options.manual) {
|
|
75
|
+
if (options.manual || options.fromClipboard) {
|
|
76
76
|
const result = await shopConnectManual(root, options);
|
|
77
77
|
if (result.ok) rmSync(join(receiptDir(root, options.storage ?? receiptHome()), 'test.json'), { force: true });
|
|
78
78
|
return result;
|
|
@@ -133,7 +133,7 @@ export async function shopConnect(root, options = {}) {
|
|
|
133
133
|
const w = wrangler ?? runner(root, studio.cloudflare?.accountId ? { CLOUDFLARE_ACCOUNT_ID: studio.cloudflare.accountId } : {});
|
|
134
134
|
const receiptFile = join(dir, `${mode}.json`);
|
|
135
135
|
const receipt = existsSync(receiptFile) ? json(readFileSync(receiptFile, 'utf8')) : null;
|
|
136
|
-
const result = await syncPaymentLinks({ api, shop: checked.shop, slug: studio.slug, site: target.origin, mode, w, receipt,
|
|
136
|
+
const result = await syncPaymentLinks({ root, api, shop: checked.shop, slug: studio.slug, site: target.origin, mode, w, receipt,
|
|
137
137
|
saveReceipt: (record) => {
|
|
138
138
|
writeFileSync(`${receiptFile}.tmp`, JSON.stringify(record), { mode: 0o600 });
|
|
139
139
|
renameSync(`${receiptFile}.tmp`, receiptFile);
|
|
@@ -2071,6 +2071,91 @@
|
|
|
2071
2071
|
}
|
|
2072
2072
|
}
|
|
2073
2073
|
}
|
|
2074
|
+
},
|
|
2075
|
+
"0.37.2": {
|
|
2076
|
+
"files": {
|
|
2077
|
+
"CLAUDE.md": "ddf2b2a0376202f6",
|
|
2078
|
+
"README.md": "684b3cca8001673c",
|
|
2079
|
+
"HANDOFF.md": "29337e2ace95b2e7",
|
|
2080
|
+
"games/README.md": "5ad32fe80c61f117",
|
|
2081
|
+
"music/README.md": "a3dc042dadd39df8",
|
|
2082
|
+
"videos/README.md": "16babc3437d5545b",
|
|
2083
|
+
"posts/README.md": "37841aae476bd540",
|
|
2084
|
+
"site/README.md": "732894273a32dc3f",
|
|
2085
|
+
"site/src/worker.mjs": "fc90f3609cad18ba"
|
|
2086
|
+
},
|
|
2087
|
+
"sections": {
|
|
2088
|
+
"AGENTS.md": {
|
|
2089
|
+
"(intro)": "214aa46eb3a895d5",
|
|
2090
|
+
"## Layout": "c18362ed28975d81",
|
|
2091
|
+
"## Apps: one screen, roles and parts": "4db0abe4cdfac09f",
|
|
2092
|
+
"## Commands (all through the pinned CLI in node_modules)": "a7f290579585368c",
|
|
2093
|
+
"## The site": "0cb8dd9a6f8a0096",
|
|
2094
|
+
"## Making games": "2da7fa74c8884834",
|
|
2095
|
+
"## The Game Codex and progress": "0738ef719cef3a4c",
|
|
2096
|
+
"## Art direction and models": "a48fff7bf0a5f2fc",
|
|
2097
|
+
"## Running live games (the back office)": "51ebf2ab756f6e4d",
|
|
2098
|
+
"## Servers and AI seats": "98d08705087a33ec",
|
|
2099
|
+
"## Selling things (the shop)": "9e020d69dd6a954e",
|
|
2100
|
+
"## Continuing a build from the Claude app": "0d68081e924ca419",
|
|
2101
|
+
"## Rules": "2e6b29afb85dcf7e",
|
|
2102
|
+
"## Telling Homie": "80b46fdd9e578db8",
|
|
2103
|
+
"## Beta": "9d02638207a26e73"
|
|
2104
|
+
}
|
|
2105
|
+
},
|
|
2106
|
+
"named": {
|
|
2107
|
+
"Homie Arcade": {
|
|
2108
|
+
"sections": {
|
|
2109
|
+
"AGENTS.md": {
|
|
2110
|
+
"## Commands (all through the pinned CLI in node_modules)": "7cad8fd10f62fef1"
|
|
2111
|
+
}
|
|
2112
|
+
}
|
|
2113
|
+
}
|
|
2114
|
+
}
|
|
2115
|
+
},
|
|
2116
|
+
"0.38.0": {
|
|
2117
|
+
"files": {
|
|
2118
|
+
"CLAUDE.md": "ddf2b2a0376202f6",
|
|
2119
|
+
"README.md": "684b3cca8001673c",
|
|
2120
|
+
"HANDOFF.md": "29337e2ace95b2e7",
|
|
2121
|
+
"games/README.md": "5ad32fe80c61f117",
|
|
2122
|
+
"music/README.md": "a3dc042dadd39df8",
|
|
2123
|
+
"videos/README.md": "16babc3437d5545b",
|
|
2124
|
+
"posts/README.md": "37841aae476bd540",
|
|
2125
|
+
"site/README.md": "732894273a32dc3f",
|
|
2126
|
+
"site/src/worker.mjs": "fc90f3609cad18ba"
|
|
2127
|
+
},
|
|
2128
|
+
"sections": {
|
|
2129
|
+
"AGENTS.md": {
|
|
2130
|
+
"(intro)": "214aa46eb3a895d5",
|
|
2131
|
+
"## Layout": [
|
|
2132
|
+
"8baf49e4291e7e88",
|
|
2133
|
+
"c18362ed28975d81"
|
|
2134
|
+
],
|
|
2135
|
+
"## Apps: one screen, roles and parts": "4db0abe4cdfac09f",
|
|
2136
|
+
"## Commands (all through the pinned CLI in node_modules)": "a7f290579585368c",
|
|
2137
|
+
"## The site": "0cb8dd9a6f8a0096",
|
|
2138
|
+
"## Making games": "2da7fa74c8884834",
|
|
2139
|
+
"## The Game Codex and progress": "0738ef719cef3a4c",
|
|
2140
|
+
"## Art direction and models": "a48fff7bf0a5f2fc",
|
|
2141
|
+
"## Running live games (the back office)": "51ebf2ab756f6e4d",
|
|
2142
|
+
"## Servers and AI seats": "98d08705087a33ec",
|
|
2143
|
+
"## Selling things (the shop)": "9e020d69dd6a954e",
|
|
2144
|
+
"## Continuing a build from the Claude app": "0d68081e924ca419",
|
|
2145
|
+
"## Rules": "2e6b29afb85dcf7e",
|
|
2146
|
+
"## Telling Homie": "80b46fdd9e578db8",
|
|
2147
|
+
"## Beta": "9d02638207a26e73"
|
|
2148
|
+
}
|
|
2149
|
+
},
|
|
2150
|
+
"named": {
|
|
2151
|
+
"Homie Arcade": {
|
|
2152
|
+
"sections": {
|
|
2153
|
+
"AGENTS.md": {
|
|
2154
|
+
"## Commands (all through the pinned CLI in node_modules)": "7cad8fd10f62fef1"
|
|
2155
|
+
}
|
|
2156
|
+
}
|
|
2157
|
+
}
|
|
2158
|
+
}
|
|
2074
2159
|
}
|
|
2075
2160
|
}
|
|
2076
2161
|
}
|
package/netplay/NETPLAY.md
CHANGED
|
@@ -2151,12 +2151,26 @@ and holds that lead from `lead`. A step whose sample (or claim) changed, or that
|
|
|
2151
2151
|
makes an entry. A period with no entry sends nothing; after 250 ms without a frame the held values
|
|
2152
2152
|
are sent again as an ordinary entry. A hidden tab sends one neutral entry and stops stepping.
|
|
2153
2153
|
|
|
2154
|
-
**
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2154
|
+
**Server movement with immediate local prediction.** The host runs `move` for every body.
|
|
2155
|
+
The view library runs the same guarded module for its own player, one step per tick and one
|
|
2156
|
+
throwaway preview between ticks. A snapshot replaces that body's position, velocity, heading,
|
|
2157
|
+
grounded flag and motion; the view replays pending inputs from there. Late presses are mirrored
|
|
2158
|
+
onto the next replay tick while they remain inside the quarter-second window. Scores, pickups,
|
|
2159
|
+
collisions between entities and effects are always the host's decisions. Bot and AI bodies
|
|
2160
|
+
are interpolated, never predicted by a player's view.
|
|
2161
|
+
|
|
2162
|
+
Small errors fade as a visual offset with a nominal 100 ms blend. Overlapping small corrections
|
|
2163
|
+
queue their individual fades, so their accumulated offset is not erased all at once. A disagreement
|
|
2164
|
+
beyond three ticks of travel plays the corrected path from the snapshot at 1.25 times speed,
|
|
2165
|
+
showing the hold and slide of a knock. Offset fades are limited by progress along the drawn
|
|
2166
|
+
path and the body's declared maximum movement speed, so a late-input correction cannot erase more travel than the player makes or compound a fast knock's catch-up speed. Catch-up preserves the drawn pose when a snapshot replaces its history, and uses the next movement preview when its next whole-tick history entry is not yet available. When movement
|
|
2167
|
+
stops after catch-up, the remaining fade follows the configured blend time, still bounded by that speed. An effect on the body waits for its drawn tick. Placements, epoch changes and clock rebases
|
|
2168
|
+
jump deliberately. Other bodies keep the helper's adaptive interpolation, with one send period
|
|
2169
|
+
as the default minimum delay. The `room.predict` settings customize these defaults.
|
|
2170
|
+
|
|
2171
|
+
`body: { move: 'owner' }` remains available for existing rules games: the browser supplies its
|
|
2172
|
+
position and the host limits each claim to `maxSpeed`. It cannot prevent a modified owner walking
|
|
2173
|
+
through a wall. New Coin Dash and Gem Rush use authoritative server movement instead.
|
|
2160
2174
|
|
|
2161
2175
|
### 29.3 The life of a server-hosted room
|
|
2162
2176
|
|
package/netplay/netplay.ts
CHANGED
|
@@ -631,6 +631,8 @@ export interface NetplayOptions<C = unknown> {
|
|
|
631
631
|
checkpointMs?: number;
|
|
632
632
|
/** Fixed interpolation delay in ms. Default: adaptive (arrival age p90 + 1.2 intervals). */
|
|
633
633
|
interpDelayMs?: number;
|
|
634
|
+
/** Floor under the adaptive interpolation buffer (rules views). */
|
|
635
|
+
interpFloorMs?: number;
|
|
634
636
|
/** Host: return the full rules state. Called every `checkpointMs`, before yielding, and on pagehide. */
|
|
635
637
|
checkpoint?: () => C;
|
|
636
638
|
/** ms to wait for a welcome before falling back to offline host, when the socket could not even open. Default 4000. */
|
|
@@ -2947,7 +2949,7 @@ export function createNetplay<S = unknown, A = unknown, C = unknown>(opts: Netpl
|
|
|
2947
2949
|
|
|
2948
2950
|
function interpDelay(): number {
|
|
2949
2951
|
if (opts.interpDelayMs !== undefined) return opts.interpDelayMs;
|
|
2950
|
-
return delaySmooth || Math.max(50, iv * 1.6 + 8);
|
|
2952
|
+
return Math.max(opts.interpFloorMs ?? 0, delaySmooth || Math.max(50, iv * 1.6 + 8));
|
|
2951
2953
|
}
|
|
2952
2954
|
|
|
2953
2955
|
// ---------------------------------------------------------------- connect
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@homie-rocks/studio",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.38.0",
|
|
4
4
|
"description": "A studio for games, music and video on its own Cloudflare: scaffold, multiplayer starters on the netplay contract, a port toolkit that makes existing single-player web games multiplayer, the site Worker with public rooms (Table/Lobby Durable Objects and D1, all on the free Workers plan, no payment method), a site with the hub's sections (Home, Games, Music, Videos, Rooms, Posts with feeds) in the studio's own look, an epic landing page for every game, big songs and videos in the studio's own R2 once it has storage (uploaded, checked by SHA-256, served at the same addresses), deploy, and the homie.rocks directory.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -83,7 +83,8 @@
|
|
|
83
83
|
"./apps": {
|
|
84
84
|
"types": "./apps/apps.ts",
|
|
85
85
|
"default": "./apps/apps.ts"
|
|
86
|
-
}
|
|
86
|
+
},
|
|
87
|
+
"./worker/selling": "./worker/selling/index.mjs"
|
|
87
88
|
},
|
|
88
89
|
"files": [
|
|
89
90
|
"bin/",
|
|
@@ -121,16 +122,25 @@
|
|
|
121
122
|
"@babel/parser": "8.0.7",
|
|
122
123
|
"@babel/traverse": "8.0.7",
|
|
123
124
|
"@babel/types": "8.0.6",
|
|
125
|
+
"@coinbase/x402": "2.1.0",
|
|
124
126
|
"@gltf-transform/core": "4.5.1",
|
|
125
127
|
"@gltf-transform/extensions": "4.5.1",
|
|
126
128
|
"@gltf-transform/functions": "4.5.1",
|
|
127
129
|
"@jridgewell/trace-mapping": "0.3.31",
|
|
130
|
+
"@modelcontextprotocol/sdk": "1.32.1",
|
|
131
|
+
"@x402/core": "2.28.0",
|
|
132
|
+
"@x402/evm": "2.28.0",
|
|
128
133
|
"esbuild": "0.28.1",
|
|
129
134
|
"gltf-validator": "2.0.0-dev.3.10",
|
|
135
|
+
"jose": "6.2.12",
|
|
130
136
|
"meshoptimizer": "1.3.0",
|
|
137
|
+
"mppx": "0.13.2",
|
|
131
138
|
"puppeteer-core": "25.8.0",
|
|
132
139
|
"sharp": "0.35.5",
|
|
140
|
+
"spdx-expression-parse": "5.0.0",
|
|
141
|
+
"stripe": "23.0.0",
|
|
133
142
|
"three": "0.185.1",
|
|
143
|
+
"viem": "2.57.4",
|
|
134
144
|
"typescript": "5.9.3"
|
|
135
145
|
},
|
|
136
146
|
"scripts": {
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Optional commercial licence starting text
|
|
2
|
+
|
|
3
|
+
This is a drafting aid, not a selected policy or a substitute for the seller's review.
|
|
4
|
+
Copy and adapt it into the release's licenseTerms file, use a LicenseRef identifier,
|
|
5
|
+
and hash that file by packing the release. Fill in a complete agreement before sharing.
|
|
6
|
+
|
|
7
|
+
The seller grants the purchasing studio the rights specified in the signed purchase
|
|
8
|
+
schedule for the identified part and release. That schedule's scope, licensed game,
|
|
9
|
+
seat count, source inclusion, commercial-use permission, transfer permission, update
|
|
10
|
+
coverage and existing-use rights after refund or expiry control over conflicting general
|
|
11
|
+
language in this agreement. A seat means one human contributor; authorised build systems
|
|
12
|
+
may process the part on that contributor's behalf. A game licence covers only the named
|
|
13
|
+
game; a studio licence covers the purchasing studio's team and games.
|
|
14
|
+
|
|
15
|
+
State the permitted incorporation and distribution in finished games, whether attribution
|
|
16
|
+
is required, any restrictions on redistribution of source or standalone assets, support
|
|
17
|
+
and delivery availability, and the exact refund procedure and terms. Choose applicable
|
|
18
|
+
law, warranties and liability provisions suitable for the seller's circumstances and
|
|
19
|
+
mandatory buyer rights. Do not leave these decisions to the assistant. If the structured
|
|
20
|
+
schedule cannot express the intended agreement, do not sell through this contract until
|
|
21
|
+
both the schema and enforcement have been extended and reviewed.
|