@tollwyn/baukasten 0.0.2 → 1.2.3

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 (213) hide show
  1. package/README.md +254 -1
  2. package/dist/bin/check.d.ts +2 -0
  3. package/dist/bin/check.js +38 -0
  4. package/dist/bin/migrate.d.ts +2 -0
  5. package/dist/bin/migrate.js +111 -0
  6. package/dist/bin/stubs.d.ts +2 -0
  7. package/dist/bin/stubs.js +38 -0
  8. package/dist/bin/test-accounts.d.ts +2 -0
  9. package/dist/bin/test-accounts.js +62 -0
  10. package/dist/check.d.ts +20 -0
  11. package/dist/check.js +285 -0
  12. package/dist/index.d.ts +7 -0
  13. package/dist/index.js +9 -0
  14. package/dist/product.d.ts +161 -0
  15. package/dist/product.js +152 -0
  16. package/dist/server/accounts.d.ts +128 -0
  17. package/dist/server/accounts.js +282 -0
  18. package/dist/server/adapter/db.d.ts +5 -0
  19. package/dist/server/adapter/db.js +63 -0
  20. package/dist/server/adapter/log.d.ts +3 -0
  21. package/dist/server/adapter/log.js +12 -0
  22. package/dist/server/adapter/mailer.d.ts +4 -0
  23. package/dist/server/adapter/mailer.js +56 -0
  24. package/dist/server/adapter/migrate.d.ts +21 -0
  25. package/dist/server/adapter/migrate.js +113 -0
  26. package/dist/server/adapter/secrets.d.ts +2 -0
  27. package/dist/server/adapter/secrets.js +40 -0
  28. package/dist/server/adapter/stripe.d.ts +11 -0
  29. package/dist/server/adapter/stripe.js +333 -0
  30. package/dist/server/api.d.ts +25 -0
  31. package/dist/server/api.js +98 -0
  32. package/dist/server/baukasten-migrations.d.ts +4 -0
  33. package/dist/server/baukasten-migrations.js +126 -0
  34. package/dist/server/billing.d.ts +94 -0
  35. package/dist/server/billing.js +349 -0
  36. package/dist/server/catalog.d.ts +39 -0
  37. package/dist/server/catalog.js +193 -0
  38. package/dist/server/config.d.ts +108 -0
  39. package/dist/server/config.js +244 -0
  40. package/dist/server/endpoints.d.ts +44 -0
  41. package/dist/server/endpoints.js +159 -0
  42. package/dist/server/fields.d.ts +7 -0
  43. package/dist/server/fields.js +18 -0
  44. package/dist/server/headers.d.ts +1 -0
  45. package/dist/server/headers.js +9 -0
  46. package/dist/server/health.d.ts +34 -0
  47. package/dist/server/health.js +116 -0
  48. package/dist/server/hook.d.ts +92 -0
  49. package/dist/server/hook.js +293 -0
  50. package/dist/server/index.d.ts +18 -0
  51. package/dist/server/index.js +11 -0
  52. package/dist/server/legal/de.d.ts +2 -0
  53. package/dist/server/legal/de.js +251 -0
  54. package/dist/server/legal/document.d.ts +37 -0
  55. package/dist/server/legal/document.js +30 -0
  56. package/dist/server/legal/en.d.ts +2 -0
  57. package/dist/server/legal/en.js +251 -0
  58. package/dist/server/legal/fleet.d.ts +114 -0
  59. package/dist/server/legal/fleet.js +89 -0
  60. package/dist/server/legal/index.d.ts +3 -0
  61. package/dist/server/legal/index.js +11 -0
  62. package/dist/server/limits.d.ts +58 -0
  63. package/dist/server/limits.js +76 -0
  64. package/dist/server/log.d.ts +26 -0
  65. package/dist/server/log.js +61 -0
  66. package/dist/server/machine.d.ts +4 -0
  67. package/dist/server/machine.js +57 -0
  68. package/dist/server/mail.d.ts +20 -0
  69. package/dist/server/mail.js +29 -0
  70. package/dist/server/messages.d.ts +105 -0
  71. package/dist/server/messages.js +232 -0
  72. package/dist/server/migrations.d.ts +28 -0
  73. package/dist/server/migrations.js +59 -0
  74. package/dist/server/openapi.d.ts +24 -0
  75. package/dist/server/openapi.js +173 -0
  76. package/dist/server/pages.d.ts +158 -0
  77. package/dist/server/pages.js +491 -0
  78. package/dist/server/ports.d.ts +273 -0
  79. package/dist/server/ports.js +1 -0
  80. package/dist/server/runs.d.ts +30 -0
  81. package/dist/server/runs.js +55 -0
  82. package/dist/server/sales.d.ts +38 -0
  83. package/dist/server/sales.js +132 -0
  84. package/dist/server/services.d.ts +57 -0
  85. package/dist/server/services.js +44 -0
  86. package/dist/server/start.d.ts +14 -0
  87. package/dist/server/start.js +65 -0
  88. package/dist/server/stripe.d.ts +33 -0
  89. package/dist/server/stripe.js +29 -0
  90. package/dist/server/terms.d.ts +7 -0
  91. package/dist/server/terms.js +9 -0
  92. package/dist/server/test-accounts.d.ts +28 -0
  93. package/dist/server/test-accounts.js +62 -0
  94. package/dist/server/via.d.ts +9 -0
  95. package/dist/server/via.js +15 -0
  96. package/dist/server/web.d.ts +48 -0
  97. package/dist/server/web.js +105 -0
  98. package/dist/shopify/adapter/shopify.d.ts +8 -0
  99. package/dist/shopify/adapter/shopify.js +232 -0
  100. package/dist/shopify/admin.d.ts +33 -0
  101. package/dist/shopify/admin.js +47 -0
  102. package/dist/shopify/app.d.ts +60 -0
  103. package/dist/shopify/app.js +67 -0
  104. package/dist/shopify/config.d.ts +53 -0
  105. package/dist/shopify/config.js +133 -0
  106. package/dist/shopify/constants.d.ts +27 -0
  107. package/dist/shopify/constants.js +27 -0
  108. package/dist/shopify/headers.d.ts +1 -0
  109. package/dist/shopify/headers.js +8 -0
  110. package/dist/shopify/hmac.d.ts +1 -0
  111. package/dist/shopify/hmac.js +9 -0
  112. package/dist/shopify/hook.d.ts +60 -0
  113. package/dist/shopify/hook.js +508 -0
  114. package/dist/shopify/id-token.d.ts +12 -0
  115. package/dist/shopify/id-token.js +66 -0
  116. package/dist/shopify/index.d.ts +19 -0
  117. package/dist/shopify/index.js +15 -0
  118. package/dist/shopify/legal/de.d.ts +3 -0
  119. package/dist/shopify/legal/de.js +123 -0
  120. package/dist/shopify/legal/en.d.ts +3 -0
  121. package/dist/shopify/legal/en.js +121 -0
  122. package/dist/shopify/legal/index.d.ts +19 -0
  123. package/dist/shopify/legal/index.js +8 -0
  124. package/dist/shopify/legal/shopify.d.ts +16 -0
  125. package/dist/shopify/legal/shopify.js +21 -0
  126. package/dist/shopify/migrations.d.ts +4 -0
  127. package/dist/shopify/migrations.js +67 -0
  128. package/dist/shopify/pages.d.ts +45 -0
  129. package/dist/shopify/pages.js +146 -0
  130. package/dist/shopify/ports.d.ts +77 -0
  131. package/dist/shopify/ports.js +1 -0
  132. package/dist/shopify/sales.d.ts +4 -0
  133. package/dist/shopify/sales.js +27 -0
  134. package/dist/shopify/scopes.d.ts +2 -0
  135. package/dist/shopify/scopes.js +13 -0
  136. package/dist/shopify/seal.d.ts +12 -0
  137. package/dist/shopify/seal.js +60 -0
  138. package/dist/shopify/shop.d.ts +4 -0
  139. package/dist/shopify/shop.js +15 -0
  140. package/dist/shopify/shops.d.ts +76 -0
  141. package/dist/shopify/shops.js +256 -0
  142. package/dist/shopify/sveltekit.d.ts +19 -0
  143. package/dist/shopify/sveltekit.js +18 -0
  144. package/dist/shopify/texts.d.ts +28 -0
  145. package/dist/shopify/texts.js +49 -0
  146. package/dist/shopify/toml.d.ts +22 -0
  147. package/dist/shopify/toml.js +125 -0
  148. package/dist/shopify/webhooks.d.ts +65 -0
  149. package/dist/shopify/webhooks.js +220 -0
  150. package/dist/stubs.d.ts +15 -0
  151. package/dist/stubs.js +287 -0
  152. package/dist/sveltekit.d.ts +20 -0
  153. package/dist/sveltekit.js +18 -0
  154. package/dist/testing/app.d.ts +36 -0
  155. package/dist/testing/app.js +155 -0
  156. package/dist/testing/browser.d.ts +21 -0
  157. package/dist/testing/browser.js +21 -0
  158. package/dist/testing/connection.d.ts +7 -0
  159. package/dist/testing/connection.js +303 -0
  160. package/dist/testing/database.d.ts +21 -0
  161. package/dist/testing/database.js +54 -0
  162. package/dist/testing/env-example.d.ts +4 -0
  163. package/dist/testing/env-example.js +33 -0
  164. package/dist/testing/index.d.ts +9 -0
  165. package/dist/testing/index.js +9 -0
  166. package/dist/testing/shopify-connection.d.ts +8 -0
  167. package/dist/testing/shopify-connection.js +313 -0
  168. package/dist/testing/shopify.d.ts +71 -0
  169. package/dist/testing/shopify.js +278 -0
  170. package/dist/testing/smtp.d.ts +38 -0
  171. package/dist/testing/smtp.js +165 -0
  172. package/dist/testing/stripe.d.ts +128 -0
  173. package/dist/testing/stripe.js +511 -0
  174. package/dist/texts.d.ts +139 -0
  175. package/dist/texts.js +265 -0
  176. package/dist/ui/AccountPage.svelte +143 -0
  177. package/dist/ui/AccountPage.svelte.d.ts +10 -0
  178. package/dist/ui/ConfirmEmailPage.svelte +21 -0
  179. package/dist/ui/ConfirmEmailPage.svelte.d.ts +10 -0
  180. package/dist/ui/ErrorPage.svelte +14 -0
  181. package/dist/ui/ErrorPage.svelte.d.ts +3 -0
  182. package/dist/ui/Field.svelte +12 -0
  183. package/dist/ui/Field.svelte.d.ts +10 -0
  184. package/dist/ui/ForgotPasswordPage.svelte +32 -0
  185. package/dist/ui/ForgotPasswordPage.svelte.d.ts +7 -0
  186. package/dist/ui/Form.svelte +30 -0
  187. package/dist/ui/Form.svelte.d.ts +11 -0
  188. package/dist/ui/Frame.svelte +84 -0
  189. package/dist/ui/Frame.svelte.d.ts +12 -0
  190. package/dist/ui/LegalPage.svelte +38 -0
  191. package/dist/ui/LegalPage.svelte.d.ts +9 -0
  192. package/dist/ui/Linked.svelte +5 -0
  193. package/dist/ui/Linked.svelte.d.ts +8 -0
  194. package/dist/ui/LoginPage.svelte +41 -0
  195. package/dist/ui/LoginPage.svelte.d.ts +10 -0
  196. package/dist/ui/LogoutPage.svelte +19 -0
  197. package/dist/ui/LogoutPage.svelte.d.ts +9 -0
  198. package/dist/ui/ResetPasswordPage.svelte +30 -0
  199. package/dist/ui/ResetPasswordPage.svelte.d.ts +10 -0
  200. package/dist/ui/SignupPage.svelte +52 -0
  201. package/dist/ui/SignupPage.svelte.d.ts +10 -0
  202. package/dist/ui/VerifyEmailPage.svelte +50 -0
  203. package/dist/ui/VerifyEmailPage.svelte.d.ts +10 -0
  204. package/dist/ui/base.css +287 -0
  205. package/dist/ui/data.d.ts +167 -0
  206. package/dist/ui/data.js +1 -0
  207. package/dist/ui/frame.d.ts +17 -0
  208. package/dist/ui/frame.js +27 -0
  209. package/dist/ui/index.d.ts +15 -0
  210. package/dist/ui/index.js +15 -0
  211. package/dist/variables.d.ts +20 -0
  212. package/dist/variables.js +62 -0
  213. package/package.json +88 -4
@@ -0,0 +1,193 @@
1
+ import { WEBHOOK_PATH } from './stripe.js';
2
+ export function pricesOf(catalog) {
3
+ const currency = catalog.currency.toLowerCase();
4
+ return catalog.products.flatMap((product) => {
5
+ switch (product.kind) {
6
+ case 'subscription':
7
+ return product.prices.map((price) => ({
8
+ product: product.id,
9
+ lookupKey: price.lookupKey,
10
+ amount: price.amount,
11
+ currency,
12
+ type: 'recurring',
13
+ interval: price.interval
14
+ }));
15
+ case 'purchase':
16
+ return product.prices.map((price) => ({
17
+ product: product.id,
18
+ lookupKey: price.lookupKey,
19
+ amount: price.amount,
20
+ currency,
21
+ type: 'one_time'
22
+ }));
23
+ }
24
+ });
25
+ }
26
+ /** Whether the price at Stripe charges what the catalog says. */
27
+ export function priceFits(found, wanted) {
28
+ if (found.product !== wanted.product ||
29
+ found.amount !== wanted.amount ||
30
+ found.currency !== wanted.currency) {
31
+ return false;
32
+ }
33
+ switch (wanted.type) {
34
+ case 'one_time':
35
+ return found.type === 'one_time';
36
+ case 'recurring':
37
+ return (found.type === 'recurring' &&
38
+ found.interval === wanted.interval &&
39
+ found.intervalCount === 1);
40
+ }
41
+ }
42
+ /**
43
+ * The default configuration of the customer portal names the product and links its own pages: an
44
+ * account copied from another one brings that one's along.
45
+ */
46
+ export function portalConfiguration(catalog, shop, priceId) {
47
+ const subscriptions = catalog.products.flatMap((product) => product.kind === 'subscription' ? [product] : []);
48
+ const prices = subscriptions.reduce((count, product) => count + product.prices.length, 0);
49
+ const allowed = [
50
+ ...(prices > 1 ? ['price'] : []),
51
+ ...(subscriptions.some((product) => product.quantity !== undefined)
52
+ ? ['quantity']
53
+ : [])
54
+ ];
55
+ return {
56
+ business_profile: {
57
+ headline: shop.name,
58
+ privacy_policy_url: `${shop.origin}/privacy`,
59
+ terms_of_service_url: `${shop.origin}/terms`
60
+ },
61
+ default_return_url: `${shop.origin}/account`,
62
+ features: {
63
+ customer_update: { enabled: true, allowed_updates: ['address', 'name', 'tax_id'] },
64
+ invoice_history: { enabled: true },
65
+ payment_method_update: { enabled: true },
66
+ subscription_cancel: { enabled: true, mode: 'at_period_end', proration_behavior: 'none' },
67
+ subscription_update: allowed.length === 0
68
+ ? { enabled: false }
69
+ : {
70
+ enabled: true,
71
+ default_allowed_updates: allowed,
72
+ proration_behavior: 'create_prorations',
73
+ products: subscriptions.map((product) => ({
74
+ product: product.id,
75
+ prices: product.prices.map((price) => priceId(price.lookupKey)),
76
+ adjustable_quantity: product.quantity === undefined
77
+ ? { enabled: false }
78
+ : { enabled: true, minimum: 1, maximum: product.quantity.max }
79
+ }))
80
+ }
81
+ }
82
+ };
83
+ }
84
+ /**
85
+ * Whether `found` holds every value of `wanted`. Lists hold the same items in any order: Stripe
86
+ * need not keep the order it was given.
87
+ */
88
+ export function contains(found, wanted) {
89
+ if (Array.isArray(wanted)) {
90
+ if (!Array.isArray(found) || found.length !== wanted.length)
91
+ return false;
92
+ const taken = new Set();
93
+ return wanted.every((item) => {
94
+ const at = found.findIndex((candidate, index) => !taken.has(index) && contains(candidate, item));
95
+ taken.add(at);
96
+ return at >= 0;
97
+ });
98
+ }
99
+ if (typeof wanted === 'object' && wanted !== null) {
100
+ if (typeof found !== 'object' || found === null || Array.isArray(found))
101
+ return false;
102
+ return Object.entries(wanted).every(([key, value]) => contains(Reflect.get(found, key), value));
103
+ }
104
+ return found === wanted;
105
+ }
106
+ /** Only the Dashboard creates the default configuration of the customer portal. */
107
+ export const NO_DEFAULT_PORTAL = 'the Stripe account has no default configuration of the customer portal in this mode: save the settings of the customer portal once in the Stripe Dashboard, then migrate again';
108
+ export async function setUpCatalog(stripe, catalog, shop) {
109
+ const steps = [];
110
+ for (const product of catalog.products) {
111
+ const found = await stripe.product(product.id);
112
+ const wanted = { id: product.id, name: product.name };
113
+ if (found === null) {
114
+ await stripe.createProduct(wanted);
115
+ steps.push({ subject: 'product', id: product.id, action: 'created' });
116
+ }
117
+ else if (!found.active || found.name !== product.name) {
118
+ await stripe.updateProduct(wanted);
119
+ steps.push({ subject: 'product', id: product.id, action: 'updated' });
120
+ }
121
+ else {
122
+ steps.push({ subject: 'product', id: product.id, action: 'exists' });
123
+ }
124
+ }
125
+ const ids = new Map();
126
+ for (const wanted of pricesOf(catalog)) {
127
+ const found = await stripe.activePrice(wanted.lookupKey);
128
+ if (found !== null && priceFits(found, wanted)) {
129
+ ids.set(wanted.lookupKey, found.id);
130
+ steps.push({ subject: 'price', id: wanted.lookupKey, action: 'exists' });
131
+ }
132
+ else {
133
+ ids.set(wanted.lookupKey, await stripe.createPrice(wanted));
134
+ steps.push({
135
+ subject: 'price',
136
+ id: wanted.lookupKey,
137
+ action: found === null ? 'created' : 'updated'
138
+ });
139
+ }
140
+ }
141
+ const portal = await stripe.defaultPortal();
142
+ if (portal === null)
143
+ throw new Error(NO_DEFAULT_PORTAL);
144
+ const wanted = portalConfiguration(catalog, shop, (key) => {
145
+ const id = ids.get(key);
146
+ if (id === undefined)
147
+ throw new Error(`the price ${key} was not set up`);
148
+ return id;
149
+ });
150
+ if (contains(portal.configuration, wanted)) {
151
+ steps.push({ subject: 'portal', id: portal.id, action: 'exists' });
152
+ }
153
+ else {
154
+ await stripe.updatePortal(portal.id, wanted);
155
+ steps.push({ subject: 'portal', id: portal.id, action: 'updated' });
156
+ }
157
+ return steps;
158
+ }
159
+ /** What the migration would change at Stripe, as `product <id>`, `price <key>` or `portal`. */
160
+ export async function catalogDifferences(stripe, catalog, shop) {
161
+ const [products, prices, portal] = await Promise.all([
162
+ Promise.all(catalog.products.map(async (product) => [product, await stripe.product(product.id)])),
163
+ Promise.all(pricesOf(catalog).map(async (price) => [price, await stripe.activePrice(price.lookupKey)])),
164
+ stripe.defaultPortal()
165
+ ]);
166
+ const differences = [
167
+ ...products.flatMap(([product, found]) => found !== null && found.active && found.name === product.name ? [] : [`product ${product.id}`]),
168
+ ...prices.flatMap(([wanted, found]) => found !== null && priceFits(found, wanted) ? [] : [`price ${wanted.lookupKey}`])
169
+ ];
170
+ // With a price missing, the portal would point at a price the migration has yet to create.
171
+ if (differences.some((difference) => difference.startsWith('price ')))
172
+ return differences;
173
+ const ids = new Map(prices.flatMap(([wanted, found]) => found === null ? [] : [[wanted.lookupKey, found.id]]));
174
+ const fits = portal !== null &&
175
+ contains(portal.configuration, portalConfiguration(catalog, shop, (key) => {
176
+ const id = ids.get(key);
177
+ if (id === undefined)
178
+ throw new Error(`the price ${key} is not at Stripe`);
179
+ return id;
180
+ }));
181
+ return fits ? differences : [...differences, 'portal'];
182
+ }
183
+ /**
184
+ * The enabled webhook endpoints of the Stripe account that are not this product's: an account copied
185
+ * from another product brings that one's along, and they would get the events of this one. Named by
186
+ * id and host: the path of an endpoint often carries a secret.
187
+ */
188
+ export async function foreignWebhooks(stripe, origin) {
189
+ const own = `${origin}${WEBHOOK_PATH}`;
190
+ return (await stripe.webhookEndpoints())
191
+ .filter((endpoint) => endpoint.enabled && endpoint.url !== own)
192
+ .map(({ id, url }) => ({ id, host: URL.canParse(url) ? new URL(url).host : 'no address' }));
193
+ }
@@ -0,0 +1,108 @@
1
+ import { z } from 'zod';
2
+ import type { Language, Product, ProductEnv } from '../product.js';
3
+ import { BAUKASTEN_VARIABLES, STANDARD_VARIABLES, type StandardVariable } from '../variables.js';
4
+ export { BAUKASTEN_VARIABLES, STANDARD_VARIABLES };
5
+ export type Env = Readonly<Record<string, string | undefined>>;
6
+ export declare class ConfigError extends Error {
7
+ readonly problems: readonly string[];
8
+ constructor(problems: readonly string[]);
9
+ }
10
+ /** The relay of the fleet, plain SMTP without a login; locally without it every mail goes to the log. */
11
+ export type Mail = {
12
+ kind: 'smtp';
13
+ host: string;
14
+ port: number;
15
+ from: string;
16
+ } | {
17
+ kind: 'log';
18
+ };
19
+ /** The mode of Stripe, by the prefix of the key: test until the delivery service switches to live. */
20
+ export type StripeMode = 'test' | 'live';
21
+ export type Stripe = {
22
+ kind: 'stripe';
23
+ secretKey: string;
24
+ webhookSecret: string;
25
+ mode: StripeMode;
26
+ /** Stripe itself, or in the tests of a product its double on this machine. */
27
+ api: {
28
+ kind: 'stripe';
29
+ } | {
30
+ kind: 'double';
31
+ url: string;
32
+ };
33
+ } | {
34
+ kind: 'none';
35
+ };
36
+ /** The commercial register: no entry, or the company identification number (UID) of the entry. */
37
+ export type Register = {
38
+ kind: 'none';
39
+ } | {
40
+ kind: 'entered';
41
+ uid: string;
42
+ };
43
+ /** Who runs the product, for its legal pages. */
44
+ export interface Operator {
45
+ name: string;
46
+ address: string;
47
+ register: Register;
48
+ /**
49
+ * Not registered for VAT: with a VAT number the product does not start, since it would have to
50
+ * charge VAT through Stripe Tax, which the baukasten does not support yet.
51
+ */
52
+ vat: 'none';
53
+ /** The place of jurisdiction of the terms, in Switzerland. */
54
+ jurisdiction: string;
55
+ }
56
+ export interface Config<E extends ProductEnv = ProductEnv> {
57
+ mode: 'production' | 'local';
58
+ language: Language;
59
+ /** Every address of the product starts with it. */
60
+ publicOrigin: string;
61
+ port: number;
62
+ databaseUrl: string;
63
+ /** The name of the product everywhere: MAIL_FROM_NAME. */
64
+ name: string;
65
+ mail: Mail;
66
+ stripe: Stripe;
67
+ operator: Operator;
68
+ /** The bearer token of GET /vertrieb/zahlen. */
69
+ salesToken: string;
70
+ /** What runs without locally; empty in production. */
71
+ missing: readonly StandardVariable[];
72
+ product: z.output<z.ZodObject<E>>;
73
+ }
74
+ /** The schema of each standard variable of a web app; a Shopify app takes those it shares. */
75
+ export declare const SHAPE: {
76
+ PUBLIC_ORIGIN: z.ZodPipe<z.ZodURL, z.ZodTransform<string, string>>;
77
+ PORT: z.ZodPipe<z.ZodString, z.ZodTransform<number, string>>;
78
+ DATABASE_URL: z.ZodURL;
79
+ SMTP_HOST: z.ZodString;
80
+ SMTP_PORT: z.ZodPipe<z.ZodString, z.ZodTransform<number, string>>;
81
+ MAIL_FROM: z.ZodEmail;
82
+ MAIL_FROM_NAME: z.ZodString;
83
+ OPERATOR_NAME: z.ZodString;
84
+ OPERATOR_ADDRESS: z.ZodString;
85
+ OPERATOR_REGISTER: z.ZodUnion<readonly [z.ZodLiteral<"none">, z.ZodString]>;
86
+ OPERATOR_VAT: z.ZodLiteral<"none">;
87
+ OPERATOR_JURISDICTION: z.ZodString;
88
+ HOSTING_LOCATION: z.ZodString;
89
+ STRIPE_SECRET_KEY: z.ZodString;
90
+ STRIPE_WEBHOOK_SECRET: z.ZodString;
91
+ VERTRIEB_TOKEN: z.ZodString;
92
+ };
93
+ /** What a value must be; the problem never repeats the value, which may be a secret. */
94
+ export declare const MUST: Record<StandardVariable, string>;
95
+ export declare const DEFAULT_PORT = 3000;
96
+ type Input = Record<string, string | undefined>;
97
+ /** One problem per variable, in the order of `names`: missing, or what its value must be. */
98
+ export declare function problemsOf<N extends string>(issues: readonly z.core.$ZodIssue[], names: readonly N[], input: Input, invalid: (name: N, issue: z.core.$ZodIssue) => string): string[];
99
+ /** A variable that is empty or blank counts as missing. */
100
+ export declare function valueOf(env: Env, name: string): string | undefined;
101
+ /** The port of the server by the rule of loadConfig, which has checked it before the server listens. */
102
+ export declare function portOf(env: Env): number;
103
+ /** The database of the migration command, which needs nothing else. */
104
+ export declare function databaseUrlOf(env: Env): string;
105
+ /** The mode a key of Stripe belongs to; the configuration took only test and live keys. */
106
+ export declare function stripeModeOf(secretKey: string): StripeMode;
107
+ /** Reads the configuration; ConfigError names every problem at once. */
108
+ export declare function loadConfig<E extends ProductEnv>(env: Env, product: Product<E>): Config<E>;
@@ -0,0 +1,244 @@
1
+ // In production no variable may be missing. Locally mail and Stripe may be missing (mails go to the
2
+ // log, there is nothing to pay), and `missing` names them.
3
+ import { z } from 'zod';
4
+ import { BAUKASTEN_VARIABLES, STANDARD_VARIABLES } from '../variables.js';
5
+ export { BAUKASTEN_VARIABLES, STANDARD_VARIABLES };
6
+ export class ConfigError extends Error {
7
+ problems;
8
+ constructor(problems) {
9
+ super(`invalid configuration: ${problems.join('; ')}`);
10
+ this.problems = problems;
11
+ }
12
+ }
13
+ const tcpPort = z
14
+ .string()
15
+ .regex(/^\d{1,5}$/)
16
+ .transform(Number)
17
+ .refine((port) => port >= 1 && port <= 65_535);
18
+ const text = z.string();
19
+ const NONE = 'none';
20
+ const UID_WEIGHTS = [5, 4, 3, 2, 7, 6, 5, 4];
21
+ /** A company identification number, CHE-123.456.788: its ninth digit checks the others (modulo 11). */
22
+ function isUid(value) {
23
+ if (!/^CHE-\d{3}\.\d{3}\.\d{3}$/.test(value))
24
+ return false;
25
+ const digits = value.replace(/\D/g, '');
26
+ let sum = 0;
27
+ for (const [index, weight] of UID_WEIGHTS.entries())
28
+ sum += weight * Number(digits.charAt(index));
29
+ const check = (11 - (sum % 11)) % 11;
30
+ return check !== 10 && check === Number(digits.charAt(8));
31
+ }
32
+ /** The schema of each standard variable of a web app; a Shopify app takes those it shares. */
33
+ export const SHAPE = {
34
+ PUBLIC_ORIGIN: z.url({ protocol: /^https?$/ }).transform((value) => new URL(value).origin),
35
+ PORT: tcpPort,
36
+ DATABASE_URL: z.url({ protocol: /^postgres(?:ql)?$/ }),
37
+ SMTP_HOST: z.string().regex(/^[A-Za-z0-9.-]+$/),
38
+ SMTP_PORT: tcpPort,
39
+ MAIL_FROM: z.email(),
40
+ MAIL_FROM_NAME: text,
41
+ OPERATOR_NAME: text,
42
+ OPERATOR_ADDRESS: text,
43
+ OPERATOR_REGISTER: z.union([z.literal(NONE), z.string().refine(isUid)]),
44
+ OPERATOR_VAT: z.literal(NONE),
45
+ OPERATOR_JURISDICTION: text,
46
+ // Required like every standard variable, yet unread: the legal pages name the server of the
47
+ // fleet (legal/fleet.ts).
48
+ HOSTING_LOCATION: text,
49
+ STRIPE_SECRET_KEY: z.string().regex(/^(?:sk|rk)_(?:test|live)_[A-Za-z0-9]+$/),
50
+ STRIPE_WEBHOOK_SECRET: z.string().regex(/^whsec_[A-Za-z0-9]+$/),
51
+ VERTRIEB_TOKEN: z.string().min(32)
52
+ };
53
+ /**
54
+ * The Stripe double of the tests, which takes the key and moves no money: only on this machine, so
55
+ * that no setting sends the key anywhere else.
56
+ */
57
+ const STRIPE_API_BASE = z
58
+ .url({ protocol: /^http$/, hostname: /^(?:127\.0\.0\.1|localhost|\[::1\])$/ })
59
+ .transform((value) => new URL(value).origin);
60
+ /** What a value must be; the problem never repeats the value, which may be a secret. */
61
+ export const MUST = {
62
+ PUBLIC_ORIGIN: 'an http(s) origin',
63
+ PORT: 'a TCP port',
64
+ DATABASE_URL: 'a postgres:// or postgresql:// URL',
65
+ SMTP_HOST: 'a host name',
66
+ SMTP_PORT: 'a TCP port',
67
+ MAIL_FROM: 'an email address',
68
+ MAIL_FROM_NAME: 'a name',
69
+ OPERATOR_NAME: 'a name',
70
+ OPERATOR_ADDRESS: 'an address',
71
+ OPERATOR_REGISTER: 'none or the company identification number of the entry with its check digit, like CHE-123.456.788',
72
+ OPERATOR_VAT: 'none: with a VAT number the product would have to charge VAT through Stripe Tax, which this version of the baukasten does not support',
73
+ OPERATOR_JURISDICTION: 'a place in Switzerland',
74
+ HOSTING_LOCATION: 'a place',
75
+ STRIPE_SECRET_KEY: 'a secret or restricted key of Stripe (sk_ or rk_, then test_ or live_)',
76
+ STRIPE_WEBHOOK_SECRET: 'a signing secret of Stripe (whsec_)',
77
+ VERTRIEB_TOKEN: 'at least 32 characters'
78
+ };
79
+ export const DEFAULT_PORT = 3000;
80
+ const MAIL = ['SMTP_HOST', 'SMTP_PORT', 'MAIL_FROM'];
81
+ const STRIPE = ['STRIPE_SECRET_KEY', 'STRIPE_WEBHOOK_SECRET'];
82
+ const PRODUCTION = z.object(SHAPE);
83
+ const LOCAL = z.object({
84
+ ...SHAPE,
85
+ PORT: SHAPE.PORT.optional(),
86
+ SMTP_HOST: SHAPE.SMTP_HOST.optional(),
87
+ SMTP_PORT: SHAPE.SMTP_PORT.optional(),
88
+ MAIL_FROM: SHAPE.MAIL_FROM.optional(),
89
+ STRIPE_SECRET_KEY: SHAPE.STRIPE_SECRET_KEY.optional(),
90
+ STRIPE_WEBHOOK_SECRET: SHAPE.STRIPE_WEBHOOK_SECRET.optional()
91
+ });
92
+ /** One problem per variable, in the order of `names`: missing, or what its value must be. */
93
+ export function problemsOf(issues, names, input, invalid) {
94
+ const first = new Map();
95
+ for (const issue of issues) {
96
+ const name = names.find((n) => n === issue.path[0]);
97
+ if (name === undefined) {
98
+ throw new Error(`a configuration issue outside the variables: ${issue.message}`);
99
+ }
100
+ if (!first.has(name))
101
+ first.set(name, issue);
102
+ }
103
+ return names.flatMap((name) => {
104
+ const issue = first.get(name);
105
+ if (issue === undefined)
106
+ return [];
107
+ return [input[name] === undefined ? `${name} is missing` : invalid(name, issue)];
108
+ });
109
+ }
110
+ function read(schema, input, build) {
111
+ const result = schema.safeParse(input);
112
+ if (result.success)
113
+ return { kind: 'ok', value: build(result.data) };
114
+ return {
115
+ kind: 'problems',
116
+ problems: problemsOf(result.error.issues, STANDARD_VARIABLES, input, (name) => `${name} must be ${MUST[name]}`)
117
+ };
118
+ }
119
+ function common(data, rest) {
120
+ return {
121
+ publicOrigin: data.PUBLIC_ORIGIN,
122
+ databaseUrl: data.DATABASE_URL,
123
+ name: data.MAIL_FROM_NAME,
124
+ operator: {
125
+ name: data.OPERATOR_NAME,
126
+ address: data.OPERATOR_ADDRESS,
127
+ register: data.OPERATOR_REGISTER === NONE
128
+ ? { kind: 'none' }
129
+ : { kind: 'entered', uid: data.OPERATOR_REGISTER },
130
+ vat: data.OPERATOR_VAT,
131
+ jurisdiction: data.OPERATOR_JURISDICTION
132
+ },
133
+ salesToken: data.VERTRIEB_TOKEN,
134
+ ...rest
135
+ };
136
+ }
137
+ function production(data) {
138
+ return common(data, {
139
+ port: data.PORT,
140
+ mail: { kind: 'smtp', host: data.SMTP_HOST, port: data.SMTP_PORT, from: data.MAIL_FROM },
141
+ stripe: {
142
+ kind: 'stripe',
143
+ secretKey: data.STRIPE_SECRET_KEY,
144
+ webhookSecret: data.STRIPE_WEBHOOK_SECRET
145
+ }
146
+ });
147
+ }
148
+ function local(data) {
149
+ const { SMTP_HOST: host, SMTP_PORT: port, MAIL_FROM: from } = data;
150
+ const { STRIPE_SECRET_KEY: secretKey, STRIPE_WEBHOOK_SECRET: webhookSecret } = data;
151
+ return common(data, {
152
+ port: data.PORT ?? DEFAULT_PORT,
153
+ mail: host !== undefined && port !== undefined && from !== undefined
154
+ ? { kind: 'smtp', host, port, from }
155
+ : { kind: 'log' },
156
+ stripe: secretKey !== undefined && webhookSecret !== undefined
157
+ ? { kind: 'stripe', secretKey, webhookSecret }
158
+ : { kind: 'none' }
159
+ });
160
+ }
161
+ /** "A, B and C" */
162
+ function list(names) {
163
+ return names.length < 2 ? names.join('') : `${names.slice(0, -1).join(', ')} and ${names.at(-1)}`;
164
+ }
165
+ /** Locally a group is set in full or not at all. */
166
+ function partly(label, names, input) {
167
+ const absent = names.filter((name) => input[name] === undefined);
168
+ if (absent.length === 0 || absent.length === names.length)
169
+ return [];
170
+ return [
171
+ `${label} is set only in part: ${list(absent)} ${absent.length === 1 ? 'is' : 'are'} missing`
172
+ ];
173
+ }
174
+ /** A variable that is empty or blank counts as missing. */
175
+ export function valueOf(env, name) {
176
+ const raw = env[name]?.trim();
177
+ return raw === '' ? undefined : raw;
178
+ }
179
+ /** The port of the server by the rule of loadConfig, which has checked it before the server listens. */
180
+ export function portOf(env) {
181
+ const raw = valueOf(env, 'PORT');
182
+ if (raw === undefined)
183
+ return DEFAULT_PORT;
184
+ const port = SHAPE.PORT.safeParse(raw);
185
+ if (!port.success)
186
+ throw new ConfigError([`PORT must be ${MUST.PORT}`]);
187
+ return port.data;
188
+ }
189
+ /** The database of the migration command, which needs nothing else. */
190
+ export function databaseUrlOf(env) {
191
+ const raw = valueOf(env, 'DATABASE_URL');
192
+ if (raw === undefined)
193
+ throw new ConfigError(['DATABASE_URL is missing']);
194
+ const url = SHAPE.DATABASE_URL.safeParse(raw);
195
+ if (!url.success)
196
+ throw new ConfigError([`DATABASE_URL must be ${MUST.DATABASE_URL}`]);
197
+ return url.data;
198
+ }
199
+ /** The mode a key of Stripe belongs to; the configuration took only test and live keys. */
200
+ export function stripeModeOf(secretKey) {
201
+ return /^(?:sk|rk)_live_/.test(secretKey) ? 'live' : 'test';
202
+ }
203
+ /** Reads the configuration; ConfigError names every problem at once. */
204
+ export function loadConfig(env, product) {
205
+ const value = (name) => valueOf(env, name);
206
+ const inProduction = env.NODE_ENV === 'production';
207
+ const input = Object.fromEntries(STANDARD_VARIABLES.map((name) => [name, value(name)]));
208
+ const names = Object.keys(product.env);
209
+ const ownInput = Object.fromEntries(names.map((name) => [name, value(name)]));
210
+ const base = inProduction ? read(PRODUCTION, input, production) : read(LOCAL, input, local);
211
+ const own = z.object(product.env).safeParse(ownInput);
212
+ const apiBase = value('STRIPE_API_BASE');
213
+ const double = apiBase === undefined ? null : STRIPE_API_BASE.safeParse(apiBase);
214
+ const problems = [
215
+ ...(base.kind === 'problems' ? base.problems : []),
216
+ ...(double === null || double.success
217
+ ? []
218
+ : ['STRIPE_API_BASE must be an http origin on this machine, for the Stripe double']),
219
+ ...(own.success
220
+ ? []
221
+ : problemsOf(own.error.issues, names, ownInput, (name, issue) => `${name} is invalid: ${issue.message}`)),
222
+ ...(inProduction ? [] : [...partly('mail', MAIL, input), ...partly('Stripe', STRIPE, input)])
223
+ ];
224
+ if (base.kind === 'problems' || !own.success || problems.length > 0) {
225
+ throw new ConfigError(problems);
226
+ }
227
+ const stripe = base.value.stripe;
228
+ return {
229
+ mode: inProduction ? 'production' : 'local',
230
+ language: product.language,
231
+ ...base.value,
232
+ stripe: stripe.kind === 'none'
233
+ ? stripe
234
+ : {
235
+ ...stripe,
236
+ mode: stripeModeOf(stripe.secretKey),
237
+ api: double === null || !double.success
238
+ ? { kind: 'stripe' }
239
+ : { kind: 'double', url: double.data }
240
+ },
241
+ missing: inProduction ? [] : [...MAIL, ...STRIPE].filter((name) => input[name] === undefined),
242
+ product: own.data
243
+ };
244
+ }
@@ -0,0 +1,44 @@
1
+ import type { RequestEvent } from '@sveltejs/kit';
2
+ import { z } from 'zod';
3
+ import { type Account } from './accounts.js';
4
+ import type { Messages } from './messages.js';
5
+ import { type Services } from './services.js';
6
+ export declare const API_PATHS: {
7
+ readonly signup: "/api/v1/signup";
8
+ readonly account: "/api/v1/account";
9
+ readonly checkout: "/api/v1/billing/checkout";
10
+ readonly portal: "/api/v1/billing/portal";
11
+ };
12
+ type ApiPath = (typeof API_PATHS)[keyof typeof API_PATHS];
13
+ export declare const API_METHODS: Record<ApiPath, string>;
14
+ export declare function accountJson(account: Account): {
15
+ id: string;
16
+ email: string;
17
+ email_verified: boolean;
18
+ created_at: string;
19
+ };
20
+ /** The account of the bearer key, counted against the limit of the key. */
21
+ export declare function apiAccount(s: Services, event: {
22
+ request: Request;
23
+ }): Promise<{
24
+ account: Account;
25
+ keyId: string;
26
+ }>;
27
+ /** The body of the sign-up, every problem in the language of the product. */
28
+ export declare function signupBody(m: Messages, terms: string): z.ZodObject<{
29
+ email: z.ZodPipe<z.ZodString, z.ZodEmail>;
30
+ accept_terms: z.ZodLiteral<true>;
31
+ acting_for_business: z.ZodLiteral<true>;
32
+ via: z.ZodOptional<z.ZodString>;
33
+ }, z.core.$strip>;
34
+ /**
35
+ * The body of a Checkout over the API: a price of the catalog by its lookup key, and a quantity,
36
+ * which the price decides on (checkout below).
37
+ */
38
+ export declare function checkoutBody(m: Messages): z.ZodObject<{
39
+ price: z.ZodString;
40
+ quantity: z.ZodOptional<z.ZodUnknown>;
41
+ }, z.core.$strip>;
42
+ /** The answer of a path of the baukasten's API, or null for any other path. */
43
+ export declare function serveApi(s: Services, event: RequestEvent): Promise<Response> | null;
44
+ export {};