@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/README.md CHANGED
@@ -1,3 +1,256 @@
1
1
  # @tollwyn/baukasten
2
2
 
3
- Placeholder. Use a 1.x version of @tollwyn/baukasten.
3
+ What every product built on this kit shares, finished and tested: accounts with sessions and API
4
+ keys, limits, mail, payment through Stripe, the sales numbers, the legal pages, health, log and
5
+ server, the database with its migrations, the frame of the pages, and the tools to test a product.
6
+ A product builds its core and nothing of this.
7
+
8
+ ## Set up a product
9
+
10
+ A product is a SvelteKit 2 app with `@sveltejs/adapter-node` 5 and Svelte 5, tested with Playwright.
11
+
12
+ ```sh
13
+ npm install --save-exact @tollwyn/baukasten@<version>
14
+ npm install --save-dev @sveltejs/adapter-node@5 @playwright/test smtp-server
15
+ npx --no-install baukasten-stubs
16
+ # fill in what the product owns (below)
17
+ npx --no-install baukasten-check
18
+ ```
19
+
20
+ `baukasten-stubs` writes the files that only pass on to the package: the pages of the baukasten
21
+ under `src/routes` (sign-up, login, account, password, email, legal pages), the root layout and the
22
+ error page, `src/app.html`, `src/hooks.server.ts`, `server.js`, and `e2e/baukasten.spec.ts`. Never
23
+ change them: `baukasten-check` holds them byte for byte, and a new version brings its own. Where
24
+ they are missing, it also writes the files the product owns, as a starting point, and never over
25
+ them: `baukasten.config.ts`, `src/app.css`, `src/lib/server/baukasten.ts` and
26
+ `src/lib/server/handle.ts`.
27
+
28
+ ## What the product owns
29
+
30
+ - `baukasten.config.ts`: its language (`en` or `de`), the schema of its own variables, its
31
+ navigation, where an account lands and the prefix of its keys, the texts of the sign-up page, its
32
+ catalog for Stripe, and its statements for the legal pages, which must be true. Nothing in it may
33
+ stay empty: the product does not build until it is filled in.
34
+ - `src/app.css`: its own styles and colours (the variables `--bk-*` of the baukasten), which the root
35
+ layout loads after the base styles, so that they reach the pages of the baukasten too. See
36
+ «Design» below.
37
+ - `static/`: its logo, icon and picture for shared links, named under `brand` in
38
+ `baukasten.config.ts`.
39
+ - `src/lib/server/baukasten.ts`: the hooks the baukasten calls (`Hooks` of `/server`): its checks for
40
+ `/health/deep`, start and stop of its runs (`periodic`), deleting its data of an account, plans
41
+ from a subscription, a paid purchase, its own sales numbers.
42
+ - `src/lib/server/handle.ts`: its own handle, after the baukasten's.
43
+ - Its own pages. Their links to other pages go through `useLink` of `/ui`, else the referral tag
44
+ `?via=` is lost on the way to the sign-up.
45
+ - `vite.config.ts`, passing the options of the baukasten to SvelteKit:
46
+ `sveltekit({ ...SVELTEKIT_OPTIONS, adapter: adapter() })`, with `SVELTEKIT_OPTIONS` from
47
+ `@tollwyn/baukasten`. No page is prerendered.
48
+ - `migrations/`: its tables in the schema `public`, plain SQL, forward-only and expanding (the
49
+ previous version must run against the new schema). Every table that holds data of an account
50
+ points at it with `REFERENCES baukasten.accounts (id) ON DELETE CASCADE`. It reads the tables of
51
+ the baukasten only through its functions and never changes them.
52
+ - `.env.example`, naming every variable without a value (`envExampleProblems` of `/testing`).
53
+ - `playwright.config.ts` with `testDir: 'e2e'` and `TEST_DATABASE_URL`, a Postgres server on which the
54
+ tests create their databases. It sets the variables the configuration of the product requires in
55
+ `process.env`: the connection test starts the product with them.
56
+ - The image: `npm run build`, then `baukasten.config.ts` and `migrations/` in its root beside
57
+ `build/`, `server.js` and the production `node_modules`; it starts with `CMD ["node", "server.js"]`.
58
+ - `deploy/auslieferung.json`: version 2, `art: webapp`, the migration
59
+ `["node", "node_modules/@tollwyn/baukasten/dist/bin/migrate.js"]`, and under `stripe` a
60
+ webhook `endpunkt` at `WEBHOOK_PATH` with the events `WEBHOOK_EVENTS`, and at least the rights
61
+ `KEY_RIGHTS` (all three from `/server`).
62
+
63
+ ## Design
64
+
65
+ A product brings its own design; the frame and the pages of the baukasten take it on and give way to
66
+ it. `muster/` shows all of it with Tailwind CSS 4, Skeleton and a font from fontsource.
67
+
68
+ - **Layers.** The base styles lie in two cascade layers and fix their order first:
69
+ `@layer bk-root, properties, theme, base, bk, components, utilities`. `bk-root`, below everything,
70
+ holds the variables, the page and its links; `bk`, above Tailwind's preflight and below its
71
+ components and utilities, holds the frame and the pages of the baukasten. Element styles of the
72
+ baukasten apply only inside its own pages (`.bk-ui`), the head, the notice, the foot and every
73
+ `Form`; the product's own pages get none of them. Any CSS of the product wins: unlayered, a
74
+ component or a utility. Style the frame through its classes (`.bk-head`, `.bk-name`, `.bk-nav`,
75
+ `.bk-main`, `.bk-foot`), as a component. Element styles meant for the product's own pages, like a
76
+ reset, belong in `@layer base`: unlayered, they reach the pages of the baukasten too.
77
+ - **Colours and font.** Point the variables at the product's theme in `src/app.css`, light and dark:
78
+ `--bk-text`, `--bk-muted`, `--bk-background`, `--bk-surface`, `--bk-border`, `--bk-accent`,
79
+ `--bk-accent-text`, `--bk-error`, `--bk-success`, `--bk-radius`, `--bk-font`. With Skeleton its
80
+ paired tokens do both schemes at once, like `--bk-text: var(--color-surface-950-50)`, and it sets
81
+ `color-scheme` to the scheme of the visitor. Without Skeleton the base styles are light; a product
82
+ that follows a dark scheme sets `color-scheme: light dark` itself, with its dark values.
83
+ - **Marks.** Under `brand` in `baukasten.config.ts`: `logo` (sign and name as one SVG, with its size;
84
+ the head shows it with the name as its text, so it must read on the head in either scheme),
85
+ `icon` (SVG or PNG), `image` (a PNG of 1200 × 630 pixels for shared links, named on every page at
86
+ the whole address of `PUBLIC_ORIGIN`) and optionally `theme`, which goes on `<html data-theme>`
87
+ before the first paint. The files lie in `static/`. The connection test checks that they load, of
88
+ their kind and size.
89
+ - **Fonts and pictures** come from the product's own address: fontsource packages imported in
90
+ `src/app.css`, which Vite bundles (a small font inlined as `data:`, which the policy allows), and
91
+ pictures in `static/` or the product's code. Nothing from another server: the policy refuses it.
92
+
93
+ ## The check and the connection test
94
+
95
+ `baukasten-check` runs in the root of the product, before its gate, against the installed version:
96
+ the stubs are there and unchanged, `deploy/auslieferung.json` names the webhook and the rights of
97
+ the baukasten, SvelteKit and Svelte fit its peer ranges and adapter-node is version 5, `overrides`
98
+ in `package.json` replaces nothing the baukasten depends on, no migration of the product names the
99
+ schema `baukasten` beyond its references to `baukasten.accounts`, and the configuration and
100
+ `SVELTEKIT_OPTIONS` are in place. Exit 0 when the product fits; else exit 1 with one line per
101
+ problem.
102
+
103
+ `e2e/baukasten.spec.ts` runs the connection test in the gate of the product, against the built
104
+ product with the doubles of Stripe and mail: sign-up with a referral tag, mail and confirmation,
105
+ login and a new password, an API key, Checkout and the customer portal, `/vertrieb/zahlen`, the
106
+ legal pages with the operator, the security headers, the session as the only cookie, and deleting
107
+ the account. Playwright places its tests in the package, so a file filter misses them; run it alone
108
+ with `npx playwright test -g "the connection of the baukasten"`.
109
+
110
+ # A Shopify app
111
+
112
+ A Shopify app built on this kit is a public app in the Shopify App Store, embedded in the admin of
113
+ each store that installs it. Shopify installs it (managed installation) and bills it (Shopify App
114
+ Pricing); the merchant never signs up or pays anywhere else. It is a SvelteKit 2 app with
115
+ `@sveltejs/adapter-node` 5 and Svelte 5, tested with Playwright, like a web app, and on the same
116
+ server; the accounts, Stripe and the legal pages of a web app are not part of it.
117
+
118
+ What the baukasten brings for it: the installation with the token exchange and expiring offline
119
+ tokens, refreshed under a lock per store and kept sealed; the check of the ID token of App Bridge on
120
+ every request; the frame of the admin (content security policy per store, App Bridge and Polaris from
121
+ the CDN of Shopify); the webhooks about privacy and the end of an installation, and the signature of
122
+ every webhook; the plan of a store through the Partner API, and the way to Shopify's plan page;
123
+ the Admin API of a store for the app's own queries; health, log and server, the database with its
124
+ migrations and the work mark of runs; mail; the numbers for sales; the privacy text with the
125
+ processing on behalf of the merchant; and the tools to test the app, the double of Shopify and the
126
+ connection test among them.
127
+
128
+ ## Set up a Shopify app
129
+
130
+ ```sh
131
+ npm install --save-exact @tollwyn/baukasten@<version>
132
+ npm install --save-dev @sveltejs/adapter-node@5 @playwright/test smtp-server
133
+ npx --no-install baukasten-stubs shopify
134
+ # fill in what the app owns (below)
135
+ npx --no-install baukasten-check
136
+ ```
137
+
138
+ `baukasten-stubs shopify` writes the files that only pass on to the package: `server.js`,
139
+ `src/app.html` (App Bridge first, then Polaris, from the CDN of Shopify; the client ID filled in by the
140
+ server), `src/hooks.server.ts`, and `e2e/baukasten.spec.ts`. Never change them. Where they are
141
+ missing, it writes the files the app owns as a starting point: `baukasten.config.ts`,
142
+ `shopify.app.toml`, `src/lib/server/baukasten.ts` and `src/lib/server/handle.ts`.
143
+
144
+ ## What the Shopify app owns
145
+
146
+ - `baukasten.config.ts`, with `defineShopifyApp` of `@tollwyn/baukasten`: its language (`en`
147
+ or `de`, for the privacy text and the few pages of the baukasten; its own pages may speak the
148
+ language of its market), the schema of its own variables, its plans with the handles they have in
149
+ the Partner Dashboard and whether each costs money, and its statements for the privacy text,
150
+ which must be true: what it does (`service`), the personal data it processes for the merchant
151
+ (`data`), and who else gets them (`recipients`).
152
+ - `shopify.app.toml`, the configuration of the app at Shopify, deployed with the Shopify CLI.
153
+ `baukasten-check` holds: `application_url` is `https://<subdomain>.tollwyn.com` with the
154
+ `subdomain` of `deploy/auslieferung.json`, `auth.redirect_urls` is exactly
155
+ `["https://<subdomain>.tollwyn.com/shopify/auth"]`, `embedded = true`, no
156
+ `use_legacy_install_flow`, `webhooks.api_version` is `SHOPIFY_API_VERSION` of `/shopify`, every
157
+ subscription goes to `/shopify/webhooks` (`SHOPIFY_WEBHOOK_PATH`), and they name
158
+ `app/uninstalled` and the three compliance topics. The app's own topics go to the same path. Its
159
+ scopes are only what its core needs (`read_all_orders` only with a reason). `client_id` stays empty
160
+ until the app is linked. No app proxy, events or customer accounts, whose requests would reach the
161
+ app unchecked, and no extensions, whose commands the Shopify CLI would run when it deploys the
162
+ app.
163
+ - `src/lib/server/baukasten.ts`: the kit, `shopifyBaukasten(app, hooks)` of `/shopify`, with the
164
+ hooks it calls (`ShopifyHooks`): checks for `/health/deep`, start and stop of runs (`periodic`),
165
+ `webhook` for the app's own topics, and, if the app keeps personal data for the merchant (`data` is
166
+ not empty, and then they are required), `customerData` (what it keeps of a customer, as text,
167
+ which the baukasten mails to the store) and `redactCustomer` (deletes it). The baukasten answers
168
+ a webhook at once and applies it after, in a run of its own: each hook runs in the transaction of
169
+ its webhook, once; a failure rolls it back, the webhook is tried again later, for about a day, and
170
+ after the last try it gives up with an error in the log.
171
+ - Its pages, with the web components of Polaris (`<s-page>`, `<s-section>`, …). In a `load` or
172
+ action, `kit.shop(event)` gives the store of the request (`domain`) and its plan (`plan.handle`);
173
+ every page is opened by a store with a plan, so the app gates its features by the handle. A store
174
+ without a plan the baukasten leads to the plan page of Shopify, which also changes a plan:
175
+ `https://admin.shopify.com/store/<store>/charges/<app handle>/pricing_plans`, opened with
176
+ `window.open(url, '_top')`. Forms post with `use:enhance`: App Bridge adds the ID token to every
177
+ request of the page to the app, and a request without it gets no answer of the app.
178
+ - The Admin API: `kit.admin(domain).graphql(query, variables)` gives the data unparsed; the app
179
+ parses them with Zod. Over the store's limit it waits and tries again a few times, then fails. It
180
+ throws `ShopGone` when the store gave no token that works (removed, or only the merchant can give
181
+ a new one by opening the app), and `AdminRefused` for errors of the query. Only GraphQL, in
182
+ `SHOPIFY_API_VERSION`.
183
+ - Runs: `periodic` with the work mark, as in a web app; a run asks the Admin API of a store with
184
+ `kit.admin(domain)` and skips a store that is gone.
185
+ - `vite.config.ts`: `sveltekit({ ...SHOPIFY_SVELTEKIT_OPTIONS, adapter: adapter() })`, with
186
+ `SHOPIFY_SVELTEKIT_OPTIONS` from `@tollwyn/baukasten`. No page is prerendered.
187
+ - `migrations/`: its tables in the schema `public`, forward-only and expanding. Every table that
188
+ holds data of a store points at it with `REFERENCES baukasten.shops (domain) ON DELETE CASCADE`:
189
+ so `shop/redact` deletes them with the store.
190
+ - `.env.example` (`envExampleProblems` of `/testing`) and `playwright.config.ts`, as in a web app.
191
+ - The image: as a web app's, with `shopify.app.toml` in its root as well.
192
+ - `deploy/auslieferung.json`: version 2, `art: shopify`, `name`, `subdomain` (without `shopify` or
193
+ `example`, which the review refuses in an address), the migration
194
+ `["node", "node_modules/@tollwyn/baukasten/dist/bin/migrate.js", "shopify"]`, its own
195
+ `variablen`, and no `stripe`.
196
+
197
+ ## Testing a Shopify app
198
+
199
+ `createTestDatabase({ art: 'shopify' })` migrates the layer of a Shopify app. `startShopify({ scopes })`
200
+ of `/testing` is a double of Shopify, which grants the tokens the scopes of the app's
201
+ `shopify.app.toml`: `env()` gives the variables that send the app to it, with a client
202
+ ID of its own (`SHOPIFY_API_BASE`, `SHOPIFY_CLIENT_ID`, tests only); `addShop` makes a store,
203
+ `idToken` mints the ID token App Bridge would give, `subscribe` chooses a plan, `uninstall` removes
204
+ the app, `answerAdmin` answers the app's own queries of the Admin API, and `webhook` sends a signed
205
+ webhook. A page of the admin is `/?embedded=1&shop=<store>&host=…&id_token=<ID token>`; a request of
206
+ a page carries `authorization: Bearer <ID token>`.
207
+
208
+ `e2e/baukasten.spec.ts` runs the connection test of a Shopify app: health, the page outside the
209
+ admin, the bounce page, the installation and the way to the plans, the frame and the headers, the ID
210
+ token as a bearer token, the webhooks with and without signature, the numbers for sales, the end of
211
+ an installation and `shop/redact`, the privacy text, and the log. A webhook is applied after its
212
+ answer, so a test waits for its effect (`expect.poll`).
213
+
214
+ ## The Shopify sample against a store
215
+
216
+ `muster-shopify/` runs against the double in the gate. Against a development store it runs from a
217
+ copy outside the repo: there `shopify.app.toml` has `automatically_update_urls_on_dev = true` and
218
+ the client ID the Shopify CLI gives it when it creates the app, a `shopify.web.toml` whose `dev`
219
+ command starts the built sample with `PUBLIC_ORIGIN` from the address of the tunnel and
220
+ `SHOPIFY_API_SECRET` from the CLI, and `shopify app dev` installs it in the store. The app needs
221
+ Shopify App Pricing with the plans `basic` and `free` in the Partner Dashboard, and the variables of
222
+ the Partner API (a token that may manage apps, and the number of the organization).
223
+
224
+ ## Linking and deploying the app
225
+
226
+ The client ID comes into `shopify.app.toml` when the app is created in the Dev Dashboard and linked.
227
+ Linking rewrites the file with the defaults of a new app: only its `client_id` is kept, the rest
228
+ comes back from git, and `baukasten-check` passes again before anything is deployed. The Partner
229
+ Dashboard gets the distribution «Public», the fields of protected customer data with their reasons,
230
+ and Shopify App Pricing with the plans under the handles of `baukasten.config.ts`, all as the build
231
+ report names them; `shopify app deploy` publishes the configuration. The privacy text is at
232
+ `<origin>/privacy`, for the listing.
233
+
234
+ The questions of «Data protection details» in the Partner Dashboard, answered by what the fleet and
235
+ the baukasten do:
236
+
237
+ - Minimum data, purpose, retention: the app asks only the scopes and fields its core needs (the
238
+ build report names each with its reason); it processes them only for its `service`; a store's data are
239
+ deleted at `shop/redact`, 48 hours after the app is removed, a customer's at `customers/redact`,
240
+ and backups end after about 30 days.
241
+ - Merchants informed, agreement with merchants: the privacy text at `<origin>/privacy` says what the
242
+ app processes and holds the clauses of processing on behalf of the merchant (Art. 28 GDPR), which
243
+ the merchant accepts by installing the app.
244
+ - Consent, opt-outs, automated decisions: the app uses no data for marketing, sells none, and makes no
245
+ automated decisions with legal effect.
246
+ - Encryption: every connection is TLS; the database of the app (Neon) is encrypted at rest; the
247
+ tokens of the stores are sealed with AES-256-GCM; every backup is encrypted (age) before it leaves
248
+ the server.
249
+ - Test and production data: kept apart. Tests run with the double of Shopify and their own databases,
250
+ never with data of a store.
251
+ - Access: the database of the app is reached only by the app and, for its encrypted backups, by the
252
+ delivery service; one operator, no other staff. The application logs every request with the store
253
+ it served.
254
+ - Loss and incidents: on a breach the operator stops the app through the delivery service, finds what
255
+ was reached from the logs, and tells the stores concerned by email without undue delay, as the
256
+ privacy text promises.
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,38 @@
1
+ #!/usr/bin/env node
2
+ import { readdirSync, readFileSync } from 'node:fs';
3
+ import path from 'node:path';
4
+ import { z } from 'zod';
5
+ import { productProblems } from '../check.js';
6
+ const MANIFEST = z.object({
7
+ name: z.string(),
8
+ version: z.string(),
9
+ dependencies: z.record(z.string(), z.string()),
10
+ peerDependencies: z.record(z.string(), z.string())
11
+ });
12
+ /** The package.json of this very installation: dist/bin/ lies two folders below it. */
13
+ const baukasten = MANIFEST.parse(JSON.parse(readFileSync(new URL('../../package.json', import.meta.url), 'utf8')));
14
+ const root = process.cwd();
15
+ /** What is not there is undefined; any other failure to read is one of the check. */
16
+ function unlessMissing(read) {
17
+ try {
18
+ return read();
19
+ }
20
+ catch (error) {
21
+ if (error instanceof Error && Reflect.get(error, 'code') === 'ENOENT')
22
+ return undefined;
23
+ throw error;
24
+ }
25
+ }
26
+ const files = {
27
+ read: (file) => unlessMissing(() => readFileSync(path.join(root, file), 'utf8')),
28
+ names: (folder) => unlessMissing(() => readdirSync(path.join(root, folder))) ?? []
29
+ };
30
+ const problems = productProblems(files, baukasten);
31
+ if (problems.length > 0) {
32
+ for (const problem of problems)
33
+ process.stderr.write(`${problem}\n`);
34
+ process.exitCode = 1;
35
+ }
36
+ else {
37
+ process.stdout.write(`the product fits ${baukasten.name} ${baukasten.version}\n`);
38
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,111 @@
1
+ #!/usr/bin/env node
2
+ // One JSON line says what the command did; the exit code is the verdict. A database that a newer
3
+ // version migrated further is fine, with a warning: the delivery service falls back to this version
4
+ // when the newer one starts unhealthy.
5
+ import { existsSync, readFileSync } from 'node:fs';
6
+ import path from 'node:path';
7
+ import { pathToFileURL } from 'node:url';
8
+ import { isProduct } from '../product.js';
9
+ import { createDb } from '../server/adapter/db.js';
10
+ import { exitAfterLog, log } from '../server/adapter/log.js';
11
+ import { migrate, migrationSources } from '../server/adapter/migrate.js';
12
+ import { createStripe } from '../server/adapter/stripe.js';
13
+ import { foreignWebhooks, setUpCatalog } from '../server/catalog.js';
14
+ import { ConfigError, databaseUrlOf, loadConfig } from '../server/config.js';
15
+ import { testData } from '../server/test-accounts.js';
16
+ import { isShopifyApp } from '../shopify/app.js';
17
+ import { loadShopifyConfig } from '../shopify/config.js';
18
+ import { APP_TOML } from '../shopify/toml.js';
19
+ const TEST_ACCOUNTS = 'node node_modules/@tollwyn/baukasten/dist/bin/test-accounts.js';
20
+ async function configurationIn(root) {
21
+ const loaded = await import(pathToFileURL(path.join(root, 'baukasten.config.ts')).href);
22
+ return typeof loaded === 'object' && loaded !== null ? Reflect.get(loaded, 'default') : undefined;
23
+ }
24
+ function configured(product) {
25
+ if (!isProduct(product)) {
26
+ throw new ConfigError([
27
+ 'baukasten.config.ts does not export the product of defineProduct as its default'
28
+ ]);
29
+ }
30
+ return { product, config: loadConfig(process.env, product) };
31
+ }
32
+ async function webapp(root, databaseUrl) {
33
+ const withStripe = process.env.NODE_ENV === 'production' || (process.env.STRIPE_SECRET_KEY ?? '').trim() !== '';
34
+ // Read before anything changes: a configuration that does not hold migrates nothing.
35
+ const setup = withStripe ? configured(await configurationIn(root)) : null;
36
+ const { applied, ahead } = await migrate(databaseUrl, migrationSources(root, 'webapp'));
37
+ warnAhead(ahead);
38
+ const stripe = setup === null
39
+ ? 'skipped: no STRIPE_SECRET_KEY'
40
+ : await stripeOf(setup.config, setup.product, databaseUrl);
41
+ log.info('migrate: done', { applied, stripe });
42
+ }
43
+ /** Migrates a Shopify app, whose configuration holds in production before anything changes. */
44
+ async function shopify(root, databaseUrl) {
45
+ if (process.env.NODE_ENV === 'production') {
46
+ const app = await configurationIn(root);
47
+ if (!isShopifyApp(app)) {
48
+ throw new ConfigError([
49
+ 'baukasten.config.ts does not export the app of defineShopifyApp as its default'
50
+ ]);
51
+ }
52
+ const toml = path.join(root, APP_TOML);
53
+ loadShopifyConfig(process.env, app, existsSync(toml) ? readFileSync(toml, 'utf8') : undefined);
54
+ }
55
+ const { applied, ahead } = await migrate(databaseUrl, migrationSources(root, 'shopify'));
56
+ warnAhead(ahead);
57
+ log.info('migrate: done', { applied });
58
+ }
59
+ function warnAhead(ahead) {
60
+ if (ahead.baukasten.length > 0 || ahead.product.length > 0) {
61
+ log.warn('migrate: the database is ahead of this version', { ahead });
62
+ }
63
+ }
64
+ async function stripeOf(config, product, databaseUrl) {
65
+ if (config.stripe.kind === 'none')
66
+ return 'skipped: no STRIPE_SECRET_KEY';
67
+ const stripe = createStripe(config.stripe);
68
+ const shop = { name: config.name, origin: config.publicOrigin };
69
+ const steps = await setUpCatalog(stripe, product.catalog, shop);
70
+ const foreign = await foreignWebhooks(stripe, shop.origin);
71
+ if (foreign.length > 0) {
72
+ log.warn('migrate: webhook endpoints of another product in the Stripe account', {
73
+ endpoints: foreign
74
+ });
75
+ }
76
+ if (config.stripe.mode === 'live') {
77
+ const db = createDb(databaseUrl, log);
78
+ try {
79
+ const data = await testData(db);
80
+ // Counted, without addresses: the command lists them for whoever decides.
81
+ if (data.accounts > 0) {
82
+ log.warn('migrate: accounts from the test mode remain', {
83
+ ...data,
84
+ command: TEST_ACCOUNTS
85
+ });
86
+ }
87
+ }
88
+ finally {
89
+ await db.close();
90
+ }
91
+ }
92
+ return steps;
93
+ }
94
+ try {
95
+ const root = process.cwd();
96
+ const databaseUrl = databaseUrlOf(process.env);
97
+ const art = process.argv.slice(2);
98
+ if (art.length === 0)
99
+ await webapp(root, databaseUrl);
100
+ else if (art.length === 1 && art[0] === 'shopify')
101
+ await shopify(root, databaseUrl);
102
+ else
103
+ throw new ConfigError([`the migration command takes shopify or nothing, not ${art.join(' ')}`]);
104
+ }
105
+ catch (error) {
106
+ if (error instanceof ConfigError)
107
+ log.error('migrate: not started', { problems: error.problems });
108
+ else
109
+ log.error('migrate: failed', { error });
110
+ await exitAfterLog(1);
111
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,38 @@
1
+ #!/usr/bin/env node
2
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
3
+ import path from 'node:path';
4
+ import { SHOPIFY_STARTING_POINTS, SHOPIFY_STUBS, STARTING_POINTS, STUBS } from '../stubs.js';
5
+ function write(root, file, content, note) {
6
+ const target = path.join(root, file);
7
+ mkdirSync(path.dirname(target), { recursive: true });
8
+ writeFileSync(target, content);
9
+ process.stdout.write(`wrote ${file}${note}\n`);
10
+ }
11
+ const root = process.cwd();
12
+ const art = process.argv.slice(2);
13
+ const files = art.length === 0
14
+ ? { stubs: STUBS, starting: STARTING_POINTS }
15
+ : art.length === 1 && art[0] === 'shopify'
16
+ ? { stubs: SHOPIFY_STUBS, starting: SHOPIFY_STARTING_POINTS }
17
+ : null;
18
+ if (files === null) {
19
+ process.stderr.write(`baukasten-stubs: it takes shopify or nothing, not ${art.join(' ')}\n`);
20
+ process.exitCode = 1;
21
+ }
22
+ else if (!existsSync(path.join(root, 'package.json'))) {
23
+ process.stderr.write('baukasten-stubs: no package.json here; run it in the root of the product\n');
24
+ process.exitCode = 1;
25
+ }
26
+ else {
27
+ for (const [file, content] of Object.entries(files.stubs)) {
28
+ const target = path.join(root, file);
29
+ if (!existsSync(target) || readFileSync(target, 'utf8') !== content) {
30
+ write(root, file, content, '');
31
+ }
32
+ }
33
+ for (const [file, content] of Object.entries(files.starting)) {
34
+ if (!existsSync(path.join(root, file))) {
35
+ write(root, file, content, ', a starting point: fill it in');
36
+ }
37
+ }
38
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,62 @@
1
+ #!/usr/bin/env node
2
+ // Lists or, with --delete, deletes the accounts from the test mode of Stripe once the product runs
3
+ // with the live key, in its container and with its environment; it never touches Stripe.
4
+ import { createDb } from '../server/adapter/db.js';
5
+ import { log } from '../server/adapter/log.js';
6
+ import { ConfigError, databaseUrlOf, stripeModeOf } from '../server/config.js';
7
+ import { deleteTestAccounts, testAccounts } from '../server/test-accounts.js';
8
+ const COMMAND = 'node node_modules/@tollwyn/baukasten/dist/bin/test-accounts.js';
9
+ function say(line) {
10
+ process.stdout.write(`${line}\n`);
11
+ }
12
+ function refuse(reason) {
13
+ process.stderr.write(`test-accounts: ${reason}\n`);
14
+ process.exitCode = 1;
15
+ }
16
+ async function run(args) {
17
+ if (args.length > 1 || (args.length === 1 && args[0] !== '--delete')) {
18
+ return refuse(`usage: ${COMMAND} [--delete]`);
19
+ }
20
+ const key = (process.env.STRIPE_SECRET_KEY ?? '').trim();
21
+ if (key === '' || stripeModeOf(key) !== 'live') {
22
+ return refuse('STRIPE_SECRET_KEY is no live key: every account is from the test mode');
23
+ }
24
+ const db = createDb(databaseUrlOf(process.env), log);
25
+ try {
26
+ if (args[0] === '--delete') {
27
+ const deleted = await deleteTestAccounts(db);
28
+ say(`Deleted accounts from the test mode: ${deleted.accounts}, their invoices: ${deleted.invoices}, their purchases: ${deleted.purchases}.`);
29
+ return;
30
+ }
31
+ const accounts = await testAccounts(db);
32
+ if (accounts.length === 0)
33
+ return say('No accounts from the test mode.');
34
+ say(`Accounts from the test mode: ${accounts.length}.`);
35
+ for (const account of accounts) {
36
+ say([
37
+ account.id,
38
+ // A deleted account keeps its row without the address.
39
+ account.email ?? '(deleted)',
40
+ `created ${account.createdAt.toISOString()}`,
41
+ `via ${account.via}`,
42
+ `subscriptions ${account.subscriptions.length === 0 ? 'none' : account.subscriptions.join(', ')}`,
43
+ `invoices ${account.invoices}`,
44
+ `purchases ${account.purchases}`
45
+ ].join(' '));
46
+ }
47
+ say(`Delete them, and everything on them, with: ${COMMAND} --delete`);
48
+ }
49
+ finally {
50
+ await db.close();
51
+ }
52
+ }
53
+ try {
54
+ await run(process.argv.slice(2));
55
+ }
56
+ catch (error) {
57
+ refuse(error instanceof ConfigError
58
+ ? error.problems.join('; ')
59
+ : error instanceof Error
60
+ ? error.message
61
+ : String(error));
62
+ }
@@ -0,0 +1,20 @@
1
+ /** What the check reads of the package.json of the installed baukasten. */
2
+ export interface InstalledBaukasten {
3
+ name: string;
4
+ version: string;
5
+ dependencies: Readonly<Record<string, string>>;
6
+ peerDependencies: Readonly<Record<string, string>>;
7
+ }
8
+ /** The files of the product, by their path from its root. */
9
+ export interface ProductFiles {
10
+ /** A file, or undefined where there is none. */
11
+ read(path: string): string | undefined;
12
+ /** The names in a folder, as the migration reads them; none where there is no folder. */
13
+ names(folder: string): readonly string[];
14
+ }
15
+ /** The migration command of a Shopify app in its delivery manifest. */
16
+ export declare const SHOPIFY_MIGRATION: string[];
17
+ /** Whether `version` lies in the caret range `range` from 1.0.0 on; a pre-release lies in none. */
18
+ export declare function fits(version: string, range: string): boolean;
19
+ /** What keeps the product from fitting the installed baukasten; empty when it fits. */
20
+ export declare function productProblems(files: ProductFiles, baukasten: InstalledBaukasten): string[];