@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,293 @@
1
+ import { createAccounts } from './accounts.js';
2
+ import { createDb } from './adapter/db.js';
3
+ import { exitAfterLog, log } from './adapter/log.js';
4
+ import { createMailer } from './adapter/mailer.js';
5
+ import { migrationSources, migrationStatus } from './adapter/migrate.js';
6
+ import { secrets } from './adapter/secrets.js';
7
+ import { createStripe } from './adapter/stripe.js';
8
+ import { asApiError, readBody, respond } from './api.js';
9
+ import { createBilling, noBilling } from './billing.js';
10
+ import { ConfigError, loadConfig } from './config.js';
11
+ import { apiAccount, serveApi } from './endpoints.js';
12
+ import { CHECK_TIMEOUT_MS, databaseCheck, deepHealth, mailCheck, stripeCheck } from './health.js';
13
+ import { SECURITY_HEADERS } from './headers.js';
14
+ import { legalPages } from './legal/index.js';
15
+ import { createLimits } from './limits.js';
16
+ import { serveMachine } from './machine.js';
17
+ import { MESSAGES } from './messages.js';
18
+ import { mergeOpenApi, openApiPart } from './openapi.js';
19
+ import { createPages } from './pages.js';
20
+ import { BAUKASTEN_TERMS_DATE, termsVersion } from './terms.js';
21
+ import { clearSessionCookie, clientAddress, isCrossSite, isMachinePath, SESSION_COOKIE } from './web.js';
22
+ const HEALTH = '/health';
23
+ const DEEP = '/health/deep';
24
+ /**
25
+ * The product's src/app.html writes `<html lang="%baukasten.lang%" %baukasten.theme%>`; the handle
26
+ * fills in its language and, with a theme in its marks, `data-theme="…"`, else nothing.
27
+ */
28
+ const LANG = '%baukasten.lang%';
29
+ const THEME = '%baukasten.theme%';
30
+ /** The attribute of the theme on `<html>`: before the first paint, so no page flashes another. */
31
+ function themeAttribute(theme) {
32
+ return theme === undefined ? '' : `data-theme="${theme}"`;
33
+ }
34
+ /** The queries of the database check end before the check does, and give their connection back. */
35
+ const PROBE_TIMEOUT_MS = CHECK_TIMEOUT_MS - 1_000;
36
+ function json(body, status) {
37
+ return new Response(JSON.stringify(body), {
38
+ status,
39
+ headers: { 'content-type': 'application/json', 'cache-control': 'no-store' }
40
+ });
41
+ }
42
+ export function baukasten(product, hooks = {}) {
43
+ const own = hooks.checks ?? {};
44
+ if ('database' in own || 'mail' in own || 'stripe' in own) {
45
+ throw new Error('the checks database, mail and stripe belong to the baukasten');
46
+ }
47
+ let state = { kind: 'waiting' };
48
+ function running() {
49
+ switch (state.kind) {
50
+ case 'running':
51
+ return state;
52
+ case 'building':
53
+ throw new Error('the baukasten is used in the build: its pages read the configuration of the running product and cannot be prerendered');
54
+ case 'waiting':
55
+ throw new Error('the baukasten is used before init ran');
56
+ }
57
+ }
58
+ async function stop(context) {
59
+ try {
60
+ await hooks.stop?.(context);
61
+ }
62
+ catch (error) {
63
+ process.exitCode = 1;
64
+ log.error('the stop hook of the product failed', { error });
65
+ }
66
+ await context.db.close();
67
+ log.info('stopped');
68
+ }
69
+ async function start() {
70
+ let config;
71
+ try {
72
+ config = loadConfig(process.env, product);
73
+ }
74
+ catch (error) {
75
+ if (!(error instanceof ConfigError))
76
+ throw error;
77
+ log.error('configuration incomplete, not starting', { problems: error.problems });
78
+ return exitAfterLog(1);
79
+ }
80
+ if (config.missing.length > 0) {
81
+ log.warn('running without what production needs', { missing: config.missing });
82
+ }
83
+ const db = createDb(config.databaseUrl, log);
84
+ const mail = createMailer(config.mail, config.name, log);
85
+ const limits = createLimits({ now: () => Date.now() });
86
+ const context = { config, db, log, mail, limits };
87
+ const version = termsVersion(BAUKASTEN_TERMS_DATE, product.termsDate);
88
+ const messages = MESSAGES[config.language];
89
+ const shop = { name: config.name, origin: config.publicOrigin };
90
+ const paid = config.stripe.kind === 'stripe'
91
+ ? { stripe: createStripe(config.stripe), mode: config.stripe.mode }
92
+ : null;
93
+ const billing = paid === null
94
+ ? noBilling(db)
95
+ : createBilling({
96
+ db,
97
+ stripe: paid.stripe,
98
+ catalog: product.catalog,
99
+ shop,
100
+ mode: paid.mode,
101
+ hooks: {
102
+ // A product without plans or purchases of its own has nothing to derive.
103
+ subscriptionChanged: hooks.subscriptionChanged ?? (() => Promise.resolve()),
104
+ purchasePaid: hooks.purchasePaid ?? (() => Promise.resolve())
105
+ },
106
+ log,
107
+ now: () => new Date(),
108
+ termsMessage: (url) => messages.stripe.terms(config.name, url)
109
+ });
110
+ const services = {
111
+ config,
112
+ product,
113
+ db,
114
+ log,
115
+ mail,
116
+ limits,
117
+ accounts: createAccounts(db, secrets),
118
+ billing,
119
+ messages,
120
+ termsVersion: version,
121
+ legal: legalPages({
122
+ language: config.language,
123
+ name: config.name,
124
+ origin: config.publicOrigin,
125
+ operator: config.operator,
126
+ legal: product.legal,
127
+ version
128
+ }),
129
+ // A product that keeps nothing of its own per account has nothing to delete.
130
+ deleteHook: hooks.deleteAccount ?? (() => Promise.resolve()),
131
+ // A product without numbers of its own gives the two of the baukasten.
132
+ salesNumbers: hooks.salesNumbers ?? (() => Promise.resolve({})),
133
+ address: (event) => clientAddress(event.request.headers, config.mode, () => event.getClientAddress()),
134
+ now: () => new Date()
135
+ };
136
+ const sources = migrationSources(process.cwd(), 'webapp');
137
+ const probe = db.withTimeout(PROBE_TIMEOUT_MS);
138
+ const checks = Object.fromEntries(Object.entries(own).map(([name, check]) => [name, () => check(context)]));
139
+ // Without Stripe, locally, there is nothing to check.
140
+ const stripeHealth = paid === null
141
+ ? () => Promise.resolve()
142
+ : stripeCheck(paid.stripe, product.catalog, shop, log);
143
+ const health = deepHealth({
144
+ checks: {
145
+ database: databaseCheck(probe, () => migrationStatus(probe, sources), log),
146
+ mail: mailCheck(mail, log),
147
+ stripe: stripeHealth,
148
+ ...checks
149
+ },
150
+ now: () => Date.now(),
151
+ log
152
+ });
153
+ state = { kind: 'running', context, services, health };
154
+ log.info('starting', {
155
+ mode: config.mode,
156
+ origin: config.publicOrigin,
157
+ language: config.language
158
+ });
159
+ await hooks.start?.(context);
160
+ process.once('sveltekit:shutdown', () => void stop(context));
161
+ }
162
+ const init = (building) => () => {
163
+ if (!building)
164
+ return start();
165
+ state = { kind: 'building' };
166
+ };
167
+ async function health(event) {
168
+ if (event.request.method !== 'GET' && event.request.method !== 'HEAD') {
169
+ return new Response(null, { status: 405, headers: { allow: 'GET, HEAD' } });
170
+ }
171
+ // Plain health answers without the database: Neon may sleep, the process still lives.
172
+ if (event.url.pathname === HEALTH)
173
+ return json({ status: 'ok' }, 200);
174
+ const report = await running().health();
175
+ return json(report, report.status === 'ok' ? 200 : 503);
176
+ }
177
+ async function respondTo(event, resolve) {
178
+ const path = event.url.pathname;
179
+ if (path === HEALTH || path === DEEP)
180
+ return health(event);
181
+ const s = running().services;
182
+ event.locals.account = null;
183
+ if (isCrossSite(event.request, s.config.publicOrigin)) {
184
+ log.warn('request refused: from another origin', { path });
185
+ return path.startsWith('/api/')
186
+ ? Response.json({ error: { code: 'forbidden', message: s.messages.api.forbidden } }, { status: 403 })
187
+ : new Response(s.messages.crossSite, {
188
+ status: 403,
189
+ headers: { 'content-type': 'text/plain; charset=utf-8' }
190
+ });
191
+ }
192
+ if (!isMachinePath(path)) {
193
+ const token = event.cookies.get(SESSION_COOKIE);
194
+ if (token !== undefined) {
195
+ event.locals.account = await s.accounts.forSession(token);
196
+ if (event.locals.account === null)
197
+ clearSessionCookie(event.cookies);
198
+ }
199
+ }
200
+ const machine = serveMachine(s, event);
201
+ if (machine !== null)
202
+ return machine;
203
+ const api = serveApi(s, event);
204
+ if (api !== null)
205
+ return api;
206
+ const response = await resolve(event, {
207
+ transformPageChunk: ({ html }) => html.replace(LANG, s.config.language).replace(THEME, themeAttribute(s.product.brand?.theme))
208
+ });
209
+ return path.startsWith('/api/') ? asApiError(response, s.messages) : withPolicy(response, path);
210
+ }
211
+ /** Whether a page went out without its content security policy; said once in the log. */
212
+ let policyMissing = false;
213
+ /**
214
+ * A page without its content security policy does not go out: the policy comes from
215
+ * SVELTEKIT_OPTIONS in the configuration of the product, and without it every page is a 500 that
216
+ * the delivery service sees, not a product that runs without the policy unnoticed.
217
+ */
218
+ function withPolicy(response, path) {
219
+ const page = response.headers.get('content-type')?.startsWith('text/html') === true;
220
+ if (!page || response.status >= 500 || response.headers.has('content-security-policy')) {
221
+ return response;
222
+ }
223
+ if (!policyMissing) {
224
+ policyMissing = true;
225
+ log.error('a page without its content security policy: SVELTEKIT_OPTIONS of the baukasten is missing in sveltekit() of vite.config.ts, or an endpoint of the product answers HTML of its own; every such answer is a 500', { path });
226
+ }
227
+ return new Response('Internal Error', {
228
+ status: 500,
229
+ headers: { 'content-type': 'text/plain; charset=utf-8' }
230
+ });
231
+ }
232
+ const handle = async ({ event, resolve }) => {
233
+ // Prerendering in the build is no request of anyone.
234
+ if (state.kind === 'building')
235
+ return resolve(event);
236
+ const started = performance.now();
237
+ let status = 500;
238
+ try {
239
+ const response = await respondTo(event, resolve);
240
+ for (const [name, value] of Object.entries(SECURITY_HEADERS)) {
241
+ response.headers.set(name, value);
242
+ }
243
+ status = response.status;
244
+ return response;
245
+ }
246
+ finally {
247
+ // The container polls /health; it is logged only at debug. The path goes without its query,
248
+ // which may carry a token.
249
+ (event.url.pathname === HEALTH ? log.debug : log.info)('request', {
250
+ method: event.request.method,
251
+ path: event.url.pathname,
252
+ status,
253
+ ms: Math.round(performance.now() - started)
254
+ });
255
+ }
256
+ };
257
+ const handleError = ({ error, event, status, message }) => {
258
+ // An unknown path is already in the request log.
259
+ if (status === 404)
260
+ return { message };
261
+ (status >= 500 ? log.error : log.warn)('request failed', {
262
+ method: event.request.method,
263
+ path: event.url.pathname,
264
+ status,
265
+ error
266
+ });
267
+ return { message: status >= 500 ? 'Internal Error' : message };
268
+ };
269
+ return {
270
+ init,
271
+ handle,
272
+ handleError,
273
+ context: () => running().context,
274
+ pages: createPages(() => running().services),
275
+ clientAddress: (event) => running().services.address(event),
276
+ api: {
277
+ respond: (handler) => {
278
+ const s = running().services;
279
+ return respond(s.log, s.messages, handler);
280
+ },
281
+ body: (request, schema) => readBody(request, schema, running().services.messages),
282
+ account: async (event) => (await apiAccount(running().services, event)).account
283
+ },
284
+ openApi: (document) => {
285
+ const s = running().services;
286
+ return mergeOpenApi(document, openApiPart(s.config.publicOrigin, s.messages));
287
+ },
288
+ billing: {
289
+ checkout: (account, order) => running().services.billing.checkout(account, order),
290
+ portal: (account, back) => running().services.billing.portal(account, back)
291
+ }
292
+ };
293
+ }
@@ -0,0 +1,18 @@
1
+ export type { Account, ApiKey } from './accounts.js';
2
+ export { log } from './adapter/log.js';
3
+ export { ApiError } from './api.js';
4
+ export { StripeFailed, type CheckoutPage, type Order, type Purchase, type Subscription, type SubscriptionChange } from './billing.js';
5
+ export type { Config, Mail as MailConfig, Operator, Register, Stripe } from './config.js';
6
+ export { CheckFailed } from './health.js';
7
+ export { baukasten, type Context, type Hooks, type Kit } from './hook.js';
8
+ export { LIMITS, type Limits, type Rule } from './limits.js';
9
+ export type { Log } from './log.js';
10
+ export { MailFailed } from './mail.js';
11
+ export type { OpenApiDocument } from './openapi.js';
12
+ export type { Pages } from './pages.js';
13
+ export type { Db, Mail, Mailer, Queryable, SubscriptionStatus } from './ports.js';
14
+ export { periodic, type Run, type RunOptions } from './runs.js';
15
+ export type { ProductNumbers } from './sales.js';
16
+ export { start, type NodeHandler } from './start.js';
17
+ export { KEY_RIGHTS, WEBHOOK_EVENTS, WEBHOOK_PATH } from './stripe.js';
18
+ export { requireAccount, tooMany } from './web.js';
@@ -0,0 +1,11 @@
1
+ export { log } from './adapter/log.js';
2
+ export { ApiError } from './api.js';
3
+ export { StripeFailed } from './billing.js';
4
+ export { CheckFailed } from './health.js';
5
+ export { baukasten } from './hook.js';
6
+ export { LIMITS } from './limits.js';
7
+ export { MailFailed } from './mail.js';
8
+ export { periodic } from './runs.js';
9
+ export { start } from './start.js';
10
+ export { KEY_RIGHTS, WEBHOOK_EVENTS, WEBHOOK_PATH } from './stripe.js';
11
+ export { requireAccount, tooMany } from './web.js';
@@ -0,0 +1,2 @@
1
+ import { type LegalTexts } from './document.js';
2
+ export declare const de: LegalTexts;
@@ -0,0 +1,251 @@
1
+ import { SESSION_DAYS } from '../accounts.js';
2
+ import { code, heading, lettered, link, list, longDate, mail, note, numbered, paragraph, strong } from './document.js';
3
+ import { FLEET, FLEET_PROCESSORS } from './fleet.js';
4
+ const { cloudflare, hoster, neon, stripe, webServer } = FLEET;
5
+ const BASIS = {
6
+ contract: 'Erfüllung des Vertrags, Art. 6 Abs. 1 lit. b DSGVO',
7
+ legitimateInterest: 'berechtigtes Interesse, Art. 6 Abs. 1 lit. f DSGVO'
8
+ };
9
+ const TRANSFER = {
10
+ adequate: 'Land mit angemessenem Schutz',
11
+ dpf: 'unter dem Data Privacy Framework zertifiziert',
12
+ scc: 'mit den Standardvertragsklauseln der EU, vom EDÖB anerkannt'
13
+ };
14
+ function register(entry) {
15
+ switch (entry.kind) {
16
+ case 'none':
17
+ return 'Einzelunternehmen, nicht im Handelsregister eingetragen.';
18
+ case 'entered':
19
+ return `Handelsregister: ${entry.uid}.`;
20
+ }
21
+ }
22
+ const VAT = {
23
+ none: {
24
+ imprint: 'Nicht im MWST-Register eingetragen; es gibt keine MWST-Nummer.',
25
+ terms: 'Wir sind nicht im MWST-Register eingetragen und verrechnen keine MWST; Steuern, die in Ihrem Land anfallen, rechnen Sie selbst ab.'
26
+ }
27
+ };
28
+ const BROWSER = {
29
+ networkErrorLogging: `${cloudflare.name} lässt Ihren Browser ausserdem eine Anweisung speichern, gescheiterte Verbindungen zu melden (Network Error Logging). Löschen Sie Cookies und Websitedaten im Browser, fällt auch diese Anweisung weg.`
30
+ };
31
+ const EDGE = {
32
+ worldwideUnderScc: 'im Rechenzentrum, das Ihnen am nächsten liegt und ausserhalb der Schweiz und der EU stehen kann; dort gelten die Standardvertragsklauseln seines Auftragsbearbeitungsvertrags'
33
+ };
34
+ const ACCESS_LOG = {
35
+ none: 'Unser Webserver führt kein Zugriffsprotokoll'
36
+ };
37
+ const ERROR_LOG = {
38
+ requestWithoutAddress: 'nur eine Anfrage, die er nicht an die Anwendung weitergeben kann, schreibt er in sein Fehlerprotokoll, ohne Ihre IP-Adresse und ohne Parameter und Kopfzeilen'
39
+ };
40
+ const NEW_VERSION = {
41
+ removed: 'es fällt ausserdem bei jedem Start einer neuen Version weg',
42
+ stays: 'beim Start einer neuen Version bleibt es'
43
+ };
44
+ const STRIPE_ROLE = {
45
+ controller: 'Stripe bearbeitet Daten auch als eigener Verantwortlicher, etwa um Betrug zu verhindern und seine gesetzlichen Pflichten zu erfüllen; siehe '
46
+ };
47
+ /** The section of the terms on processing on behalf, which the privacy policy points to. */
48
+ const ON_BEHALF = 'Daten, die wir in Ihrem Auftrag bearbeiten';
49
+ const who = (f) => `${f.operator.name}, ${f.operator.address}`;
50
+ const version = (f) => note(`Stand: ${longDate(f.version, 'de')}.`);
51
+ const from = (data) => data.source === undefined ? '' : `, aus ${data.source}`;
52
+ export const de = {
53
+ imprint: (f) => ({
54
+ title: 'Impressum',
55
+ blocks: [
56
+ paragraph(`${f.name} wird betrieben von`),
57
+ paragraph(strong(f.operator.name)),
58
+ paragraph(f.operator.address),
59
+ paragraph(`${register(f.operator.register)} ${VAT[f.operator.vat].imprint}`),
60
+ paragraph('E-Mail: ', mail(FLEET.mailbox), '. E-Mail ist der einzige Supportkanal; eine Telefon-Hotline gibt es nicht.'),
61
+ paragraph('Für den Inhalt dieser Website ist der oben genannte Betreiber verantwortlich.')
62
+ ]
63
+ }),
64
+ privacy: (f) => {
65
+ const own = f.legal.data;
66
+ const processor = f.legal.processor;
67
+ return {
68
+ title: 'Datenschutzerklärung',
69
+ blocks: [
70
+ version(f),
71
+ paragraph(`Diese Erklärung beschreibt, welche Personendaten ${f.name} bearbeitet, wozu, wo und wie lange. Verantwortlich ist ${who(f)}, erreichbar unter `, mail(FLEET.mailbox), '. Sie folgt dem Bundesgesetz über den Datenschutz (DSG) und, wo sie anwendbar ist, der Datenschutz-Grundverordnung der EU (DSGVO).'),
72
+ heading('Was wir bearbeiten, wozu und gestützt worauf'),
73
+ list([
74
+ strong('Konto'),
75
+ `: Ihre E-Mail-Adresse, ein gesalzener Hash Ihres Passworts, wann Sie sich registriert und welche Fassung der Bedingungen Sie angenommen haben, wann Sie Ihre Adresse bestätigt haben, die Links, die wir Ihnen per E-Mail schicken (mit einer neuen Adresse, die Sie verlangen), Ihre Anmeldesitzungen und Hashes Ihrer API-Schlüssel – um Sie anzumelden, die Anfragen Ihrer API-Schlüssel zu beantworten, Ihnen E-Mails zum Konto wie die Bestätigung Ihrer Adresse und das Zurücksetzen des Passworts zu schicken und zu wissen, welche Bedingungen gelten (${BASIS.contract}). Ohne E-Mail-Adresse können Sie ${f.name} nicht nutzen.`
76
+ ], ...own.map((data) => [
77
+ strong(data.name),
78
+ `: ${data.what}${from(data)} – ${data.purpose} (${BASIS[data.basis]}).`
79
+ ]), [
80
+ strong('Zahlung'),
81
+ `: Stripe bearbeitet Ihre E-Mail-Adresse, Zahlungsangaben und Rechnungsadresse sowie eine Steuernummer, wenn Sie eine angeben; wir behalten Ihre Kunden- und Abonummer bei Stripe, den Stand Ihres Abos sowie Beträge und Zeitpunkte Ihrer Zahlungen (${BASIS.contract}; für die Aufbewahrung danach unser berechtigtes Interesse, das Schweizer Buchführungsrecht einzuhalten, Art. 6 Abs. 1 lit. f DSGVO).`
82
+ ], [
83
+ strong('Partnerlinks'),
84
+ ': Kommen Sie über einen Link mit ',
85
+ code('?via=…'),
86
+ ` zu ${f.name}, tragen die Links der Seite diesen Partnernamen weiter, und bei der Registrierung speichern wir ihn mit Ihrem Konto; Software, die sich über die API registriert, kann einen im Feld `,
87
+ code('via'),
88
+ ` mitschicken. Gespeichert wird nur der Partnername, und wir nutzen ihn, um zu wissen, welche Partner Kunden bringen (${BASIS.legitimateInterest}). Dafür setzen wir kein Cookie und speichern nichts in Ihrem Browser.`
89
+ ], [
90
+ strong('Sicherheit und Betrieb'),
91
+ `: Unsere Anwendung protokolliert jede Anfrage (Methode, Pfad ohne Parameter, Status, Dauer) und Ereignisse des Dienstes mit internen Nummern wie der Ihres Kontos, ohne IP-Adressen, E-Mail-Adressen, Passwörter oder Schlüssel. ${ACCESS_LOG[webServer.accessLog]}; ${ERROR_LOG[webServer.errorLog.content]}. Unser Mail-Relay protokolliert für jede E-Mail, die wir senden, die Empfängeradresse und den Zustellstatus und bewahrt beides höchstens ${FLEET.relayLogDays} Tage auf. Um Registrierungen, Anmeldungen und andere Versuche zu begrenzen, zählen wir sie im Arbeitsspeicher der Anwendung, nie auf der Festplatte: je IP-Adresse, je Konto, je API-Schlüssel und je E-Mail-Adresse, diese nur als Hash; ein Zähler gilt höchstens eine Stunde und ist spätestens beim Neustart der Anwendung vergessen (berechtigtes Interesse an einem sicheren und funktionierenden Dienst, Art. 6 Abs. 1 lit. f DSGVO).`
92
+ ], [
93
+ strong('E-Mails an uns'),
94
+ ': Schreiben Sie uns, bewahren wir Ihre Nachricht und Adresse auf, um Ihnen zu antworten und den Austausch zu belegen (berechtigtes Interesse; bei Kunden Erfüllung des Vertrags).'
95
+ ]),
96
+ ...(processor.kind === 'yes'
97
+ ? [
98
+ paragraph(`Für Personendaten ${processor.data}, die ${processor.subjects} betreffen, sind Sie verantwortlich, und wir bearbeiten sie in Ihrem Auftrag (siehe «${ON_BEHALF}» in den `, link('Bedingungen', '/terms'), ').')
99
+ ]
100
+ : []),
101
+ paragraph(strong('Cookies'), ': Wir setzen ein einziges Cookie, nur für diese Website und mit HttpOnly, Secure und SameSite=Lax: ', code('__Host-session'), ` hält Sie angemeldet, für ${SESSION_DAYS} Tage oder bis Sie sich abmelden, und ist für die Web-App nötig. Sie können Cookies in Ihrem Browser ablehnen; anmelden können Sie sich in der Web-App dann nicht, die API funktioniert weiter mit Schlüsseln. ${BROWSER[cloudflare.browser]} Wir verwenden keine Werbe- oder Analyse-Tracker, verkaufen keine Daten und entscheiden nicht automatisiert über Sie.`),
102
+ heading('Wer Daten erhält und wo'),
103
+ list([
104
+ `${cloudflare.name} (${cloudflare.country.de}; ${TRANSFER[cloudflare.transfer]}) steht vor ${f.name}: Es entschlüsselt die Verbindung (TLS), leitet Anfragen weiter und filtert Angriffe. Es sieht darum jede Anfrage samt Ihrer IP-Adresse, ${EDGE[cloudflare.edge]}. Ausserdem speichert es die verschlüsselten Sicherungen unserer Datenbank (Cloudflare R2, ${cloudflare.backups.where.de}), die es nicht lesen kann.`
105
+ ], [
106
+ `Anwendungsserver: ${hoster.name} (${hoster.country.de}), in deren Rechenzentren in ${hoster.dataCentres.de}; der Server ist gemietet und wird vom Betreiber verwaltet.`
107
+ ], [
108
+ `Datenbank: ${neon.name} (ein Unternehmen von ${neon.owner}, ${neon.country.de}; ${TRANSFER[neon.transfer]}), gespeichert in der Region ${neon.region} von ${neon.cloud} in ${neon.place.de}; ein Zugriff aus dem Land des Anbieters ist möglich.`
109
+ ], [
110
+ `Zahlungen: ${stripe.name} (${stripe.country.de}), mit Übermittlung an ${stripe.parent.name} (${stripe.parent.country.de}; ${TRANSFER[stripe.parent.transfer]}). ${STRIPE_ROLE[stripe.role]}`,
111
+ link(stripe.privacy, `https://${stripe.privacy}`),
112
+ '.'
113
+ ], [
114
+ `E-Mail: ${FLEET.mail.name} (${FLEET.mail.country.de}) versendet unsere E-Mails und führt unser Postfach.`
115
+ ], ...f.legal.recipients.map((recipient) => [
116
+ `${recipient.name} (${recipient.place}; ${TRANSFER[recipient.transfer]}): ${recipient.purpose}.`
117
+ ])),
118
+ paragraph(`Daten können also in den oben genannten Ländern bearbeitet werden und, bei ${cloudflare.name}, im Land des Rechenzentrums, das Ihnen antwortet. Jeder Empfänger ausserhalb der Schweiz und der EU steht oben mit der Garantie, die ihn deckt.`),
119
+ heading('Wie lange'),
120
+ list([
121
+ 'Ihr Konto mit seinen Sitzungen, Links und Schlüsseln bleibt, bis Sie es unter «Konto» löschen; dann wird es sofort gelöscht.'
122
+ ], ...own.map((data) => [strong(data.name), `: ${data.kept}.`]), [
123
+ `Unser Datenbankanbieter führt höchstens ${neon.historyDays} Tage lang eine Historie, um die Datenbank auf einen früheren Zeitpunkt zurückzusetzen, und unsere verschlüsselten Sicherungen davon werden nach rund ${cloudflare.backups.days} Tagen gelöscht; gelöschte Daten verschwinden innert dieser Zeit aus beiden.`
124
+ ], [
125
+ 'Nach der Löschung bleibt nur ein pseudonymer Datensatz des Kontos, ohne Ihre E-Mail-Adresse: der Partnername, die Zeitpunkte des Kontos (etwa der Registrierung) und die angenommene Fassung der Bedingungen, Kunden- und Abonummer bei Stripe mit dem letzten Stand des Abos sowie Beträge und Zeitpunkte Ihrer Zahlungen. Wir bewahren ihn zehn Jahre auf, wie es das Schweizer Buchführungsrecht verlangt (Art. 958f OR). Stripe führt eigene Aufzeichnungen, auch Ihre Rechnungen.'
126
+ ], [
127
+ `Das Anwendungsprotokoll rotiert nach Grösse, in ${FLEET.appLog.files} Dateien zu ${FLEET.appLog.megabytes} MB, und wird überschrieben, sobald neue Einträge kommen; ${NEW_VERSION[FLEET.appLog.newVersion]}. Das Fehlerprotokoll des Webservers rotiert ebenfalls nach Grösse, in ${webServer.errorLog.files} Dateien zu ${webServer.errorLog.megabytes} MB, und wird überschrieben, sobald neue Einträge kommen; ${NEW_VERSION[webServer.errorLog.newVersion]}. Das Protokoll des Mail-Relays bleibt höchstens ${FLEET.relayLogDays} Tage.`
128
+ ], [
129
+ `Die Sitzung dauert ${SESSION_DAYS} Tage oder bis Sie sich abmelden. Die Zähler je IP-Adresse, Konto, API-Schlüssel und E-Mail-Adresse gelten höchstens eine Stunde, liegen nur im Arbeitsspeicher und sind spätestens beim Neustart der Anwendung vergessen.`
130
+ ], [
131
+ 'E-Mails an uns: so lange, wie der Austausch es braucht, und bei Kunden so lange, wie Vertrag und Buchführungsrecht es verlangen.'
132
+ ]),
133
+ heading('Ihre Rechte'),
134
+ paragraph('Sie können Auskunft verlangen, was wir über Sie bearbeiten, und diese Daten in einem gängigen elektronischen Format erhalten, sie berichtigen oder löschen lassen und ihre Bearbeitung einschränken lassen. Einer Bearbeitung, die auf unserem berechtigten Interesse beruht, können Sie jederzeit widersprechen. Ihre E-Mail-Adresse ändern und Ihr Konto löschen können Sie selbst unter «Konto»; für alles andere schreiben Sie an ', mail(FLEET.mailbox), '. Wir antworten innert 30 Tagen. Sie können sich beim Eidgenössischen Datenschutz- und Öffentlichkeitsbeauftragten (EDÖB) beschweren oder, in der EU, bei der Datenschutzbehörde Ihres Landes.'),
135
+ heading('Änderungen'),
136
+ paragraph('Es gilt die hier veröffentlichte Fassung. Über wichtige Änderungen informieren wir Kunden per E-Mail.')
137
+ ]
138
+ };
139
+ },
140
+ terms: (f) => {
141
+ const processor = f.legal.processor;
142
+ const processors = [
143
+ ...FLEET_PROCESSORS,
144
+ ...f.legal.recipients
145
+ .filter((recipient) => recipient.role === 'processor')
146
+ .map((recipient) => recipient.name)
147
+ ];
148
+ const sections = [
149
+ {
150
+ title: 'Nur für Unternehmen',
151
+ blocks: [
152
+ paragraph(`${f.name} richtet sich nur an Unternehmen, öffentliche Stellen und Selbständige für ihre berufliche Tätigkeit, nicht an Konsumenten.`)
153
+ ]
154
+ },
155
+ {
156
+ title: 'Die Leistung',
157
+ blocks: [
158
+ paragraph(`${f.legal.service} Wir dürfen ${f.name} weiterentwickeln, entfernen aber während einer bezahlten Periode keine seiner Hauptfunktionen.`)
159
+ ]
160
+ },
161
+ {
162
+ title: 'Was wir nicht gewährleisten',
163
+ blocks: [
164
+ paragraph(`${f.legal.notGuaranteed} Wir gewährleisten nicht, dass ${f.name} jederzeit verfügbar ist: Wartung und Ausfälle unserer Hosting- und Netzanbieter oder von Stripe können den Dienst verzögern oder unterbrechen.`)
165
+ ]
166
+ },
167
+ {
168
+ title: 'Konto und Testphase',
169
+ blocks: [
170
+ paragraph(`Sie brauchen ein Konto mit einer gültigen E-Mail-Adresse. Bietet ${f.name} eine kostenlose Testphase an, gelten deren Dauer und Grenzen, wie die Preisseite sie nennt; sie endet von selbst und kostet nichts, solange Sie nicht bezahlen. Halten Sie Ihr Passwort und Ihre API-Schlüssel geheim; Sie sind verantwortlich für das, was damit geschieht.`)
171
+ ]
172
+ },
173
+ {
174
+ title: 'Preise und Zahlung',
175
+ blocks: [
176
+ paragraph(`Es gelten die Preise der Preisseite, in der dort genannten Währung, im Voraus bezahlt über Stripe. ${VAT[f.operator.vat].terms} Ein Abo verlängert sich um seine Periode, bis Sie es im Kundenportal kündigen; es endet dann mit der bezahlten Periode. Wechseln Sie im Kundenportal den Plan, wird die Differenz anteilig belastet oder gutgeschrieben, wie Stripe es dort zeigt; sonst werden bezahlte Perioden und Käufe nicht erstattet, auch nicht, wenn Sie Ihr Konto löschen, was ein Abo sofort beendet. Nicht verbrauchtes Guthaben verfällt ohne Erstattung, wenn Sie Ihr Konto löschen. Scheitert eine Zahlung, dürfen wir die bezahlten Leistungen sperren, bis sie erfolgt ist. Preise dürfen wir mit 30 Tagen Vorlauf per E-Mail ändern; Sie können kündigen, bevor die Änderung gilt.`)
177
+ ]
178
+ },
179
+ {
180
+ title: 'Nutzung',
181
+ blocks: [
182
+ paragraph(`${f.legal.use} Sie dürfen ${f.name} nicht rechtswidrig nutzen und nicht, um unseren Dienst zu überlasten oder zu stören.`)
183
+ ]
184
+ },
185
+ {
186
+ title: 'Ihre Daten',
187
+ blocks: [
188
+ paragraph(`Was Sie in ${f.name} eingeben, bleibt Ihres. Löschen Sie Ihr Konto, wird es gelöscht, wie es die Datenschutzerklärung beschreibt.`)
189
+ ]
190
+ },
191
+ ...(processor.kind === 'yes'
192
+ ? [
193
+ {
194
+ title: ON_BEHALF,
195
+ blocks: [
196
+ paragraph(`Für Personendaten ${processor.data}, die ${processor.subjects} betreffen, sind Sie verantwortlich und wir Ihr Auftragsbearbeiter (Art. 9 DSG, Art. 28 DSGVO). Wir bearbeiten diese Daten, solange Ihr Konto besteht, und nur, um Ihnen ${f.name} zu erbringen. Wir:`),
197
+ lettered([
198
+ 'bearbeiten sie nur nach Ihren dokumentierten Weisungen, das sind diese Bedingungen und Ihre Einstellungen, und sagen es Ihnen, wenn wir eine Weisung für datenschutzwidrig halten;'
199
+ ], ['verpflichten alle, die Zugang zu ihnen haben, zur Vertraulichkeit;'], [
200
+ 'schützen sie mit angemessenen technischen und organisatorischen Massnahmen, darunter verschlüsselte Verbindungen, gehashte Passwörter und Schlüssel und einen Zugang, der auf den Betreiber beschränkt ist;'
201
+ ], [
202
+ `setzen als Unterauftragsbearbeiter nur die in der Datenschutzerklärung genannten ein (${processors.join('; ')}), denen Sie zustimmen, und verpflichten sie auf gleichwertige Datenschutzpflichten; einen neuen kündigen wir 30 Tage im Voraus per E-Mail an, und Sie können widersprechen, indem Sie vor seinem Einsatz kündigen;`
203
+ ], [
204
+ 'helfen Ihnen, soweit wir können, Anfragen betroffener Personen zu beantworten und Ihre Pflichten zu Datensicherheit, Meldung von Verletzungen und Folgenabschätzungen zu erfüllen;'
205
+ ], [
206
+ 'melden Ihnen ohne unangemessene Verzögerung eine Verletzung der Datensicherheit, die diese Daten betrifft;'
207
+ ], [
208
+ `löschen sie, wenn Sie Ihr Konto löschen, ausser das Gesetz verlangt, sie aufzubewahren; vorher können Sie sie über ${processor.retrieve} lesen oder exportieren; Kopien in der Historie der Datenbank und in unseren Sicherungen enden, wie es die Datenschutzerklärung beschreibt;`
209
+ ], [
210
+ 'geben Ihnen die Informationen, die Sie brauchen, um die Einhaltung dieser Pflichten nachzuweisen, und lassen Prüfungen durch Sie oder eine zur Vertraulichkeit verpflichtete prüfende Person zu, mit angemessener Vorankündigung und auf Ihre Kosten.'
211
+ ]),
212
+ paragraph('Übermittlungen in die Länder, die die Datenschutzerklärung nennt, erfolgen mit den dort genannten Garantien.')
213
+ ]
214
+ }
215
+ ]
216
+ : []),
217
+ {
218
+ title: 'Haftung',
219
+ blocks: [
220
+ paragraph('Wir haften unbeschränkt für Vorsatz und grobe Fahrlässigkeit. Für leichte Fahrlässigkeit ist unsere Haftung, soweit gesetzlich zulässig, auf die Gebühren beschränkt, die Sie in den zwölf Monaten vor dem Schaden bezahlt haben, und wir haften nicht für indirekte Schäden oder Folgeschäden, entgangenen Gewinn oder Datenverlust. Während einer kostenlosen Testphase haften wir nur für Vorsatz und grobe Fahrlässigkeit. Diese Beschränkungen gelten auch für die Haftung für unsere Hilfspersonen und Unterauftragnehmer.')
221
+ ]
222
+ },
223
+ {
224
+ title: 'Beendigung',
225
+ blocks: [
226
+ paragraph('Sie können Ihr Konto jederzeit unter «Konto» löschen. Wir können Ihr Konto mit 30 Tagen Frist per E-Mail kündigen und erstatten dann Gebühren für die Zeit nach dem Ende sowie nicht verbrauchtes Guthaben. Wir können es fristlos kündigen, wenn Sie diese Bedingungen schwer verletzen.')
227
+ ]
228
+ },
229
+ {
230
+ title: 'Änderungen dieser Bedingungen',
231
+ blocks: [
232
+ paragraph('Wir können diese Bedingungen mit 30 Tagen Vorlauf per E-Mail ändern. Kündigen Sie nicht, bevor die Änderung gilt, gilt von da an die neue Fassung.')
233
+ ]
234
+ },
235
+ {
236
+ title: 'Recht und Gerichtsstand',
237
+ blocks: [
238
+ paragraph(`Es gilt Schweizer Recht unter Ausschluss des Übereinkommens der Vereinten Nationen über Verträge über den internationalen Warenkauf (Wiener Kaufrecht). Ausschliesslicher Gerichtsstand ist ${f.operator.jurisdiction}, Schweiz; wir dürfen Sie auch an Ihrem Sitz belangen.`)
239
+ ]
240
+ }
241
+ ];
242
+ return {
243
+ title: 'Allgemeine Geschäftsbedingungen',
244
+ blocks: [
245
+ version(f),
246
+ paragraph(`Diese Bedingungen gelten zwischen ${who(f)} («wir») und dem Unternehmen, das ${f.name} unter ${f.origin} nutzt, in der Web-App oder über die API («Sie»). Sie nehmen sie an, wenn Sie sich registrieren, auf der Website oder über die API, und nochmals, wenn Sie in Stripe Checkout bezahlen. Wer sich registriert, bestätigt, für ein Unternehmen zu handeln und es verpflichten zu dürfen.`),
247
+ ...numbered(sections)
248
+ ]
249
+ };
250
+ }
251
+ };
@@ -0,0 +1,37 @@
1
+ import type { Language, ProductLegal } from '../../product.js';
2
+ import type { LegalBlock, LegalData, LegalInline } from '../../ui/data.js';
3
+ import type { Operator } from '../config.js';
4
+ export interface LegalFacts {
5
+ language: Language;
6
+ name: string;
7
+ origin: string;
8
+ operator: Operator;
9
+ legal: ProductLegal;
10
+ /** The name of the terms, a day YYYY-MM-DD: the later of the days of baukasten and product. */
11
+ version: string;
12
+ }
13
+ export interface LegalPages {
14
+ imprint: LegalData;
15
+ privacy: LegalData;
16
+ terms: LegalData;
17
+ }
18
+ export type LegalTexts = {
19
+ [Page in keyof LegalPages]: (facts: LegalFacts) => LegalData;
20
+ };
21
+ export declare const strong: (text: string) => LegalInline;
22
+ export declare const code: (text: string) => LegalInline;
23
+ export declare const mail: (address: string) => LegalInline;
24
+ export declare const link: (text: string, href: string) => LegalInline;
25
+ export declare const heading: (text: string) => LegalBlock;
26
+ export declare const paragraph: (...parts: LegalInline[]) => LegalBlock;
27
+ export declare const note: (...parts: LegalInline[]) => LegalBlock;
28
+ export declare const list: (...items: LegalInline[][]) => LegalBlock;
29
+ export declare const lettered: (...items: LegalInline[][]) => LegalBlock;
30
+ export interface Section {
31
+ title: string;
32
+ blocks: LegalBlock[];
33
+ }
34
+ /** The sections of the terms, numbered as they come: a section left out leaves no gap. */
35
+ export declare function numbered(sections: readonly Section[]): LegalBlock[];
36
+ /** A day YYYY-MM-DD as people of the language write it: 8 October 2026, 8. Oktober 2026. */
37
+ export declare function longDate(day: string, language: Language): string;
@@ -0,0 +1,30 @@
1
+ export const strong = (text) => ({ kind: 'strong', text });
2
+ export const code = (text) => ({ kind: 'code', text });
3
+ export const mail = (address) => ({
4
+ kind: 'link',
5
+ text: address,
6
+ href: `mailto:${address}`
7
+ });
8
+ export const link = (text, href) => ({ kind: 'link', text, href });
9
+ export const heading = (text) => ({ kind: 'heading', text });
10
+ export const paragraph = (...parts) => ({ kind: 'paragraph', parts });
11
+ export const note = (...parts) => ({ kind: 'note', parts });
12
+ export const list = (...items) => ({ kind: 'list', items });
13
+ export const lettered = (...items) => ({ kind: 'lettered', items });
14
+ /** The sections of the terms, numbered as they come: a section left out leaves no gap. */
15
+ export function numbered(sections) {
16
+ return sections.flatMap((section, index) => [
17
+ heading(`${index + 1}. ${section.title}`),
18
+ ...section.blocks
19
+ ]);
20
+ }
21
+ /** A day YYYY-MM-DD as people of the language write it: 8 October 2026, 8. Oktober 2026. */
22
+ export function longDate(day, language) {
23
+ const locale = { en: 'en-GB', de: 'de-CH' }[language];
24
+ return new Intl.DateTimeFormat(locale, {
25
+ day: 'numeric',
26
+ month: 'long',
27
+ year: 'numeric',
28
+ timeZone: 'UTC'
29
+ }).format(new Date(`${day}T00:00:00Z`));
30
+ }
@@ -0,0 +1,2 @@
1
+ import { type LegalTexts } from './document.js';
2
+ export declare const en: LegalTexts;