@aglyn/plugins-marketplace 1.2.0 → 1.3.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.
Files changed (73) hide show
  1. package/README.md +1 -1
  2. package/package.json +14 -14
  3. package/src/lib/components/listing-content.component.js +11 -3
  4. package/src/lib/components/listing-content.component.js.map +1 -1
  5. package/src/lib/components/marketplace-pay-panel.component.d.ts +67 -0
  6. package/src/lib/components/marketplace-pay-panel.component.js +228 -0
  7. package/src/lib/components/marketplace-pay-panel.component.js.map +1 -0
  8. package/src/lib/components/publish-artifact-dialog.component.d.ts +1 -1
  9. package/src/lib/components/publish-artifact-dialog.component.js +144 -20
  10. package/src/lib/components/publish-artifact-dialog.component.js.map +1 -1
  11. package/src/lib/components/publish-starter-fields.component.d.ts +35 -0
  12. package/src/lib/components/publish-starter-fields.component.js +176 -0
  13. package/src/lib/components/publish-starter-fields.component.js.map +1 -0
  14. package/src/lib/components/starter-picker-shelf.component.d.ts +2 -2
  15. package/src/lib/components/starter-picker-shelf.component.js +28 -6
  16. package/src/lib/components/starter-picker-shelf.component.js.map +1 -1
  17. package/src/lib/hooks/use-marketplace-actions.d.ts +9 -3
  18. package/src/lib/hooks/use-marketplace-actions.js +47 -89
  19. package/src/lib/hooks/use-marketplace-actions.js.map +1 -1
  20. package/src/lib/hooks/use-marketplace-pay.d.ts +70 -0
  21. package/src/lib/hooks/use-marketplace-pay.js +359 -0
  22. package/src/lib/hooks/use-marketplace-pay.js.map +1 -0
  23. package/src/lib/hooks/use-starter-publish-facts.d.ts +25 -0
  24. package/src/lib/hooks/use-starter-publish-facts.js +116 -0
  25. package/src/lib/hooks/use-starter-publish-facts.js.map +1 -0
  26. package/src/lib/hooks/use-starter-purchase.d.ts +17 -22
  27. package/src/lib/hooks/use-starter-purchase.js +26 -73
  28. package/src/lib/hooks/use-starter-purchase.js.map +1 -1
  29. package/src/lib/model/marketplace-invoice.d.ts +52 -0
  30. package/src/lib/model/marketplace-invoice.js +75 -0
  31. package/src/lib/model/marketplace-invoice.js.map +1 -0
  32. package/src/lib/model/marketplace-pay.d.ts +42 -0
  33. package/src/lib/model/marketplace-pay.js +33 -0
  34. package/src/lib/model/marketplace-pay.js.map +1 -0
  35. package/src/lib/model/platform-seller.d.ts +37 -0
  36. package/src/lib/model/platform-seller.js +36 -0
  37. package/src/lib/model/platform-seller.js.map +1 -0
  38. package/src/lib/model/starter-publish.d.ts +159 -0
  39. package/src/lib/model/starter-publish.js +189 -0
  40. package/src/lib/model/starter-publish.js.map +1 -0
  41. package/src/lib/model/starter-query.d.ts +0 -8
  42. package/src/lib/model/starter-query.js +0 -10
  43. package/src/lib/model/starter-query.js.map +1 -1
  44. package/src/lib/server/billing-webhook.d.ts +6 -0
  45. package/src/lib/server/billing-webhook.js +269 -211
  46. package/src/lib/server/billing-webhook.js.map +1 -1
  47. package/src/lib/server/checkout.d.ts +2 -48
  48. package/src/lib/server/checkout.js +21 -241
  49. package/src/lib/server/checkout.js.map +1 -1
  50. package/src/lib/server/pay.d.ts +18 -0
  51. package/src/lib/server/pay.js +540 -0
  52. package/src/lib/server/pay.js.map +1 -0
  53. package/src/lib/server/platform-org.d.ts +23 -0
  54. package/src/lib/server/platform-org.js +27 -0
  55. package/src/lib/server/platform-org.js.map +1 -0
  56. package/src/lib/server/publish-preconditions.js +4 -1
  57. package/src/lib/server/publish-preconditions.js.map +1 -1
  58. package/src/lib/server/publish-starter.js +5 -2
  59. package/src/lib/server/publish-starter.js.map +1 -1
  60. package/src/lib/server/publisher-profile.d.ts +5 -0
  61. package/src/lib/server/publisher-profile.js +20 -1
  62. package/src/lib/server/publisher-profile.js.map +1 -1
  63. package/src/lib/server/publisher-standing.d.ts +26 -0
  64. package/src/lib/server/publisher-standing.js +66 -0
  65. package/src/lib/server/publisher-standing.js.map +1 -0
  66. package/src/lib/server/sale.d.ts +119 -0
  67. package/src/lib/server/sale.js +304 -0
  68. package/src/lib/server/sale.js.map +1 -0
  69. package/src/lib/server/starter-publish-facts.d.ts +36 -0
  70. package/src/lib/server/starter-publish-facts.js +135 -0
  71. package/src/lib/server/starter-publish-facts.js.map +1 -0
  72. package/src/lib/server.js +8 -0
  73. package/src/lib/server.js.map +1 -1
@@ -0,0 +1,189 @@
1
+ import { _ as _extends } from "@swc/helpers/_/_extends";
2
+ /**
3
+ * @license
4
+ * Copyright 2026 Aglyn LLC
5
+ *
6
+ * Licensed under the Apache License, Version 2.0 (the "License");
7
+ * you may not use this file except in compliance with the License.
8
+ * You may obtain a copy of the License at
9
+ *
10
+ * http://www.apache.org/licenses/LICENSE-2.0
11
+ *
12
+ * Unless required by applicable law or agreed to in writing, software
13
+ * distributed under the License is distributed on an "AS IS" BASIS,
14
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ * See the License for the specific language governing permissions and
16
+ * limitations under the License.
17
+ */ import { planLockReason } from "@aglyn/aglyn/app-utils/plan-entitlements";
18
+ import { siteFontPairing } from "@aglyn/aglyn/app-utils/site-font-pairings";
19
+ import { SITE_STARTER_MAX_SCREENS } from "@aglyn/aglyn/app-utils/site-starter";
20
+ /**
21
+ * The pages the door will capture: every page in the host's routing map
22
+ * (`screens`, id → slug) with a published version and not deleted, as the
23
+ * door walks them. An email screen is never in the routing map.
24
+ */ export function starterPublishPages(routing, screens) {
25
+ const map = routing && typeof routing === 'object' && !Array.isArray(routing) ? routing : {};
26
+ const published = screens.filter((screen)=>screen.$id in map && !!screen.versionId && !screen.deletedAt);
27
+ return {
28
+ count: published.length,
29
+ hasHome: published.some((screen)=>map[screen.$id] === '/')
30
+ };
31
+ }
32
+ const sectionOf = (facts, section)=>facts.marketplacePath ? `${facts.marketplacePath}/${section}` : null;
33
+ /** Whether the profile read says there is one, as the door reads it: a handle. */ const hasProfile = (profile)=>typeof profile['handle'] === 'string' && !!profile['handle'];
34
+ /**
35
+ * Whether a profile's payout account can take a sale, as far as the console
36
+ * can tell: an account that takes charges. The door also checks which Stripe
37
+ * world the account belongs to, which only the server knows, and says so if
38
+ * that is what is missing.
39
+ */ export const payoutsReady = (profile)=>typeof profile['stripeAccountId'] === 'string' && !!String(profile['stripeAccountId']).trim() && profile['stripeChargesEnabled'] === true;
40
+ /**
41
+ * Every step that stands between this site and a starter publish, the setup
42
+ * steps first, then the site's own. A read still pending refuses nothing.
43
+ */ export function starterPublishSteps(facts) {
44
+ const steps = [];
45
+ if (facts.plan.ready && facts.plan.locked) {
46
+ var _facts_plan_upgradeHref;
47
+ steps.push({
48
+ id: 'plan',
49
+ text: planLockReason('Publishing to the marketplace', facts.plan.planLabel),
50
+ href: (_facts_plan_upgradeHref = facts.plan.upgradeHref) != null ? _facts_plan_upgradeHref : null,
51
+ actionLabel: 'See plans'
52
+ });
53
+ }
54
+ const profile = facts.profile;
55
+ if (profile === null || profile && !hasProfile(profile)) {
56
+ steps.push({
57
+ id: 'profile',
58
+ text: 'Set up your publisher profile first: the handle and name people see on your listings.',
59
+ href: sectionOf(facts, 'profile'),
60
+ actionLabel: 'Publisher Profile'
61
+ });
62
+ } else if (profile && facts.priceUsd > 0 && !facts.sellsAsPlatform && !payoutsReady(profile)) {
63
+ steps.push({
64
+ id: 'payouts',
65
+ text: 'Set up payouts first to sell a starter with a price, or publish it free.',
66
+ href: sectionOf(facts, 'payouts'),
67
+ actionLabel: 'Payouts'
68
+ });
69
+ }
70
+ const pages = facts.pages;
71
+ if (pages && !pages.hasHome) {
72
+ // The starter check's own words (`siteStarterRefusal`), then the fix.
73
+ steps.push({
74
+ id: 'home',
75
+ text: 'A starter needs a published Home page at /. Publish the page at / under Pages.',
76
+ href: facts.pagesPath,
77
+ actionLabel: 'Pages'
78
+ });
79
+ } else if (pages && pages.count > SITE_STARTER_MAX_SCREENS) {
80
+ steps.push({
81
+ id: 'pages',
82
+ text: `A starter can have at most ${SITE_STARTER_MAX_SCREENS} pages, ` + `and this site has ${pages.count}.`,
83
+ href: facts.pagesPath,
84
+ actionLabel: 'Pages'
85
+ });
86
+ }
87
+ return steps;
88
+ }
89
+ /**
90
+ * Where a setup refusal from the door (412) is fixed. The door's words are
91
+ * shown as they are; this picks the link beside them from what the dialog
92
+ * read: the plan, the profile, or payouts for a priced starter. With nothing
93
+ * read to say which, the publisher profile, where the other two are set up.
94
+ */ export function starterRefusalLink(facts) {
95
+ if (facts.plan.ready && facts.plan.locked) {
96
+ var _facts_plan_upgradeHref;
97
+ return {
98
+ href: (_facts_plan_upgradeHref = facts.plan.upgradeHref) != null ? _facts_plan_upgradeHref : null,
99
+ actionLabel: 'See plans'
100
+ };
101
+ }
102
+ const profile = facts.profile;
103
+ if (profile !== null && profile && hasProfile(profile) && facts.priceUsd > 0) {
104
+ return {
105
+ href: sectionOf(facts, 'payouts'),
106
+ actionLabel: 'Payouts'
107
+ };
108
+ }
109
+ return {
110
+ href: sectionOf(facts, 'profile'),
111
+ actionLabel: 'Publisher Profile'
112
+ };
113
+ }
114
+ /**
115
+ * The starter half of the publish body, or `null` until it can be sent: no
116
+ * theme chosen (neither is chosen for the publisher), or the built-in chosen
117
+ * on a site that wears none. The full theme is the site's own, read by the
118
+ * door off the site itself.
119
+ */ export function starterPublishBody(input) {
120
+ const facets = {
121
+ kind: input.kind || null
122
+ };
123
+ if (input.choice === 'full') return {
124
+ theme: {
125
+ kind: 'full'
126
+ },
127
+ mode: 'base',
128
+ facets
129
+ };
130
+ if (input.choice === 'builtin' && input.builtin) {
131
+ return {
132
+ theme: input.builtin.theme,
133
+ mode: 'base',
134
+ facets
135
+ };
136
+ }
137
+ return null;
138
+ }
139
+ /** What a publish captures, in a sentence, from the pages counted before the click. */ export function starterCapturesSentence(pages) {
140
+ return (pages.count === 1 ? 'Captures its one published page, ' : `Captures its ${pages.count} published pages, `) + 'the shared header and footer they sit in, and the theme you choose below.';
141
+ }
142
+ /** The fonts a pairing sets, as a person names them. */ function pairingLabel(id) {
143
+ const pairing = siteFontPairing(id);
144
+ if (!pairing) return null;
145
+ return pairing.heading.family === pairing.body.family ? pairing.body.family : `${pairing.heading.family} and ${pairing.body.family}`;
146
+ }
147
+ /**
148
+ * The line under the built-in theme choice: what the starter would carry, or
149
+ * why this site has no built-in to name. `hostReady` is false while the site
150
+ * is still being read.
151
+ */ export function starterBuiltinDetail(builtin, hostReady = true) {
152
+ if (!hostReady) return "Reading this site's theme…";
153
+ if (!builtin) {
154
+ return 'This site wears its own theme, not a built-in one, so a starter can carry it only as its full theme.';
155
+ }
156
+ const fonts = pairingLabel(builtin.theme.fonts);
157
+ const recolored = Object.values(builtin.theme.brand).some((colors)=>colors && Object.keys(colors).length);
158
+ return `${builtin.baseName}, ${recolored ? 'with the brand colors this site set over it' : 'in its own colors'}` + `${fonts ? `, set in ${fonts}` : ''}. A site created from it can switch to another built-in theme.`;
159
+ }
160
+ /** What the built-in choice leaves behind, for a site whose fonts it cannot name, or `null`. */ export function starterFontsNote(builtin) {
161
+ if (!builtin || builtin.fontsCarried) return null;
162
+ return `This site's fonts are not a pairing a built-in theme can name, so the starter uses ` + `${builtin.baseName}'s own fonts. Choose the full theme to keep them.`;
163
+ }
164
+ /** The answer, from what the door would read. Pure: the route hands in what it read. */ export function starterPublishFactsAnswer(input) {
165
+ var _facts_pages, _ref;
166
+ const { facts, builtin } = input;
167
+ const free = starterPublishSteps(_extends({}, facts, {
168
+ priceUsd: 0
169
+ }));
170
+ const priced = starterPublishSteps(_extends({}, facts, {
171
+ priceUsd: 1
172
+ }));
173
+ return {
174
+ captures: starterCapturesSentence((_facts_pages = facts.pages) != null ? _facts_pages : {
175
+ count: 0,
176
+ hasHome: false
177
+ }),
178
+ builtin: (_ref = builtin == null ? void 0 : builtin.theme) != null ? _ref : null,
179
+ builtinDetail: starterBuiltinDetail(builtin),
180
+ fontsNote: starterFontsNote(builtin),
181
+ steps: Object.fromEntries(priced.map((step)=>[
182
+ step.id,
183
+ step.text
184
+ ])),
185
+ blocked: free.length > 0
186
+ };
187
+ }
188
+
189
+ //# sourceMappingURL=starter-publish.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/marketplace/src/lib/model/starter-publish.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { planLockReason } from '@aglyn/aglyn/app-utils/plan-entitlements'\nimport { siteFontPairing } from '@aglyn/aglyn/app-utils/site-font-pairings'\nimport { SITE_STARTER_MAX_SCREENS } from '@aglyn/aglyn/app-utils/site-starter'\nimport type { SiteStarterBuiltinReading } from '@aglyn/aglyn/app-utils/site-starter-theme'\n\n/*==========================================\n * \"PUBLISH AS A STARTER\", BEFORE THE CLICK (AGL-4049).\n *\n * The starter door (`server/publish-starter.ts`) refuses a publish for four\n * things the workspace sets up elsewhere — a plan that sells on the\n * marketplace, a publisher profile, payouts for a starter with a price, a\n * published Home page — and for a site past the most pages a starter may\n * have. Each is read here from what the dialog already loads, so the dialog\n * says it before the click, disabled with the reason and a link to where it\n * is fixed, never after it. The door still decides: these are what it would\n * say, read early.\n *\n * Pure: the dialog's hook hands in what it read.\n *=========================================*/\n\n/** The pages a starter publish captures, as counted before the click. */\nexport interface StarterPublishPages {\n /** Pages in the site's routing that were saved at least once and not deleted. */\n count: number\n /** Whether one of them is the Home page, at `/`. */\n hasHome: boolean\n}\n\n/** One screen document, as the dialog reads it. */\nexport interface StarterPublishScreen {\n $id: string\n versionId?: unknown\n deletedAt?: unknown\n [field: string]: unknown\n}\n\n/**\n * The pages the door will capture: every page in the host's routing map\n * (`screens`, id → slug) with a published version and not deleted, as the\n * door walks them. An email screen is never in the routing map.\n */\nexport function starterPublishPages(\n routing: unknown,\n screens: readonly StarterPublishScreen[],\n): StarterPublishPages {\n const map =\n routing && typeof routing === 'object' && !Array.isArray(routing)\n ? (routing as Record<string, unknown>)\n : {}\n const published = screens.filter(\n (screen) => screen.$id in map && !!screen.versionId && !screen.deletedAt,\n )\n return {\n count: published.length,\n hasHome: published.some((screen) => map[screen.$id] === '/'),\n }\n}\n\nexport type StarterPublishStepId = 'plan' | 'profile' | 'payouts' | 'home' | 'pages'\n\n/** One thing that stands between this site and a starter publish. */\nexport interface StarterPublishStep {\n id: StarterPublishStepId\n /** What is missing, in one sentence. */\n text: string\n /** Where it is fixed, or `null` until the address is known. */\n href: string | null\n /** The link's label: the page it opens. */\n actionLabel: string\n}\n\n/** What the dialog has read, each `undefined`/`null` while it is pending. */\nexport interface StarterPublishFacts {\n /** The owning org's plan, as `usePlanFeature(hostId, 'marketplaceSelling')` resolves it. */\n plan: { ready: boolean; locked: boolean; planLabel?: string; upgradeHref?: string }\n /**\n * `publisherProfiles/{orgId}`: `undefined` while it loads or when it cannot\n * be read, `null` when the org has none.\n */\n profile: Record<string, unknown> | null | undefined\n /** The pages it will capture, or `null` while they load. */\n pages: StarterPublishPages | null\n /** The price typed in the form, in whole dollars. */\n priceUsd: number\n /**\n * The server says this org is the platform at the staff fee waiver, so a\n * price needs no payout account (AGL-4049). Only the server can say.\n */\n sellsAsPlatform?: boolean\n /** `/{org}/marketplace`, or `null` until the org's address is known. */\n marketplacePath: string | null\n /** The site's Pages list, or `null` until its address is known. */\n pagesPath: string | null\n}\n\nconst sectionOf = (facts: StarterPublishFacts, section: string) =>\n facts.marketplacePath ? `${facts.marketplacePath}/${section}` : null\n\n/** Whether the profile read says there is one, as the door reads it: a handle. */\nconst hasProfile = (profile: Record<string, unknown>) =>\n typeof profile['handle'] === 'string' && !!profile['handle']\n\n/**\n * Whether a profile's payout account can take a sale, as far as the console\n * can tell: an account that takes charges. The door also checks which Stripe\n * world the account belongs to, which only the server knows, and says so if\n * that is what is missing.\n */\nexport const payoutsReady = (profile: Record<string, unknown>) =>\n typeof profile['stripeAccountId'] === 'string' &&\n !!String(profile['stripeAccountId']).trim() &&\n profile['stripeChargesEnabled'] === true\n\n/**\n * Every step that stands between this site and a starter publish, the setup\n * steps first, then the site's own. A read still pending refuses nothing.\n */\nexport function starterPublishSteps(facts: StarterPublishFacts): StarterPublishStep[] {\n const steps: StarterPublishStep[] = []\n if (facts.plan.ready && facts.plan.locked) {\n steps.push({\n id: 'plan',\n text: planLockReason('Publishing to the marketplace', facts.plan.planLabel),\n href: facts.plan.upgradeHref ?? null,\n actionLabel: 'See plans',\n })\n }\n const profile = facts.profile\n if (profile === null || (profile && !hasProfile(profile))) {\n steps.push({\n id: 'profile',\n text:\n 'Set up your publisher profile first: the handle and name people see on your listings.',\n href: sectionOf(facts, 'profile'),\n actionLabel: 'Publisher Profile',\n })\n } else if (profile && facts.priceUsd > 0 && !facts.sellsAsPlatform && !payoutsReady(profile)) {\n steps.push({\n id: 'payouts',\n text: 'Set up payouts first to sell a starter with a price, or publish it free.',\n href: sectionOf(facts, 'payouts'),\n actionLabel: 'Payouts',\n })\n }\n const pages = facts.pages\n if (pages && !pages.hasHome) {\n // The starter check's own words (`siteStarterRefusal`), then the fix.\n steps.push({\n id: 'home',\n text: 'A starter needs a published Home page at /. Publish the page at / under Pages.',\n href: facts.pagesPath,\n actionLabel: 'Pages',\n })\n } else if (pages && pages.count > SITE_STARTER_MAX_SCREENS) {\n steps.push({\n id: 'pages',\n text:\n `A starter can have at most ${SITE_STARTER_MAX_SCREENS} pages, ` +\n `and this site has ${pages.count}.`,\n href: facts.pagesPath,\n actionLabel: 'Pages',\n })\n }\n return steps\n}\n\n/**\n * Where a setup refusal from the door (412) is fixed. The door's words are\n * shown as they are; this picks the link beside them from what the dialog\n * read: the plan, the profile, or payouts for a priced starter. With nothing\n * read to say which, the publisher profile, where the other two are set up.\n */\nexport function starterRefusalLink(\n facts: StarterPublishFacts,\n): { href: string | null; actionLabel: string } {\n if (facts.plan.ready && facts.plan.locked) {\n return { href: facts.plan.upgradeHref ?? null, actionLabel: 'See plans' }\n }\n const profile = facts.profile\n if (profile !== null && profile && hasProfile(profile) && facts.priceUsd > 0) {\n return { href: sectionOf(facts, 'payouts'), actionLabel: 'Payouts' }\n }\n return { href: sectionOf(facts, 'profile'), actionLabel: 'Publisher Profile' }\n}\n\n/** The two theme shapes a publisher chooses between. */\nexport type StarterThemeChoice = 'builtin' | 'full'\n\n/** What a starter publish adds to the listing form's body. */\nexport interface StarterPublishBody {\n theme: SiteStarterBuiltinReading['theme'] | { kind: 'full' }\n /** Always a base starter: publishers' AI starters are not offered yet. */\n mode: 'base'\n facets: { kind: string | null }\n}\n\n/**\n * The starter half of the publish body, or `null` until it can be sent: no\n * theme chosen (neither is chosen for the publisher), or the built-in chosen\n * on a site that wears none. The full theme is the site's own, read by the\n * door off the site itself.\n */\nexport function starterPublishBody(input: {\n choice: StarterThemeChoice | null\n builtin: SiteStarterBuiltinReading | null\n kind: string | null\n}): StarterPublishBody | null {\n const facets = { kind: input.kind || null }\n if (input.choice === 'full') return { theme: { kind: 'full' }, mode: 'base', facets }\n if (input.choice === 'builtin' && input.builtin) {\n return { theme: input.builtin.theme, mode: 'base', facets }\n }\n return null\n}\n\n/** What a publish captures, in a sentence, from the pages counted before the click. */\nexport function starterCapturesSentence(pages: StarterPublishPages): string {\n return (\n (pages.count === 1\n ? 'Captures its one published page, '\n : `Captures its ${pages.count} published pages, `) +\n 'the shared header and footer they sit in, and the theme you choose below.'\n )\n}\n\n/** The fonts a pairing sets, as a person names them. */\nfunction pairingLabel(id: string | null): string | null {\n const pairing = siteFontPairing(id)\n if (!pairing) return null\n return pairing.heading.family === pairing.body.family\n ? pairing.body.family\n : `${pairing.heading.family} and ${pairing.body.family}`\n}\n\n/**\n * The line under the built-in theme choice: what the starter would carry, or\n * why this site has no built-in to name. `hostReady` is false while the site\n * is still being read.\n */\nexport function starterBuiltinDetail(\n builtin: SiteStarterBuiltinReading | null,\n hostReady = true,\n): string {\n if (!hostReady) return \"Reading this site's theme…\"\n if (!builtin) {\n return 'This site wears its own theme, not a built-in one, so a starter can carry it only as its full theme.'\n }\n const fonts = pairingLabel(builtin.theme.fonts)\n const recolored = Object.values(builtin.theme.brand).some((colors) => colors && Object.keys(colors).length)\n return (\n `${builtin.baseName}, ${recolored ? 'with the brand colors this site set over it' : 'in its own colors'}` +\n `${fonts ? `, set in ${fonts}` : ''}. A site created from it can switch to another built-in theme.`\n )\n}\n\n/** What the built-in choice leaves behind, for a site whose fonts it cannot name, or `null`. */\nexport function starterFontsNote(builtin: SiteStarterBuiltinReading | null): string | null {\n if (!builtin || builtin.fontsCarried) return null\n return (\n `This site's fonts are not a pairing a built-in theme can name, so the starter uses ` +\n `${builtin.baseName}'s own fonts. Choose the full theme to keep them.`\n )\n}\n\n/**\n * What \"Publish as a starter\" reads before the click, for a surface that\n * cannot read it (AGL-4049): the native apps ask\n * `GET /api/marketplace/starter-publish-facts`, and show these words.\n */\nexport interface StarterPublishFactsAnswer {\n /** What the publish captures, in a sentence. */\n captures: string\n /** The site's built-in theme as the publish body sends it, or `null` when it wears none. */\n builtin: SiteStarterBuiltinReading['theme'] | null\n /** The line under the built-in choice. */\n builtinDetail: string\n /** What the built-in choice leaves behind, or `null`. */\n fontsNote: string | null\n /**\n * What stands in the way, by step: a step left out is one that is done.\n * `payouts` says what a price would need, whether or not one is typed yet.\n */\n steps: Partial<Record<StarterPublishStepId, string>>\n /** Whether a step stands in the way of a free publish too, so Publish is off. */\n blocked: boolean\n}\n\n/** The answer, from what the door would read. Pure: the route hands in what it read. */\nexport function starterPublishFactsAnswer(input: {\n facts: Omit<StarterPublishFacts, 'priceUsd'>\n builtin: SiteStarterBuiltinReading | null\n}): StarterPublishFactsAnswer {\n const { facts, builtin } = input\n const free = starterPublishSteps({ ...facts, priceUsd: 0 })\n const priced = starterPublishSteps({ ...facts, priceUsd: 1 })\n return {\n captures: starterCapturesSentence(facts.pages ?? { count: 0, hasHome: false }),\n builtin: builtin?.theme ?? null,\n builtinDetail: starterBuiltinDetail(builtin),\n fontsNote: starterFontsNote(builtin),\n steps: Object.fromEntries(priced.map((step) => [step.id, step.text])),\n blocked: free.length > 0,\n }\n}\n"],"names":["planLockReason","siteFontPairing","SITE_STARTER_MAX_SCREENS","starterPublishPages","routing","screens","map","Array","isArray","published","filter","screen","$id","versionId","deletedAt","count","length","hasHome","some","sectionOf","facts","section","marketplacePath","hasProfile","profile","payoutsReady","String","trim","starterPublishSteps","steps","plan","ready","locked","push","id","text","planLabel","href","upgradeHref","actionLabel","priceUsd","sellsAsPlatform","pages","pagesPath","starterRefusalLink","starterPublishBody","input","facets","kind","choice","theme","mode","builtin","starterCapturesSentence","pairingLabel","pairing","heading","family","body","starterBuiltinDetail","hostReady","fonts","recolored","Object","values","brand","colors","keys","baseName","starterFontsNote","fontsCarried","starterPublishFactsAnswer","free","priced","captures","builtinDetail","fontsNote","fromEntries","step","blocked"],"mappings":"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"}
@@ -161,8 +161,6 @@ export declare function starterAnswersQueryFrom(params: URLSearchParams): Starte
161
161
  * the facets as a summary reads them; `null` for anything without answers.
162
162
  */
163
163
  export declare function readStarterAnswersAnswer(json: unknown): StarterAnswersAnswer | null;
164
- /** Where a purchase is opened. */
165
- export declare const STARTER_CHECKOUT_PATH = "/api/marketplace/checkout";
166
164
  /**
167
165
  * The one place other than the marketplace a purchase may send its buyer
168
166
  * back to (AGL-4049): Create site, reopened on the starter they picked. A
@@ -178,9 +176,3 @@ export type StarterCheckoutReturn = typeof STARTER_CHECKOUT_RETURN;
178
176
  export declare function readStarterCheckoutReturn(value: unknown): StarterCheckoutReturn | null | {
179
177
  error: string;
180
178
  };
181
- /** The body "Buy and continue" sends: the starter, the workspace buying it, and the way back. */
182
- export declare function starterCheckoutBody(input: {
183
- listingId: string;
184
- orgId: string;
185
- gaClientId?: string | null;
186
- }): Record<string, string>;
@@ -280,7 +280,6 @@ const idOrNull = (value)=>typeof value === 'string' && value.trim() ? value.trim
280
280
  facets
281
281
  };
282
282
  }
283
- /** Where a purchase is opened. */ export const STARTER_CHECKOUT_PATH = '/api/marketplace/checkout';
284
283
  /**
285
284
  * The one place other than the marketplace a purchase may send its buyer
286
285
  * back to (AGL-4049): Create site, reopened on the starter they picked. A
@@ -296,14 +295,5 @@ const idOrNull = (value)=>typeof value === 'string' && value.trim() ? value.trim
296
295
  error: 'Unknown return'
297
296
  };
298
297
  }
299
- /** The body "Buy and continue" sends: the starter, the workspace buying it, and the way back. */ export function starterCheckoutBody(input) {
300
- return _extends({
301
- listingId: input.listingId,
302
- orgId: input.orgId,
303
- return: STARTER_CHECKOUT_RETURN
304
- }, input.gaClientId ? {
305
- gaClientId: input.gaClientId
306
- } : {});
307
- }
308
298
 
309
299
  //# sourceMappingURL=starter-query.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/marketplace/src/lib/model/starter-query.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { PLATFORM_BRAND_NAME } from '@aglyn/aglyn/app-utils/platform-brand'\nimport { isSiteStyleId, siteStyleOf } from '@aglyn/aglyn/app-utils/site-styles'\nimport type {\n SiteStarterFacets,\n SiteStarterMode,\n} from '@aglyn/aglyn/app-utils/site-starter'\nimport type { SiteStarterSummary } from '@aglyn/aglyn/plugin-manager/plugin-site-starters'\nimport {\n SITE_STARTER_KNOWN_IDS,\n SITE_STARTER_SEARCH_MAX,\n siteStarterSearchLimit,\n} from './site-starter-listing'\n\n/**\n * The starter search as a request and an answer (AGL-4049): what \"Choose\n * your starter\" asks `GET /api/marketplace/starters`, how the route reads the\n * question back, and how a card words what it shows.\n *\n * Pure and dependency-light on purpose, so the native apps mirror it line for\n * line (phase 5's `StarterQuery`): the same parameters in the same order, the\n * same defensive read of the answer, the same \"Free\" and \"$12\".\n */\n\n/** Where the console asks. */\nexport const STARTER_SEARCH_PATH = '/api/marketplace/starters'\n\n/** The longest search a request carries; the rest is dropped, not refused. */\nexport const STARTER_SEARCH_WORD_MAX = 200\n\n/** The facets a search may narrow to by query parameter, in the order they are written. */\nexport const STARTER_FILTER_KEYS = ['kind', 'base', 'fonts'] as const\nexport type StarterFilterKey = (typeof STARTER_FILTER_KEYS)[number]\n\n/** The names the cards and the filters show for the ids a starter carries. */\nexport interface StarterSearchLabels {\n /** A built-in theme's short name (`material3`) to the name the theme picker shows. */\n base: Record<string, string>\n /** A font pairing id to its families, as a person reads them. */\n fonts: Record<string, string>\n}\n\n/** What the route answers: the starters as summaries, and the names for their ids. */\nexport interface StarterSearchAnswer {\n rows: SiteStarterSummary[]\n labels: StarterSearchLabels\n}\n\n/** One search, as the console and the native apps build it. */\nexport interface StarterQueryInput {\n /** The workspace the site will belong to. */\n orgId: string\n /** How the site will be filled: its pages as they are, or by the AI site start. */\n mode: SiteStarterMode\n /** The person's words, as typed. */\n word?: string\n /** The facets the person narrowed to; an absent one is any. */\n filter?: Partial<SiteStarterFacets>\n /** At most this many; the route caps it. */\n limit?: number\n /**\n * A listing the answer must carry whatever the search finds (AGL-4049):\n * the person's own pick, restored on the way back from buying it, which a\n * search for other words or another mode would not list.\n */\n include?: string | null\n}\n\n/** One search, as the route reads it back, every part checked. */\nexport interface StarterQuery {\n orgId: string\n mode: SiteStarterMode\n word: string\n /** `mode` is always among them, so the source asks for that mode only. */\n filter: Partial<SiteStarterFacets>\n limit: number\n /** The listing to carry whatever the search finds, or `null`. */\n include: string | null\n}\n\nconst ORG_ID = /^[A-Za-z0-9_-]{1,128}$/\nconst LISTING_ID = /^[A-Za-z0-9_-]{1,128}$/\nconst FACET_ID = /^[a-z0-9][a-z0-9-]{0,63}$/\n\nconst trimmed = (value: unknown): string =>\n typeof value === 'string' ? value.trim() : ''\n\n/**\n * The query string for one search, its parameters always in one order —\n * `orgId`, `mode`, `q`, `kind`, `base`, `fonts`, `limit`, `include` — so the\n * same search is the same URL, and a hook can key a request on it.\n */\nexport function starterQueryParams(input: StarterQueryInput): URLSearchParams {\n const params = new URLSearchParams()\n params.set('orgId', trimmed(input.orgId))\n params.set('mode', input.mode === 'ai' ? 'ai' : 'base')\n const word = trimmed(input.word).slice(0, STARTER_SEARCH_WORD_MAX)\n if (word) params.set('q', word)\n for (const key of STARTER_FILTER_KEYS) {\n const value = trimmed(input.filter?.[key])\n if (value) params.set(key, value)\n }\n params.set(\n 'limit',\n String(siteStarterSearchLimit(input.limit ?? SITE_STARTER_SEARCH_MAX) || SITE_STARTER_SEARCH_MAX),\n )\n const include = trimmed(input.include)\n if (LISTING_ID.test(include)) params.set('include', include)\n return params\n}\n\n/**\n * The route's reading of a search, or the refusal it answers with a 400.\n *\n * A base theme or a font pairing is checked against the lists a starter may\n * name (`SITE_STARTER_KNOWN_IDS`), and a kind against the styles a person\n * chooses among (core's `SITE_STYLES`, which are the kinds of site), so a\n * mistyped one is named rather than answered with nothing.\n */\nexport function starterQueryFrom(\n params: URLSearchParams,\n): StarterQuery | { error: string } {\n const orgId = trimmed(params.get('orgId'))\n if (!ORG_ID.test(orgId)) return { error: 'Missing orgId' }\n const askedMode = trimmed(params.get('mode'))\n if (askedMode && askedMode !== 'base' && askedMode !== 'ai') {\n return { error: 'Unknown mode' }\n }\n const mode: SiteStarterMode = askedMode === 'ai' ? 'ai' : 'base'\n const filter: Partial<SiteStarterFacets> = { mode }\n const kind = trimmed(params.get('kind'))\n if (kind) {\n if (!FACET_ID.test(kind) || !isSiteStyleId(kind)) return { error: 'Unknown kind of site' }\n filter.kind = kind\n }\n const base = trimmed(params.get('base'))\n if (base) {\n if (!SITE_STARTER_KNOWN_IDS.bases?.has(base)) return { error: 'Unknown base theme' }\n filter.base = base\n }\n const fonts = trimmed(params.get('fonts'))\n if (fonts) {\n if (!SITE_STARTER_KNOWN_IDS.fonts?.has(fonts)) return { error: 'Unknown font pairing' }\n filter.fonts = fonts\n }\n const include = trimmed(params.get('include'))\n if (include && !LISTING_ID.test(include)) return { error: 'Unknown starter' }\n return {\n orgId,\n mode,\n word: trimmed(params.get('q')).slice(0, STARTER_SEARCH_WORD_MAX),\n filter,\n limit: siteStarterSearchLimit(params.get('limit')) || SITE_STARTER_SEARCH_MAX,\n include: include || null,\n }\n}\n\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\n !!value && typeof value === 'object' && !Array.isArray(value)\n\nconst textOr = (value: unknown, fallback: string): string =>\n typeof value === 'string' ? value : fallback\n\nconst idOrNull = (value: unknown): string | null =>\n typeof value === 'string' && value.trim() ? value.trim() : null\n\n/**\n * One starter as a card shows it, read field by field, or `null` for a row\n * with no listing id.\n *\n * The route answers each row through this, as the console and the apps read\n * it, so what leaves the server is a summary and nothing else: a source that\n * handed back a paid AI starter's answers, or its pages, would have them\n * dropped here. Each field falls back the way a card needs it to: a missing\n * price is free, a missing picture is none, a page count is a whole number\n * from zero, a mode is `base` unless it says `ai`, and `owned` is kept only\n * where the answer gave it as a yes or a no.\n */\nexport function readStarterSummary(value: unknown): SiteStarterSummary | null {\n if (!isRecord(value)) return null\n const listingId = idOrNull(value['listingId'])\n if (!listingId) return null\n const price = Number(value['priceUsd'])\n const facets = isRecord(value['facets']) ? value['facets'] : {}\n const pages = Number(facets['pages'])\n const preview = value['previewImageUrl']\n return {\n listingId,\n displayName: textOr(value['displayName'], ''),\n description: textOr(value['description'], ''),\n authorName: textOr(value['authorName'], ''),\n official: value['official'] === true,\n priceUsd: Number.isFinite(price) && price > 0 ? price : 0,\n previewImageUrl: typeof preview === 'string' && preview ? preview : null,\n facets: {\n kind: idOrNull(facets['kind']),\n base: idOrNull(facets['base']),\n fonts: idOrNull(facets['fonts']),\n pages: Number.isFinite(pages) && pages > 0 ? Math.floor(pages) : 0,\n mode: facets['mode'] === 'ai' ? 'ai' : 'base',\n },\n category: idOrNull(value['category']),\n // Only where the answer says: absent is \"the search does not know\".\n ...(typeof value['owned'] === 'boolean' ? { owned: value['owned'] } : {}),\n }\n}\n\n/** A map of id to name, keeping only string names under non-empty ids. */\nconst namesOf = (value: unknown): Record<string, string> => {\n if (!isRecord(value)) return {}\n const names: Record<string, string> = {}\n for (const [id, name] of Object.entries(value)) {\n if (id && typeof name === 'string' && name) names[id] = name\n }\n return names\n}\n\n/**\n * The route's answer, read defensively: rows that are not starters are\n * dropped, as is a duplicate of a listing already read, and missing labels\n * are empty. Never throws, whatever it is handed.\n */\nexport function parseStarterSearchAnswer(json: unknown): StarterSearchAnswer {\n const body = isRecord(json) ? json : {}\n const seen = new Set<string>()\n const rows: SiteStarterSummary[] = []\n for (const value of Array.isArray(body['rows']) ? body['rows'] : []) {\n const row = readStarterSummary(value)\n if (!row || seen.has(row.listingId)) continue\n seen.add(row.listingId)\n rows.push(row)\n }\n const labels = isRecord(body['labels']) ? body['labels'] : {}\n return { rows, labels: { base: namesOf(labels['base']), fonts: namesOf(labels['fonts']) } }\n}\n\n/**\n * A starter's price as a card shows it: \"Free\" at zero, whole dollars as\n * \"$12\", and cents only where there are some (\"$12.50\").\n */\nexport function starterPriceLabel(usd: number): string {\n if (!Number.isFinite(usd) || usd <= 0) return 'Free'\n return Number.isInteger(usd) ? `$${usd}` : `$${usd.toFixed(2)}`\n}\n\n/** How many pages a starter creates, as a card says it, or `null` when it does not say. */\nexport function starterPagesLabel(pages: number): string | null {\n if (!Number.isFinite(pages) || pages < 1) return null\n const whole = Math.floor(pages)\n return whole === 1 ? '1 page' : `${whole} pages`\n}\n\n/** One fact a starter's card shows as a chip, keyed so a surface can style each kind its own way. */\nexport interface StarterCardFact {\n key: 'price' | 'official' | 'ai' | 'kind' | 'pages'\n label: string\n}\n\n/**\n * The chips on a starter's card, in the order they are read: the price,\n * every card carrying one (\"Free\" included, so a missing chip never reads as\n * a forgotten price); the platform's name for its own; \"AI\" for a starter the\n * AI site start fills; the kind of site in words; and its page count.\n *\n * The kind is the style's own name for the listing's kind of site (core's\n * `SITE_STYLES`), the same words the kind filters show — never the listing's\n * marketplace category, which for a starter that names no kind is a shelf\n * of the marketplace (`design`), not a kind of site.\n */\nexport function starterCardFacts(row: SiteStarterSummary): StarterCardFact[] {\n const facts: StarterCardFact[] = [{ key: 'price', label: starterPriceLabel(row.priceUsd) }]\n if (row.official) facts.push({ key: 'official', label: PLATFORM_BRAND_NAME })\n if (row.facets.mode === 'ai') facts.push({ key: 'ai', label: 'AI' })\n const kind = siteStyleOf(row.facets.kind)?.name\n if (kind) facts.push({ key: 'kind', label: kind })\n const pages = starterPagesLabel(row.facets.pages)\n if (pages) facts.push({ key: 'pages', label: pages })\n return facts\n}\n\n// ── An AI starter's answers (AGL-4049 phase 4) ─────────────────────────────\n\n/** Where the console reads the answers an AI starter carries. */\nexport const STARTER_ANSWERS_PATH = '/api/marketplace/starters/answers'\n\n/** One AI starter's answers, asked for a workspace. */\nexport interface StarterAnswersQuery {\n orgId: string\n listingId: string\n}\n\n/** What the route answers: the starter's answers, and its facets. */\nexport interface StarterAnswersAnswer {\n answers: Record<string, unknown>\n facets: SiteStarterFacets\n}\n\n/** The query string for one starter's answers, `orgId` then `listingId`. */\nexport function starterAnswersParams(input: StarterAnswersQuery): URLSearchParams {\n const params = new URLSearchParams()\n params.set('orgId', trimmed(input.orgId))\n params.set('listingId', trimmed(input.listingId))\n return params\n}\n\n/**\n * The route's reading of the question, or the refusal a 400 answers with:\n * the workspace, and a listing id as a listing may have one, since it\n * becomes a document path on the way to the listing.\n */\nexport function starterAnswersQueryFrom(params: URLSearchParams): StarterAnswersQuery | { error: string } {\n const orgId = trimmed(params.get('orgId'))\n if (!ORG_ID.test(orgId)) return { error: 'Missing orgId' }\n const listingId = trimmed(params.get('listingId'))\n if (!LISTING_ID.test(listingId)) return { error: 'Unknown starter' }\n return { orgId, listingId }\n}\n\n/**\n * The route's answer, read defensively: the answers as a plain object, and\n * the facets as a summary reads them; `null` for anything without answers.\n */\nexport function readStarterAnswersAnswer(json: unknown): StarterAnswersAnswer | null {\n if (!isRecord(json) || !isRecord(json['answers'])) return null\n const facets = readStarterSummary({ listingId: 'answers', facets: json['facets'] })?.facets\n if (!facets) return null\n return { answers: json['answers'], facets }\n}\n\n/** Where a purchase is opened. */\nexport const STARTER_CHECKOUT_PATH = '/api/marketplace/checkout'\n\n/**\n * The one place other than the marketplace a purchase may send its buyer\n * back to (AGL-4049): Create site, reopened on the starter they picked. A\n * name the checkout turns into an address itself, never an address the\n * browser hands it.\n */\nexport const STARTER_CHECKOUT_RETURN = 'create-site'\nexport type StarterCheckoutReturn = typeof STARTER_CHECKOUT_RETURN\n\n/**\n * A checkout body's `return`, read: `create-site`, `null` when it names none\n * (the marketplace, as always), or the refusal a 400 answers with.\n */\nexport function readStarterCheckoutReturn(value: unknown): StarterCheckoutReturn | null | { error: string } {\n if (value === undefined || value === null || value === '') return null\n return value === STARTER_CHECKOUT_RETURN ? STARTER_CHECKOUT_RETURN : { error: 'Unknown return' }\n}\n\n/** The body \"Buy and continue\" sends: the starter, the workspace buying it, and the way back. */\nexport function starterCheckoutBody(input: {\n listingId: string\n orgId: string\n gaClientId?: string | null\n}): Record<string, string> {\n return {\n listingId: input.listingId,\n orgId: input.orgId,\n return: STARTER_CHECKOUT_RETURN,\n ...(input.gaClientId ? { gaClientId: input.gaClientId } : {}),\n }\n}\n"],"names":["PLATFORM_BRAND_NAME","isSiteStyleId","siteStyleOf","SITE_STARTER_KNOWN_IDS","SITE_STARTER_SEARCH_MAX","siteStarterSearchLimit","STARTER_SEARCH_PATH","STARTER_SEARCH_WORD_MAX","STARTER_FILTER_KEYS","ORG_ID","LISTING_ID","FACET_ID","trimmed","value","trim","starterQueryParams","input","params","URLSearchParams","set","orgId","mode","word","slice","key","filter","String","limit","include","test","starterQueryFrom","get","error","askedMode","kind","base","bases","has","fonts","isRecord","Array","isArray","textOr","fallback","idOrNull","readStarterSummary","listingId","price","Number","facets","pages","preview","displayName","description","authorName","official","priceUsd","isFinite","previewImageUrl","Math","floor","category","owned","namesOf","names","id","name","Object","entries","parseStarterSearchAnswer","json","body","seen","Set","rows","row","add","push","labels","starterPriceLabel","usd","isInteger","toFixed","starterPagesLabel","whole","starterCardFacts","facts","label","STARTER_ANSWERS_PATH","starterAnswersParams","starterAnswersQueryFrom","readStarterAnswersAnswer","answers","STARTER_CHECKOUT_PATH","STARTER_CHECKOUT_RETURN","readStarterCheckoutReturn","undefined","starterCheckoutBody","return","gaClientId"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/marketplace/src/lib/model/starter-query.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { PLATFORM_BRAND_NAME } from '@aglyn/aglyn/app-utils/platform-brand'\nimport { isSiteStyleId, siteStyleOf } from '@aglyn/aglyn/app-utils/site-styles'\nimport type {\n SiteStarterFacets,\n SiteStarterMode,\n} from '@aglyn/aglyn/app-utils/site-starter'\nimport type { SiteStarterSummary } from '@aglyn/aglyn/plugin-manager/plugin-site-starters'\nimport {\n SITE_STARTER_KNOWN_IDS,\n SITE_STARTER_SEARCH_MAX,\n siteStarterSearchLimit,\n} from './site-starter-listing'\n\n/**\n * The starter search as a request and an answer (AGL-4049): what \"Choose\n * your starter\" asks `GET /api/marketplace/starters`, how the route reads the\n * question back, and how a card words what it shows.\n *\n * Pure and dependency-light on purpose, so the native apps mirror it line for\n * line (phase 5's `StarterQuery`): the same parameters in the same order, the\n * same defensive read of the answer, the same \"Free\" and \"$12\".\n */\n\n/** Where the console asks. */\nexport const STARTER_SEARCH_PATH = '/api/marketplace/starters'\n\n/** The longest search a request carries; the rest is dropped, not refused. */\nexport const STARTER_SEARCH_WORD_MAX = 200\n\n/** The facets a search may narrow to by query parameter, in the order they are written. */\nexport const STARTER_FILTER_KEYS = ['kind', 'base', 'fonts'] as const\nexport type StarterFilterKey = (typeof STARTER_FILTER_KEYS)[number]\n\n/** The names the cards and the filters show for the ids a starter carries. */\nexport interface StarterSearchLabels {\n /** A built-in theme's short name (`material3`) to the name the theme picker shows. */\n base: Record<string, string>\n /** A font pairing id to its families, as a person reads them. */\n fonts: Record<string, string>\n}\n\n/** What the route answers: the starters as summaries, and the names for their ids. */\nexport interface StarterSearchAnswer {\n rows: SiteStarterSummary[]\n labels: StarterSearchLabels\n}\n\n/** One search, as the console and the native apps build it. */\nexport interface StarterQueryInput {\n /** The workspace the site will belong to. */\n orgId: string\n /** How the site will be filled: its pages as they are, or by the AI site start. */\n mode: SiteStarterMode\n /** The person's words, as typed. */\n word?: string\n /** The facets the person narrowed to; an absent one is any. */\n filter?: Partial<SiteStarterFacets>\n /** At most this many; the route caps it. */\n limit?: number\n /**\n * A listing the answer must carry whatever the search finds (AGL-4049):\n * the person's own pick, restored on the way back from buying it, which a\n * search for other words or another mode would not list.\n */\n include?: string | null\n}\n\n/** One search, as the route reads it back, every part checked. */\nexport interface StarterQuery {\n orgId: string\n mode: SiteStarterMode\n word: string\n /** `mode` is always among them, so the source asks for that mode only. */\n filter: Partial<SiteStarterFacets>\n limit: number\n /** The listing to carry whatever the search finds, or `null`. */\n include: string | null\n}\n\nconst ORG_ID = /^[A-Za-z0-9_-]{1,128}$/\nconst LISTING_ID = /^[A-Za-z0-9_-]{1,128}$/\nconst FACET_ID = /^[a-z0-9][a-z0-9-]{0,63}$/\n\nconst trimmed = (value: unknown): string =>\n typeof value === 'string' ? value.trim() : ''\n\n/**\n * The query string for one search, its parameters always in one order —\n * `orgId`, `mode`, `q`, `kind`, `base`, `fonts`, `limit`, `include` — so the\n * same search is the same URL, and a hook can key a request on it.\n */\nexport function starterQueryParams(input: StarterQueryInput): URLSearchParams {\n const params = new URLSearchParams()\n params.set('orgId', trimmed(input.orgId))\n params.set('mode', input.mode === 'ai' ? 'ai' : 'base')\n const word = trimmed(input.word).slice(0, STARTER_SEARCH_WORD_MAX)\n if (word) params.set('q', word)\n for (const key of STARTER_FILTER_KEYS) {\n const value = trimmed(input.filter?.[key])\n if (value) params.set(key, value)\n }\n params.set(\n 'limit',\n String(siteStarterSearchLimit(input.limit ?? SITE_STARTER_SEARCH_MAX) || SITE_STARTER_SEARCH_MAX),\n )\n const include = trimmed(input.include)\n if (LISTING_ID.test(include)) params.set('include', include)\n return params\n}\n\n/**\n * The route's reading of a search, or the refusal it answers with a 400.\n *\n * A base theme or a font pairing is checked against the lists a starter may\n * name (`SITE_STARTER_KNOWN_IDS`), and a kind against the styles a person\n * chooses among (core's `SITE_STYLES`, which are the kinds of site), so a\n * mistyped one is named rather than answered with nothing.\n */\nexport function starterQueryFrom(\n params: URLSearchParams,\n): StarterQuery | { error: string } {\n const orgId = trimmed(params.get('orgId'))\n if (!ORG_ID.test(orgId)) return { error: 'Missing orgId' }\n const askedMode = trimmed(params.get('mode'))\n if (askedMode && askedMode !== 'base' && askedMode !== 'ai') {\n return { error: 'Unknown mode' }\n }\n const mode: SiteStarterMode = askedMode === 'ai' ? 'ai' : 'base'\n const filter: Partial<SiteStarterFacets> = { mode }\n const kind = trimmed(params.get('kind'))\n if (kind) {\n if (!FACET_ID.test(kind) || !isSiteStyleId(kind)) return { error: 'Unknown kind of site' }\n filter.kind = kind\n }\n const base = trimmed(params.get('base'))\n if (base) {\n if (!SITE_STARTER_KNOWN_IDS.bases?.has(base)) return { error: 'Unknown base theme' }\n filter.base = base\n }\n const fonts = trimmed(params.get('fonts'))\n if (fonts) {\n if (!SITE_STARTER_KNOWN_IDS.fonts?.has(fonts)) return { error: 'Unknown font pairing' }\n filter.fonts = fonts\n }\n const include = trimmed(params.get('include'))\n if (include && !LISTING_ID.test(include)) return { error: 'Unknown starter' }\n return {\n orgId,\n mode,\n word: trimmed(params.get('q')).slice(0, STARTER_SEARCH_WORD_MAX),\n filter,\n limit: siteStarterSearchLimit(params.get('limit')) || SITE_STARTER_SEARCH_MAX,\n include: include || null,\n }\n}\n\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\n !!value && typeof value === 'object' && !Array.isArray(value)\n\nconst textOr = (value: unknown, fallback: string): string =>\n typeof value === 'string' ? value : fallback\n\nconst idOrNull = (value: unknown): string | null =>\n typeof value === 'string' && value.trim() ? value.trim() : null\n\n/**\n * One starter as a card shows it, read field by field, or `null` for a row\n * with no listing id.\n *\n * The route answers each row through this, as the console and the apps read\n * it, so what leaves the server is a summary and nothing else: a source that\n * handed back a paid AI starter's answers, or its pages, would have them\n * dropped here. Each field falls back the way a card needs it to: a missing\n * price is free, a missing picture is none, a page count is a whole number\n * from zero, a mode is `base` unless it says `ai`, and `owned` is kept only\n * where the answer gave it as a yes or a no.\n */\nexport function readStarterSummary(value: unknown): SiteStarterSummary | null {\n if (!isRecord(value)) return null\n const listingId = idOrNull(value['listingId'])\n if (!listingId) return null\n const price = Number(value['priceUsd'])\n const facets = isRecord(value['facets']) ? value['facets'] : {}\n const pages = Number(facets['pages'])\n const preview = value['previewImageUrl']\n return {\n listingId,\n displayName: textOr(value['displayName'], ''),\n description: textOr(value['description'], ''),\n authorName: textOr(value['authorName'], ''),\n official: value['official'] === true,\n priceUsd: Number.isFinite(price) && price > 0 ? price : 0,\n previewImageUrl: typeof preview === 'string' && preview ? preview : null,\n facets: {\n kind: idOrNull(facets['kind']),\n base: idOrNull(facets['base']),\n fonts: idOrNull(facets['fonts']),\n pages: Number.isFinite(pages) && pages > 0 ? Math.floor(pages) : 0,\n mode: facets['mode'] === 'ai' ? 'ai' : 'base',\n },\n category: idOrNull(value['category']),\n // Only where the answer says: absent is \"the search does not know\".\n ...(typeof value['owned'] === 'boolean' ? { owned: value['owned'] } : {}),\n }\n}\n\n/** A map of id to name, keeping only string names under non-empty ids. */\nconst namesOf = (value: unknown): Record<string, string> => {\n if (!isRecord(value)) return {}\n const names: Record<string, string> = {}\n for (const [id, name] of Object.entries(value)) {\n if (id && typeof name === 'string' && name) names[id] = name\n }\n return names\n}\n\n/**\n * The route's answer, read defensively: rows that are not starters are\n * dropped, as is a duplicate of a listing already read, and missing labels\n * are empty. Never throws, whatever it is handed.\n */\nexport function parseStarterSearchAnswer(json: unknown): StarterSearchAnswer {\n const body = isRecord(json) ? json : {}\n const seen = new Set<string>()\n const rows: SiteStarterSummary[] = []\n for (const value of Array.isArray(body['rows']) ? body['rows'] : []) {\n const row = readStarterSummary(value)\n if (!row || seen.has(row.listingId)) continue\n seen.add(row.listingId)\n rows.push(row)\n }\n const labels = isRecord(body['labels']) ? body['labels'] : {}\n return { rows, labels: { base: namesOf(labels['base']), fonts: namesOf(labels['fonts']) } }\n}\n\n/**\n * A starter's price as a card shows it: \"Free\" at zero, whole dollars as\n * \"$12\", and cents only where there are some (\"$12.50\").\n */\nexport function starterPriceLabel(usd: number): string {\n if (!Number.isFinite(usd) || usd <= 0) return 'Free'\n return Number.isInteger(usd) ? `$${usd}` : `$${usd.toFixed(2)}`\n}\n\n/** How many pages a starter creates, as a card says it, or `null` when it does not say. */\nexport function starterPagesLabel(pages: number): string | null {\n if (!Number.isFinite(pages) || pages < 1) return null\n const whole = Math.floor(pages)\n return whole === 1 ? '1 page' : `${whole} pages`\n}\n\n/** One fact a starter's card shows as a chip, keyed so a surface can style each kind its own way. */\nexport interface StarterCardFact {\n key: 'price' | 'official' | 'ai' | 'kind' | 'pages'\n label: string\n}\n\n/**\n * The chips on a starter's card, in the order they are read: the price,\n * every card carrying one (\"Free\" included, so a missing chip never reads as\n * a forgotten price); the platform's name for its own; \"AI\" for a starter the\n * AI site start fills; the kind of site in words; and its page count.\n *\n * The kind is the style's own name for the listing's kind of site (core's\n * `SITE_STYLES`), the same words the kind filters show — never the listing's\n * marketplace category, which for a starter that names no kind is a shelf\n * of the marketplace (`design`), not a kind of site.\n */\nexport function starterCardFacts(row: SiteStarterSummary): StarterCardFact[] {\n const facts: StarterCardFact[] = [{ key: 'price', label: starterPriceLabel(row.priceUsd) }]\n if (row.official) facts.push({ key: 'official', label: PLATFORM_BRAND_NAME })\n if (row.facets.mode === 'ai') facts.push({ key: 'ai', label: 'AI' })\n const kind = siteStyleOf(row.facets.kind)?.name\n if (kind) facts.push({ key: 'kind', label: kind })\n const pages = starterPagesLabel(row.facets.pages)\n if (pages) facts.push({ key: 'pages', label: pages })\n return facts\n}\n\n// ── An AI starter's answers (AGL-4049 phase 4) ─────────────────────────────\n\n/** Where the console reads the answers an AI starter carries. */\nexport const STARTER_ANSWERS_PATH = '/api/marketplace/starters/answers'\n\n/** One AI starter's answers, asked for a workspace. */\nexport interface StarterAnswersQuery {\n orgId: string\n listingId: string\n}\n\n/** What the route answers: the starter's answers, and its facets. */\nexport interface StarterAnswersAnswer {\n answers: Record<string, unknown>\n facets: SiteStarterFacets\n}\n\n/** The query string for one starter's answers, `orgId` then `listingId`. */\nexport function starterAnswersParams(input: StarterAnswersQuery): URLSearchParams {\n const params = new URLSearchParams()\n params.set('orgId', trimmed(input.orgId))\n params.set('listingId', trimmed(input.listingId))\n return params\n}\n\n/**\n * The route's reading of the question, or the refusal a 400 answers with:\n * the workspace, and a listing id as a listing may have one, since it\n * becomes a document path on the way to the listing.\n */\nexport function starterAnswersQueryFrom(params: URLSearchParams): StarterAnswersQuery | { error: string } {\n const orgId = trimmed(params.get('orgId'))\n if (!ORG_ID.test(orgId)) return { error: 'Missing orgId' }\n const listingId = trimmed(params.get('listingId'))\n if (!LISTING_ID.test(listingId)) return { error: 'Unknown starter' }\n return { orgId, listingId }\n}\n\n/**\n * The route's answer, read defensively: the answers as a plain object, and\n * the facets as a summary reads them; `null` for anything without answers.\n */\nexport function readStarterAnswersAnswer(json: unknown): StarterAnswersAnswer | null {\n if (!isRecord(json) || !isRecord(json['answers'])) return null\n const facets = readStarterSummary({ listingId: 'answers', facets: json['facets'] })?.facets\n if (!facets) return null\n return { answers: json['answers'], facets }\n}\n\n/**\n * The one place other than the marketplace a purchase may send its buyer\n * back to (AGL-4049): Create site, reopened on the starter they picked. A\n * name the checkout turns into an address itself, never an address the\n * browser hands it.\n */\nexport const STARTER_CHECKOUT_RETURN = 'create-site'\nexport type StarterCheckoutReturn = typeof STARTER_CHECKOUT_RETURN\n\n/**\n * A checkout body's `return`, read: `create-site`, `null` when it names none\n * (the marketplace, as always), or the refusal a 400 answers with.\n */\nexport function readStarterCheckoutReturn(value: unknown): StarterCheckoutReturn | null | { error: string } {\n if (value === undefined || value === null || value === '') return null\n return value === STARTER_CHECKOUT_RETURN ? STARTER_CHECKOUT_RETURN : { error: 'Unknown return' }\n}\n"],"names":["PLATFORM_BRAND_NAME","isSiteStyleId","siteStyleOf","SITE_STARTER_KNOWN_IDS","SITE_STARTER_SEARCH_MAX","siteStarterSearchLimit","STARTER_SEARCH_PATH","STARTER_SEARCH_WORD_MAX","STARTER_FILTER_KEYS","ORG_ID","LISTING_ID","FACET_ID","trimmed","value","trim","starterQueryParams","input","params","URLSearchParams","set","orgId","mode","word","slice","key","filter","String","limit","include","test","starterQueryFrom","get","error","askedMode","kind","base","bases","has","fonts","isRecord","Array","isArray","textOr","fallback","idOrNull","readStarterSummary","listingId","price","Number","facets","pages","preview","displayName","description","authorName","official","priceUsd","isFinite","previewImageUrl","Math","floor","category","owned","namesOf","names","id","name","Object","entries","parseStarterSearchAnswer","json","body","seen","Set","rows","row","add","push","labels","starterPriceLabel","usd","isInteger","toFixed","starterPagesLabel","whole","starterCardFacts","facts","label","STARTER_ANSWERS_PATH","starterAnswersParams","starterAnswersQueryFrom","readStarterAnswersAnswer","answers","STARTER_CHECKOUT_RETURN","readStarterCheckoutReturn","undefined"],"mappings":"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"}
@@ -15,6 +15,12 @@
15
15
  * limitations under the License.
16
16
  */
17
17
  import type { BillingWebhookHandler } from '@aglyn/aglyn/server';
18
+ /**
19
+ * A paid marketplace invoice, recorded (AGL-4049). `amount_paid` is the
20
+ * tax-inclusive gross, as a session's `amount_total` is, and `customer_address`
21
+ * is the address Stripe computed the tax from, frozen at finalize.
22
+ */
23
+ export declare function recordMarketplaceInvoicePurchase(invoice: any): Promise<void>;
18
24
  /**
19
25
  * Marketplace-purchase section of the platform Stripe webhook (AGL-46/418):
20
26
  * keyed by session id (idempotent on Stripe redelivery) — relocated