@softure-ai/privacy 0.0.0-stage → 0.1.5

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 (161) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +272 -2
  3. package/dist/contract.d.ts +58 -0
  4. package/dist/contract.d.ts.map +1 -0
  5. package/dist/contract.js +2 -0
  6. package/dist/contract.js.map +1 -0
  7. package/dist/index.d.ts +90 -0
  8. package/dist/index.d.ts.map +1 -0
  9. package/dist/index.js +52 -0
  10. package/dist/index.js.map +1 -0
  11. package/dist/messages/en.d.ts +46 -0
  12. package/dist/messages/en.d.ts.map +1 -0
  13. package/dist/messages/en.js +46 -0
  14. package/dist/messages/en.js.map +1 -0
  15. package/dist/messages/index.d.ts +99 -0
  16. package/dist/messages/index.d.ts.map +1 -0
  17. package/dist/messages/index.js +14 -0
  18. package/dist/messages/index.js.map +1 -0
  19. package/dist/messages/pl.d.ts +3 -0
  20. package/dist/messages/pl.d.ts.map +1 -0
  21. package/dist/messages/pl.js +46 -0
  22. package/dist/messages/pl.js.map +1 -0
  23. package/dist/next/actions.d.ts +4 -0
  24. package/dist/next/actions.d.ts.map +1 -0
  25. package/dist/next/actions.js +57 -0
  26. package/dist/next/actions.js.map +1 -0
  27. package/dist/next/context.d.ts +4 -0
  28. package/dist/next/context.d.ts.map +1 -0
  29. package/dist/next/context.js +13 -0
  30. package/dist/next/context.js.map +1 -0
  31. package/dist/next/index.d.ts +5 -0
  32. package/dist/next/index.d.ts.map +1 -0
  33. package/dist/next/index.js +7 -0
  34. package/dist/next/index.js.map +1 -0
  35. package/dist/next/messages.d.ts +5 -0
  36. package/dist/next/messages.d.ts.map +1 -0
  37. package/dist/next/messages.js +7 -0
  38. package/dist/next/messages.js.map +1 -0
  39. package/dist/next/pages.d.ts +2 -0
  40. package/dist/next/pages.d.ts.map +1 -0
  41. package/dist/next/pages.js +22 -0
  42. package/dist/next/pages.js.map +1 -0
  43. package/dist/next/route.d.ts +6 -0
  44. package/dist/next/route.d.ts.map +1 -0
  45. package/dist/next/route.js +49 -0
  46. package/dist/next/route.js.map +1 -0
  47. package/dist/next/session-cookie.d.ts +3 -0
  48. package/dist/next/session-cookie.d.ts.map +1 -0
  49. package/dist/next/session-cookie.js +17 -0
  50. package/dist/next/session-cookie.js.map +1 -0
  51. package/dist/options.d.ts +27 -0
  52. package/dist/options.d.ts.map +1 -0
  53. package/dist/options.js +70 -0
  54. package/dist/options.js.map +1 -0
  55. package/dist/schema.d.ts +162 -0
  56. package/dist/schema.d.ts.map +1 -0
  57. package/dist/schema.js +17 -0
  58. package/dist/schema.js.map +1 -0
  59. package/dist/server/collect.d.ts +16 -0
  60. package/dist/server/collect.d.ts.map +1 -0
  61. package/dist/server/collect.js +44 -0
  62. package/dist/server/collect.js.map +1 -0
  63. package/dist/server/consents-contributor.d.ts +14 -0
  64. package/dist/server/consents-contributor.d.ts.map +1 -0
  65. package/dist/server/consents-contributor.js +40 -0
  66. package/dist/server/consents-contributor.js.map +1 -0
  67. package/dist/server/consents.d.ts +42 -0
  68. package/dist/server/consents.d.ts.map +1 -0
  69. package/dist/server/consents.js +117 -0
  70. package/dist/server/consents.js.map +1 -0
  71. package/dist/server/context.d.ts +5 -0
  72. package/dist/server/context.d.ts.map +1 -0
  73. package/dist/server/context.js +2 -0
  74. package/dist/server/context.js.map +1 -0
  75. package/dist/server/contributors.d.ts +23 -0
  76. package/dist/server/contributors.d.ts.map +1 -0
  77. package/dist/server/contributors.js +38 -0
  78. package/dist/server/contributors.js.map +1 -0
  79. package/dist/server/erase.d.ts +10 -0
  80. package/dist/server/erase.d.ts.map +1 -0
  81. package/dist/server/erase.js +42 -0
  82. package/dist/server/erase.js.map +1 -0
  83. package/dist/server/health.d.ts +3 -0
  84. package/dist/server/health.d.ts.map +1 -0
  85. package/dist/server/health.js +11 -0
  86. package/dist/server/health.js.map +1 -0
  87. package/dist/server/index.d.ts +11 -0
  88. package/dist/server/index.d.ts.map +1 -0
  89. package/dist/server/index.js +12 -0
  90. package/dist/server/index.js.map +1 -0
  91. package/dist/server/legal-documents.d.ts +9 -0
  92. package/dist/server/legal-documents.d.ts.map +1 -0
  93. package/dist/server/legal-documents.js +18 -0
  94. package/dist/server/legal-documents.js.map +1 -0
  95. package/dist/server/options.d.ts +15 -0
  96. package/dist/server/options.d.ts.map +1 -0
  97. package/dist/server/options.js +20 -0
  98. package/dist/server/options.js.map +1 -0
  99. package/dist/server/rate-limits.d.ts +13 -0
  100. package/dist/server/rate-limits.d.ts.map +1 -0
  101. package/dist/server/rate-limits.js +28 -0
  102. package/dist/server/rate-limits.js.map +1 -0
  103. package/dist/server/registration-consent.d.ts +19 -0
  104. package/dist/server/registration-consent.d.ts.map +1 -0
  105. package/dist/server/registration-consent.js +28 -0
  106. package/dist/server/registration-consent.js.map +1 -0
  107. package/dist/server/self-service.d.ts +29 -0
  108. package/dist/server/self-service.d.ts.map +1 -0
  109. package/dist/server/self-service.js +37 -0
  110. package/dist/server/self-service.js.map +1 -0
  111. package/dist/ui/delete-account-form.d.ts +16 -0
  112. package/dist/ui/delete-account-form.d.ts.map +1 -0
  113. package/dist/ui/delete-account-form.js +19 -0
  114. package/dist/ui/delete-account-form.js.map +1 -0
  115. package/dist/ui/index.d.ts +4 -0
  116. package/dist/ui/index.d.ts.map +1 -0
  117. package/dist/ui/index.js +7 -0
  118. package/dist/ui/index.js.map +1 -0
  119. package/dist/ui/legal-document.d.ts +51 -0
  120. package/dist/ui/legal-document.d.ts.map +1 -0
  121. package/dist/ui/legal-document.js +50 -0
  122. package/dist/ui/legal-document.js.map +1 -0
  123. package/dist/ui/legal-footer.d.ts +18 -0
  124. package/dist/ui/legal-footer.d.ts.map +1 -0
  125. package/dist/ui/legal-footer.js +13 -0
  126. package/dist/ui/legal-footer.js.map +1 -0
  127. package/migrations/0001_create_consents.sql +37 -0
  128. package/module.json +15 -0
  129. package/package.json +64 -4
  130. package/src/contract.ts +70 -0
  131. package/src/index.ts +73 -0
  132. package/src/messages/en.ts +45 -0
  133. package/src/messages/index.ts +18 -0
  134. package/src/messages/pl.ts +47 -0
  135. package/src/next/actions.ts +61 -0
  136. package/src/next/context.ts +14 -0
  137. package/src/next/index.ts +6 -0
  138. package/src/next/messages.ts +9 -0
  139. package/src/next/next-modules.d.ts +12 -0
  140. package/src/next/pages.tsx +36 -0
  141. package/src/next/route.ts +50 -0
  142. package/src/next/session-cookie.ts +18 -0
  143. package/src/options.ts +87 -0
  144. package/src/schema.ts +18 -0
  145. package/src/server/collect.ts +58 -0
  146. package/src/server/consents-contributor.ts +48 -0
  147. package/src/server/consents.ts +142 -0
  148. package/src/server/context.ts +5 -0
  149. package/src/server/contributors.ts +58 -0
  150. package/src/server/erase.ts +42 -0
  151. package/src/server/health.ts +12 -0
  152. package/src/server/index.ts +41 -0
  153. package/src/server/legal-documents.ts +24 -0
  154. package/src/server/options.ts +33 -0
  155. package/src/server/rate-limits.ts +32 -0
  156. package/src/server/registration-consent.ts +43 -0
  157. package/src/server/self-service.ts +59 -0
  158. package/src/ui/delete-account-form.tsx +58 -0
  159. package/src/ui/index.ts +21 -0
  160. package/src/ui/legal-document.tsx +189 -0
  161. package/src/ui/legal-footer.tsx +50 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 SOFTURE
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,273 @@
1
- # Temporary Holding Version
1
+ # @softure-ai/privacy
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ GDPR self-service and consent evidence for a Next.js app: a signed-in user downloads everything
4
+ the app stores about them as JSON, or deletes their account, and neither lists tables by hand; every
5
+ consent is recorded with the version of the legal document it was given to; legal pages render
6
+ from the app's own text. Every enabled module that
7
+ holds user data contributes its own export and deletion; the app adds its own contributors. Built
8
+ from FIRE_TRACKER's `src/db/{account-export,account-deletion}.ts`, `src/app/api/moje-dane/` and
9
+ `scripts/delete-empty-account.mts`, where the tables were listed by hand and deletion ran only from
10
+ a CLI, and FIRE's `components/{legal-document,legal-section,legal-footer}.tsx`; FIRE checked the
11
+ registration checkbox but stored nothing.
12
+
13
+ ## 1. What it provides
14
+
15
+ - **A contributor registry.** Each module states what it holds through
16
+ `defineModule({ privacy: { exportUserData, deleteUserData } })` (`@softure-ai/core`, checked
17
+ against its manifest's `privacy` flags); the app adds `privacy({ contributors })`. A module is in
18
+ the export and the deletion by being enabled.
19
+ - **The export.** `GET /api/privacy/export` answers a JSON attachment (`account-data-<date>.json`)
20
+ with every contributor's part under its id, read in one repeatable-read transaction, bounded by
21
+ `export.maxBytes`.
22
+ - **The deletion.** A page with the export link and a delete form that asks for the current
23
+ password and an explicit confirmation. Every contributor deletes in one transaction; a refusal or
24
+ a failure rolls everything back. The sessions end with the account, the cookie is cleared, and the
25
+ browser lands on `afterDelete`.
26
+ - **The consent ledger.** `privacy.consents` records who agreed to what, when, where and against
27
+ which version of which legal document. It is append-only: a withdrawal is a new row, and the
28
+ database refuses to update one. A ready hook records the registration checkbox of
29
+ `@softure-ai/auth`; other modules (the waitlist) record their own scopes.
30
+ - **The legal document shell.** `LegalDocument`, `LegalSection` and `LegalFooter` render the app's
31
+ terms and privacy policy: version, effective date, table of contents, change history.
32
+
33
+ `@softure-ai/auth`, `@softure-ai/feature-switches` and `@softure-ai/mcp-access` contribute already (their README section 11).
34
+
35
+ ## 2. Installation
36
+
37
+ ```bash
38
+ npm install @softure-ai/privacy @softure-ai/auth @softure-ai/security @softure-ai/core @softure-ai/db @softure-ai/ui drizzle-orm
39
+ ```
40
+
41
+ Peer dependencies: `next` 16, `react` 19, `drizzle-orm`. The module depends on `auth` (the account
42
+ and the session) and `security` (rate limits): a configuration without them fails at startup.
43
+
44
+ ## 3. Configuration
45
+
46
+ ```ts
47
+ import { auth, AUTH_RATE_LIMIT_BUCKETS } from "@softure-ai/auth";
48
+ import { privacy, PRIVACY_RATE_LIMIT_BUCKETS } from "@softure-ai/privacy";
49
+ import { security } from "@softure-ai/security";
50
+ import { deleteProfile, exportProfile } from "./lib/profile-privacy";
51
+
52
+ // in defineSoftureConfig({ modules: [...] }):
53
+ security({ clientIp: cloudflareIp(), buckets: { ...AUTH_RATE_LIMIT_BUCKETS, ...PRIVACY_RATE_LIMIT_BUCKETS } }),
54
+ auth({ ... }),
55
+ auth({ onRegistered: recordRegistrationConsent() }), // from @softure-ai/privacy/server
56
+ privacy({
57
+ documents: [
58
+ { id: "terms", version: "2026-10-01" },
59
+ { id: "privacy-policy", version: "2026-10-01" },
60
+ ],
61
+ contributors: [{ id: "profile", exportUserData: exportProfile, deleteUserData: deleteProfile }],
62
+ }),
63
+ ```
64
+
65
+ | Option | Type | Default | Meaning |
66
+ | --- | --- | --- | --- |
67
+ | `contributors` | `AppPrivacyContributor[]` | `[]` | The app's own data: `{ id, exportUserData?, deleteUserData? }`, at least one function each. |
68
+ | `contributors[].id` | `string` | required | Kebab-case, at most 64 characters, unique, and not the id of an enabled module: it is the key of the contributor's part of the export. |
69
+ | `documents` | `{ id, version }[]` | `[]` | The app's legal documents and the versions in force. A consent names a document by id and privacy records this version with it. Change `version` whenever the published text changes (a date such as `2026-10-01` works well). |
70
+ | `documents[].id` | `string` | required | Kebab-case, at most 64 characters, unique, e.g. `terms`, `privacy-policy`. |
71
+ | `documents[].version` | `string` | required | 1-32 letters, digits, `.`, `_` or `-`. |
72
+ | `export.maxBytes` | `number` | `10485760` (10 MiB) | The largest export, in bytes of JSON (1 KiB to 100 MiB). A larger one is refused with `privacy.export_too_large`. |
73
+ | `export.fileName` | `string` | `account-data` | The download's name before the date. |
74
+ | `routes` | `{ account, export, afterDelete }` | `/account/privacy`, `/api/privacy/export`, `/` | Where the page and the route are mounted, and where a deleted account lands. |
75
+ | `messages` | partial `pl` / `en` dictionaries | built in | Copy overrides (section 9). |
76
+
77
+ A contributor is two functions over the module context (`{ db, clock, config }`, `db` being the
78
+ transaction) and the user id:
79
+
80
+ ```ts
81
+ import { ok, type ModuleContext } from "@softure-ai/core";
82
+ import type { Queryable } from "@softure-ai/db";
83
+ import { eq } from "drizzle-orm";
84
+ import { profiles } from "../db/schema";
85
+
86
+ export async function exportProfile(context: ModuleContext, userId: string) {
87
+ const db = context.db as Queryable;
88
+ const [profile] = await db.select().from(profiles).where(eq(profiles.userId, userId));
89
+ return ok({ displayName: profile?.displayName ?? null });
90
+ }
91
+
92
+ export async function deleteProfile(context: ModuleContext, userId: string) {
93
+ await (context.db as Queryable).delete(profiles).where(eq(profiles.userId, userId));
94
+ return ok();
95
+ }
96
+ ```
97
+
98
+ Rules for a contributor:
99
+
100
+ - Query through `context.db` only: it is the transaction. A query on another handle is outside the
101
+ rollback and, under Postgres, waits on the transaction's locks.
102
+ - `exportUserData` only reads: the export runs in a read-only transaction. Export plain JSON
103
+ values (dates become ISO strings), and never secrets: password hashes, token hashes, API keys.
104
+ - Delete (or anonymise) everything that names the user. To keep data for a legal reason, return an
105
+ `Err` from `deleteUserData`: nothing is deleted, the user sees `privacy.deletion_refused`, and the
106
+ contributor's id and code are logged.
107
+ - Throw only for bugs and database failures; both roll the deletion back.
108
+
109
+ **Order.** The export runs the modules in the config's dependency order (dependencies first), then
110
+ the app's contributors in their listed order. The deletion runs the reverse: the app's contributors
111
+ first (last listed first), then each module before the modules it depends on, auth last. A row is
112
+ therefore always deleted before the rows it references, whatever its foreign key's `ON DELETE`.
113
+
114
+ ### Consents
115
+
116
+ ```ts
117
+ import { getConsent, hasConsent, listConsents, recordConsent } from "@softure-ai/privacy/server";
118
+
119
+ await recordConsent(ctx, { subject: { userId }, purpose: "newsletter", granted: true, source: "account" });
120
+ await recordConsent(ctx, { subject: { email }, purpose: "newsletter", granted: false, source: "unsubscribe" });
121
+ await recordConsent(ctx, { subject: { userId }, purpose: "terms", granted: true, document: "terms", source: "account" });
122
+ await hasConsent(ctx, { subject: { userId }, purpose: "terms" }); // latest row granted, current version
123
+ ```
124
+
125
+ - `subject` is an account (`{ userId }`) or an email address without one (`{ email }`, stored as a
126
+ SHA-256 key, never as the address; `{ emailKey }` names the same subject by that key, for a
127
+ caller that holds only the key, such as mailing's unsubscribe hook). Reads keep the two apart; the export and the deletion of an
128
+ account cover both, so a waitlist consent given before registering goes with the account.
129
+ - `purpose` and `source` are kebab-case (at most 64 characters); `document` names a declared
130
+ document, whose configured version is recorded. A bad subject, purpose or source gives
131
+ `privacy.consent_invalid`, an undeclared document `privacy.document_unknown`.
132
+ - A withdrawal is `granted: false`: a new row, the earlier one stays as evidence. The state of a
133
+ purpose is its latest row: `getConsent` returns it with `isCurrentVersion` (false once the
134
+ document's configured version moved on, or the document is no longer declared), `hasConsent` is
135
+ true only for a granted row of the current version, `listConsents` gives the whole history.
136
+ - Call these inside the transaction of the action they belong to (`ctx.db` being the transaction),
137
+ so the action and its evidence commit together.
138
+
139
+ **Registration.** `recordRegistrationConsent({ documents? })` is an `onRegistered` hook for auth:
140
+ when the app keeps `requireConsent` on, it records one row per document (purpose = the document's
141
+ id, source `registration`) at the account's creation time, in the account's transaction. By
142
+ default it records every declared document; `documents: ["terms"]` narrows it. With no document
143
+ declared, or an undeclared one named, it throws and the registration rolls back: an account never
144
+ exists without the evidence of its consent.
145
+
146
+ ## 4. Mounting
147
+
148
+ ```ts
149
+ // app/account/privacy/page.tsx: the export link and the delete form; redirects to login without a session.
150
+ export { PrivacyPage as default } from "@softure-ai/privacy/next";
151
+ export const dynamic = "force-dynamic";
152
+ ```
153
+
154
+ ```ts
155
+ // app/api/privacy/export/route.ts: the JSON download for the session's user.
156
+ export { exportRoute as GET } from "@softure-ai/privacy/next";
157
+ ```
158
+
159
+ Legal pages are the app's own pages around `LegalDocument`, with the text from the app's
160
+ dictionaries or content files and the version from the config:
161
+
162
+ ```tsx
163
+ // app/legal/terms/page.tsx
164
+ import { getPrivacyMessages } from "@softure-ai/privacy/next";
165
+ import { getLegalDocument } from "@softure-ai/privacy/server";
166
+ import { LegalDocument } from "@softure-ai/privacy/ui";
167
+ import config from "../../../softure.config";
168
+ import { terms } from "../../../content/terms";
169
+
170
+ export default function TermsPage() {
171
+ return (
172
+ <LegalDocument
173
+ title={terms.title}
174
+ version={getLegalDocument(config, "terms").version}
175
+ effectiveFrom="2026-10-01"
176
+ sections={terms.sections} // [{ id: "your-account", title: "2. Your account", content: <p>…</p> }]
177
+ changes={terms.changes} // newest first: [{ version, date: "2026-10-01", summary }]
178
+ messages={getPrivacyMessages(config)}
179
+ locale={config.locale}
180
+ />
181
+ );
182
+ }
183
+ ```
184
+
185
+ `LegalFooter` (`links: [{ href, label }]`, an optional `note`) goes into the root layout.
186
+
187
+ Link the page from the app's account page. To build your own page, compose `DeleteAccountForm`
188
+ from `@softure-ai/privacy/ui` with `deleteAccountAction` from `/next`, and link to the export route.
189
+
190
+ The route answers 401 without a session, 429 with `retry-after` over the `privacy-export` limit,
191
+ 413 over `export.maxBytes` and 500 when a contributor fails, each with `{ error: <code> }`. The
192
+ action reads the user from the session before it reads the form, and Next refuses it from another
193
+ origin. Outside a request, call `collectUserData(ctx, userId)` and `eraseUserData(ctx, userId)`
194
+ from `/server` (for example from an operator script); they apply no rate limit and ask for no
195
+ password.
196
+
197
+ ## 5. Migrations and tables
198
+
199
+ Schema `privacy`, one migration:
200
+
201
+ | Table | Columns | Notes |
202
+ | --- | --- | --- |
203
+ | `consents` | `id` (identity), `user_id` (FK `auth.users`, cascade) or `email_key` (base64url SHA-256 of the trimmed, lowercased address), `purpose`, `granted`, `document_id` + `document_version` (both or neither), `source`, `recorded_at` | exactly one subject (`consents_one_subject`); a `BEFORE UPDATE` trigger refuses every update; indexed by subject, purpose and time |
204
+
205
+ The export and the deletion run every other contributor on its own tables. `GET /api/health` of
206
+ `@softure-ai/ops` checks that the table answers.
207
+
208
+ ## 6. Environment variables
209
+
210
+ None.
211
+
212
+ ## 7. Switches
213
+
214
+ None.
215
+
216
+ ## 8. Appearance
217
+
218
+ The page is built from `@softure-ai/ui` (`Card`, `ButtonLink`, `PasswordField`, `Checkbox`,
219
+ `FormError`, `Button` with the `danger` variant) and uses only its compiled classes, so
220
+ `@softure-ai/ui/styles.css` styles it and the `--sft-*` tokens theme it. `DeleteAccountForm` takes
221
+ `classNames` for its slots (`root`, `description`, `form`) and `unstyled`.
222
+
223
+ The legal components are server components with the same rules. `LegalDocument` slots: `root`,
224
+ `header`, `title`, `meta`, `intro`, `contents`, `contentsTitle`, `contentsList`, `link`, `changes`,
225
+ `changesTitle`, `changesList`, `change`, `changeMeta`, and `sectionClassNames` for every
226
+ `LegalSection` (`root`, `title`, `body`). `LegalFooter` slots: `root`, `list`, `link`, `note`.
227
+ Sections scroll into view below a sticky header through `scroll-mt`; dates (`YYYY-MM-DD`) are
228
+ written in the app's locale, in UTC so the server's time zone cannot move them.
229
+
230
+ ## 9. Copy
231
+
232
+ `privacyMessages.{en,pl}`: `page` (title, lead), `export` (title, description, button), `delete`
233
+ (title, description, password label, confirmation, submit and pending labels), `legal` (contents,
234
+ version, effective from, change history, the footer's navigation name) and `errors` for every
235
+ code the form and the route can give (`privacy.*`, `auth.unauthenticated`, `security.rate_limited`,
236
+ `core.*`). Override any of them with
237
+ `privacy({ messages: { pl: { errors: { privacy: { deletion_refused: "…" } } } } })`, for example to
238
+ say how to reach the app's support.
239
+
240
+ ## 10. Hooks
241
+
242
+ The contributors (section 3) are the hooks. `recordRegistrationConsent()` is a ready hook for
243
+ auth's `onRegistered` (section 3). The rate limit buckets, both per user, are
244
+ `PRIVACY_RATE_LIMIT_BUCKETS`: `privacy-export` (5 an hour) and `privacy-delete` (5 attempts in 15
245
+ minutes, each a password check). Spread them into `security({ buckets })`; a missing bucket throws
246
+ on first use and names it.
247
+
248
+ ## 11. GDPR
249
+
250
+ This module is the GDPR surface: right of access (the export) and right to erasure (the deletion),
251
+ and the evidence of consent (Art. 7(1)). What each module exports and deletes is in that module's
252
+ section 11.
253
+
254
+ Its own part is the consent ledger. The export lists every consent of the account and of its email
255
+ address (`privacy.consents`, oldest first, each marked `account` or `email`; the email key is left
256
+ out). The deletion removes them: once the data is gone there is nothing left to prove consent for.
257
+ An email subject is stored only as a SHA-256 key, so the table holds no address; the key is a
258
+ pseudonym, not anonymous data, because anyone with the address can compute it.
259
+ Rate limit rows of `security` hold only SHA-256 prefixes of user ids and emails and are pruned two
260
+ windows after they start.
261
+
262
+ ## 12. Limitations
263
+
264
+ - The export is built in memory and bounded by `export.maxBytes`; there is no streaming or
265
+ background export with a download link yet.
266
+ - Deletion is immediate: there is no grace period and no undo. Back up the database if the app
267
+ needs to restore accounts.
268
+ - No deletion by an admin or from a CLI yet; call `eraseUserData` from a script of the app.
269
+ - No page to review or withdraw consents yet; withdrawals come from the module that asked (the
270
+ waitlist, an unsubscribe) or from the app calling `recordConsent` with `granted: false`.
271
+ - A new document version does not ask anyone to accept it again: `hasConsent` turns false and the
272
+ app decides what to show.
273
+ - Legal text is the app's: the shell renders it, it does not write it.
@@ -0,0 +1,58 @@
1
+ import type { CoreErrorCode } from "@softure-ai/core";
2
+ export type PrivacyErrorCode = "privacy.export_failed" | "privacy.export_too_large" | "privacy.deletion_refused" | "privacy.password_invalid" | "privacy.confirmation_required" | "privacy.consent_invalid" | "privacy.document_unknown";
3
+ /**
4
+ * Whose consent it is: an account, or an email address that has none (a waitlist sign-up). An
5
+ * account's consents and its email's consents are both its own in the export and the deletion.
6
+ * `{ emailKey }` names the same email subject by its stored key (`getEmailKey`), for a caller that
7
+ * only holds the key, such as mailing's unsubscribe link.
8
+ */
9
+ export type ConsentSubject = {
10
+ readonly userId: string;
11
+ } | {
12
+ readonly email: string;
13
+ } | {
14
+ readonly emailKey: string;
15
+ };
16
+ /** One row of the ledger: a consent given (`granted`) or withdrawn. */
17
+ export interface ConsentRecord {
18
+ readonly purpose: string;
19
+ readonly granted: boolean;
20
+ /** The legal document and its version at the moment of consent; null for a purpose without one. */
21
+ readonly document: {
22
+ readonly id: string;
23
+ readonly version: string;
24
+ } | null;
25
+ /** Where it was given, e.g. `registration`, `waitlist`. */
26
+ readonly source: string;
27
+ readonly recordedAt: Date;
28
+ }
29
+ /** The current state of one purpose: its latest record, checked against the configured version. */
30
+ export interface ConsentState extends ConsentRecord {
31
+ /**
32
+ * False when the record names a document version that is no longer the configured one (or a
33
+ * document the app no longer declares): the person agreed to an earlier text.
34
+ */
35
+ readonly isCurrentVersion: boolean;
36
+ }
37
+ /** The file a user downloads: every contributor's part under its id. */
38
+ export interface PrivacyExport {
39
+ readonly format: "softure.privacy-export";
40
+ readonly version: 1;
41
+ readonly userId: string;
42
+ /** ISO 8601, UTC. */
43
+ readonly exportedAt: string;
44
+ /** Each contributor's data under its id (a module id or an app contributor id), in export order. */
45
+ readonly data: Readonly<Record<string, unknown>>;
46
+ }
47
+ /** Every code the delete form can show: its own, the session and rate limit refusals, the generic ones. */
48
+ export type DeleteAccountErrorCode = Extract<PrivacyErrorCode, "privacy.deletion_refused" | "privacy.password_invalid" | "privacy.confirmation_required"> | "auth.unauthenticated" | "security.rate_limited" | CoreErrorCode;
49
+ export type DeleteAccountField = "password" | "confirm";
50
+ /** What the delete action returns to its form (`useActionState`); success redirects instead. */
51
+ export interface DeleteAccountFormState {
52
+ readonly status: "idle" | "error";
53
+ readonly error?: DeleteAccountErrorCode;
54
+ /** The field the error belongs to; none for a form-level error. */
55
+ readonly field?: DeleteAccountField;
56
+ }
57
+ export declare const INITIAL_DELETE_ACCOUNT_STATE: DeleteAccountFormState;
58
+ //# sourceMappingURL=contract.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"contract.d.ts","sourceRoot":"","sources":["../src/contract.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAEtD,MAAM,MAAM,gBAAgB,GACxB,uBAAuB,GACvB,0BAA0B,GAC1B,0BAA0B,GAC1B,0BAA0B,GAC1B,+BAA+B,GAC/B,yBAAyB,GACzB,0BAA0B,CAAC;AAE/B;;;;;GAKG;AACH,MAAM,MAAM,cAAc,GAAG;IAAE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAC;AAEtH,uEAAuE;AACvE,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,mGAAmG;IACnG,QAAQ,CAAC,QAAQ,EAAE;QAAE,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IAC5E,2DAA2D;IAC3D,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,UAAU,EAAE,IAAI,CAAC;CAC3B;AAED,mGAAmG;AACnG,MAAM,WAAW,YAAa,SAAQ,aAAa;IACjD;;;OAGG;IACH,QAAQ,CAAC,gBAAgB,EAAE,OAAO,CAAC;CACpC;AAED,wEAAwE;AACxE,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,MAAM,EAAE,wBAAwB,CAAC;IAC1C,QAAQ,CAAC,OAAO,EAAE,CAAC,CAAC;IACpB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,qBAAqB;IACrB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,oGAAoG;IACpG,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;CAClD;AAED,2GAA2G;AAC3G,MAAM,MAAM,sBAAsB,GAC9B,OAAO,CAAC,gBAAgB,EAAE,0BAA0B,GAAG,0BAA0B,GAAG,+BAA+B,CAAC,GACpH,sBAAsB,GACtB,uBAAuB,GACvB,aAAa,CAAC;AAElB,MAAM,MAAM,kBAAkB,GAAG,UAAU,GAAG,SAAS,CAAC;AAExD,gGAAgG;AAChG,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC;IAClC,QAAQ,CAAC,KAAK,CAAC,EAAE,sBAAsB,CAAC;IACxC,mEAAmE;IACnE,QAAQ,CAAC,KAAK,CAAC,EAAE,kBAAkB,CAAC;CACrC;AAED,eAAO,MAAM,4BAA4B,EAAE,sBAA2C,CAAC"}
@@ -0,0 +1,2 @@
1
+ export const INITIAL_DELETE_ACCOUNT_STATE = { status: "idle" };
2
+ //# sourceMappingURL=contract.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"contract.js","sourceRoot":"","sources":["../src/contract.ts"],"names":[],"mappings":"AAqEA,MAAM,CAAC,MAAM,4BAA4B,GAA2B,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC"}
@@ -0,0 +1,90 @@
1
+ export declare const MODULE_ID = "privacy";
2
+ /**
3
+ * Rate limit buckets privacy consumes, with defaults to spread into `security({ buckets })`, both
4
+ * per user: `privacy-export` (downloads) and `privacy-delete` (deletion attempts, each one a
5
+ * password check).
6
+ */
7
+ export declare const PRIVACY_RATE_LIMIT_BUCKETS: {
8
+ readonly "privacy-export": {
9
+ readonly limit: 5;
10
+ readonly windowMinutes: 60;
11
+ };
12
+ readonly "privacy-delete": {
13
+ readonly limit: 5;
14
+ readonly windowMinutes: 15;
15
+ };
16
+ };
17
+ /**
18
+ * Enables the GDPR export, self-service account deletion and the consent ledger in
19
+ * `softure.config.ts` (after `auth({ ... })`):
20
+ * `privacy({ documents: [{ id: "terms", version: "2026-10-01" }], contributors: [{ id: "profile", exportUserData, deleteUserData }] })`.
21
+ * Every enabled module with user data contributes its own part; `contributors` adds the app's.
22
+ */
23
+ export declare const privacy: import("@softure-ai/core").ModuleFactory<{
24
+ account: string;
25
+ export: string;
26
+ afterDelete: string;
27
+ }, {
28
+ page: {
29
+ title: string;
30
+ lead: string;
31
+ };
32
+ export: {
33
+ title: string;
34
+ description: string;
35
+ button: string;
36
+ };
37
+ delete: {
38
+ title: string;
39
+ description: string;
40
+ password: string;
41
+ confirm: string;
42
+ submit: string;
43
+ pending: string;
44
+ };
45
+ legal: {
46
+ contents: string;
47
+ version: string;
48
+ effectiveFrom: string;
49
+ changes: string;
50
+ footer: string;
51
+ };
52
+ errors: {
53
+ privacy: {
54
+ export_failed: string;
55
+ export_too_large: string;
56
+ deletion_refused: string;
57
+ password_invalid: string;
58
+ confirmation_required: string;
59
+ };
60
+ auth: {
61
+ unauthenticated: string;
62
+ };
63
+ security: {
64
+ rate_limited: string;
65
+ };
66
+ core: {
67
+ database_failed: string;
68
+ unexpected: string;
69
+ };
70
+ };
71
+ }, import("zod").ZodObject<{
72
+ contributors: import("zod").ZodDefault<import("zod").ZodArray<import("zod").ZodObject<{
73
+ id: import("zod").ZodString;
74
+ exportUserData: import("zod").ZodOptional<import("zod").ZodCustom<(context: import("@softure-ai/core").ModuleContext, userId: string) => Promise<import("@softure-ai/core").Result<unknown>>, (context: import("@softure-ai/core").ModuleContext, userId: string) => Promise<import("@softure-ai/core").Result<unknown>>>>;
75
+ deleteUserData: import("zod").ZodOptional<import("zod").ZodCustom<(context: import("@softure-ai/core").ModuleContext, userId: string) => Promise<import("@softure-ai/core").Result<undefined>>, (context: import("@softure-ai/core").ModuleContext, userId: string) => Promise<import("@softure-ai/core").Result<undefined>>>>;
76
+ }, import("zod/v4/core").$strict>>>;
77
+ documents: import("zod").ZodDefault<import("zod").ZodArray<import("zod").ZodObject<{
78
+ id: import("zod").ZodString;
79
+ version: import("zod").ZodString;
80
+ }, import("zod/v4/core").$strict>>>;
81
+ export: import("zod").ZodPrefault<import("zod").ZodObject<{
82
+ maxBytes: import("zod").ZodDefault<import("zod").ZodNumber>;
83
+ fileName: import("zod").ZodDefault<import("zod").ZodString>;
84
+ }, import("zod/v4/core").$strict>>;
85
+ }, import("zod/v4/core").$strict>>;
86
+ export { INITIAL_DELETE_ACCOUNT_STATE, type ConsentRecord, type ConsentState, type ConsentSubject, type DeleteAccountErrorCode, type DeleteAccountField, type DeleteAccountFormState, type PrivacyErrorCode, type PrivacyExport, } from "./contract.js";
87
+ export { getPrivacyErrorMessage, privacyMessages, type PrivacyMessages } from "./messages/index.js";
88
+ export { CONTRIBUTOR_ID_PATTERN, DEFAULT_EXPORT_MAX_BYTES, DOCUMENT_VERSION_PATTERN, type AppPrivacyContributor, type LegalDocumentDeclaration, type PrivacyOptions, type PrivacyOptionsInput, } from "./options.js";
89
+ export { consents, privacySchema } from "./schema.js";
90
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAUA,eAAO,MAAM,SAAS,YAAY,CAAC;AAEnC;;;;GAIG;AACH,eAAO,MAAM,0BAA0B;;;;;;;;;CAG7B,CAAC;AAEX;;;;;GAKG;AACH,eAAO,MAAM,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;kCAqBlB,CAAC;AAEH,OAAO,EACL,4BAA4B,EAC5B,KAAK,aAAa,EAClB,KAAK,YAAY,EACjB,KAAK,cAAc,EACnB,KAAK,sBAAsB,EAC3B,KAAK,kBAAkB,EACvB,KAAK,sBAAsB,EAC3B,KAAK,gBAAgB,EACrB,KAAK,aAAa,GACnB,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,sBAAsB,EAAE,eAAe,EAAE,KAAK,eAAe,EAAE,MAAM,qBAAqB,CAAC;AACpG,OAAO,EACL,sBAAsB,EACtB,wBAAwB,EACxB,wBAAwB,EACxB,KAAK,qBAAqB,EAC1B,KAAK,wBAAwB,EAC7B,KAAK,cAAc,EACnB,KAAK,mBAAmB,GACzB,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,QAAQ,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,52 @@
1
+ // Public API of @softure-ai/privacy: the module factory for softure.config.ts, its types, messages,
2
+ // rate limit buckets and the consents table. The registry, export, deletion and the consent ledger
3
+ // are in `@softure-ai/privacy/server`, the Next.js adapter (page, export route, delete action) in
4
+ // `/next`, the delete form and the legal document shell in `/ui`.
5
+ import { defineModule, resolveMigrationsDir } from "@softure-ai/core";
6
+ import { privacyMessages } from "./messages/index.js";
7
+ import { privacyOptionsSchema } from "./options.js";
8
+ import { privacyConsentsContributor } from "./server/consents-contributor.js";
9
+ import { checkConsentsTable } from "./server/health.js";
10
+ export const MODULE_ID = "privacy";
11
+ /**
12
+ * Rate limit buckets privacy consumes, with defaults to spread into `security({ buckets })`, both
13
+ * per user: `privacy-export` (downloads) and `privacy-delete` (deletion attempts, each one a
14
+ * password check).
15
+ */
16
+ export const PRIVACY_RATE_LIMIT_BUCKETS = {
17
+ "privacy-export": { limit: 5, windowMinutes: 60 },
18
+ "privacy-delete": { limit: 5, windowMinutes: 15 },
19
+ };
20
+ /**
21
+ * Enables the GDPR export, self-service account deletion and the consent ledger in
22
+ * `softure.config.ts` (after `auth({ ... })`):
23
+ * `privacy({ documents: [{ id: "terms", version: "2026-10-01" }], contributors: [{ id: "profile", exportUserData, deleteUserData }] })`.
24
+ * Every enabled module with user data contributes its own part; `contributors` adds the app's.
25
+ */
26
+ export const privacy = defineModule({
27
+ manifest: {
28
+ id: MODULE_ID,
29
+ version: "0.1.5",
30
+ dependsOn: { auth: "^0.1.0", security: "^0.1.0" },
31
+ dbSchema: "privacy",
32
+ tables: ["consents"],
33
+ env: [],
34
+ switches: [],
35
+ routes: { account: "/account/privacy", export: "/api/privacy/export", afterDelete: "/" },
36
+ mount: [
37
+ { kind: "page", path: "app/account/privacy/page.tsx", export: "PrivacyPage" },
38
+ { kind: "route-handler", path: "app/api/privacy/export/route.ts", export: "exportRoute" },
39
+ ],
40
+ privacy: { exports: true, deletes: true },
41
+ },
42
+ messages: privacyMessages,
43
+ options: privacyOptionsSchema,
44
+ migrations: { dir: resolveMigrationsDir(import.meta.url, "../migrations/") },
45
+ privacy: privacyConsentsContributor,
46
+ health: checkConsentsTable,
47
+ });
48
+ export { INITIAL_DELETE_ACCOUNT_STATE, } from "./contract.js";
49
+ export { getPrivacyErrorMessage, privacyMessages } from "./messages/index.js";
50
+ export { CONTRIBUTOR_ID_PATTERN, DEFAULT_EXPORT_MAX_BYTES, DOCUMENT_VERSION_PATTERN, } from "./options.js";
51
+ export { consents, privacySchema } from "./schema.js";
52
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,oGAAoG;AACpG,mGAAmG;AACnG,kGAAkG;AAClG,kEAAkE;AAClE,OAAO,EAAE,YAAY,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAC;AACtE,OAAO,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AACtD,OAAO,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAC;AACpD,OAAO,EAAE,0BAA0B,EAAE,MAAM,kCAAkC,CAAC;AAC9E,OAAO,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AAExD,MAAM,CAAC,MAAM,SAAS,GAAG,SAAS,CAAC;AAEnC;;;;GAIG;AACH,MAAM,CAAC,MAAM,0BAA0B,GAAG;IACxC,gBAAgB,EAAE,EAAE,KAAK,EAAE,CAAC,EAAE,aAAa,EAAE,EAAE,EAAE;IACjD,gBAAgB,EAAE,EAAE,KAAK,EAAE,CAAC,EAAE,aAAa,EAAE,EAAE,EAAE;CACzC,CAAC;AAEX;;;;;GAKG;AACH,MAAM,CAAC,MAAM,OAAO,GAAG,YAAY,CAAC;IAClC,QAAQ,EAAE;QACR,EAAE,EAAE,SAAS;QACb,OAAO,EAAE,OAAO;QAChB,SAAS,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,QAAQ,EAAE,QAAQ,EAAE;QACjD,QAAQ,EAAE,SAAS;QACnB,MAAM,EAAE,CAAC,UAAU,CAAC;QACpB,GAAG,EAAE,EAAE;QACP,QAAQ,EAAE,EAAE;QACZ,MAAM,EAAE,EAAE,OAAO,EAAE,kBAAkB,EAAE,MAAM,EAAE,qBAAqB,EAAE,WAAW,EAAE,GAAG,EAAE;QACxF,KAAK,EAAE;YACL,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,8BAA8B,EAAE,MAAM,EAAE,aAAa,EAAE;YAC7E,EAAE,IAAI,EAAE,eAAe,EAAE,IAAI,EAAE,iCAAiC,EAAE,MAAM,EAAE,aAAa,EAAE;SAC1F;QACD,OAAO,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE;KAC1C;IACD,QAAQ,EAAE,eAAe;IACzB,OAAO,EAAE,oBAAoB;IAC7B,UAAU,EAAE,EAAE,GAAG,EAAE,oBAAoB,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE,gBAAgB,CAAC,EAAE;IAC5E,OAAO,EAAE,0BAA0B;IACnC,MAAM,EAAE,kBAAkB;CAC3B,CAAC,CAAC;AAEH,OAAO,EACL,4BAA4B,GAS7B,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,sBAAsB,EAAE,eAAe,EAAwB,MAAM,qBAAqB,CAAC;AACpG,OAAO,EACL,sBAAsB,EACtB,wBAAwB,EACxB,wBAAwB,GAKzB,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,QAAQ,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC"}
@@ -0,0 +1,46 @@
1
+ export declare const en: {
2
+ page: {
3
+ title: string;
4
+ lead: string;
5
+ };
6
+ export: {
7
+ title: string;
8
+ description: string;
9
+ button: string;
10
+ };
11
+ delete: {
12
+ title: string;
13
+ description: string;
14
+ password: string;
15
+ confirm: string;
16
+ submit: string;
17
+ pending: string;
18
+ };
19
+ legal: {
20
+ contents: string;
21
+ version: string;
22
+ effectiveFrom: string;
23
+ changes: string;
24
+ footer: string;
25
+ };
26
+ errors: {
27
+ privacy: {
28
+ export_failed: string;
29
+ export_too_large: string;
30
+ deletion_refused: string;
31
+ password_invalid: string;
32
+ confirmation_required: string;
33
+ };
34
+ auth: {
35
+ unauthenticated: string;
36
+ };
37
+ security: {
38
+ rate_limited: string;
39
+ };
40
+ core: {
41
+ database_failed: string;
42
+ unexpected: string;
43
+ };
44
+ };
45
+ };
46
+ //# sourceMappingURL=en.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"en.d.ts","sourceRoot":"","sources":["../../src/messages/en.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA4Cd,CAAC"}
@@ -0,0 +1,46 @@
1
+ export const en = {
2
+ page: {
3
+ title: "Your data",
4
+ lead: "Download a copy of what this app stores about you, or delete your account.",
5
+ },
6
+ export: {
7
+ title: "Download your data",
8
+ description: "A JSON file with everything this app stores about your account.",
9
+ button: "Download my data",
10
+ },
11
+ delete: {
12
+ title: "Delete your account",
13
+ description: "This deletes your account and your data and signs you out on every device. It cannot be undone.",
14
+ password: "Current password",
15
+ confirm: "I understand that my account and my data will be deleted for good.",
16
+ submit: "Delete my account",
17
+ pending: "Deleting…",
18
+ },
19
+ legal: {
20
+ contents: "Contents",
21
+ version: "Version",
22
+ effectiveFrom: "Effective from",
23
+ changes: "Change history",
24
+ footer: "Legal information",
25
+ },
26
+ errors: {
27
+ privacy: {
28
+ export_failed: "Your data could not be collected. Try again in a moment.",
29
+ export_too_large: "Your data is too large to download here. Contact us and we will send it to you.",
30
+ deletion_refused: "Your account cannot be deleted right now, because some of its data must be kept for a while. Contact us for details.",
31
+ password_invalid: "This is not your current password.",
32
+ confirmation_required: "Confirm that you want to delete your account.",
33
+ },
34
+ auth: {
35
+ unauthenticated: "Your session has ended. Sign in again.",
36
+ },
37
+ security: {
38
+ rate_limited: "Too many attempts. Wait a moment and try again.",
39
+ },
40
+ core: {
41
+ database_failed: "Something went wrong on our side. Try again in a moment.",
42
+ unexpected: "Something went wrong on our side. Try again in a moment.",
43
+ },
44
+ },
45
+ };
46
+ //# sourceMappingURL=en.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"en.js","sourceRoot":"","sources":["../../src/messages/en.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,EAAE,GAAG;IAChB,IAAI,EAAE;QACJ,KAAK,EAAE,WAAW;QAClB,IAAI,EAAE,4EAA4E;KACnF;IACD,MAAM,EAAE;QACN,KAAK,EAAE,oBAAoB;QAC3B,WAAW,EAAE,iEAAiE;QAC9E,MAAM,EAAE,kBAAkB;KAC3B;IACD,MAAM,EAAE;QACN,KAAK,EAAE,qBAAqB;QAC5B,WAAW,EAAE,iGAAiG;QAC9G,QAAQ,EAAE,kBAAkB;QAC5B,OAAO,EAAE,oEAAoE;QAC7E,MAAM,EAAE,mBAAmB;QAC3B,OAAO,EAAE,WAAW;KACrB;IACD,KAAK,EAAE;QACL,QAAQ,EAAE,UAAU;QACpB,OAAO,EAAE,SAAS;QAClB,aAAa,EAAE,gBAAgB;QAC/B,OAAO,EAAE,gBAAgB;QACzB,MAAM,EAAE,mBAAmB;KAC5B;IACD,MAAM,EAAE;QACN,OAAO,EAAE;YACP,aAAa,EAAE,0DAA0D;YACzE,gBAAgB,EAAE,iFAAiF;YACnG,gBAAgB,EAAE,sHAAsH;YACxI,gBAAgB,EAAE,oCAAoC;YACtD,qBAAqB,EAAE,+CAA+C;SACvE;QACD,IAAI,EAAE;YACJ,eAAe,EAAE,wCAAwC;SAC1D;QACD,QAAQ,EAAE;YACR,YAAY,EAAE,iDAAiD;SAChE;QACD,IAAI,EAAE;YACJ,eAAe,EAAE,0DAA0D;YAC3E,UAAU,EAAE,0DAA0D;SACvE;KACF;CACF,CAAC"}