@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.
- package/LICENSE +21 -0
- package/README.md +272 -2
- package/dist/contract.d.ts +58 -0
- package/dist/contract.d.ts.map +1 -0
- package/dist/contract.js +2 -0
- package/dist/contract.js.map +1 -0
- package/dist/index.d.ts +90 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +52 -0
- package/dist/index.js.map +1 -0
- package/dist/messages/en.d.ts +46 -0
- package/dist/messages/en.d.ts.map +1 -0
- package/dist/messages/en.js +46 -0
- package/dist/messages/en.js.map +1 -0
- package/dist/messages/index.d.ts +99 -0
- package/dist/messages/index.d.ts.map +1 -0
- package/dist/messages/index.js +14 -0
- package/dist/messages/index.js.map +1 -0
- package/dist/messages/pl.d.ts +3 -0
- package/dist/messages/pl.d.ts.map +1 -0
- package/dist/messages/pl.js +46 -0
- package/dist/messages/pl.js.map +1 -0
- package/dist/next/actions.d.ts +4 -0
- package/dist/next/actions.d.ts.map +1 -0
- package/dist/next/actions.js +57 -0
- package/dist/next/actions.js.map +1 -0
- package/dist/next/context.d.ts +4 -0
- package/dist/next/context.d.ts.map +1 -0
- package/dist/next/context.js +13 -0
- package/dist/next/context.js.map +1 -0
- package/dist/next/index.d.ts +5 -0
- package/dist/next/index.d.ts.map +1 -0
- package/dist/next/index.js +7 -0
- package/dist/next/index.js.map +1 -0
- package/dist/next/messages.d.ts +5 -0
- package/dist/next/messages.d.ts.map +1 -0
- package/dist/next/messages.js +7 -0
- package/dist/next/messages.js.map +1 -0
- package/dist/next/pages.d.ts +2 -0
- package/dist/next/pages.d.ts.map +1 -0
- package/dist/next/pages.js +22 -0
- package/dist/next/pages.js.map +1 -0
- package/dist/next/route.d.ts +6 -0
- package/dist/next/route.d.ts.map +1 -0
- package/dist/next/route.js +49 -0
- package/dist/next/route.js.map +1 -0
- package/dist/next/session-cookie.d.ts +3 -0
- package/dist/next/session-cookie.d.ts.map +1 -0
- package/dist/next/session-cookie.js +17 -0
- package/dist/next/session-cookie.js.map +1 -0
- package/dist/options.d.ts +27 -0
- package/dist/options.d.ts.map +1 -0
- package/dist/options.js +70 -0
- package/dist/options.js.map +1 -0
- package/dist/schema.d.ts +162 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +17 -0
- package/dist/schema.js.map +1 -0
- package/dist/server/collect.d.ts +16 -0
- package/dist/server/collect.d.ts.map +1 -0
- package/dist/server/collect.js +44 -0
- package/dist/server/collect.js.map +1 -0
- package/dist/server/consents-contributor.d.ts +14 -0
- package/dist/server/consents-contributor.d.ts.map +1 -0
- package/dist/server/consents-contributor.js +40 -0
- package/dist/server/consents-contributor.js.map +1 -0
- package/dist/server/consents.d.ts +42 -0
- package/dist/server/consents.d.ts.map +1 -0
- package/dist/server/consents.js +117 -0
- package/dist/server/consents.js.map +1 -0
- package/dist/server/context.d.ts +5 -0
- package/dist/server/context.d.ts.map +1 -0
- package/dist/server/context.js +2 -0
- package/dist/server/context.js.map +1 -0
- package/dist/server/contributors.d.ts +23 -0
- package/dist/server/contributors.d.ts.map +1 -0
- package/dist/server/contributors.js +38 -0
- package/dist/server/contributors.js.map +1 -0
- package/dist/server/erase.d.ts +10 -0
- package/dist/server/erase.d.ts.map +1 -0
- package/dist/server/erase.js +42 -0
- package/dist/server/erase.js.map +1 -0
- package/dist/server/health.d.ts +3 -0
- package/dist/server/health.d.ts.map +1 -0
- package/dist/server/health.js +11 -0
- package/dist/server/health.js.map +1 -0
- package/dist/server/index.d.ts +11 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +12 -0
- package/dist/server/index.js.map +1 -0
- package/dist/server/legal-documents.d.ts +9 -0
- package/dist/server/legal-documents.d.ts.map +1 -0
- package/dist/server/legal-documents.js +18 -0
- package/dist/server/legal-documents.js.map +1 -0
- package/dist/server/options.d.ts +15 -0
- package/dist/server/options.d.ts.map +1 -0
- package/dist/server/options.js +20 -0
- package/dist/server/options.js.map +1 -0
- package/dist/server/rate-limits.d.ts +13 -0
- package/dist/server/rate-limits.d.ts.map +1 -0
- package/dist/server/rate-limits.js +28 -0
- package/dist/server/rate-limits.js.map +1 -0
- package/dist/server/registration-consent.d.ts +19 -0
- package/dist/server/registration-consent.d.ts.map +1 -0
- package/dist/server/registration-consent.js +28 -0
- package/dist/server/registration-consent.js.map +1 -0
- package/dist/server/self-service.d.ts +29 -0
- package/dist/server/self-service.d.ts.map +1 -0
- package/dist/server/self-service.js +37 -0
- package/dist/server/self-service.js.map +1 -0
- package/dist/ui/delete-account-form.d.ts +16 -0
- package/dist/ui/delete-account-form.d.ts.map +1 -0
- package/dist/ui/delete-account-form.js +19 -0
- package/dist/ui/delete-account-form.js.map +1 -0
- package/dist/ui/index.d.ts +4 -0
- package/dist/ui/index.d.ts.map +1 -0
- package/dist/ui/index.js +7 -0
- package/dist/ui/index.js.map +1 -0
- package/dist/ui/legal-document.d.ts +51 -0
- package/dist/ui/legal-document.d.ts.map +1 -0
- package/dist/ui/legal-document.js +50 -0
- package/dist/ui/legal-document.js.map +1 -0
- package/dist/ui/legal-footer.d.ts +18 -0
- package/dist/ui/legal-footer.d.ts.map +1 -0
- package/dist/ui/legal-footer.js +13 -0
- package/dist/ui/legal-footer.js.map +1 -0
- package/migrations/0001_create_consents.sql +37 -0
- package/module.json +15 -0
- package/package.json +64 -4
- package/src/contract.ts +70 -0
- package/src/index.ts +73 -0
- package/src/messages/en.ts +45 -0
- package/src/messages/index.ts +18 -0
- package/src/messages/pl.ts +47 -0
- package/src/next/actions.ts +61 -0
- package/src/next/context.ts +14 -0
- package/src/next/index.ts +6 -0
- package/src/next/messages.ts +9 -0
- package/src/next/next-modules.d.ts +12 -0
- package/src/next/pages.tsx +36 -0
- package/src/next/route.ts +50 -0
- package/src/next/session-cookie.ts +18 -0
- package/src/options.ts +87 -0
- package/src/schema.ts +18 -0
- package/src/server/collect.ts +58 -0
- package/src/server/consents-contributor.ts +48 -0
- package/src/server/consents.ts +142 -0
- package/src/server/context.ts +5 -0
- package/src/server/contributors.ts +58 -0
- package/src/server/erase.ts +42 -0
- package/src/server/health.ts +12 -0
- package/src/server/index.ts +41 -0
- package/src/server/legal-documents.ts +24 -0
- package/src/server/options.ts +33 -0
- package/src/server/rate-limits.ts +32 -0
- package/src/server/registration-consent.ts +43 -0
- package/src/server/self-service.ts +59 -0
- package/src/ui/delete-account-form.tsx +58 -0
- package/src/ui/index.ts +21 -0
- package/src/ui/legal-document.tsx +189 -0
- 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
|
-
#
|
|
1
|
+
# @softure-ai/privacy
|
|
2
2
|
|
|
3
|
-
|
|
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"}
|
package/dist/contract.js
ADDED
|
@@ -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"}
|
package/dist/index.d.ts
ADDED
|
@@ -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"}
|