@homie-rocks/studio 0.51.8 → 0.53.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 +59 -0
- package/apps/APPS.md +117 -0
- package/apps/apps.ts +68 -1
- package/bin/homie-studio.mjs +8 -2
- package/docs/shop-migration.md +9 -0
- package/docs/toolkit-batch.md +49 -0
- package/docs/toolkit-place-patterns.md +6 -0
- package/functions/FUNCTIONS.md +42 -1
- package/functions/functions.ts +4 -0
- package/guides/app/PLACES.md +146 -0
- package/guides/app/SKILL.md +3 -0
- package/guides/game/SKILL.md +3 -0
- package/guides/shop/SKILL.md +27 -12
- package/lib/app-needs.mjs +59 -15
- package/lib/app-ready.mjs +1 -1
- package/lib/build.mjs +8 -2
- package/lib/check.mjs +40 -16
- package/lib/cloudflare.mjs +2 -2
- package/lib/dev-restart.mjs +25 -0
- package/lib/dev.mjs +7 -1
- package/lib/fork.mjs +47 -0
- package/lib/functions-build.mjs +3 -0
- package/lib/jobs.mjs +1 -0
- package/lib/mcp-tools.mjs +3 -3
- package/lib/needs-headless.mjs +4 -3
- package/lib/scaffold.mjs +29 -1
- package/lib/shop-links.mjs +7 -7
- package/lib/shop-mcp.mjs +88 -14
- package/lib/stripe-connect.mjs +46 -6
- package/lib/template-history.json +84 -0
- package/lib/tools-cli.mjs +25 -3
- package/lib/upgrade-places.mjs +49 -0
- package/lib/upgrade.mjs +2 -1
- package/lib/worker-config.mjs +7 -3
- package/links/links.d.mts +5 -0
- package/links/links.d.ts +2 -0
- package/links/links.mjs +2 -0
- package/links/time.mjs +20 -0
- package/package.json +1 -1
- package/rules/rules.ts +1 -1
- package/shop/SHOP.md +27 -12
- package/shop/shop.ts +3 -3
- package/tools/tools.ts +13 -0
- package/worker/app-format.mjs +6 -3
- package/worker/app-records.mjs +21 -5
- package/worker/app-shell.mjs +28 -5
- package/worker/app-streams.mjs +57 -0
- package/worker/app-tools.mjs +44 -0
- package/worker/app-web.mjs +23 -0
- package/worker/booking-calendar.mjs +26 -0
- package/worker/booking-holds.mjs +100 -0
- package/worker/booking-options.mjs +23 -0
- package/worker/booking-resources.mjs +55 -0
- package/worker/build-id.mjs +1 -1
- package/worker/business-events.mjs +31 -0
- package/worker/customer-mcp.mjs +1 -1
- package/worker/event-resources.mjs +1 -1
- package/worker/functions.mjs +5 -0
- package/worker/index.mjs +54 -9
- package/worker/local-test.mjs +10 -0
- package/worker/mcp-events.mjs +3 -3
- package/worker/mcp-tools.mjs +23 -2
- package/worker/mcp.mjs +3 -3
- package/worker/office.mjs +27 -0
- package/worker/pages.mjs +4 -4
- package/worker/records-page.mjs +3 -3
- package/worker/shop-links.mjs +12 -0
- package/worker/shop.mjs +20 -5
- package/worker/version.mjs +1 -1
package/worker/shop.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import {claimHoldStatement,ownedHold} from './booking-holds.mjs';
|
|
1
2
|
import { stripeEventOwner } from './stripe-ownership.mjs';
|
|
2
3
|
import { localSession } from './local-test.mjs';
|
|
3
4
|
import { bindSubscription, subscriptionEvent, subscriptionPortal } from './shop-subscriptions.mjs';
|
|
@@ -154,7 +155,7 @@ export async function readiness(env, shop) {
|
|
|
154
155
|
let migration = null;
|
|
155
156
|
if (!env?.DB) missing.push('database');
|
|
156
157
|
else if ((migration = await migrationNeeded(env))) missing.push('migration');
|
|
157
|
-
return { ready: missing.length === 0, missing, next: missing.length ? MISSING_WORDS[missing[0]] : null, migration, mode: paidItems ? shopMode(env) : null };
|
|
158
|
+
return { ready: missing.length === 0, missing, next: missing.length ? (missing[0]==='payment-links' && env.STRIPE_SHOP_SETUP || MISSING_WORDS[missing[0]]) : null, migration, mode: paidItems ? shopMode(env) : null };
|
|
158
159
|
}
|
|
159
160
|
|
|
160
161
|
export const MISSING_WORDS = Object.freeze({
|
|
@@ -516,6 +517,7 @@ async function startCheckout(env, url, cat, shop, item, { player, game, amount,
|
|
|
516
517
|
lines ??= [{ item, amount, quantity: 1 }];
|
|
517
518
|
const keyless = (amount > 0 || lines.some(l => l.item.interval)) && !hasStripeKey(env);
|
|
518
519
|
const config=linkConfig(env);
|
|
520
|
+
if(booking?.hold&&keyless)return {error:fail(409,'booking','A held slot needs session checkout; keyless payment links cannot claim it.')};
|
|
519
521
|
if(booking && amount===0 && !hasStripeKey(env) && config?.slots?.some(l=>l.app===game&&l.slots===booking.slots&&l.slot===booking.slot))return {error:fail(409,'booking','Publish a separate local slot for a free booking; it cannot share a paid Stripe capacity budget.')};
|
|
520
522
|
const menu=keyless && !booking && lines.length>0 ? config?.menus?.find(m=>(m.basket || lines.length>1)&&lines.every(l=>m.items.some(i=>i.item===l.item.id))) : null;
|
|
521
523
|
const link=keyless ? booking ? config?.slots?.find(l=>l.app===game&&l.collection===booking.collection&&l.slots===booking.slots&&l.slot===booking.slot&&l.item===item.id) : menu ?? config?.items.find(l=>l.item===item.id) : null;
|
|
@@ -546,7 +548,7 @@ async function startCheckout(env, url, cat, shop, item, { player, game, amount,
|
|
|
546
548
|
lines = lines.map((line, i) => ({ ...line, id: `${order.id}_${i}` }));
|
|
547
549
|
const reserve = async () => {
|
|
548
550
|
const results = await env.DB.batch([reservation(),
|
|
549
|
-
...(booking ? [bookingStatement(env, { ...booking, pending: keyless, id: order.id, app: booking.app ?? game, order: order.id })] : []),
|
|
551
|
+
...(booking ? [booking.hold?claimHoldStatement(env,{...booking,order:order.id}):bookingStatement(env, { ...booking, pending: keyless, id: order.id, app: booking.app ?? game, order: order.id })] : []),
|
|
550
552
|
...(reference === null ? [] : [env.DB.prepare("INSERT INTO meta(key,value) SELECT ?1,?2 WHERE EXISTS (SELECT 1 FROM shop_orders WHERE id=?3)").bind(`shop-reference:${order.id}`, reference, order.id)]),
|
|
551
553
|
...(menu ? [env.DB.prepare('INSERT INTO meta(key,value) VALUES(?1,?2)').bind(`shop-menu:${order.id}`,JSON.stringify(lines))] : []),
|
|
552
554
|
...lines.map((line, i) => env.DB.prepare("INSERT INTO shop_order_lines (id, order_id, position, item, quantity, unit_amount, amount, snapshot) SELECT ?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8 WHERE EXISTS (SELECT 1 FROM shop_orders WHERE id = ?2)")
|
|
@@ -558,7 +560,7 @@ async function startCheckout(env, url, cat, shop, item, { player, game, amount,
|
|
|
558
560
|
await reconcileOrders(env, shop, player.id, { expireOpen: true, before: Math.min(requestAt, now - 60000) });
|
|
559
561
|
let reserved;
|
|
560
562
|
try { reserved = await reserve(); } catch (error) {
|
|
561
|
-
if (/booking capacity unavailable/.test(error.message)) return { error: fail(409, 'capacity', 'That slot is full. Choose another slot.') };
|
|
563
|
+
if (/booking capacity unavailable|booking hold changed or expired/.test(error.message)) return { error: fail(409, 'capacity', 'That slot is full. Choose another slot.') };
|
|
562
564
|
throw error;
|
|
563
565
|
}
|
|
564
566
|
const reconciled = !reserved.meta?.changes;
|
|
@@ -575,7 +577,7 @@ async function startCheckout(env, url, cat, shop, item, { player, game, amount,
|
|
|
575
577
|
}
|
|
576
578
|
if (keyless) {
|
|
577
579
|
const checkout = new URL(link.url); checkout.searchParams.set('client_reference_id', order.id);
|
|
578
|
-
if(menu?.basket)checkout.searchParams.set('prefilled_amount',String(amount));
|
|
580
|
+
if(menu?.basket || item.kind==='deposit')checkout.searchParams.set('prefilled_amount',String(amount));
|
|
579
581
|
return { order, url: checkout.href, reconciled };
|
|
580
582
|
}
|
|
581
583
|
let session = null;
|
|
@@ -648,6 +650,8 @@ async function buyRoute(request, env, url, cat, shop, ready, ctx, requestAt) {
|
|
|
648
650
|
if (!item) return fail(404, 'item', 'no such item here');
|
|
649
651
|
const quantity = entry.quantity ?? 1;
|
|
650
652
|
if (!Number.isSafeInteger(quantity) || quantity < 1) return fail(400, 'arithmetic', 'Quantity is a positive safe integer.');
|
|
653
|
+
if(!hasStripeKey(env) && ['supporter','deposit'].includes(item.kind) && quantity!==1)return fail(400,'quantity','This payment link has quantity fixed at one.');
|
|
654
|
+
if(item.kind==='deposit' && (!reference || !Number.isSafeInteger(entry.amount??item.price) || (entry.amount??item.price)<=0))return fail(400,'deposit','A deposit requires the app reference and its saved positive amount.');
|
|
651
655
|
if (!shop.policy.repeatPurchases && (quantity > 1 || seen.has(item.id))) return fail(403, 'owned', 'The studio has disabled repeat purchases.');
|
|
652
656
|
seen.add(item.id);
|
|
653
657
|
const v = await itemsView(env, shop, { game, ...facts, player, open: true, item, purchasing: true });
|
|
@@ -677,11 +681,20 @@ async function buyRoute(request, env, url, cat, shop, ready, ctx, requestAt) {
|
|
|
677
681
|
const collection = requestedBooking?.collection;
|
|
678
682
|
const spec = app?.records?.collections?.[collection]?.booking;
|
|
679
683
|
if (!spec || !/^[A-Za-z0-9_-]{1,64}$/.test(requestedBooking?.slot ?? '') || lines.length !== 1 || item.id !== spec.item || item.interval) return fail(400, 'booking', 'Choose a declared booking slot and its service item.');
|
|
684
|
+
if(spec.resources&&requestedBooking.hold===undefined)return fail(409,'booking','Reserve the resource through the booking tool and pass its hold before checkout.');
|
|
680
685
|
const local = await localSession(request, env);
|
|
681
686
|
booking = { app: local?.app === game && local.scope ? `${game}:check:${local.scope}` : game, collection, slots: spec.slots, slot: requestedBooking.slot, quantity: lines[0].quantity };
|
|
687
|
+
if(requestedBooking.hold!==undefined){
|
|
688
|
+
try {
|
|
689
|
+
const caller=local?.app===game?{id:`local-test-${local.app}-${local.role}`,testScope:local.scope}:{id:player.guest?null:player.id};
|
|
690
|
+
const hold=await ownedHold(env,caller,{id:requestedBooking.hold,token:requestedBooking.token});
|
|
691
|
+
if(hold.app!==booking.app||hold.collection!==collection||hold.slot!==booking.slot||hold.quantity!==booking.quantity||hold.order_id!=='hold:'+hold.id||!['held','confirmed'].includes(hold.state)||(hold.state==='held'&&hold.hold_until<=Date.now()))return fail(409,'booking','Hold changed or expired; reload.');
|
|
692
|
+
booking.hold=hold.id;
|
|
693
|
+
}catch{return fail(403,'booking','Booking owner required');}
|
|
694
|
+
}
|
|
682
695
|
}
|
|
683
696
|
const r = await startCheckout(env, url, cat, shop, item, { player, game, amount, lines, reference, booking, requestAt });
|
|
684
|
-
const response = r.error ?? json({ ok: true, order: r.order.id, url: r.url, ...(booking ? { booking: r.order.id } : {}) });
|
|
697
|
+
const response = r.error ?? json({ ok: true, order: r.order.id, url: r.url, ...(booking ? { booking: booking.hold??r.order.id } : {}) });
|
|
685
698
|
if (!r.error && !r.reconciled && ctx?.waitUntil) ctx.waitUntil(reconcileOrders(env, shop, player.id).catch((error) => logOrderError(env, r.order.id, error)));
|
|
686
699
|
for (const cookie of cookies) response.headers.append('set-cookie', cookie);
|
|
687
700
|
return response;
|
|
@@ -966,7 +979,9 @@ async function bindLinkSession(env, session, mode) {
|
|
|
966
979
|
if (!link || (session.payment_link?.id ?? session.payment_link) !== link.id || session.metadata?.revision !== link.revision || session.currency !== source.currency) return false;
|
|
967
980
|
const amount = session.amount_subtotal;
|
|
968
981
|
if (!Number.isSafeInteger(amount) || amount <= 0) return false;
|
|
982
|
+
if(item.kind==='deposit' && amount!==source.amount)return false;
|
|
969
983
|
const quantity = item.price === 'choose' ? 1 : amount / item.price;
|
|
984
|
+
if(['supporter','deposit'].includes(item.kind) && quantity!==1)return false;
|
|
970
985
|
if (!Number.isSafeInteger(quantity) || quantity < 1 || quantity > 999999) return false;
|
|
971
986
|
if (item.price === 'choose' && (amount < item.min || item.max !== null && amount > item.max)) return false;
|
|
972
987
|
// Claim the first session atomically. A repeated URL creates a separate paid order, never loses a payment.
|
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.
|
|
2
|
+
export const STUDIO_VERSION_TAG = '0.53.0';
|