@tollwyn/baukasten 0.0.2 → 1.2.2

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
package/dist/check.js ADDED
@@ -0,0 +1,285 @@
1
+ import { z } from 'zod';
2
+ import { KEY_RIGHTS, WEBHOOK_EVENTS, WEBHOOK_PATH } from './server/stripe.js';
3
+ import { APP_TOML, appTomlProblems } from './shopify/toml.js';
4
+ import { SHOPIFY_STUBS, STUBS } from './stubs.js';
5
+ const ANGABE_PATH = 'deploy/auslieferung.json';
6
+ const STUBS_COMMAND = 'npx --no-install baukasten-stubs';
7
+ const ANGABE = z.looseObject({
8
+ version: z.literal(2, 'must be 2, the version that names the art of the product'),
9
+ art: z.literal('webapp', 'must be webapp'),
10
+ stripe: z.looseObject({
11
+ webhook: z.discriminatedUnion('art', [
12
+ z.looseObject({ art: z.literal('keiner') }),
13
+ z.looseObject({
14
+ art: z.literal('endpunkt'),
15
+ pfad: z.string(),
16
+ ereignisse: z.array(z.string())
17
+ })
18
+ ], 'must be { art: endpunkt, pfad, ereignisse }'),
19
+ rechte: z.array(z.looseObject({ ressource: z.string(), zugriff: z.enum(['lesen', 'schreiben']) }))
20
+ })
21
+ });
22
+ /** The migration command of a Shopify app in its delivery manifest. */
23
+ export const SHOPIFY_MIGRATION = [
24
+ 'node',
25
+ 'node_modules/@tollwyn/baukasten/dist/bin/migrate.js',
26
+ 'shopify'
27
+ ];
28
+ const SHOPIFY_ANGABE = z.looseObject({
29
+ version: z.literal(2, 'must be 2, the version that names the art of the product'),
30
+ art: z.literal('shopify'),
31
+ subdomain: z.string('must name the subdomain of the app'),
32
+ migration: z.array(z.string(), 'must give the migration command of the baukasten')
33
+ });
34
+ /** Words the review of the App Store refuses in an address of an app. */
35
+ const REFUSED_IN_ADDRESS = ['shopify', 'example'];
36
+ const ART = z.looseObject({ art: z.literal('shopify') });
37
+ const PACKAGE = z.looseObject({ overrides: z.record(z.string(), z.unknown()).optional() });
38
+ const PEER = z.looseObject({ version: z.string() });
39
+ /**
40
+ * What the product builds with, checked like a peer: adapter-node 6 no longer reads ORIGIN, which the
41
+ * server of the baukasten sets from PUBLIC_ORIGIN. It is no peer, since only the build needs it, and
42
+ * npm keeps a peer of a dependency in the image.
43
+ */
44
+ const BUILT_WITH = { '@sveltejs/adapter-node': '^5.0.0' };
45
+ /**
46
+ * A reference to an account of a web app or a shop of a Shopify app: the one way a migration of the
47
+ * product names the schema baukasten.
48
+ */
49
+ const REFERENCE = {
50
+ webapp: /\bREFERENCES\s+"?baukasten"?\s*\.\s*"?accounts"?(?![\w$])/gi,
51
+ shopify: /\bREFERENCES\s+"?baukasten"?\s*\.\s*"?shops"?(?![\w$])/gi
52
+ };
53
+ const REFERENCED = {
54
+ webapp: 'baukasten.accounts',
55
+ shopify: 'baukasten.shops'
56
+ };
57
+ const SQL_COMMENTS = /--[^\n]*|\/\*[\s\S]*?\*\//g;
58
+ function json(text) {
59
+ try {
60
+ return { kind: 'json', value: JSON.parse(text) };
61
+ }
62
+ catch {
63
+ return { kind: 'broken' };
64
+ }
65
+ }
66
+ /** The art the manifest names: a Shopify app, or else a web app, whose manifest the check reads. */
67
+ function artOf(files) {
68
+ const text = files.read(ANGABE_PATH);
69
+ const parsed = text === undefined ? null : json(text);
70
+ return parsed?.kind === 'json' && ART.safeParse(parsed.value).success ? 'shopify' : 'webapp';
71
+ }
72
+ const STUBS_OF = {
73
+ webapp: { stubs: STUBS, command: STUBS_COMMAND },
74
+ shopify: { stubs: SHOPIFY_STUBS, command: `${STUBS_COMMAND} shopify` }
75
+ };
76
+ function stubProblems(files, version, art) {
77
+ const { stubs, command } = STUBS_OF[art];
78
+ return Object.entries(stubs).flatMap(([path, content]) => {
79
+ const there = files.read(path);
80
+ if (there === undefined)
81
+ return [`${path} is missing: run ${command}`];
82
+ if (there !== content) {
83
+ return [`${path} is not the stub of version ${version}: run ${command}`];
84
+ }
85
+ return [];
86
+ });
87
+ }
88
+ function shopifyProblems(files) {
89
+ const text = files.read(ANGABE_PATH);
90
+ const parsed = text === undefined ? null : json(text);
91
+ if (parsed?.kind !== 'json')
92
+ throw new Error(`the art was read from ${ANGABE_PATH}, which is gone`);
93
+ const angabe = SHOPIFY_ANGABE.safeParse(parsed.value);
94
+ if (!angabe.success) {
95
+ return angabe.error.issues.map((issue) => `${ANGABE_PATH}: ${issue.path.join('.')} ${issue.message}`);
96
+ }
97
+ const { subdomain, migration } = angabe.data;
98
+ const problems = [];
99
+ if ('stripe' in angabe.data) {
100
+ problems.push(`${ANGABE_PATH}: a Shopify app has no stripe; Shopify bills it`);
101
+ }
102
+ if (JSON.stringify(migration) !== JSON.stringify(SHOPIFY_MIGRATION)) {
103
+ problems.push(`${ANGABE_PATH}: migration must be ${JSON.stringify(SHOPIFY_MIGRATION)}`);
104
+ }
105
+ for (const word of REFUSED_IN_ADDRESS.filter((w) => subdomain.includes(w))) {
106
+ problems.push(`${ANGABE_PATH}: the subdomain names ${word}, which the review of the App Store refuses in the address of an app`);
107
+ }
108
+ const toml = files.read(APP_TOML);
109
+ if (toml === undefined) {
110
+ problems.push(`${APP_TOML} is missing: ${STUBS_OF.shopify.command} writes a starting point`);
111
+ }
112
+ else {
113
+ problems.push(...appTomlProblems(toml, subdomain));
114
+ }
115
+ if (files.names('extensions').length > 0) {
116
+ problems.push('extensions/ holds extensions of Shopify, which this version does not serve');
117
+ }
118
+ return problems;
119
+ }
120
+ function angabeProblems(files) {
121
+ const text = files.read(ANGABE_PATH);
122
+ if (text === undefined)
123
+ return [`${ANGABE_PATH} is missing`];
124
+ const parsed = json(text);
125
+ if (parsed.kind === 'broken')
126
+ return [`${ANGABE_PATH} is no JSON`];
127
+ const angabe = ANGABE.safeParse(parsed.value);
128
+ if (!angabe.success) {
129
+ return angabe.error.issues.map((issue) => `${ANGABE_PATH}: ${issue.path.join('.')} ${issue.message}`);
130
+ }
131
+ const { webhook, rechte } = angabe.data.stripe;
132
+ const problems = [];
133
+ if (webhook.art === 'keiner') {
134
+ problems.push(`${ANGABE_PATH}: stripe.webhook is keiner; the baukasten needs its webhook at ${WEBHOOK_PATH}`);
135
+ }
136
+ else {
137
+ if (webhook.pfad !== WEBHOOK_PATH) {
138
+ problems.push(`${ANGABE_PATH}: stripe.webhook.pfad must be ${WEBHOOK_PATH}`);
139
+ }
140
+ const events = WEBHOOK_EVENTS;
141
+ for (const event of WEBHOOK_EVENTS.filter((e) => !webhook.ereignisse.includes(e))) {
142
+ problems.push(`${ANGABE_PATH}: stripe.webhook.ereignisse lacks ${event}`);
143
+ }
144
+ for (const event of webhook.ereignisse.filter((e) => !events.includes(e))) {
145
+ problems.push(`${ANGABE_PATH}: stripe.webhook.ereignisse names ${event}, which the baukasten does not apply`);
146
+ }
147
+ }
148
+ const rights = KEY_RIGHTS;
149
+ for (const right of rights) {
150
+ // Writing includes reading.
151
+ const granted = rechte.some((r) => r.ressource === right.ressource &&
152
+ (r.zugriff === 'schreiben' || r.zugriff === right.zugriff));
153
+ if (!granted) {
154
+ problems.push(`${ANGABE_PATH}: stripe.rechte lacks ${right.ressource} ${right.zugriff}, a right of the baukasten`);
155
+ }
156
+ }
157
+ return problems;
158
+ }
159
+ const numbers = (match) => [
160
+ Number(match[1]),
161
+ Number(match[2]),
162
+ Number(match[3])
163
+ ];
164
+ /** Whether `version` lies in the caret range `range` from 1.0.0 on; a pre-release lies in none. */
165
+ export function fits(version, range) {
166
+ const wanted = /^\^(\d+)\.(\d+)\.(\d+)$/.exec(range);
167
+ if (wanted === null || numbers(wanted)[0] === 0) {
168
+ throw new Error(`the peer range ${range} is no caret range from 1.0.0 on, as the check reads`);
169
+ }
170
+ const given = /^(\d+)\.(\d+)\.(\d+)$/.exec(version);
171
+ if (given === null)
172
+ return false;
173
+ const [major, minor, patch] = numbers(given);
174
+ const [least, leastMinor, leastPatch] = numbers(wanted);
175
+ return major === least && (minor > leastMinor || (minor === leastMinor && patch >= leastPatch));
176
+ }
177
+ function versionProblems(files, baukasten) {
178
+ const wanted = { ...baukasten.peerDependencies, ...BUILT_WITH };
179
+ return Object.entries(wanted).flatMap(([name, range]) => {
180
+ const manifest = files.read(`node_modules/${name}/package.json`);
181
+ if (manifest === undefined)
182
+ return [`${name} is not installed; the baukasten needs ${range}`];
183
+ const parsed = json(manifest);
184
+ const peer = parsed.kind === 'json' ? PEER.safeParse(parsed.value) : null;
185
+ if (peer === null || !peer.success) {
186
+ return [`node_modules/${name}/package.json names no version`];
187
+ }
188
+ const { version } = peer.data;
189
+ return fits(version, range)
190
+ ? []
191
+ : [`${name} ${version} is installed; the baukasten needs ${range}`];
192
+ });
193
+ }
194
+ /** The name of a package in a key of `overrides`, like `pg`, `pg@^8` or `@scope/name@1`. */
195
+ function packageName(key) {
196
+ const at = key.indexOf('@', 1);
197
+ return at < 0 ? key : key.slice(0, at);
198
+ }
199
+ /** Every package `overrides` names, at any depth; `.` stands for its parent. */
200
+ function overridden(overrides) {
201
+ if (typeof overrides !== 'object' || overrides === null)
202
+ return [];
203
+ return Object.entries(overrides).flatMap(([key, value]) => [
204
+ ...(key === '.' ? [] : [packageName(key)]),
205
+ ...overridden(value)
206
+ ]);
207
+ }
208
+ function packageProblems(files, baukasten) {
209
+ const text = files.read('package.json');
210
+ if (text === undefined)
211
+ return ['package.json is missing'];
212
+ const parsed = json(text);
213
+ const manifest = parsed.kind === 'json' ? PACKAGE.safeParse(parsed.value) : null;
214
+ if (manifest === null || !manifest.success) {
215
+ return ['package.json is no JSON object, or its overrides are no object'];
216
+ }
217
+ const own = new Set([
218
+ baukasten.name,
219
+ ...Object.keys(baukasten.dependencies),
220
+ ...Object.keys(baukasten.peerDependencies)
221
+ ]);
222
+ const names = [...new Set(overridden(manifest.data.overrides))].filter((name) => own.has(name));
223
+ return names.map((name) => `package.json overrides ${name}, which the baukasten brings itself; remove it`);
224
+ }
225
+ function migrationProblems(files, art) {
226
+ // Like the migration: every entry read, a folder failing loudly, then the .sql files.
227
+ return files
228
+ .names('migrations')
229
+ .map((name) => {
230
+ const path = `migrations/${name}`;
231
+ const sql = files.read(path);
232
+ if (sql === undefined)
233
+ throw new Error(`${path} was listed, yet is gone`);
234
+ return { name, path, sql };
235
+ })
236
+ .filter(({ name }) => name.endsWith('.sql'))
237
+ .flatMap(({ path, sql }) => {
238
+ const rest = sql.replace(SQL_COMMENTS, '').replace(REFERENCE[art], '');
239
+ return /\bbaukasten\b/i.test(rest)
240
+ ? [
241
+ `${path} names the schema baukasten beyond a reference to ${REFERENCED[art]}: the product never changes the tables of the baukasten`
242
+ ]
243
+ : [];
244
+ });
245
+ }
246
+ /** Read from the text of vite.config.ts only; the handle guards the policy at run time. */
247
+ const OPTIONS = {
248
+ webapp: {
249
+ name: 'SVELTEKIT_OPTIONS',
250
+ passes: /\bsveltekit\(\s*\{[\s\S]*?\.\.\.SVELTEKIT_OPTIONS\b/,
251
+ imports: /import\s*\{[^}]*\bSVELTEKIT_OPTIONS\b[^}]*\}\s*from\s*['"]@tollwyn\/baukasten['"]/
252
+ },
253
+ shopify: {
254
+ name: 'SHOPIFY_SVELTEKIT_OPTIONS',
255
+ passes: /\bsveltekit\(\s*\{[\s\S]*?\.\.\.SHOPIFY_SVELTEKIT_OPTIONS\b/,
256
+ imports: /import\s*\{[^}]*\bSHOPIFY_SVELTEKIT_OPTIONS\b[^}]*\}\s*from\s*['"]@tollwyn\/baukasten['"]/
257
+ }
258
+ };
259
+ function configurationProblems(files, art) {
260
+ const problems = [];
261
+ if (files.read('baukasten.config.ts') === undefined) {
262
+ problems.push(`baukasten.config.ts is missing: the configuration of the product (${STUBS_OF[art].command} writes a starting point)`);
263
+ }
264
+ const vite = files.read('vite.config.ts');
265
+ const options = OPTIONS[art];
266
+ if (vite === undefined) {
267
+ problems.push('vite.config.ts is missing');
268
+ }
269
+ else if (!options.imports.test(vite) || !options.passes.test(vite)) {
270
+ problems.push(`vite.config.ts does not pass ${options.name} of @tollwyn/baukasten to sveltekit(): sveltekit({ ...${options.name}, adapter: adapter() })`);
271
+ }
272
+ return problems;
273
+ }
274
+ /** What keeps the product from fitting the installed baukasten; empty when it fits. */
275
+ export function productProblems(files, baukasten) {
276
+ const art = artOf(files);
277
+ return [
278
+ ...stubProblems(files, baukasten.version, art),
279
+ ...(art === 'shopify' ? shopifyProblems(files) : angabeProblems(files)),
280
+ ...versionProblems(files, baukasten),
281
+ ...packageProblems(files, baukasten),
282
+ ...migrationProblems(files, art),
283
+ ...configurationProblems(files, art)
284
+ ];
285
+ }
@@ -0,0 +1,7 @@
1
+ export { z } from 'zod';
2
+ export { defineProduct, LANGUAGES, type Brand, type Catalog, type CatalogProduct, type Language, type NavigationLink, type Product, type ProductEnv, type ProductLegal, type PurchasePrice, type SubscriptionPrice } from './product.js';
3
+ export { defineShopifyApp, type ShopifyApp, type ShopifyPlan, type ShopifyPrivacy } from './shopify/app.js';
4
+ export { SHOPIFY_SVELTEKIT_OPTIONS } from './shopify/sveltekit.js';
5
+ export { SVELTEKIT_OPTIONS } from './sveltekit.js';
6
+ export { TEXTS, type Linked, type Texts } from './texts.js';
7
+ export { BAUKASTEN_VARIABLES, SHOPIFY_STANDARD_VARIABLES, SHOPIFY_VARIABLES, STANDARD_VARIABLES, WEBAPP_VARIABLES, type ShopifyStandardVariable, type StandardVariable } from './variables.js';
package/dist/index.js ADDED
@@ -0,0 +1,9 @@
1
+ // The entry a product may import anywhere, in the browser too: nothing here touches the server. The
2
+ // schema of the product's variables takes the Zod of the baukasten from here.
3
+ export { z } from 'zod';
4
+ export { defineProduct, LANGUAGES } from './product.js';
5
+ export { defineShopifyApp } from './shopify/app.js';
6
+ export { SHOPIFY_SVELTEKIT_OPTIONS } from './shopify/sveltekit.js';
7
+ export { SVELTEKIT_OPTIONS } from './sveltekit.js';
8
+ export { TEXTS } from './texts.js';
9
+ export { BAUKASTEN_VARIABLES, SHOPIFY_STANDARD_VARIABLES, SHOPIFY_VARIABLES, STANDARD_VARIABLES, WEBAPP_VARIABLES } from './variables.js';
@@ -0,0 +1,161 @@
1
+ import { z } from 'zod';
2
+ export declare const LANGUAGES: readonly ["en", "de"];
3
+ export type Language = (typeof LANGUAGES)[number];
4
+ /** The schema of each variable of the product, by its name; it gets the trimmed text or undefined. */
5
+ export type ProductEnv = Record<string, z.ZodType>;
6
+ /** A link in the head of every page, to a path of the product. */
7
+ export interface NavigationLink {
8
+ href: string;
9
+ label: string;
10
+ }
11
+ /** The grounds on which a product may process personal data, as the privacy policy names them. */
12
+ export declare const LEGAL_BASES: readonly ["contract", "legitimateInterest"];
13
+ export type LegalBasis = (typeof LEGAL_BASES)[number];
14
+ /**
15
+ * What covers personal data that go to a recipient: a country with adequate protection (only
16
+ * Switzerland, the EEA and the countries in Annex 1 of the Swiss Data Protection Ordinance, DSV), the
17
+ * recipient's certification under the Data Privacy Framework, or the EU standard contractual clauses.
18
+ */
19
+ export declare const TRANSFERS: readonly ["adequate", "dpf", "scc"];
20
+ export type Transfer = (typeof TRANSFERS)[number];
21
+ /** Whether a recipient processes the data for the operator, or as a controller of its own. */
22
+ export declare const ROLES: readonly ["processor", "controller"];
23
+ export type Role = (typeof ROLES)[number];
24
+ /**
25
+ * What the legal pages say of the product, in its language, and true. The sentences are whole, each
26
+ * ending with a full stop; the parts of a sentence have none at their end.
27
+ */
28
+ export interface ProductLegal {
29
+ /** What the product does for its customers, in whole sentences: the terms, «The service». */
30
+ service: string;
31
+ /** What it does not guarantee, besides being always available, in whole sentences. */
32
+ notGuaranteed: string;
33
+ /** What customers may do with it and what not, in whole sentences: the terms, «Use». */
34
+ use: string;
35
+ /** The personal data it keeps itself, besides the accounts, payments and logs of the baukasten. */
36
+ data: readonly {
37
+ /** A short name, like «Notes». */
38
+ name: string;
39
+ /** What they are, like «the texts you add, with the time you added them». */
40
+ what: string;
41
+ /** Where they come from, when not from the customer, like «the official gazette». */
42
+ source?: string;
43
+ /**
44
+ * Why, like «to keep them for you and show them to you and your API keys». On legitimate
45
+ * interest it names the interest.
46
+ */
47
+ purpose: string;
48
+ basis: LegalBasis;
49
+ /** How long, like «until you delete your account; then they are deleted at once». */
50
+ kept: string;
51
+ }[];
52
+ /** Who gets personal data from it besides the providers of the fleet. */
53
+ recipients: readonly {
54
+ /** Like «the official gazettes portal of the Swiss Confederation». */
55
+ name: string;
56
+ /** The country, like «Switzerland». */
57
+ place: string;
58
+ transfer: Transfer;
59
+ /** A processor is one of the sub-processors the terms have the customer approve. */
60
+ role: Role;
61
+ /** What it gets and why, like «gets the UIDs on your list, without who watches them, …». */
62
+ purpose: string;
63
+ }[];
64
+ /**
65
+ * Whether the operator processes personal data on behalf of the customers, as their processor:
66
+ * then the terms carry the clauses for it. `data` ends «For personal data …», like «in your
67
+ * notes»; `subjects` ends «which concern …», like «the people you write about»; `retrieve` ends
68
+ * «you can read or export them through …», like «the notes page and the API».
69
+ */
70
+ processor: {
71
+ kind: 'no';
72
+ } | {
73
+ kind: 'yes';
74
+ data: string;
75
+ subjects: string;
76
+ retrieve: string;
77
+ };
78
+ }
79
+ /** A price of a subscription: an amount in the smallest unit of the currency, per month or year. */
80
+ export interface SubscriptionPrice {
81
+ /** The price at Stripe has this lookup key; a new amount becomes a new price under the same key. */
82
+ lookupKey: string;
83
+ amount: number;
84
+ interval: 'month' | 'year';
85
+ }
86
+ /** A price paid once, in the smallest unit of the currency. */
87
+ export interface PurchasePrice {
88
+ lookupKey: string;
89
+ amount: number;
90
+ }
91
+ /** A product at Stripe, under its fixed id in every mode: sold as a subscription, or once. */
92
+ export type CatalogProduct = {
93
+ kind: 'subscription';
94
+ id: string;
95
+ name: string;
96
+ prices: readonly SubscriptionPrice[];
97
+ /** A subscription of this product counts a quantity from 1 to `max`, like seats. */
98
+ quantity?: {
99
+ max: number;
100
+ };
101
+ } | {
102
+ kind: 'purchase';
103
+ id: string;
104
+ name: string;
105
+ prices: readonly PurchasePrice[];
106
+ };
107
+ /** What the product sells at Stripe; the migration sets it up there. */
108
+ export interface Catalog {
109
+ /** The currency of every price, like CHF; the sales numbers count revenue in it alone. */
110
+ currency: string;
111
+ products: readonly CatalogProduct[];
112
+ }
113
+ export interface Product<E extends ProductEnv = ProductEnv> {
114
+ /** Every page, mail, error message and legal text of the baukasten comes in this language. */
115
+ language: Language;
116
+ env: E;
117
+ /** The navigation of the product in the head of every page, in its language. */
118
+ navigation: readonly NavigationLink[];
119
+ accounts: {
120
+ /** Where sign-up, login and a new password lead: a path of the product. */
121
+ home: string;
122
+ /** API keys start with it and an underscore, like `mu_…`: 2 to 8 small letters. */
123
+ keyPrefix: string;
124
+ /** The heading and the lead of the sign-up page, in the language of the product. */
125
+ signup: {
126
+ heading: string;
127
+ lead: string;
128
+ };
129
+ };
130
+ /**
131
+ * The day the product's part of its legal texts (`legal`) last changed, as YYYY-MM-DD. The terms
132
+ * an account accepts are named by the later of this day and that of the baukasten's part.
133
+ */
134
+ termsDate: string;
135
+ legal: ProductLegal;
136
+ catalog: Catalog;
137
+ /** The marks of the product; without them the frame shows its name as text. */
138
+ brand?: Brand;
139
+ }
140
+ /**
141
+ * The marks of a product, files of its own under `static/`, named by their path on its origin. The
142
+ * frame shows the logo in the head of every page, with the name of the product as its text, and every
143
+ * page names the icon and the picture of a shared link.
144
+ */
145
+ export interface Brand {
146
+ /** Sign and name as one SVG, with its size in pixels; the head shows it at most 40 pixels high. */
147
+ logo: {
148
+ src: string;
149
+ width: number;
150
+ height: number;
151
+ };
152
+ /** The icon of the tab: an SVG or a PNG. */
153
+ icon: string;
154
+ /** The picture of a shared link (`og:image`): a PNG of 1200 × 630 pixels. */
155
+ image: string;
156
+ /** The theme on `<html data-theme>`, as Skeleton names its themes; without it, none. */
157
+ theme?: string;
158
+ }
159
+ export declare function isProduct(value: unknown): value is Product;
160
+ /** Declares the configuration of the product; anything wrong with it stops the product loudly. */
161
+ export declare function defineProduct<const E extends ProductEnv>(product: Product<E>): Product<E>;
@@ -0,0 +1,152 @@
1
+ import { z } from 'zod';
2
+ import { BAUKASTEN_VARIABLES } from './variables.js';
3
+ export const LANGUAGES = ['en', 'de'];
4
+ /** The grounds on which a product may process personal data, as the privacy policy names them. */
5
+ export const LEGAL_BASES = ['contract', 'legitimateInterest'];
6
+ /**
7
+ * What covers personal data that go to a recipient: a country with adequate protection (only
8
+ * Switzerland, the EEA and the countries in Annex 1 of the Swiss Data Protection Ordinance, DSV), the
9
+ * recipient's certification under the Data Privacy Framework, or the EU standard contractual clauses.
10
+ */
11
+ export const TRANSFERS = ['adequate', 'dpf', 'scc'];
12
+ /** Whether a recipient processes the data for the operator, or as a controller of its own. */
13
+ export const ROLES = ['processor', 'controller'];
14
+ const NAME = /^[A-Z][A-Z0-9_]*$/;
15
+ const OWN = new Set(BAUKASTEN_VARIABLES);
16
+ const ownPath = z
17
+ .string()
18
+ .regex(/^\/(?!\/)[^\s\\]*$/, 'must be a path of the product, like /notes');
19
+ const text = z
20
+ .string('must not be empty')
21
+ .trim()
22
+ .min(1, { error: 'must not be empty', abort: true });
23
+ const sentences = text.regex(/[.!?]$/, 'must be whole sentences, ending with a full stop');
24
+ const part = text.regex(/[^.!?;:,]$/, 'must be part of a sentence, without a full stop at the end');
25
+ const LEGAL = z.object({
26
+ service: sentences,
27
+ notGuaranteed: sentences,
28
+ use: sentences,
29
+ data: z.array(z.object({
30
+ name: part,
31
+ what: part,
32
+ source: part.optional(),
33
+ purpose: part,
34
+ basis: z.enum(LEGAL_BASES, 'must be contract or legitimateInterest'),
35
+ kept: part
36
+ })),
37
+ recipients: z.array(z.object({
38
+ name: part,
39
+ place: part,
40
+ transfer: z.enum(TRANSFERS, 'must be adequate, dpf or scc'),
41
+ role: z.enum(ROLES, 'must be processor or controller'),
42
+ purpose: part
43
+ })),
44
+ processor: z.discriminatedUnion('kind', [
45
+ z.object({ kind: z.literal('no') }),
46
+ z.object({ kind: z.literal('yes'), data: part, subjects: part, retrieve: part })
47
+ ], 'must be { kind: no } or { kind: yes, data, subjects, retrieve }')
48
+ }, 'must be given');
49
+ const staticFile = (...endings) => z
50
+ .string()
51
+ .regex(new RegExp(`^/[a-z0-9][a-z0-9._/-]*\\.(${endings.join('|')})$`), `must be the path of a file under static/ ending in .${endings.join(' or .')}, like /name.${endings[0]}`)
52
+ .refine((path) => !path.includes('..') && !path.includes('//'), 'must not leave static/');
53
+ const pixels = z.int('must be a whole number of pixels').positive('must be more than nothing');
54
+ const BRAND = z.object({
55
+ logo: z.object({ src: staticFile('svg'), width: pixels, height: pixels }),
56
+ icon: staticFile('svg', 'png'),
57
+ image: staticFile('png'),
58
+ theme: z
59
+ .string()
60
+ .regex(/^[a-z][a-z0-9-]*$/, 'must be the name of a theme, like cerberus')
61
+ .optional()
62
+ });
63
+ /** An id or a lookup key at Stripe, the same in every mode. */
64
+ const stripeKey = z
65
+ .string()
66
+ .regex(/^[a-z][a-z0-9_]{2,63}$/, 'must be 3 to 64 small letters, digits and _, first a letter');
67
+ const amount = z
68
+ .int('must be a whole amount in the smallest unit of the currency')
69
+ .positive('must be more than nothing');
70
+ const CATALOG = z
71
+ .object({
72
+ currency: z.string().regex(/^[A-Z]{3}$/, 'must be a currency code like CHF'),
73
+ products: z
74
+ .array(z.discriminatedUnion('kind', [
75
+ z.object({
76
+ kind: z.literal('subscription'),
77
+ id: stripeKey,
78
+ name: text,
79
+ prices: z
80
+ .array(z.object({
81
+ lookupKey: stripeKey,
82
+ amount,
83
+ interval: z.enum(['month', 'year'], 'must be month or year')
84
+ }))
85
+ .min(1, 'must name a price'),
86
+ quantity: z
87
+ .object({ max: z.int().min(2, 'must be 2 or more').max(10_000, 'is too large') })
88
+ .optional()
89
+ }),
90
+ z.object({
91
+ kind: z.literal('purchase'),
92
+ id: stripeKey,
93
+ name: text,
94
+ prices: z
95
+ .array(z.object({ lookupKey: stripeKey, amount }))
96
+ .min(1, 'must name a price')
97
+ })
98
+ ], 'must be a subscription or a purchase'))
99
+ .min(1, 'must name a product')
100
+ })
101
+ .superRefine((catalog, ctx) => {
102
+ const twice = (names) => names.filter((name, i) => names.indexOf(name) !== i);
103
+ for (const id of twice(catalog.products.map((p) => p.id))) {
104
+ ctx.addIssue({ code: 'custom', path: ['products'], message: `names the id ${id} twice` });
105
+ }
106
+ const keys = catalog.products.flatMap((p) => p.prices.map((price) => price.lookupKey));
107
+ for (const key of twice(keys)) {
108
+ ctx.addIssue({
109
+ code: 'custom',
110
+ path: ['products'],
111
+ message: `names the lookup key ${key} twice`
112
+ });
113
+ }
114
+ });
115
+ const SETTINGS = z.object({
116
+ language: z.enum(LANGUAGES, 'must be en or de'),
117
+ navigation: z.array(z.object({ href: ownPath, label: text })),
118
+ accounts: z.object({
119
+ home: ownPath,
120
+ keyPrefix: z.string().regex(/^[a-z]{2,8}$/, 'must be 2 to 8 small letters'),
121
+ signup: z.object({ heading: text, lead: text })
122
+ }),
123
+ termsDate: z.iso.date('must be a day like 2026-10-08'),
124
+ legal: LEGAL,
125
+ catalog: CATALOG,
126
+ brand: BRAND.optional()
127
+ });
128
+ /** What defineProduct accepted; the migration command finds the product by it. */
129
+ const DEFINED = new WeakSet();
130
+ export function isProduct(value) {
131
+ return typeof value === 'object' && value !== null && DEFINED.has(value);
132
+ }
133
+ /** Declares the configuration of the product; anything wrong with it stops the product loudly. */
134
+ export function defineProduct(product) {
135
+ const names = Object.keys(product.env);
136
+ const problems = [];
137
+ const taken = names.filter((name) => OWN.has(name));
138
+ if (taken.length > 0) {
139
+ problems.push(`variables ${taken.join(', ')} ${taken.length === 1 ? 'is' : 'are'} read by the baukasten itself`);
140
+ }
141
+ const malformed = names.filter((name) => !NAME.test(name));
142
+ if (malformed.length > 0)
143
+ problems.push(`variables ${malformed.join(', ')} must look like NAME_IN_CAPITALS`);
144
+ const settings = SETTINGS.safeParse(product);
145
+ if (!settings.success) {
146
+ problems.push(...settings.error.issues.map((issue) => `${issue.path.join('.')} ${issue.message}`));
147
+ }
148
+ if (problems.length > 0)
149
+ throw new Error(`the product: ${problems.join('; ')}`);
150
+ DEFINED.add(product);
151
+ return product;
152
+ }