@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,123 @@
1
+ import { heading, lettered, link, list, longDate, mail, note, paragraph, strong } from '../../server/legal/document.js';
2
+ import { FLEET } from '../../server/legal/fleet.js';
3
+ import { APP_PROCESSORS, SHOPIFY } from './shopify.js';
4
+ const { cloudflare, hoster, neon, webServer } = FLEET;
5
+ const CONTRACT = 'Erfüllung des Vertrags, Art. 6 Abs. 1 lit. b DSGVO';
6
+ const TRANSFER = {
7
+ adequate: 'Land mit angemessenem Schutz',
8
+ dpf: 'unter dem Data Privacy Framework zertifiziert',
9
+ scc: 'mit den Standardvertragsklauseln der EU, vom EDÖB anerkannt'
10
+ };
11
+ const BROWSER = {
12
+ 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.`
13
+ };
14
+ const EDGE = {
15
+ worldwideUnderScc: 'im Rechenzentrum, das Ihnen am nächsten liegt und ausserhalb der Schweiz und der EU stehen kann; dort gelten die Standardvertragsklauseln seines Auftragsbearbeitungsvertrags'
16
+ };
17
+ const ACCESS_LOG = {
18
+ none: 'Unser Webserver führt kein Zugriffsprotokoll'
19
+ };
20
+ const ERROR_LOG = {
21
+ 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'
22
+ };
23
+ const NEW_VERSION = {
24
+ removed: 'es fällt ausserdem bei jedem Start einer neuen Version weg',
25
+ stays: 'beim Start einer neuen Version bleibt es'
26
+ };
27
+ /** The sections the text points to. */
28
+ const ON_BEHALF = 'Bearbeitung in Ihrem Auftrag';
29
+ const HOW_LONG = 'Wie lange';
30
+ const who = (f) => `${f.operator.name}, ${f.operator.address}`;
31
+ export function de(f) {
32
+ const { data, recipients } = f.privacy;
33
+ const processors = [
34
+ ...APP_PROCESSORS,
35
+ ...recipients
36
+ .filter((recipient) => recipient.role === 'processor')
37
+ .map((recipient) => recipient.name)
38
+ ];
39
+ return {
40
+ title: 'Datenschutzerklärung',
41
+ blocks: [
42
+ note(`Stand: ${longDate(f.version, 'de')}.`),
43
+ paragraph(`Diese Erklärung beschreibt, welche Personendaten ${f.name} bearbeitet, wozu, wo und wie lange. Sie folgt dem Bundesgesetz über den Datenschutz (DSG) und, wo sie anwendbar ist, der Datenschutz-Grundverordnung der EU (DSGVO).`),
44
+ paragraph(`${f.name}, unter ${f.origin}, ist eine App für Shopify-Shops. ${f.privacy.service} ${f.name} richtet sich nur an Unternehmen, öffentliche Stellen und Selbständige für ihre berufliche Tätigkeit, nicht an Konsumenten.`),
45
+ paragraph(`${f.name} wird betrieben von ${who(f)} («wir»), erreichbar unter `, mail(FLEET.mailbox), `. Für die Daten über Ihren Shop als unseren Kunden sind wir verantwortlich. Für die Personendaten, die ${f.name} für Sie bearbeitet, etwa die Ihrer Kunden, sind Sie verantwortlich, und wir bearbeiten sie in Ihrem Auftrag (siehe «${ON_BEHALF}»).`),
46
+ heading('Was wir bearbeiten, wozu und gestützt worauf'),
47
+ list([
48
+ strong('Ihr Shop'),
49
+ `: seine dauerhafte myshopify.com-Domain, die IDs Ihres Shops und von ${f.name} bei Shopify, die E-Mail-Adresse Ihres Shops bei Shopify (die seines Inhabers, an die wir die Anfragen nach Daten Ihrer Kunden schicken, die Shopify an uns weitergibt), die Berechtigungen (Scopes), die Sie ${f.name} erteilt haben, ob es sich um einen Entwicklungsshop handelt, wann ${f.name} darin installiert und wann es entfernt wurde, das Offline-Zugriffstoken und das Refresh-Token, die Shopify ${f.name} gibt (verschlüsselt gespeichert), und der Plan von ${f.name} für Ihren Shop, wie ihn die Partner-API von Shopify meldet, mit dem Ende seiner Testphase – um ${f.name} für Ihren Shop zu betreiben und seinen Plan zu kennen (${CONTRACT}).`
50
+ ], [
51
+ strong('Abrechnung'),
52
+ `: Shopify rechnet ${f.name} mit Ihnen ab und bearbeitet Ihre Zahlungsangaben; von Shopify erhalten wir nur den Plan Ihres Shops und den Stand seines Abos (${CONTRACT}).`
53
+ ], [
54
+ strong('Sicherheit und Betrieb'),
55
+ `: Unsere Anwendung protokolliert jede Anfrage (Methode, Pfad ohne Parameter, Status, Dauer) und Ereignisse des Dienstes mit der Domain Ihres Shops, ohne IP-Adressen, E-Mail-Adressen, Tokens 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. Wo wir Anfragen begrenzen, zählen wir sie je Shop im Arbeitsspeicher der Anwendung, nie auf der Festplatte; 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).`
56
+ ], [
57
+ strong('E-Mails an uns'),
58
+ ': 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).'
59
+ ], ...data.map((item) => [
60
+ strong(item.name),
61
+ `: ${item.what}, aus ${item.source} – ${item.purpose} (in Ihrem Auftrag).`
62
+ ])),
63
+ paragraph(strong('Cookies'), `: ${f.name} selbst setzt kein Cookie und speichert nichts in Ihrem Browser. Es läuft in der Verwaltung Ihres Shopify-Shops, die es lädt, und seine Seiten laden App Bridge und Polaris von den Servern von Shopify (cdn.shopify.com), wie Shopify es von Apps in seiner Verwaltung verlangt. ${BROWSER[cloudflare.browser]} Wir verwenden keine Werbe- oder Analyse-Tracker, verkaufen keine Daten und entscheiden nicht automatisiert über Sie.`),
64
+ heading('Wer Daten erhält und wo'),
65
+ list([
66
+ `Shopify, wo Ihr Shop liegt und das seine Daten nach Ihrem eigenen Vertrag mit Shopify bearbeitet: ${f.name} liest die Daten Ihres Shops dort und schreibt dorthin zurück, was Sie es tun lassen; die Partner-API von Shopify nennt uns den Plan Ihres Shops, und Shopify rechnet ${f.name} mit Ihnen ab. Was Shopify selbst bearbeitet, und wo, beschreibt es unter `,
67
+ link(SHOPIFY.privacy, `https://${SHOPIFY.privacy}`),
68
+ '.'
69
+ ], [
70
+ `${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.`
71
+ ], [
72
+ `Anwendungsserver: ${hoster.name} (${hoster.country.de}), in deren Rechenzentren in ${hoster.dataCentres.de}; der Server ist gemietet und wird vom Betreiber verwaltet.`
73
+ ], [
74
+ `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.`
75
+ ], [
76
+ `E-Mail: ${FLEET.mail.name} (${FLEET.mail.country.de}) versendet unsere E-Mails und führt unser Postfach.`
77
+ ], ...recipients.map((recipient) => [
78
+ `${recipient.name} (${recipient.place}; ${TRANSFER[recipient.transfer]}): ${recipient.purpose}.`
79
+ ])),
80
+ 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.`),
81
+ heading(HOW_LONG),
82
+ list([
83
+ `Ihr Shop und alles, was ${f.name} für ihn hält, bleiben, solange ${f.name} installiert ist. Entfernen Sie ${f.name}, werden die Tokens Ihres Shops sofort gelöscht; ${SHOPIFY.redactHours} Stunden später verlangt Shopify von uns, die Daten Ihres Shops zu löschen (shop/redact), und dann werden sie sofort gelöscht.`
84
+ ], [
85
+ `Was Shopify ${f.name} über Ihren Shop als Webhook schickt, bleibt, bis ${f.name} es angewendet hat, was bis zu einem Tag dauern kann, wenn es erneut versucht werden muss; danach bleiben nur seine ID, sein Thema und sein Zeitpunkt, eine Woche lang. Eines, das sich nicht anwenden lässt, bleibt, bis wir den Fehler behoben haben oder die Daten Ihres Shops gelöscht werden.`
86
+ ], [
87
+ `Daten eines Kunden, die ${f.name} hält, werden gelöscht, wenn Shopify seine Anfrage weitergibt, sie zu löschen (customers/redact).`
88
+ ], ...data.map((item) => [strong(item.name), `: ${item.kept}.`]), [
89
+ `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.`
90
+ ], [
91
+ `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.`
92
+ ], [
93
+ 'Die Zähler je Shop gelten höchstens eine Stunde, liegen nur im Arbeitsspeicher und sind spätestens beim Neustart der Anwendung vergessen.'
94
+ ], [
95
+ 'E-Mails an uns: so lange, wie der Austausch es braucht, und bei Kunden so lange, wie Vertrag und Buchführungsrecht es verlangen.'
96
+ ]),
97
+ heading('Anfragen Ihrer Kunden'),
98
+ paragraph(`Kunden Ihres Shops wenden sich mit Anfragen zu ihren Daten an Sie als Verantwortlichen. Fragt ein Kunde nach seinen Daten, gibt Shopify die Anfrage an ${f.name} weiter (customers/data_request), und wir schicken Ihnen per E-Mail an die Adresse Ihres Shops, was ${f.name} über diesen Kunden hält, für Ihre Antwort. Gibt Shopify die Anfrage eines Kunden weiter, seine Daten zu löschen (customers/redact), führen wir sie aus, wie es «${HOW_LONG}» beschreibt.`),
99
+ heading('Ihre Rechte'),
100
+ 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. Schreiben Sie dafür 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.'),
101
+ heading(ON_BEHALF),
102
+ paragraph(`Für die Personendaten, die ${f.name} für Sie bearbeitet, etwa die Ihrer Kunden, sind Sie verantwortlich und wir Ihr Auftragsbearbeiter (Art. 9 DSG, Art. 28 DSGVO). Wir bearbeiten diese Daten nur, um Ihnen ${f.name} zu erbringen. Wir:`),
103
+ lettered([
104
+ `bearbeiten sie nur nach Ihren dokumentierten Weisungen, das sind die Installation von ${f.name} in Ihrem Shop, Ihre Einstellungen darin und diese Erklärung, und sagen es Ihnen, wenn wir eine Weisung für datenschutzwidrig halten;`
105
+ ], ['verpflichten alle, die Zugang zu ihnen haben, zur Vertraulichkeit;'], [
106
+ 'schützen sie mit angemessenen technischen und organisatorischen Massnahmen, darunter verschlüsselte Verbindungen, verschlüsselte Tokens und Sicherungen, einen Zugang, der auf den Betreiber beschränkt ist, und Testdaten, die von den Produktionsdaten getrennt bleiben;'
107
+ ], [
108
+ `setzen als Unterauftragsbearbeiter nur die oben genannten ein (${processors.join('; ')}), denen Sie mit der Installation von ${f.name} zustimmen, und verpflichten sie auf gleichwertige Datenschutzpflichten; einen neuen kündigen wir 30 Tage im Voraus per E-Mail an die Adresse Ihres Shops an, und Sie können widersprechen, indem Sie ${f.name} vor seinem Einsatz entfernen;`
109
+ ], [
110
+ '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;'
111
+ ], [
112
+ 'melden Ihnen ohne unangemessene Verzögerung eine Verletzung der Datensicherheit, die diese Daten betrifft;'
113
+ ], [
114
+ `löschen sie, nachdem Sie ${f.name} entfernt haben, wie es «${HOW_LONG}» beschreibt, ausser das Gesetz verlangt, sie aufzubewahren; Kopien in der Historie der Datenbank und in unseren Sicherungen enden, wie es dort beschrieben ist;`
115
+ ], [
116
+ '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.'
117
+ ]),
118
+ paragraph('Übermittlungen in die oben genannten Länder erfolgen mit den dort genannten Garantien.'),
119
+ heading('Änderungen'),
120
+ paragraph('Es gilt die hier veröffentlichte Fassung. Über wichtige Änderungen informieren wir die Shops per E-Mail.')
121
+ ]
122
+ };
123
+ }
@@ -0,0 +1,3 @@
1
+ import type { LegalData } from '../../ui/data.js';
2
+ import type { ShopifyLegalFacts } from './index.js';
3
+ export declare function en(f: ShopifyLegalFacts): LegalData;
@@ -0,0 +1,121 @@
1
+ import { heading, lettered, link, list, longDate, mail, note, paragraph, strong } from '../../server/legal/document.js';
2
+ import { FLEET } from '../../server/legal/fleet.js';
3
+ import { APP_PROCESSORS, SHOPIFY } from './shopify.js';
4
+ const { cloudflare, hoster, neon, webServer } = FLEET;
5
+ const CONTRACT = 'performance of the contract, Art. 6(1)(b) GDPR';
6
+ const TRANSFER = {
7
+ adequate: 'a country with adequate protection',
8
+ dpf: 'certified under the Data Privacy Framework',
9
+ scc: 'under the EU standard contractual clauses, as recognised by the FDPIC'
10
+ };
11
+ const BROWSER = {
12
+ networkErrorLogging: `${cloudflare.name} also has your browser store an instruction to report failed connections (Network Error Logging). Clearing cookies and site data in your browser removes this instruction too.`
13
+ };
14
+ const EDGE = {
15
+ worldwideUnderScc: 'in the data centre nearest to you, which can be outside Switzerland and the EU; there the standard contractual clauses of its data processing addendum apply'
16
+ };
17
+ const ACCESS_LOG = {
18
+ none: 'Our web server keeps no access log'
19
+ };
20
+ const ERROR_LOG = {
21
+ requestWithoutAddress: 'only a request it cannot pass on to the application it writes to its error log, without your IP address and without parameters or headers'
22
+ };
23
+ const NEW_VERSION = {
24
+ removed: 'it is also removed whenever a new version starts',
25
+ stays: 'it stays when a new version starts'
26
+ };
27
+ /** The sections the text points to. */
28
+ const ON_BEHALF = 'Processing on your behalf';
29
+ const HOW_LONG = 'How long';
30
+ const who = (f) => `${f.operator.name}, ${f.operator.address}`;
31
+ export function en(f) {
32
+ const { data, recipients } = f.privacy;
33
+ const processors = [
34
+ ...APP_PROCESSORS,
35
+ ...recipients
36
+ .filter((recipient) => recipient.role === 'processor')
37
+ .map((recipient) => recipient.name)
38
+ ];
39
+ return {
40
+ title: 'Privacy policy',
41
+ blocks: [
42
+ note(`Version of ${longDate(f.version, 'en')}.`),
43
+ paragraph(`This policy explains which personal data ${f.name} processes, why, where and for how long. It follows the Swiss Federal Act on Data Protection (FADP) and, where it applies, the EU General Data Protection Regulation (GDPR).`),
44
+ paragraph(`${f.name}, at ${f.origin}, is an app for Shopify stores. ${f.privacy.service} ${f.name} is offered only to businesses, public bodies and the self-employed for their professional activity, not to consumers.`),
45
+ paragraph(`${f.name} is operated by ${who(f)} (“we”), reachable at `, mail(FLEET.mailbox), `. For the data about your store as our customer, we are the controller. For the personal data ${f.name} processes for you, such as those of your customers, you are the controller and we process them on your behalf (see “${ON_BEHALF}”).`),
46
+ heading('What we process, why, and on what basis'),
47
+ list([
48
+ strong('Your store'),
49
+ `: its permanent myshopify.com domain, the ids of your store and of ${f.name} at Shopify, the email address of your store at Shopify (that of its owner, to which we send the requests for data of your customers that Shopify passes on to us), the access scopes you granted ${f.name}, whether it is a development store, when ${f.name} was installed in it and when it was removed, the offline access token and refresh token Shopify gives ${f.name} (stored encrypted), and the plan of ${f.name} for your store as Shopify’s Partner API reports it, with the end of its trial — to run ${f.name} for your store and to know its plan (${CONTRACT}).`
50
+ ], [
51
+ strong('Billing'),
52
+ `: Shopify bills you for ${f.name} and processes your payment details; from Shopify we receive only the plan of your store and the state of its subscription (${CONTRACT}).`
53
+ ], [
54
+ strong('Security and operations'),
55
+ `: our application logs each request (method, path without parameters, status, duration) and events of the service with the domain of your store, without IP addresses, email addresses, tokens or keys. ${ACCESS_LOG[webServer.accessLog]}; ${ERROR_LOG[webServer.errorLog.content]}. Our mail relay logs the recipient address and the delivery status of each email we send and keeps them for at most ${FLEET.relayLogDays} days. Where we limit requests, we count them per store in the memory of the application, never on disk; a count applies for at most one hour and is forgotten at the latest when the application restarts (legitimate interest in a secure and working service, Art. 6(1)(f) GDPR).`
56
+ ], [
57
+ strong('Emails to us'),
58
+ ': when you write to us, we keep your message and address to answer you and to keep a record of the exchange (legitimate interest; for customers, performance of the contract).'
59
+ ], ...data.map((item) => [
60
+ strong(item.name),
61
+ `: ${item.what}, from ${item.source} — ${item.purpose} (on your behalf).`
62
+ ])),
63
+ paragraph(strong('Cookies'), `: ${f.name} itself sets no cookie and stores nothing in your browser. It runs inside the Shopify admin, which loads it, and its pages load Shopify’s App Bridge and Polaris from Shopify’s servers (cdn.shopify.com), as Shopify requires of apps in its admin. ${BROWSER[cloudflare.browser]} We use no advertising or analytics trackers, sell no data, and make no decisions about you by automated means.`),
64
+ heading('Who receives data, and where'),
65
+ list([
66
+ `Shopify, where your store is and which processes its data under your own agreement with Shopify: ${f.name} reads the data of your store there and writes back there what you have it do; Shopify’s Partner API tells us the plan of your store, and Shopify bills you for ${f.name}. For what Shopify processes itself, and where, see `,
67
+ link(SHOPIFY.privacy, `https://${SHOPIFY.privacy}`),
68
+ '.'
69
+ ], [
70
+ `${cloudflare.name} (${cloudflare.country.en}; ${TRANSFER[cloudflare.transfer]}) runs in front of ${f.name}: it terminates the encrypted connection, passes requests on and filters attacks. It therefore sees every request, including your IP address, ${EDGE[cloudflare.edge]}. It also stores the encrypted backups of our database (Cloudflare R2, ${cloudflare.backups.where.en}), which it cannot read.`
71
+ ], [
72
+ `Application server: ${hoster.name} (${hoster.country.en}), in its data centres in ${hoster.dataCentres.en}; the server is rented and administered by the operator.`
73
+ ], [
74
+ `Database: ${neon.name} (a ${neon.owner} company, ${neon.country.en}; ${TRANSFER[neon.transfer]}), stored in ${neon.cloud} region ${neon.region}, ${neon.place.en}; access from the provider’s country is possible.`
75
+ ], [
76
+ `Email: ${FLEET.mail.name} (${FLEET.mail.country.en}) sends our emails and keeps our mailbox.`
77
+ ], ...recipients.map((recipient) => [
78
+ `${recipient.name} (${recipient.place}; ${TRANSFER[recipient.transfer]}): ${recipient.purpose}.`
79
+ ])),
80
+ paragraph(`Data may therefore be processed in the countries named above and, for ${cloudflare.name}, in the country of the data centre that answers you. Each recipient outside Switzerland and the EU is named above with the safeguard that covers it.`),
81
+ heading(HOW_LONG),
82
+ list([
83
+ `Your store and everything ${f.name} keeps for it stay while ${f.name} is installed. When you remove ${f.name}, the tokens of your store are deleted at once; ${SHOPIFY.redactHours} hours later Shopify asks us to delete the data of your store (shop/redact), and then they are deleted at once.`
84
+ ], [
85
+ `What Shopify sends ${f.name} about your store as a webhook is kept until ${f.name} has applied it, which can take up to a day when it must be tried again; then only its id, its topic and its time stay, for a week. One that cannot be applied stays until we have solved the failure, or until the data of your store are deleted.`
86
+ ], [
87
+ `Data of a customer that ${f.name} keeps are deleted when Shopify passes on the customer’s request to delete them (customers/redact).`
88
+ ], ...data.map((item) => [strong(item.name), `: ${item.kept}.`]), [
89
+ `Our database provider keeps a history for restoring the database to an earlier point in time for at most ${neon.historyDays} days, and our encrypted backups of it are deleted after about ${cloudflare.backups.days} days; deleted data leave both within that time.`
90
+ ], [
91
+ `The application log rotates by size, in ${FLEET.appLog.files} files of ${FLEET.appLog.megabytes} MB, and is overwritten as new entries come in; ${NEW_VERSION[FLEET.appLog.newVersion]}. The error log of the web server rotates by size as well, in ${webServer.errorLog.files} files of ${webServer.errorLog.megabytes} MB, and is overwritten as new entries come in; ${NEW_VERSION[webServer.errorLog.newVersion]}. The mail relay log is kept for at most ${FLEET.relayLogDays} days.`
92
+ ], [
93
+ 'The counts per store apply for at most one hour, are kept in memory only and are forgotten at the latest when the application restarts.'
94
+ ], [
95
+ 'Emails to us: as long as the exchange needs it and, for customers, as long as the contract and accounting law require.'
96
+ ]),
97
+ heading('Requests of your customers'),
98
+ paragraph(`Customers of your store turn to you, as the controller, with requests about their data. When a customer asks for their data, Shopify passes the request on to ${f.name} (customers/data_request), and we send you by email, to the address of your store, what ${f.name} keeps about that customer, for your answer. When Shopify passes on a customer’s request to delete their data (customers/redact), we carry it out as “${HOW_LONG}” describes.`),
99
+ heading('Your rights'),
100
+ paragraph('You may ask what we hold about you and receive it in a common electronic format, have it corrected or deleted, and have its processing restricted. You may object at any time to processing based on our legitimate interest. For this, write to ', mail(FLEET.mailbox), '. We answer within 30 days. You may complain to the Federal Data Protection and Information Commissioner (FDPIC) or, in the EU, to the data protection authority of your country.'),
101
+ heading(ON_BEHALF),
102
+ paragraph(`For the personal data ${f.name} processes for you, such as those of your customers, you are the controller and we are your processor (Art. 9 FADP, Art. 28 GDPR). We process these data only to provide ${f.name} to you. We:`),
103
+ lettered([
104
+ `process them only on your documented instructions, which are installing ${f.name} in your store, your settings in it and this policy, and tell you if we think an instruction breaks data protection law;`
105
+ ], ['bind everyone who has access to them to confidentiality;'], [
106
+ 'protect them with appropriate technical and organisational measures, among them encrypted connections, encrypted tokens and backups, access limited to the operator, and test data kept apart from production data;'
107
+ ], [
108
+ `use as sub-processors only those named above (${processors.join('; ')}), which you approve by installing ${f.name}, and bind them to data protection duties equivalent to these; we announce a new one 30 days in advance by email to the address of your store, and you may object by removing ${f.name} before it starts;`
109
+ ], [
110
+ 'help you, as far as we can, to answer requests of data subjects and to meet your duties on security, breach notification and impact assessments;'
111
+ ], ['tell you without undue delay of a breach of security that concerns these data;'], [
112
+ `delete them after you remove ${f.name}, as “${HOW_LONG}” describes, unless the law requires us to keep them; copies in the history of the database and in our backups end as described there;`
113
+ ], [
114
+ 'give you the information you need to show that these duties are met, and allow audits by you or an auditor bound to confidentiality, with reasonable notice and at your cost.'
115
+ ]),
116
+ paragraph('Transfers to the countries named above are made under the safeguards named there.'),
117
+ heading('Changes'),
118
+ paragraph('The version published here applies. We tell stores about important changes by email.')
119
+ ]
120
+ };
121
+ }
@@ -0,0 +1,19 @@
1
+ import type { Language } from '../../product.js';
2
+ import type { LegalData } from '../../ui/data.js';
3
+ import type { ShopifyPrivacy } from '../app.js';
4
+ export interface ShopifyLegalFacts {
5
+ language: Language;
6
+ name: string;
7
+ /** Where the app is reached, its PUBLIC_ORIGIN. */
8
+ origin: string;
9
+ operator: {
10
+ name: string;
11
+ address: string;
12
+ };
13
+ privacy: ShopifyPrivacy;
14
+ /** The day of the text, YYYY-MM-DD: the later of SHOPIFY_PRIVACY_DATE and the app's privacyDate. */
15
+ version: string;
16
+ }
17
+ /** The day the baukasten's part of the text last changed; a change of the texts raises it. */
18
+ export declare const SHOPIFY_PRIVACY_DATE = "2026-10-09";
19
+ export declare function shopifyPrivacy(facts: ShopifyLegalFacts): LegalData;
@@ -0,0 +1,8 @@
1
+ import { de } from './de.js';
2
+ import { en } from './en.js';
3
+ /** The day the baukasten's part of the text last changed; a change of the texts raises it. */
4
+ export const SHOPIFY_PRIVACY_DATE = '2026-10-09';
5
+ const TEXTS = { en, de };
6
+ export function shopifyPrivacy(facts) {
7
+ return TEXTS[facts.language](facts);
8
+ }
@@ -0,0 +1,16 @@
1
+ export declare const SHOPIFY: {
2
+ /**
3
+ * Where Shopify says what it processes, under the agreement of each merchant with Shopify and by
4
+ * which of its companies, depending on where the store is: Shopify is the platform of the merchant,
5
+ * not a recipient of the operator.
6
+ */
7
+ readonly privacy: "shopify.com/legal/privacy";
8
+ /** Shopify sends shop/redact this long after a store removed the app. */
9
+ readonly redactHours: 48;
10
+ };
11
+ /**
12
+ * The providers of the fleet that process data for an app, which the merchant approves as
13
+ * sub-processors: not Stripe, since Shopify bills the app. Should one stop being a processor of the
14
+ * operator, this does not compile until the text follows.
15
+ */
16
+ export declare const APP_PROCESSORS: ("Cloudflare, Inc." | "Hetzner Online GmbH" | "Neon, LLC" | "Infomaniak Network SA")[];
@@ -0,0 +1,21 @@
1
+ // The facts about Shopify that the privacy text of every Shopify app states, besides those of the
2
+ // fleet (server/legal/fleet.ts), and the only place of them. They come from Shopify: its privacy
3
+ // policy (SHOPIFY.privacy) and its documentation of the privacy webhooks; a change there is a change
4
+ // here.
5
+ import { FLEET } from '../../server/legal/fleet.js';
6
+ export const SHOPIFY = {
7
+ /**
8
+ * Where Shopify says what it processes, under the agreement of each merchant with Shopify and by
9
+ * which of its companies, depending on where the store is: Shopify is the platform of the merchant,
10
+ * not a recipient of the operator.
11
+ */
12
+ privacy: 'shopify.com/legal/privacy',
13
+ /** Shopify sends shop/redact this long after a store removed the app. */
14
+ redactHours: 48
15
+ };
16
+ /**
17
+ * The providers of the fleet that process data for an app, which the merchant approves as
18
+ * sub-processors: not Stripe, since Shopify bills the app. Should one stop being a processor of the
19
+ * operator, this does not compile until the text follows.
20
+ */
21
+ export const APP_PROCESSORS = [FLEET.cloudflare, FLEET.hoster, FLEET.neon, FLEET.mail].map((provider) => provider.name);
@@ -0,0 +1,4 @@
1
+ export declare const SHOPIFY_MIGRATIONS: readonly {
2
+ name: string;
3
+ sql: string;
4
+ }[];
@@ -0,0 +1,67 @@
1
+ // The list of a Shopify app replaces the accounts and Stripe of a web app: a database belongs to an
2
+ // app of one art, so the two lists never meet. Each is forward-only and never changes once released
3
+ // (migrations.test.ts).
4
+ import { BAUKASTEN_MIGRATIONS } from '../server/baukasten-migrations.js';
5
+ const [productMigrations] = BAUKASTEN_MIGRATIONS;
6
+ if (productMigrations === undefined)
7
+ throw new Error('the baukasten lists no migration');
8
+ export const SHOPIFY_MIGRATIONS = [
9
+ productMigrations,
10
+ {
11
+ // The shops that installed the app: shop/redact deletes a shop with all of the app that refers
12
+ // to it. Tokens are sealed (shopify/seal.ts); the plan is what the Partner API said when it was
13
+ // last asked. The webhooks wait here until the run of the baukasten applies them, each once.
14
+ name: '002_shops.sql',
15
+ sql: `CREATE TABLE baukasten.shops (
16
+ domain text PRIMARY KEY CHECK (domain ~ '^[a-z0-9][a-z0-9-]*\\.myshopify\\.com$'),
17
+ shop_id text NOT NULL CHECK (shop_id LIKE 'gid://shopify/Shop/%'),
18
+ app_id text NOT NULL CHECK (app_id LIKE 'gid://shopify/App/%'),
19
+ -- The address of the shop, where requests about the data of its customers go.
20
+ email text NOT NULL,
21
+ -- A development store of a partner never pays and never counts.
22
+ development boolean NOT NULL,
23
+ -- The scopes the tokens were granted, as Shopify listed them.
24
+ scope text NOT NULL,
25
+ access_token text,
26
+ access_expires_at timestamptz,
27
+ refresh_token text,
28
+ refresh_expires_at timestamptz,
29
+ installed_at timestamptz NOT NULL DEFAULT now(),
30
+ uninstalled_at timestamptz,
31
+ -- When the token exchange last gave it tokens: an app/uninstalled triggered before belongs to an
32
+ -- installation before this one.
33
+ exchanged_at timestamptz NOT NULL,
34
+ -- The handles of the active subscription (empty without one), its trial, and when it was asked.
35
+ plan_handles text[] NOT NULL DEFAULT '{}',
36
+ plan_trial_ends_at timestamptz,
37
+ plan_checked_at timestamptz,
38
+ CHECK ((access_token IS NULL) = (access_expires_at IS NULL)),
39
+ CHECK ((refresh_token IS NULL) = (refresh_expires_at IS NULL)),
40
+ CHECK (uninstalled_at IS NULL OR (access_token IS NULL AND refresh_token IS NULL))
41
+ );
42
+
43
+ -- Every webhook Shopify sent, by the id it gives each one: its shop and payload until it is applied,
44
+ -- which may hold personal data, then only that it was, for a while; a webhook that keeps failing
45
+ -- gives up, loudly. shop/redact deletes all of its shop.
46
+ CREATE TABLE baukasten.shopify_webhooks (
47
+ id text PRIMARY KEY,
48
+ topic text NOT NULL,
49
+ shop text,
50
+ payload jsonb,
51
+ -- When Shopify says the event happened (X-Shopify-Triggered-At), else when it came.
52
+ triggered_at timestamptz NOT NULL,
53
+ received_at timestamptz NOT NULL DEFAULT now(),
54
+ attempts integer NOT NULL DEFAULT 0 CHECK (attempts >= 0),
55
+ next_at timestamptz NOT NULL DEFAULT now(),
56
+ applied_at timestamptz,
57
+ failed_at timestamptz,
58
+ CHECK ((applied_at IS NULL) = (payload IS NOT NULL)),
59
+ CHECK ((applied_at IS NULL) = (shop IS NOT NULL)),
60
+ CHECK (applied_at IS NULL OR failed_at IS NULL)
61
+ );
62
+ CREATE INDEX shopify_webhooks_shop ON baukasten.shopify_webhooks (shop);
63
+ CREATE INDEX shopify_webhooks_due ON baukasten.shopify_webhooks (next_at)
64
+ WHERE applied_at IS NULL AND failed_at IS NULL;
65
+ `
66
+ }
67
+ ];
@@ -0,0 +1,45 @@
1
+ import type { Language } from '../product.js';
2
+ import type { LegalData } from '../ui/data.js';
3
+ import type { ShopifyTexts } from './texts.js';
4
+ /** Text into HTML, in an element or a quoted attribute. */
5
+ export declare function escapeHtml(text: string): string;
6
+ /** frame-ancestors for the admin of `shop`, or for none outside it. */
7
+ export declare function frameAncestors(shop: string | null): string;
8
+ /**
9
+ * App Bridge alone: it fetches an ID token and opens `shopify-reload` of the address with it, as the
10
+ * bounce page of Shopify's own library does.
11
+ */
12
+ export declare function bouncePage(options: {
13
+ clientId: string;
14
+ shop: string;
15
+ language: Language;
16
+ }): Response;
17
+ /**
18
+ * Leaves the frame of the admin for `url`, an address of Shopify such as the plan page: from inside
19
+ * the frame only App Bridge may open the top window.
20
+ */
21
+ export declare function topPage(options: {
22
+ clientId: string;
23
+ shop: string;
24
+ url: string;
25
+ language: Language;
26
+ nonce: string;
27
+ }): Response;
28
+ /** An address of the app opened outside the admin: where it is, and its privacy text. */
29
+ export declare function outsidePage(options: {
30
+ name: string;
31
+ language: Language;
32
+ texts: ShopifyTexts;
33
+ }): Response;
34
+ /** Shopify did not answer as it should, or the app is not linked yet: a 503 to try again. */
35
+ export declare function unavailablePage(options: {
36
+ language: Language;
37
+ texts: ShopifyTexts;
38
+ shop: string | null;
39
+ }): Response;
40
+ /** The privacy text as a page of its own, public, for the listing in the App Store. */
41
+ export declare function legalPage(options: {
42
+ name: string;
43
+ language: Language;
44
+ legal: LegalData;
45
+ }): Response;
@@ -0,0 +1,146 @@
1
+ import { APP_BRIDGE_URL, SHOPIFY_ADMIN, SHOPIFY_CDN, SHOPIFY_SCRIPTS } from './constants.js';
2
+ /** Text into HTML, in an element or a quoted attribute. */
3
+ export function escapeHtml(text) {
4
+ return text
5
+ .replaceAll('&', '&')
6
+ .replaceAll('<', '&lt;')
7
+ .replaceAll('>', '&gt;')
8
+ .replaceAll('"', '&quot;')
9
+ .replaceAll("'", '&#39;');
10
+ }
11
+ /** frame-ancestors for the admin of `shop`, or for none outside it. */
12
+ export function frameAncestors(shop) {
13
+ return shop === null
14
+ ? "frame-ancestors 'none'"
15
+ : `frame-ancestors https://${shop} ${SHOPIFY_ADMIN}`;
16
+ }
17
+ const STYLE = `body{font:16px/1.5 system-ui,sans-serif;margin:0 auto;max-width:42rem;padding:2rem 1rem;color:#1a1a1a}h1{font-size:1.5rem}h2{font-size:1.15rem;margin-top:2rem}.muted{color:#5c5c5c}`;
18
+ function document(options) {
19
+ // App Bridge may load more of the scripts of Shopify, as on the pages of the app.
20
+ const scripts = [
21
+ SHOPIFY_SCRIPTS,
22
+ ...(options.nonce === undefined ? [] : [`'nonce-${options.nonce}'`])
23
+ ].join(' ');
24
+ const policy = [
25
+ "default-src 'none'",
26
+ `script-src ${scripts}`,
27
+ `style-src 'unsafe-inline' ${SHOPIFY_CDN}`,
28
+ `img-src 'self' data: ${SHOPIFY_CDN}`,
29
+ `connect-src 'self' ${SHOPIFY_CDN}`,
30
+ "base-uri 'none'",
31
+ "form-action 'none'",
32
+ frameAncestors(options.shop)
33
+ ].join('; ');
34
+ const html = `<!doctype html>
35
+ <html lang="${options.language}">
36
+ <head>
37
+ <meta charset="utf-8">
38
+ <meta name="viewport" content="width=device-width, initial-scale=1">
39
+ <title>${escapeHtml(options.title)}</title>
40
+ ${options.head ?? ''}<style>${STYLE}</style>
41
+ </head>
42
+ <body>
43
+ ${options.body}
44
+ </body>
45
+ </html>
46
+ `;
47
+ return new Response(html, {
48
+ status: options.status ?? 200,
49
+ headers: {
50
+ 'content-type': 'text/html; charset=utf-8',
51
+ 'cache-control': 'no-store',
52
+ 'content-security-policy': policy
53
+ }
54
+ });
55
+ }
56
+ const appBridge = (clientId) => `<meta name="shopify-api-key" content="${escapeHtml(clientId)}">\n<script src="${APP_BRIDGE_URL}"></script>\n`;
57
+ /**
58
+ * App Bridge alone: it fetches an ID token and opens `shopify-reload` of the address with it, as the
59
+ * bounce page of Shopify's own library does.
60
+ */
61
+ export function bouncePage(options) {
62
+ return document({
63
+ language: options.language,
64
+ title: '',
65
+ head: appBridge(options.clientId),
66
+ body: '',
67
+ shop: options.shop
68
+ });
69
+ }
70
+ /**
71
+ * Leaves the frame of the admin for `url`, an address of Shopify such as the plan page: from inside
72
+ * the frame only App Bridge may open the top window.
73
+ */
74
+ export function topPage(options) {
75
+ return document({
76
+ language: options.language,
77
+ title: '',
78
+ head: appBridge(options.clientId),
79
+ body: `<script nonce="${options.nonce}">window.open(${JSON.stringify(options.url).replaceAll('<', '\\u003c')}, '_top')</script>`,
80
+ nonce: options.nonce,
81
+ shop: options.shop
82
+ });
83
+ }
84
+ /** An address of the app opened outside the admin: where it is, and its privacy text. */
85
+ export function outsidePage(options) {
86
+ const { name, texts } = options;
87
+ return document({
88
+ language: options.language,
89
+ title: texts.outside.title(name),
90
+ body: `<h1>${escapeHtml(texts.outside.title(name))}</h1>
91
+ <p>${escapeHtml(texts.outside.text(name))}</p>
92
+ <p><a href="/privacy">${escapeHtml(texts.outside.privacy)}</a></p>`,
93
+ shop: null
94
+ });
95
+ }
96
+ /** Shopify did not answer as it should, or the app is not linked yet: a 503 to try again. */
97
+ export function unavailablePage(options) {
98
+ const { texts } = options;
99
+ return document({
100
+ language: options.language,
101
+ title: texts.unavailable.title,
102
+ body: `<h1>${escapeHtml(texts.unavailable.title)}</h1>\n<p>${escapeHtml(texts.unavailable.text)}</p>`,
103
+ shop: options.shop,
104
+ status: 503
105
+ });
106
+ }
107
+ function inline(parts) {
108
+ return parts
109
+ .map((part) => {
110
+ if (typeof part === 'string')
111
+ return escapeHtml(part);
112
+ switch (part.kind) {
113
+ case 'strong':
114
+ return `<strong>${escapeHtml(part.text)}</strong>`;
115
+ case 'code':
116
+ return `<code>${escapeHtml(part.text)}</code>`;
117
+ case 'link':
118
+ return `<a href="${escapeHtml(part.href)}">${escapeHtml(part.text)}</a>`;
119
+ }
120
+ })
121
+ .join('');
122
+ }
123
+ /** The privacy text as a page of its own, public, for the listing in the App Store. */
124
+ export function legalPage(options) {
125
+ const { legal } = options;
126
+ const blocks = legal.blocks.map((block) => {
127
+ switch (block.kind) {
128
+ case 'heading':
129
+ return `<h2>${escapeHtml(block.text)}</h2>`;
130
+ case 'paragraph':
131
+ return `<p>${inline(block.parts)}</p>`;
132
+ case 'note':
133
+ return `<p class="muted">${inline(block.parts)}</p>`;
134
+ case 'list':
135
+ return `<ul>${block.items.map((item) => `<li>${inline(item)}</li>`).join('')}</ul>`;
136
+ case 'lettered':
137
+ return `<ol type="a">${block.items.map((item) => `<li>${inline(item)}</li>`).join('')}</ol>`;
138
+ }
139
+ });
140
+ return document({
141
+ language: options.language,
142
+ title: `${legal.title} – ${options.name}`,
143
+ body: `<h1>${escapeHtml(legal.title)}</h1>\n${blocks.join('\n')}`,
144
+ shop: null
145
+ });
146
+ }