@homie-rocks/studio 0.35.0 → 0.35.1
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 +13 -1
- package/lib/shop-catalog.mjs +20 -1
- package/lib/shop-links.mjs +3 -1
- package/lib/shop.mjs +14 -8
- package/lib/stripe-connect.mjs +18 -7
- package/lib/template-history.json +40 -0
- package/package.json +1 -1
- package/worker/stripe.mjs +2 -2
- package/worker/version.mjs +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -17,9 +17,21 @@ 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.35.1] - 2026-10-08
|
|
21
|
+
|
|
22
|
+
**Plugin 0.36.0** · [#71](https://github.com/homie-rocks/homie/pull/71)
|
|
23
|
+
|
|
24
|
+
A tip with no minimum connects, and Stripe's own reason is shown when setup cannot finish.
|
|
25
|
+
|
|
26
|
+
### Fixed
|
|
27
|
+
|
|
28
|
+
- A pay-what-you-want item with no minimum connects.
|
|
29
|
+
- When Stripe refuses something during setup, you are told what Stripe said. Keys are masked, including during renewal, catalog reads and the manual connection.
|
|
30
|
+
- The test Stripe now refuses invalid amounts and incomplete shop objects, so these mistakes fail a test before release.
|
|
31
|
+
|
|
20
32
|
## [0.35.0] - 2026-10-08
|
|
21
33
|
|
|
22
|
-
**Plugin 0.36.0** · [#67](https://github.com/homie-rocks/homie/pull/67)
|
|
34
|
+
**Plugin 0.36.0** · [#67](https://github.com/homie-rocks/homie/pull/67) · [release-2026-10-08-studio-0.35.0](https://github.com/homie-rocks/homie/releases/tag/release-2026-10-08-studio-0.35.0)
|
|
23
35
|
|
|
24
36
|
A shop with no Stripe key to find. You approve once in your browser, and your AI sets up the rest in your own Stripe account.
|
|
25
37
|
|
package/lib/shop-catalog.mjs
CHANGED
|
@@ -22,7 +22,7 @@ import { readFileSync, writeFileSync } from 'node:fs';
|
|
|
22
22
|
import { readStudio } from './studio.mjs';
|
|
23
23
|
import { readShop } from './shop.mjs';
|
|
24
24
|
import { defaultTaxCode, money } from '../worker/shop-rules.mjs';
|
|
25
|
-
import { productIdOf } from '../worker/stripe.mjs';
|
|
25
|
+
import { productIdOf, redactStripe } from '../worker/stripe.mjs';
|
|
26
26
|
|
|
27
27
|
const MARK = 'shop-v1';
|
|
28
28
|
|
|
@@ -58,6 +58,22 @@ export function productsIn(value, out = [], depth = 0) {
|
|
|
58
58
|
return out;
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
+
// Saved MCP responses can wrap a Stripe refusal in text or structured content.
|
|
62
|
+
function catalogError(value, depth = 0) {
|
|
63
|
+
if (depth > 8 || value == null) return null;
|
|
64
|
+
if (typeof value === 'string') {
|
|
65
|
+
try { return catalogError(JSON.parse(value), depth + 1); } catch { return null; }
|
|
66
|
+
}
|
|
67
|
+
if (typeof value !== 'object') return null;
|
|
68
|
+
if (typeof value.error?.message === 'string') return value.error.message;
|
|
69
|
+
if (value.isError === true) return (value.content ?? []).filter((c) => c.type === 'text').map((c) => c.text).join(' ');
|
|
70
|
+
for (const child of Array.isArray(value) ? value : ['data', 'content', 'text', 'result', 'results', 'structuredContent'].map((k) => value[k])) {
|
|
71
|
+
const error = catalogError(child, depth + 1);
|
|
72
|
+
if (error) return error;
|
|
73
|
+
}
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
|
|
61
77
|
const taxCodeOf = (p) => (typeof p?.tax_code === 'string' ? p.tax_code : p?.tax_code?.id ?? null);
|
|
62
78
|
|
|
63
79
|
/** The plan: the read first, then (with what Stripe answered) the writes still needed. */
|
|
@@ -83,6 +99,8 @@ export async function catalogPlan(root, { have = null, mode = null, record = tru
|
|
|
83
99
|
],
|
|
84
100
|
};
|
|
85
101
|
}
|
|
102
|
+
const error = catalogError(have);
|
|
103
|
+
if (error) return { ok: false, command: 'shop catalog', why: `Stripe said: “${redactStripe(error).slice(0, 400)}” (GET /v1/products). Read the catalog again after resolving it.` };
|
|
86
104
|
const products = productsIn(have);
|
|
87
105
|
const ours = products.filter((p) => p.metadata?.homie_studio === String(slug) || wanted.some((w) => w.id === p.id));
|
|
88
106
|
const modes = [...new Set(ours.map((p) => (p.livemode ? 'live' : 'test')))];
|
|
@@ -149,6 +167,7 @@ export async function catalogPlan(root, { have = null, mode = null, record = tru
|
|
|
149
167
|
|
|
150
168
|
export function catalogLines(r) {
|
|
151
169
|
const lines = [];
|
|
170
|
+
if (!r.ok) return [r.why];
|
|
152
171
|
if (r.step === 'read') {
|
|
153
172
|
lines.push('The catalog shop.json asks for (one Product an item, the same ids in the sandbox and live):');
|
|
154
173
|
for (const p of r.products) lines.push(` ${p.id} ${p.name} ${p.price} (tax code ${p.tax_code})`);
|
package/lib/shop-links.mjs
CHANGED
|
@@ -37,8 +37,10 @@ export async function syncPaymentLinks({ api, shop, slug, site, mode, w, receipt
|
|
|
37
37
|
let product = products.find((p) => p.id === id);
|
|
38
38
|
if (!product) product = await api('POST', '/v1/products', { id, ...fields }, `product-${hash(id)}`);
|
|
39
39
|
else if (!same(product.metadata, metadata) || product.name !== fields.name || (product.description ?? '') !== fields.description || (product.tax_code?.id ?? product.tax_code) !== fields.tax_code || !product.active) await api('POST', `/v1/products/${id}`, fields);
|
|
40
|
+
// Omit an unset minimum: Stripe applies its settlement-currency floor itself.
|
|
41
|
+
// https://docs.stripe.com/api/prices/create#create_price-custom_unit_amount-minimum
|
|
40
42
|
const priceFields = { product: id, currency: shop.currency, tax_behavior: 'exclusive', ...(item.price === 'choose'
|
|
41
|
-
? { custom_unit_amount: { enabled: true, minimum: item.min, ...(item.max !== null ? { maximum: item.max } : {}) } }
|
|
43
|
+
? { custom_unit_amount: { enabled: true, ...(item.min > 0 ? { minimum: item.min } : {}), ...(item.max !== null ? { maximum: item.max } : {}) } }
|
|
42
44
|
: { unit_amount: item.price }) };
|
|
43
45
|
const revision = hash(priceFields);
|
|
44
46
|
let price = prices.find((p) => own(p) && p.product === id && p.metadata?.revision === revision);
|
package/lib/shop.mjs
CHANGED
|
@@ -205,7 +205,7 @@ async function makeWebhook(env, site, { fetcher }) {
|
|
|
205
205
|
const list = await listWebhookEndpoints(env, { fetcher });
|
|
206
206
|
older = (list?.data ?? []).filter((e) => e?.url === url && e?.metadata?.homie === HOOK_MARK && e?.status !== 'disabled').map((e) => e.id).filter((id) => typeof id === 'string');
|
|
207
207
|
} catch (error) {
|
|
208
|
-
if (isPermissionError(error)) return { ok: false, page: said(400, 'This key cannot make webhooks: it needs Webhook Endpoints: Write. In Stripe, open the key (… → Edit key), set Webhook Endpoints to Write and save, then go back and press Save again. Or make the webhook yourself and paste its signing secret under "I made the webhook myself".') };
|
|
208
|
+
if (isPermissionError(error)) return { ok: false, page: said(400, 'This key cannot make webhooks: it needs Webhook Endpoints: Write. In Stripe, open the key (… → Edit key), set Webhook Endpoints to Write and save, then go back and press Save again. Or make the webhook yourself and paste its signing secret under "I made the webhook myself".' + ` Stripe said: ${error.message}`) };
|
|
209
209
|
return { ok: false, page: said(502, `Stripe did not answer the webhook list (${error instanceof StripeError ? error.message : 'no answer'}). Nothing was saved. Go back and try again.`) };
|
|
210
210
|
}
|
|
211
211
|
let made = null;
|
|
@@ -215,7 +215,7 @@ async function makeWebhook(env, site, { fetcher }) {
|
|
|
215
215
|
description: 'Homie shop (made by homie-studio shop connect)', metadata: { homie: HOOK_MARK },
|
|
216
216
|
}, { fetcher });
|
|
217
217
|
} catch (error) {
|
|
218
|
-
if (isPermissionError(error)) return { ok: false, page: said(400, 'This key cannot make webhooks: it needs Webhook Endpoints: Write. In Stripe, open the key (… → Edit key), set Webhook Endpoints to Write and save, then go back and press Save again. Or make the webhook yourself and paste its signing secret under "I made the webhook myself".') };
|
|
218
|
+
if (isPermissionError(error)) return { ok: false, page: said(400, 'This key cannot make webhooks: it needs Webhook Endpoints: Write. In Stripe, open the key (… → Edit key), set Webhook Endpoints to Write and save, then go back and press Save again. Or make the webhook yourself and paste its signing secret under "I made the webhook myself".' + ` Stripe said: ${error.message}`) };
|
|
219
219
|
return { ok: false, page: said(502, `Stripe did not make the webhook (${error instanceof StripeError ? error.message : 'no answer'}). Nothing was saved. Go back and try again.`) };
|
|
220
220
|
}
|
|
221
221
|
const secret = typeof made?.secret === 'string' ? made.secret : '';
|
|
@@ -236,7 +236,7 @@ async function probeManaged(env, shop, { fetcher }) {
|
|
|
236
236
|
line_items: [{ quantity: 1, price_data: { currency: shop?.currency ?? 'usd', unit_amount: item?.price ?? 500, tax_behavior: 'exclusive', product_data: { name: 'Homie shop check (test, expired at once)', tax_code: defaultTaxCode(item ?? {}) } } }],
|
|
237
237
|
managed_payments: { enabled: true }, metadata: { homie: 'shop-check' },
|
|
238
238
|
}, { fetcher, idempotencyKey: `homie-check-${randomBytes(8).toString('hex')}` });
|
|
239
|
-
if (typeof s?.id === 'string') { try { await expireCheckoutSession(env, s.id, { fetcher }); } catch {
|
|
239
|
+
if (typeof s?.id === 'string') { try { await expireCheckoutSession(env, s.id, { fetcher }); } catch (error) { return { ok: false, words: `Stripe took the test checkout but could not expire it (${error instanceof StripeError ? error.message : 'no answer'}). It will expire by itself.` }; } }
|
|
240
240
|
return { ok: true, words: 'Stripe took a test checkout with Managed Payments: it is on for this account (test mode).' };
|
|
241
241
|
} catch (error) {
|
|
242
242
|
return { ok: false, words: `Stripe refused a test checkout with Managed Payments (${error instanceof StripeError ? error.message : 'no answer'}). Finish Managed Payments in Stripe (Settings → Managed Payments: accept the terms, pass the eligibility review), or pick "You are" the seller.` };
|
|
@@ -284,14 +284,16 @@ export async function shopConnectManual(root, { managed = null, live = false, lo
|
|
|
284
284
|
// One read with the key, so a typo is caught here and not at the first sale. It never leaves this computer
|
|
285
285
|
// except to Stripe itself.
|
|
286
286
|
if (verify) {
|
|
287
|
-
try { await stripeCall(sk, 'GET', '/v1/checkout/sessions', { limit: 1 }, { fetcher }); } catch {
|
|
288
|
-
|
|
287
|
+
try { await stripeCall(sk, 'GET', '/v1/checkout/sessions', { limit: 1 }, { fetcher }); } catch (error) {
|
|
288
|
+
const reason = error instanceof StripeError ? ` Stripe said: ${error.message}` : '';
|
|
289
|
+
log(`Stripe could not read checkouts.${reason}`);
|
|
290
|
+
say(said(400, 'Stripe did not accept that key for reading checkouts. Check its permissions (Checkout Sessions: Write) and that it is the account you mean, then go back and paste it again.' + reason)); return;
|
|
289
291
|
}
|
|
290
292
|
}
|
|
291
293
|
let webhook = { made: false };
|
|
292
294
|
if (!hook) {
|
|
293
295
|
const m = await makeWebhook(sk, site, { fetcher });
|
|
294
|
-
if (!m.ok) { say(m.page); return; }
|
|
296
|
+
if (!m.ok) { log(m.page.text); say(m.page); return; }
|
|
295
297
|
hook = m.secret;
|
|
296
298
|
webhook = { made: true, id: m.id, older: m.older };
|
|
297
299
|
}
|
|
@@ -303,10 +305,13 @@ export async function shopConnectManual(root, { managed = null, live = false, lo
|
|
|
303
305
|
// The new secret is saved: the older endpoints this kit made for the same address stop sending (turned off,
|
|
304
306
|
// not deleted: the owner can turn one back on in Stripe).
|
|
305
307
|
let disabled = 0;
|
|
308
|
+
const warnings = [];
|
|
306
309
|
if (okay && webhook.made) {
|
|
307
|
-
for (const id of webhook.older) { try { await updateWebhookEndpoint(sk, id, { disabled: true }, { fetcher }); disabled += 1; } catch {
|
|
310
|
+
for (const id of webhook.older) { try { await updateWebhookEndpoint(sk, id, { disabled: true }, { fetcher }); disabled += 1; } catch (error) { warnings.push(`Stripe could not turn off webhook ${id}: ${error instanceof StripeError ? error.message : 'no answer'}`); } }
|
|
308
311
|
}
|
|
309
312
|
const check = okay && !live && chosen === 'stripe-managed' ? await probeManaged(sk, shopNow.shop, { fetcher }) : null;
|
|
313
|
+
if (check && !check.ok) warnings.push(check.words);
|
|
314
|
+
for (const warning of warnings) log(warning);
|
|
310
315
|
let tillNote = '';
|
|
311
316
|
if (okay && shopNow.shop && !shopNow.absent && chosen !== shopNow.shop.till) {
|
|
312
317
|
try {
|
|
@@ -317,13 +322,14 @@ export async function shopConnectManual(root, { managed = null, live = false, lo
|
|
|
317
322
|
} catch { tillNote = ''; }
|
|
318
323
|
}
|
|
319
324
|
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.' : '';
|
|
320
|
-
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 ? ` ${check.words}` : ''}${narrow} You can close this page.` : 'Wrangler could not save it (is this computer signed in to Cloudflare? npx wrangler login). Nothing was kept.'));
|
|
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.'));
|
|
321
326
|
server.close();
|
|
322
327
|
done(okay
|
|
323
328
|
? {
|
|
324
329
|
ok: true, command: 'shop connect', saved: true, mode: modeOf(key), till: chosen,
|
|
325
330
|
webhook: webhook.made ? { made: true, id: webhook.id, url: hookUrlOf(site), turnedOff: disabled } : { made: false, url: hookUrlOf(site) },
|
|
326
331
|
...(check ? { managedPayments: check } : {}),
|
|
332
|
+
...(warnings.length ? { warnings } : {}),
|
|
327
333
|
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.`,
|
|
328
334
|
}
|
|
329
335
|
: { ok: false, command: 'shop connect', why: 'wrangler secret put failed (sign in with npx wrangler login, then run this again)' });
|
package/lib/stripe-connect.mjs
CHANGED
|
@@ -11,7 +11,7 @@ import { readStudio, siteUrl } from './studio.mjs';
|
|
|
11
11
|
import { hookUrlOf, readShop, shopConnectManual } from './shop.mjs';
|
|
12
12
|
import { syncPaymentLinks } from './shop-links.mjs';
|
|
13
13
|
import { LINKS_STRIPE_VERSION, fullerReason } from '../worker/shop-links.mjs';
|
|
14
|
-
import { formEncode } from '../worker/stripe.mjs';
|
|
14
|
+
import { formEncode, redactStripe } from '../worker/stripe.mjs';
|
|
15
15
|
|
|
16
16
|
const hash = (s) => createHash('sha256').update(s).digest('hex');
|
|
17
17
|
const failure = (needs, why, extra = {}) => ({ ok: false, command: 'shop connect', needs, why, ...extra });
|
|
@@ -28,6 +28,14 @@ export function stripeConnectionInfo(root, { storage = receiptHome(), now = Date
|
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
const json = (s) => { try { return JSON.parse(s); } catch { return null; } };
|
|
31
|
+
// Stripe CLI also prints login failures on stderr (stripe/stripe-cli pkg/cmd/root.go).
|
|
32
|
+
// Never fall back to stdout: successful login output can contain credentials.
|
|
33
|
+
const stripeReason = (result, call) => {
|
|
34
|
+
const message = (json(result?.stdout)?.error ?? json(result?.stderr)?.error)?.message
|
|
35
|
+
?? (result?.code !== 0 ? result?.stderr?.trim() : null);
|
|
36
|
+
return typeof message === 'string' ? ` Stripe said: “${redactStripe(message).slice(0, 400)}” (${call}).` : '';
|
|
37
|
+
};
|
|
38
|
+
|
|
31
39
|
// This output stays private, including CLI config, completion, errors and API responses.
|
|
32
40
|
export function stripeProcess(args, { cwd, env = process.env } = {}) {
|
|
33
41
|
const childEnv = { ...env, CI: '1', NO_COLOR: '1' };
|
|
@@ -94,15 +102,15 @@ export async function shopConnect(root, options = {}) {
|
|
|
94
102
|
const login = json(start.stdout);
|
|
95
103
|
const complete = completionArgs(login?.next_step);
|
|
96
104
|
if (start.code !== 0 || !stripeURL(login?.browser_url) || !complete || !/^[a-zA-Z0-9 -]{1,100}$/.test(login?.verification_code ?? '')) {
|
|
97
|
-
return failure('stripe-login', 'Stripe could not start browser approval. Update the official Stripe CLI and try shop connect again. An account administrator may need to enable CLI access in Stripe’s MCP and CLI access settings.');
|
|
105
|
+
return failure('stripe-login', 'Stripe could not start browser approval. Update the official Stripe CLI and try shop connect again. An account administrator may need to enable CLI access in Stripe’s MCP and CLI access settings.' + stripeReason(start, 'login'));
|
|
98
106
|
}
|
|
99
107
|
log(`Approve your studio’s Stripe account in your browser. Pairing code: ${login.verification_code}. ${login.browser_url}`);
|
|
100
108
|
open(login.browser_url);
|
|
101
109
|
const finished = await cli(complete);
|
|
102
|
-
if (finished.code !== 0) return failure('stripe-login', 'Stripe approval did not finish. Run shop connect again when ready.');
|
|
110
|
+
if (finished.code !== 0) return failure('stripe-login', 'Stripe approval did not finish. Run shop connect again when ready.' + stripeReason(finished, 'login completion'));
|
|
103
111
|
|
|
104
112
|
probe = await cli(['get', '/v1/products', '-d', 'limit=1', ...(live ? ['--live'] : [])]);
|
|
105
|
-
if (probe.code !== 0 || !Array.isArray(json(probe.stdout)?.data)) return failure('stripe-access', 'Stripe is signed in but cannot read Products in the requested mode. Review the account, sandbox and permissions in Stripe, then run shop connect --renew again.');
|
|
113
|
+
if (probe.code !== 0 || !Array.isArray(json(probe.stdout)?.data)) return failure('stripe-access', 'Stripe is signed in but cannot read Products in the requested mode. Review the account, sandbox and permissions in Stripe, then run shop connect --renew again.' + stripeReason(probe, 'GET /v1/products'));
|
|
106
114
|
}
|
|
107
115
|
const site = siteUrl(root);
|
|
108
116
|
if (!site) return failure('deploy', 'Stripe is connected on this computer. Deploy the studio with studio_deploy (Cloudflare approval first if needed), then run shop connect again.', { mode, connected: true });
|
|
@@ -114,7 +122,9 @@ export async function shopConnect(root, options = {}) {
|
|
|
114
122
|
for (const [k, v] of formEncode(params)) args.push('-d', `${k}=${v}`);
|
|
115
123
|
const r = await cli(args);
|
|
116
124
|
const body = json(r.stdout);
|
|
117
|
-
if (r.code !== 0 || !body || body.error)
|
|
125
|
+
if (r.code !== 0 || !body || body.error) {
|
|
126
|
+
throw Object.assign(new Error('Stripe API operation failed'), { stripeReason: stripeReason(r, `${method} ${path.replace(/\/[^/]*_[A-Za-z0-9]+$/, '/…')}`) });
|
|
127
|
+
}
|
|
118
128
|
return body;
|
|
119
129
|
};
|
|
120
130
|
const reason = fullerReason(checked.shop);
|
|
@@ -129,7 +139,8 @@ export async function shopConnect(root, options = {}) {
|
|
|
129
139
|
renameSync(`${receiptFile}.tmp`, receiptFile);
|
|
130
140
|
} });
|
|
131
141
|
return { ...result, profile };
|
|
132
|
-
} catch {
|
|
133
|
-
|
|
142
|
+
} catch (e) {
|
|
143
|
+
const said = e?.stripeReason ?? '';
|
|
144
|
+
return failure('stripe-setup', 'Stripe setup could not finish. Check the studio’s Stripe session and Products, Prices, Payment Links and Webhook Endpoints write permissions, and the Cloudflare login, then run shop connect again. No credentials were printed.' + said);
|
|
134
145
|
} finally { lock.release(); }
|
|
135
146
|
}
|
|
@@ -1906,6 +1906,46 @@
|
|
|
1906
1906
|
}
|
|
1907
1907
|
}
|
|
1908
1908
|
}
|
|
1909
|
+
},
|
|
1910
|
+
"0.35.1": {
|
|
1911
|
+
"files": {
|
|
1912
|
+
"CLAUDE.md": "ddf2b2a0376202f6",
|
|
1913
|
+
"README.md": "684b3cca8001673c",
|
|
1914
|
+
"HANDOFF.md": "29337e2ace95b2e7",
|
|
1915
|
+
"games/README.md": "5ad32fe80c61f117",
|
|
1916
|
+
"music/README.md": "a3dc042dadd39df8",
|
|
1917
|
+
"videos/README.md": "16babc3437d5545b",
|
|
1918
|
+
"posts/README.md": "37841aae476bd540",
|
|
1919
|
+
"site/README.md": "732894273a32dc3f",
|
|
1920
|
+
"site/src/worker.mjs": "fc90f3609cad18ba"
|
|
1921
|
+
},
|
|
1922
|
+
"sections": {
|
|
1923
|
+
"AGENTS.md": {
|
|
1924
|
+
"(intro)": "214aa46eb3a895d5",
|
|
1925
|
+
"## Layout": "8baf49e4291e7e88",
|
|
1926
|
+
"## Commands (all through the pinned CLI in node_modules)": "a7f290579585368c",
|
|
1927
|
+
"## The site": "0cb8dd9a6f8a0096",
|
|
1928
|
+
"## Making games": "2da7fa74c8884834",
|
|
1929
|
+
"## The Game Codex and progress": "0738ef719cef3a4c",
|
|
1930
|
+
"## Art direction and models": "a48fff7bf0a5f2fc",
|
|
1931
|
+
"## Running live games (the back office)": "51ebf2ab756f6e4d",
|
|
1932
|
+
"## Servers and AI seats": "98d08705087a33ec",
|
|
1933
|
+
"## Selling things (the shop)": "9e020d69dd6a954e",
|
|
1934
|
+
"## Continuing a build from the Claude app": "0d68081e924ca419",
|
|
1935
|
+
"## Rules": "2e6b29afb85dcf7e",
|
|
1936
|
+
"## Telling Homie": "80b46fdd9e578db8",
|
|
1937
|
+
"## Beta": "9d02638207a26e73"
|
|
1938
|
+
}
|
|
1939
|
+
},
|
|
1940
|
+
"named": {
|
|
1941
|
+
"Homie Arcade": {
|
|
1942
|
+
"sections": {
|
|
1943
|
+
"AGENTS.md": {
|
|
1944
|
+
"## Commands (all through the pinned CLI in node_modules)": "7cad8fd10f62fef1"
|
|
1945
|
+
}
|
|
1946
|
+
}
|
|
1947
|
+
}
|
|
1948
|
+
}
|
|
1909
1949
|
}
|
|
1910
1950
|
}
|
|
1911
1951
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@homie-rocks/studio",
|
|
3
|
-
"version": "0.35.
|
|
3
|
+
"version": "0.35.1",
|
|
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/worker/stripe.mjs
CHANGED
|
@@ -55,13 +55,13 @@ export function formEncode(params, prefix = '', out = new URLSearchParams()) {
|
|
|
55
55
|
export function redactStripe(value, env = {}) {
|
|
56
56
|
let text = String(value ?? '');
|
|
57
57
|
for (const secret of [env.STRIPE_KEY, env.STRIPE_WEBHOOK_SECRET]) if (secret) text = text.replaceAll(String(secret), '[redacted]');
|
|
58
|
-
return text.replace(/(?:
|
|
58
|
+
return text.replace(/(?:(?:sk|rk|pk|whsec|oak)_[A-Za-z0-9+/=_-]+|[A-Za-z0-9_]+_secret_[A-Za-z0-9_-]+)/g, '[redacted]');
|
|
59
59
|
}
|
|
60
60
|
|
|
61
61
|
export class StripeError extends Error {
|
|
62
62
|
constructor(status, body) {
|
|
63
63
|
const e = body?.error ?? {};
|
|
64
|
-
super(e.message ? redactStripe(e.message).slice(0,
|
|
64
|
+
super(e.message ? redactStripe(e.message).slice(0, 400) : `Stripe answered ${status}`);
|
|
65
65
|
this.status = status;
|
|
66
66
|
this.code = redactStripe(e.code ?? e.type ?? 'stripe');
|
|
67
67
|
this.type = e.type ? redactStripe(e.type) : null;
|
package/worker/version.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
/** The @homie-rocks/studio version this Worker code is (package.json's; a test keeps them equal). Cache-busts /_homie/site.js. */
|
|
2
|
-
export const STUDIO_VERSION_TAG = '0.35.
|
|
2
|
+
export const STUDIO_VERSION_TAG = '0.35.1';
|