@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
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// The deletion: every contributor deletes its part of one user's data, in one transaction and in
|
|
2
|
+
// deletion order (the app's data first, auth's account last). A refusal or a failure anywhere
|
|
3
|
+
// rolls back every contributor: an account is deleted whole or not at all.
|
|
4
|
+
import { err, ok } from "@softure-ai/core";
|
|
5
|
+
import { getDeletingContributors } from "./contributors.js";
|
|
6
|
+
/** Thrown inside the transaction to roll it back when a contributor refuses. */
|
|
7
|
+
class DeletionRefusal extends Error {
|
|
8
|
+
contributorId;
|
|
9
|
+
code;
|
|
10
|
+
constructor(contributorId, code) {
|
|
11
|
+
super(`contributor "${contributorId}" refused the deletion: ${code}`);
|
|
12
|
+
this.contributorId = contributorId;
|
|
13
|
+
this.code = code;
|
|
14
|
+
this.name = "DeletionRefusal";
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Deletes everything the app holds about `userId`. A contributor that must keep data (legal
|
|
19
|
+
* retention) returns an `Err`: nothing is deleted, the result is `privacy.deletion_refused`, and
|
|
20
|
+
* the contributor's id and code are logged. Thrown errors propagate after the rollback.
|
|
21
|
+
*/
|
|
22
|
+
export async function eraseUserData(ctx, userId) {
|
|
23
|
+
const contributors = getDeletingContributors(ctx.config);
|
|
24
|
+
try {
|
|
25
|
+
await ctx.db.transaction(async (tx) => {
|
|
26
|
+
const txCtx = { ...ctx, db: tx };
|
|
27
|
+
for (const contributor of contributors) {
|
|
28
|
+
const result = await contributor.deleteUserData(txCtx, userId);
|
|
29
|
+
if (!result.ok)
|
|
30
|
+
throw new DeletionRefusal(contributor.id, result.error);
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
if (!(error instanceof DeletionRefusal))
|
|
36
|
+
throw error;
|
|
37
|
+
console.warn(`@softure-ai/privacy: ${error.message}; nothing was deleted`);
|
|
38
|
+
return err("privacy.deletion_refused");
|
|
39
|
+
}
|
|
40
|
+
return ok();
|
|
41
|
+
}
|
|
42
|
+
//# sourceMappingURL=erase.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"erase.js","sourceRoot":"","sources":["../../src/server/erase.ts"],"names":[],"mappings":"AAAA,iGAAiG;AACjG,8FAA8F;AAC9F,2EAA2E;AAC3E,OAAO,EAAE,GAAG,EAAE,EAAE,EAAqC,MAAM,kBAAkB,CAAC;AAE9E,OAAO,EAAE,uBAAuB,EAAE,MAAM,mBAAmB,CAAC;AAI5D,gFAAgF;AAChF,MAAM,eAAgB,SAAQ,KAAK;IAEtB;IACA;IAFX,YACW,aAAqB,EACrB,IAAe;QAExB,KAAK,CAAC,gBAAgB,aAAa,2BAA2B,IAAI,EAAE,CAAC,CAAC;QAH7D,kBAAa,GAAb,aAAa,CAAQ;QACrB,SAAI,GAAJ,IAAI,CAAW;QAGxB,IAAI,CAAC,IAAI,GAAG,iBAAiB,CAAC;IAChC,CAAC;CACF;AAED;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,aAAa,CAAC,GAAmB,EAAE,MAAc;IACrE,MAAM,YAAY,GAAG,uBAAuB,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;IACzD,IAAI,CAAC;QACH,MAAM,GAAG,CAAC,EAAE,CAAC,WAAW,CAAC,KAAK,EAAE,EAAE,EAAE,EAAE;YACpC,MAAM,KAAK,GAAmB,EAAE,GAAG,GAAG,EAAE,EAAE,EAAE,EAAE,EAAE,CAAC;YACjD,KAAK,MAAM,WAAW,IAAI,YAAY,EAAE,CAAC;gBACvC,MAAM,MAAM,GAAG,MAAM,WAAW,CAAC,cAAc,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC;gBAC/D,IAAI,CAAC,MAAM,CAAC,EAAE;oBAAE,MAAM,IAAI,eAAe,CAAC,WAAW,CAAC,EAAE,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC;YAC1E,CAAC;QACH,CAAC,CAAC,CAAC;IACL,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,IAAI,CAAC,CAAC,KAAK,YAAY,eAAe,CAAC;YAAE,MAAM,KAAK,CAAC;QACrD,OAAO,CAAC,IAAI,CAAC,wBAAwB,KAAK,CAAC,OAAO,uBAAuB,CAAC,CAAC;QAC3E,OAAO,GAAG,CAAC,0BAA0B,CAAC,CAAC;IACzC,CAAC;IACD,OAAO,EAAE,EAAE,CAAC;AACd,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"health.d.ts","sourceRoot":"","sources":["../../src/server/health.ts"],"names":[],"mappings":"AAEA,OAAO,EAAM,KAAK,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAIxD,eAAO,MAAM,kBAAkB,EAAE,WAKhC,CAAC"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// The module's readiness probe for `GET /api/health` of `@softure-ai/ops`: the consents table
|
|
2
|
+
// exists and answers, i.e. `softure migrate` ran. It reads no rows.
|
|
3
|
+
import { ok } from "@softure-ai/core";
|
|
4
|
+
import { sql } from "drizzle-orm";
|
|
5
|
+
export const checkConsentsTable = async (context) => {
|
|
6
|
+
// Core types `db` as unknown; the health route passes the @softure-ai/db handle.
|
|
7
|
+
const db = context.db;
|
|
8
|
+
await db.execute(sql `select 1 from privacy.consents limit 0`);
|
|
9
|
+
return ok();
|
|
10
|
+
};
|
|
11
|
+
//# sourceMappingURL=health.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"health.js","sourceRoot":"","sources":["../../src/server/health.ts"],"names":[],"mappings":"AAAA,8FAA8F;AAC9F,oEAAoE;AACpE,OAAO,EAAE,EAAE,EAAoB,MAAM,kBAAkB,CAAC;AAExD,OAAO,EAAE,GAAG,EAAE,MAAM,aAAa,CAAC;AAElC,MAAM,CAAC,MAAM,kBAAkB,GAAgB,KAAK,EAAE,OAAO,EAAE,EAAE;IAC/D,iFAAiF;IACjF,MAAM,EAAE,GAAG,OAAO,CAAC,EAAe,CAAC;IACnC,MAAM,EAAE,CAAC,OAAO,CAAC,GAAG,CAAA,wCAAwC,CAAC,CAAC;IAC9D,OAAO,EAAE,EAAE,CAAC;AACd,CAAC,CAAC"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export { getConsent, getEmailKey, hasConsent, listConsents, recordConsent, type ConsentQuery, type RecordConsentInput, type RecordConsentResult, } from "./consents.js";
|
|
2
|
+
export { deleteConsentUserData, exportConsentUserData, privacyConsentsContributor, type PrivacyUserData, } from "./consents-contributor.js";
|
|
3
|
+
export { collectUserData, type CollectedUserData, type CollectUserDataResult } from "./collect.js";
|
|
4
|
+
export type { PrivacyContext } from "./context.js";
|
|
5
|
+
export { getDeletingContributors, getExportingContributors, getPrivacyContributors, type DeletingContributor, type ExportingContributor, type RegisteredContributor, } from "./contributors.js";
|
|
6
|
+
export { eraseUserData, type EraseUserDataResult } from "./erase.js";
|
|
7
|
+
export { findLegalDocument, getLegalDocument, getLegalDocuments } from "./legal-documents.js";
|
|
8
|
+
export { getPrivacyOptions, getPrivacyRoutes, type PrivacyRoutes } from "./options.js";
|
|
9
|
+
export { recordRegistrationConsent, REGISTRATION_SOURCE, type RegistrationConsentOptions } from "./registration-consent.js";
|
|
10
|
+
export { deleteOwnAccount, exportOwnData, type DeleteOwnAccountErrorCode, type DeleteOwnAccountInput, type DeleteOwnAccountResult, type ExportOwnDataInput, type ExportOwnDataResult, } from "./self-service.js";
|
|
11
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/server/index.ts"],"names":[],"mappings":"AAEA,OAAO,EACL,UAAU,EACV,WAAW,EACX,UAAU,EACV,YAAY,EACZ,aAAa,EACb,KAAK,YAAY,EACjB,KAAK,kBAAkB,EACvB,KAAK,mBAAmB,GACzB,MAAM,eAAe,CAAC;AACvB,OAAO,EACL,qBAAqB,EACrB,qBAAqB,EACrB,0BAA0B,EAC1B,KAAK,eAAe,GACrB,MAAM,2BAA2B,CAAC;AACnC,OAAO,EAAE,eAAe,EAAE,KAAK,iBAAiB,EAAE,KAAK,qBAAqB,EAAE,MAAM,cAAc,CAAC;AACnG,YAAY,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AACnD,OAAO,EACL,uBAAuB,EACvB,wBAAwB,EACxB,sBAAsB,EACtB,KAAK,mBAAmB,EACxB,KAAK,oBAAoB,EACzB,KAAK,qBAAqB,GAC3B,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAE,aAAa,EAAE,KAAK,mBAAmB,EAAE,MAAM,YAAY,CAAC;AACrE,OAAO,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;AAC9F,OAAO,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,KAAK,aAAa,EAAE,MAAM,cAAc,CAAC;AACvF,OAAO,EAAE,yBAAyB,EAAE,mBAAmB,EAAE,KAAK,0BAA0B,EAAE,MAAM,2BAA2B,CAAC;AAC5H,OAAO,EACL,gBAAgB,EAChB,aAAa,EACb,KAAK,yBAAyB,EAC9B,KAAK,qBAAqB,EAC1B,KAAK,sBAAsB,EAC3B,KAAK,kBAAkB,EACvB,KAAK,mBAAmB,GACzB,MAAM,mBAAmB,CAAC"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// Server-only API of @softure-ai/privacy. Every function receives the module context
|
|
2
|
+
// (`{ db, clock, config }`) and never reads request scope; the user id comes in as a value.
|
|
3
|
+
export { getConsent, getEmailKey, hasConsent, listConsents, recordConsent, } from "./consents.js";
|
|
4
|
+
export { deleteConsentUserData, exportConsentUserData, privacyConsentsContributor, } from "./consents-contributor.js";
|
|
5
|
+
export { collectUserData } from "./collect.js";
|
|
6
|
+
export { getDeletingContributors, getExportingContributors, getPrivacyContributors, } from "./contributors.js";
|
|
7
|
+
export { eraseUserData } from "./erase.js";
|
|
8
|
+
export { findLegalDocument, getLegalDocument, getLegalDocuments } from "./legal-documents.js";
|
|
9
|
+
export { getPrivacyOptions, getPrivacyRoutes } from "./options.js";
|
|
10
|
+
export { recordRegistrationConsent, REGISTRATION_SOURCE } from "./registration-consent.js";
|
|
11
|
+
export { deleteOwnAccount, exportOwnData, } from "./self-service.js";
|
|
12
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/server/index.ts"],"names":[],"mappings":"AAAA,qFAAqF;AACrF,4FAA4F;AAC5F,OAAO,EACL,UAAU,EACV,WAAW,EACX,UAAU,EACV,YAAY,EACZ,aAAa,GAId,MAAM,eAAe,CAAC;AACvB,OAAO,EACL,qBAAqB,EACrB,qBAAqB,EACrB,0BAA0B,GAE3B,MAAM,2BAA2B,CAAC;AACnC,OAAO,EAAE,eAAe,EAAsD,MAAM,cAAc,CAAC;AAEnG,OAAO,EACL,uBAAuB,EACvB,wBAAwB,EACxB,sBAAsB,GAIvB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAE,aAAa,EAA4B,MAAM,YAAY,CAAC;AACrE,OAAO,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;AAC9F,OAAO,EAAE,iBAAiB,EAAE,gBAAgB,EAAsB,MAAM,cAAc,CAAC;AACvF,OAAO,EAAE,yBAAyB,EAAE,mBAAmB,EAAmC,MAAM,2BAA2B,CAAC;AAC5H,OAAO,EACL,gBAAgB,EAChB,aAAa,GAMd,MAAM,mBAAmB,CAAC"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { SoftureConfig } from "@softure-ai/core";
|
|
2
|
+
import type { LegalDocumentDeclaration } from "../options.js";
|
|
3
|
+
/** Every declared document, in the order the config lists them. */
|
|
4
|
+
export declare function getLegalDocuments(config: SoftureConfig): readonly LegalDocumentDeclaration[];
|
|
5
|
+
/** The declared document with this id, or undefined. */
|
|
6
|
+
export declare function findLegalDocument(config: SoftureConfig, id: string): LegalDocumentDeclaration | undefined;
|
|
7
|
+
/** The declared document with this id. Throws for an id the config does not declare: a page naming one is a bug. */
|
|
8
|
+
export declare function getLegalDocument(config: SoftureConfig, id: string): LegalDocumentDeclaration;
|
|
9
|
+
//# sourceMappingURL=legal-documents.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"legal-documents.d.ts","sourceRoot":"","sources":["../../src/server/legal-documents.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AACtD,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,eAAe,CAAC;AAG9D,mEAAmE;AACnE,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,aAAa,GAAG,SAAS,wBAAwB,EAAE,CAE5F;AAED,wDAAwD;AACxD,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,aAAa,EAAE,EAAE,EAAE,MAAM,GAAG,wBAAwB,GAAG,SAAS,CAEzG;AAED,oHAAoH;AACpH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,aAAa,EAAE,EAAE,EAAE,MAAM,GAAG,wBAAwB,CAM5F"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { getPrivacyOptions } from "./options.js";
|
|
2
|
+
/** Every declared document, in the order the config lists them. */
|
|
3
|
+
export function getLegalDocuments(config) {
|
|
4
|
+
return getPrivacyOptions(config).documents;
|
|
5
|
+
}
|
|
6
|
+
/** The declared document with this id, or undefined. */
|
|
7
|
+
export function findLegalDocument(config, id) {
|
|
8
|
+
return getLegalDocuments(config).find((document) => document.id === id);
|
|
9
|
+
}
|
|
10
|
+
/** The declared document with this id. Throws for an id the config does not declare: a page naming one is a bug. */
|
|
11
|
+
export function getLegalDocument(config, id) {
|
|
12
|
+
const document = findLegalDocument(config, id);
|
|
13
|
+
if (document === undefined) {
|
|
14
|
+
throw new Error(`@softure-ai/privacy: no legal document "${id}"; declare it in privacy({ documents: [{ id: "${id}", version }] })`);
|
|
15
|
+
}
|
|
16
|
+
return document;
|
|
17
|
+
}
|
|
18
|
+
//# sourceMappingURL=legal-documents.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"legal-documents.js","sourceRoot":"","sources":["../../src/server/legal-documents.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AAEjD,mEAAmE;AACnE,MAAM,UAAU,iBAAiB,CAAC,MAAqB;IACrD,OAAO,iBAAiB,CAAC,MAAM,CAAC,CAAC,SAAS,CAAC;AAC7C,CAAC;AAED,wDAAwD;AACxD,MAAM,UAAU,iBAAiB,CAAC,MAAqB,EAAE,EAAU;IACjE,OAAO,iBAAiB,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,QAAQ,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC;AAC1E,CAAC;AAED,oHAAoH;AACpH,MAAM,UAAU,gBAAgB,CAAC,MAAqB,EAAE,EAAU;IAChE,MAAM,QAAQ,GAAG,iBAAiB,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;IAC/C,IAAI,QAAQ,KAAK,SAAS,EAAE,CAAC;QAC3B,MAAM,IAAI,KAAK,CAAC,2CAA2C,EAAE,iDAAiD,EAAE,kBAAkB,CAAC,CAAC;IACtI,CAAC;IACD,OAAO,QAAQ,CAAC;AAClB,CAAC"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type SoftureConfig } from "@softure-ai/core";
|
|
2
|
+
import type { PrivacyOptions } from "../options.js";
|
|
3
|
+
export interface PrivacyRoutes {
|
|
4
|
+
/** The page with the export link and the delete form. */
|
|
5
|
+
readonly account: string;
|
|
6
|
+
/** The export route handler. */
|
|
7
|
+
readonly export: string;
|
|
8
|
+
/** Where a deleted account lands, signed out. */
|
|
9
|
+
readonly afterDelete: string;
|
|
10
|
+
}
|
|
11
|
+
/** The enabled module. Throws when the app did not enable it: calling its functions then is a bug. */
|
|
12
|
+
export declare function getPrivacyModule(config: SoftureConfig): import("@softure-ai/core").AnySoftureModule;
|
|
13
|
+
export declare function getPrivacyOptions(config: SoftureConfig): PrivacyOptions;
|
|
14
|
+
export declare function getPrivacyRoutes(config: SoftureConfig): PrivacyRoutes;
|
|
15
|
+
//# sourceMappingURL=options.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"options.d.ts","sourceRoot":"","sources":["../../src/server/options.ts"],"names":[],"mappings":"AACA,OAAO,EAAa,KAAK,aAAa,EAAE,MAAM,kBAAkB,CAAC;AACjE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAIpD,MAAM,WAAW,aAAa;IAC5B,yDAAyD;IACzD,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,gCAAgC;IAChC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,iDAAiD;IACjD,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;CAC9B;AAED,sGAAsG;AACtG,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,aAAa,+CAMrD;AAED,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,aAAa,GAAG,cAAc,CAGvE;AAED,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,aAAa,GAAG,aAAa,CAGrE"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// The privacy options and routes of the running app, read from the configuration in the module context.
|
|
2
|
+
import { getModule } from "@softure-ai/core";
|
|
3
|
+
const MODULE_ID = "privacy";
|
|
4
|
+
/** The enabled module. Throws when the app did not enable it: calling its functions then is a bug. */
|
|
5
|
+
export function getPrivacyModule(config) {
|
|
6
|
+
const module = getModule(config, MODULE_ID);
|
|
7
|
+
if (module === undefined) {
|
|
8
|
+
throw new Error("@softure-ai/privacy: the module is not enabled; add privacy({ ... }) to modules in softure.config.ts");
|
|
9
|
+
}
|
|
10
|
+
return module;
|
|
11
|
+
}
|
|
12
|
+
export function getPrivacyOptions(config) {
|
|
13
|
+
// The module factory parsed these options with privacyOptionsSchema.
|
|
14
|
+
return getPrivacyModule(config).options;
|
|
15
|
+
}
|
|
16
|
+
export function getPrivacyRoutes(config) {
|
|
17
|
+
// The manifest declares exactly these routes; the factory merged the app's overrides.
|
|
18
|
+
return getPrivacyModule(config).routes;
|
|
19
|
+
}
|
|
20
|
+
//# sourceMappingURL=options.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"options.js","sourceRoot":"","sources":["../../src/server/options.ts"],"names":[],"mappings":"AAAA,wGAAwG;AACxG,OAAO,EAAE,SAAS,EAAsB,MAAM,kBAAkB,CAAC;AAGjE,MAAM,SAAS,GAAG,SAAS,CAAC;AAW5B,sGAAsG;AACtG,MAAM,UAAU,gBAAgB,CAAC,MAAqB;IACpD,MAAM,MAAM,GAAG,SAAS,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;IAC5C,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;QACzB,MAAM,IAAI,KAAK,CAAC,sGAAsG,CAAC,CAAC;IAC1H,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,MAAM,UAAU,iBAAiB,CAAC,MAAqB;IACrD,qEAAqE;IACrE,OAAO,gBAAgB,CAAC,MAAM,CAAC,CAAC,OAAyB,CAAC;AAC5D,CAAC;AAED,MAAM,UAAU,gBAAgB,CAAC,MAAqB;IACpD,sFAAsF;IACtF,OAAO,gBAAgB,CAAC,MAAM,CAAC,CAAC,MAAkC,CAAC;AACrE,CAAC"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type SoftureConfig } from "@softure-ai/core";
|
|
2
|
+
export declare const BUCKETS: {
|
|
3
|
+
readonly export: "privacy-export";
|
|
4
|
+
readonly delete: "privacy-delete";
|
|
5
|
+
};
|
|
6
|
+
/** The subject key of a user in the privacy buckets. */
|
|
7
|
+
export declare function userSubjectKey(userId: string): string;
|
|
8
|
+
/**
|
|
9
|
+
* Throws, naming every missing bucket, when the app's `security({ buckets })` lacks one privacy
|
|
10
|
+
* counts in. Checked once per config, before the first attempt is counted.
|
|
11
|
+
*/
|
|
12
|
+
export declare function assertPrivacyBuckets(config: SoftureConfig): void;
|
|
13
|
+
//# sourceMappingURL=rate-limits.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rate-limits.d.ts","sourceRoot":"","sources":["../../src/server/rate-limits.ts"],"names":[],"mappings":"AACA,OAAO,EAAa,KAAK,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAGjE,eAAO,MAAM,OAAO;;;CAGV,CAAC;AAEX,wDAAwD;AACxD,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAErD;AAID;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,aAAa,GAAG,IAAI,CAWhE"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// The rate limit buckets privacy consumes (PRIVACY_RATE_LIMIT_BUCKETS), both counted per user.
|
|
2
|
+
import { getModule } from "@softure-ai/core";
|
|
3
|
+
import { subjectKey } from "@softure-ai/security/server";
|
|
4
|
+
export const BUCKETS = {
|
|
5
|
+
export: "privacy-export",
|
|
6
|
+
delete: "privacy-delete",
|
|
7
|
+
};
|
|
8
|
+
/** The subject key of a user in the privacy buckets. */
|
|
9
|
+
export function userSubjectKey(userId) {
|
|
10
|
+
return subjectKey(`user:${userId}`);
|
|
11
|
+
}
|
|
12
|
+
const checkedConfigs = new WeakSet();
|
|
13
|
+
/**
|
|
14
|
+
* Throws, naming every missing bucket, when the app's `security({ buckets })` lacks one privacy
|
|
15
|
+
* counts in. Checked once per config, before the first attempt is counted.
|
|
16
|
+
*/
|
|
17
|
+
export function assertPrivacyBuckets(config) {
|
|
18
|
+
if (checkedConfigs.has(config))
|
|
19
|
+
return;
|
|
20
|
+
const options = getModule(config, "security")?.options;
|
|
21
|
+
const configured = options?.buckets ?? {};
|
|
22
|
+
const missing = Object.values(BUCKETS).filter((name) => !Object.hasOwn(configured, name));
|
|
23
|
+
if (missing.length > 0) {
|
|
24
|
+
throw new Error(`@softure-ai/privacy: security({ buckets }) lacks ${missing.map((name) => `"${name}"`).join(", ")}; spread PRIVACY_RATE_LIMIT_BUCKETS into it`);
|
|
25
|
+
}
|
|
26
|
+
checkedConfigs.add(config);
|
|
27
|
+
}
|
|
28
|
+
//# sourceMappingURL=rate-limits.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rate-limits.js","sourceRoot":"","sources":["../../src/server/rate-limits.ts"],"names":[],"mappings":"AAAA,+FAA+F;AAC/F,OAAO,EAAE,SAAS,EAAsB,MAAM,kBAAkB,CAAC;AACjE,OAAO,EAAE,UAAU,EAAE,MAAM,6BAA6B,CAAC;AAEzD,MAAM,CAAC,MAAM,OAAO,GAAG;IACrB,MAAM,EAAE,gBAAgB;IACxB,MAAM,EAAE,gBAAgB;CAChB,CAAC;AAEX,wDAAwD;AACxD,MAAM,UAAU,cAAc,CAAC,MAAc;IAC3C,OAAO,UAAU,CAAC,QAAQ,MAAM,EAAE,CAAC,CAAC;AACtC,CAAC;AAED,MAAM,cAAc,GAAG,IAAI,OAAO,EAAiB,CAAC;AAEpD;;;GAGG;AACH,MAAM,UAAU,oBAAoB,CAAC,MAAqB;IACxD,IAAI,cAAc,CAAC,GAAG,CAAC,MAAM,CAAC;QAAE,OAAO;IACvC,MAAM,OAAO,GAAG,SAAS,CAAC,MAAM,EAAE,UAAU,CAAC,EAAE,OAAsE,CAAC;IACtH,MAAM,UAAU,GAAG,OAAO,EAAE,OAAO,IAAI,EAAE,CAAC;IAC1C,MAAM,OAAO,GAAG,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,UAAU,EAAE,IAAI,CAAC,CAAC,CAAC;IAC1F,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACvB,MAAM,IAAI,KAAK,CACb,oDAAoD,OAAO,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,IAAI,GAAG,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,6CAA6C,CAC/I,CAAC;IACJ,CAAC;IACD,cAAc,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;AAC7B,CAAC"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { OnRegisteredHook } from "@softure-ai/auth";
|
|
2
|
+
export interface RegistrationConsentOptions {
|
|
3
|
+
/**
|
|
4
|
+
* The documents the registration checkbox accepts, by id. Defaults to every document declared
|
|
5
|
+
* in `privacy({ documents })`.
|
|
6
|
+
*/
|
|
7
|
+
readonly documents?: readonly string[];
|
|
8
|
+
}
|
|
9
|
+
/** The source of the rows this hook records. */
|
|
10
|
+
export declare const REGISTRATION_SOURCE = "registration";
|
|
11
|
+
/**
|
|
12
|
+
* An `onRegistered` hook for `auth({ onRegistered: recordRegistrationConsent() })`: one consent row
|
|
13
|
+
* per accepted document (purpose = document id, its configured version, source `registration`),
|
|
14
|
+
* at the account's creation time. Records nothing when auth's `requireConsent` is off. Throws
|
|
15
|
+
* (rolling the registration back) when no document is declared or one is unknown: a
|
|
16
|
+
* misconfiguration must not create accounts without evidence.
|
|
17
|
+
*/
|
|
18
|
+
export declare function recordRegistrationConsent(options?: RegistrationConsentOptions): OnRegisteredHook;
|
|
19
|
+
//# sourceMappingURL=registration-consent.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"registration-consent.d.ts","sourceRoot":"","sources":["../../src/server/registration-consent.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AAIzD,MAAM,WAAW,0BAA0B;IACzC;;;OAGG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CACxC;AAED,gDAAgD;AAChD,eAAO,MAAM,mBAAmB,iBAAiB,CAAC;AAElD;;;;;;GAMG;AACH,wBAAgB,yBAAyB,CAAC,OAAO,GAAE,0BAA+B,GAAG,gBAAgB,CAkBpG"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { getLegalDocuments } from "./legal-documents.js";
|
|
2
|
+
import { insertConsent } from "./consents.js";
|
|
3
|
+
/** The source of the rows this hook records. */
|
|
4
|
+
export const REGISTRATION_SOURCE = "registration";
|
|
5
|
+
/**
|
|
6
|
+
* An `onRegistered` hook for `auth({ onRegistered: recordRegistrationConsent() })`: one consent row
|
|
7
|
+
* per accepted document (purpose = document id, its configured version, source `registration`),
|
|
8
|
+
* at the account's creation time. Records nothing when auth's `requireConsent` is off. Throws
|
|
9
|
+
* (rolling the registration back) when no document is declared or one is unknown: a
|
|
10
|
+
* misconfiguration must not create accounts without evidence.
|
|
11
|
+
*/
|
|
12
|
+
export function recordRegistrationConsent(options = {}) {
|
|
13
|
+
return async (event, ctx) => {
|
|
14
|
+
if (event.consent === null)
|
|
15
|
+
return;
|
|
16
|
+
const documents = options.documents ?? getLegalDocuments(ctx.config).map((document) => document.id);
|
|
17
|
+
if (documents.length === 0) {
|
|
18
|
+
throw new Error("@softure-ai/privacy: recordRegistrationConsent() has no document to record; declare them in privacy({ documents })");
|
|
19
|
+
}
|
|
20
|
+
for (const document of documents) {
|
|
21
|
+
const recorded = await insertConsent(ctx, { subject: { userId: event.user.id }, purpose: document, granted: true, document, source: REGISTRATION_SOURCE }, event.consent.acceptedAt);
|
|
22
|
+
if (!recorded.ok) {
|
|
23
|
+
throw new Error(`@softure-ai/privacy: recording the registration consent to "${document}" failed: ${recorded.error}`);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
//# sourceMappingURL=registration-consent.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"registration-consent.js","sourceRoot":"","sources":["../../src/server/registration-consent.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;AACzD,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAU9C,gDAAgD;AAChD,MAAM,CAAC,MAAM,mBAAmB,GAAG,cAAc,CAAC;AAElD;;;;;;GAMG;AACH,MAAM,UAAU,yBAAyB,CAAC,UAAsC,EAAE;IAChF,OAAO,KAAK,EAAE,KAAK,EAAE,GAAG,EAAE,EAAE;QAC1B,IAAI,KAAK,CAAC,OAAO,KAAK,IAAI;YAAE,OAAO;QACnC,MAAM,SAAS,GAAG,OAAO,CAAC,SAAS,IAAI,iBAAiB,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC;QACpG,IAAI,SAAS,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YAC3B,MAAM,IAAI,KAAK,CAAC,oHAAoH,CAAC,CAAC;QACxI,CAAC;QACD,KAAK,MAAM,QAAQ,IAAI,SAAS,EAAE,CAAC;YACjC,MAAM,QAAQ,GAAG,MAAM,aAAa,CAClC,GAAG,EACH,EAAE,OAAO,EAAE,EAAE,MAAM,EAAE,KAAK,CAAC,IAAI,CAAC,EAAE,EAAE,EAAE,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,mBAAmB,EAAE,EAC/G,KAAK,CAAC,OAAO,CAAC,UAAU,CACzB,CAAC;YACF,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;gBACjB,MAAM,IAAI,KAAK,CAAC,+DAA+D,QAAQ,aAAa,QAAQ,CAAC,KAAK,EAAE,CAAC,CAAC;YACxH,CAAC;QACH,CAAC;IACH,CAAC,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { type Err, type Ok } from "@softure-ai/core";
|
|
2
|
+
import type { RateLimitRejection } from "@softure-ai/security";
|
|
3
|
+
import { type CollectUserDataResult } from "./collect.js";
|
|
4
|
+
import type { PrivacyContext } from "./context.js";
|
|
5
|
+
export interface ExportOwnDataInput {
|
|
6
|
+
/** The signed-in user, from the session. */
|
|
7
|
+
readonly userId: string;
|
|
8
|
+
}
|
|
9
|
+
export type ExportOwnDataResult = CollectUserDataResult | RateLimitRejection;
|
|
10
|
+
/** The signed-in user's export, counted in `privacy-export`. Database errors propagate. */
|
|
11
|
+
export declare function exportOwnData(ctx: PrivacyContext, input: ExportOwnDataInput): Promise<ExportOwnDataResult>;
|
|
12
|
+
export interface DeleteOwnAccountInput {
|
|
13
|
+
/** The signed-in user, from the session. */
|
|
14
|
+
readonly userId: string;
|
|
15
|
+
/** Their current password, typed again. */
|
|
16
|
+
readonly password: string;
|
|
17
|
+
/** Whether the "I understand" checkbox was ticked. */
|
|
18
|
+
readonly isConfirmed: boolean;
|
|
19
|
+
}
|
|
20
|
+
export type DeleteOwnAccountErrorCode = "privacy.confirmation_required" | "privacy.password_invalid" | "privacy.deletion_refused";
|
|
21
|
+
export type DeleteOwnAccountResult = Ok<undefined> | Err<DeleteOwnAccountErrorCode> | RateLimitRejection;
|
|
22
|
+
/**
|
|
23
|
+
* Deletes the signed-in user's account and data. The confirmation is checked first (cheap, and a
|
|
24
|
+
* missed checkbox should not spend an attempt), then `privacy-delete` is counted, then the password
|
|
25
|
+
* is verified: a session alone cannot delete an account, and the form is no faster a password
|
|
26
|
+
* oracle than the login. Database errors propagate after the rollback.
|
|
27
|
+
*/
|
|
28
|
+
export declare function deleteOwnAccount(ctx: PrivacyContext, input: DeleteOwnAccountInput): Promise<DeleteOwnAccountResult>;
|
|
29
|
+
//# sourceMappingURL=self-service.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"self-service.d.ts","sourceRoot":"","sources":["../../src/server/self-service.ts"],"names":[],"mappings":"AAGA,OAAO,EAAW,KAAK,GAAG,EAAE,KAAK,EAAE,EAAE,MAAM,kBAAkB,CAAC;AAE9D,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAE/D,OAAO,EAAmB,KAAK,qBAAqB,EAAE,MAAM,cAAc,CAAC;AAC3E,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAInD,MAAM,WAAW,kBAAkB;IACjC,4CAA4C;IAC5C,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,MAAM,mBAAmB,GAAG,qBAAqB,GAAG,kBAAkB,CAAC;AAE7E,2FAA2F;AAC3F,wBAAsB,aAAa,CAAC,GAAG,EAAE,cAAc,EAAE,KAAK,EAAE,kBAAkB,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAKhH;AAED,MAAM,WAAW,qBAAqB;IACpC,4CAA4C;IAC5C,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,2CAA2C;IAC3C,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,sDAAsD;IACtD,QAAQ,CAAC,WAAW,EAAE,OAAO,CAAC;CAC/B;AAED,MAAM,MAAM,yBAAyB,GAAG,+BAA+B,GAAG,0BAA0B,GAAG,0BAA0B,CAAC;AAElI,MAAM,MAAM,sBAAsB,GAAG,EAAE,CAAC,SAAS,CAAC,GAAG,GAAG,CAAC,yBAAyB,CAAC,GAAG,kBAAkB,CAAC;AAEzG;;;;;GAKG;AACH,wBAAsB,gBAAgB,CAAC,GAAG,EAAE,cAAc,EAAE,KAAK,EAAE,qBAAqB,GAAG,OAAO,CAAC,sBAAsB,CAAC,CAYzH"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// What a signed-in user does for themselves: download their data and delete their account. The
|
|
2
|
+
// caller passes the user id it took from the session (never from the request body); both
|
|
3
|
+
// operations count an attempt per user before any work.
|
|
4
|
+
import { err, ok } from "@softure-ai/core";
|
|
5
|
+
import { isCurrentPassword } from "@softure-ai/auth/server";
|
|
6
|
+
import { consumeRateLimit } from "@softure-ai/security/server";
|
|
7
|
+
import { collectUserData } from "./collect.js";
|
|
8
|
+
import { eraseUserData } from "./erase.js";
|
|
9
|
+
import { assertPrivacyBuckets, BUCKETS, userSubjectKey } from "./rate-limits.js";
|
|
10
|
+
/** The signed-in user's export, counted in `privacy-export`. Database errors propagate. */
|
|
11
|
+
export async function exportOwnData(ctx, input) {
|
|
12
|
+
assertPrivacyBuckets(ctx.config);
|
|
13
|
+
const limit = await consumeRateLimit(ctx, { bucket: BUCKETS.export, key: userSubjectKey(input.userId) });
|
|
14
|
+
if (!limit.ok)
|
|
15
|
+
return limit;
|
|
16
|
+
return collectUserData(ctx, input.userId);
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Deletes the signed-in user's account and data. The confirmation is checked first (cheap, and a
|
|
20
|
+
* missed checkbox should not spend an attempt), then `privacy-delete` is counted, then the password
|
|
21
|
+
* is verified: a session alone cannot delete an account, and the form is no faster a password
|
|
22
|
+
* oracle than the login. Database errors propagate after the rollback.
|
|
23
|
+
*/
|
|
24
|
+
export async function deleteOwnAccount(ctx, input) {
|
|
25
|
+
assertPrivacyBuckets(ctx.config);
|
|
26
|
+
if (!input.isConfirmed)
|
|
27
|
+
return err("privacy.confirmation_required");
|
|
28
|
+
const limit = await consumeRateLimit(ctx, { bucket: BUCKETS.delete, key: userSubjectKey(input.userId) });
|
|
29
|
+
if (!limit.ok)
|
|
30
|
+
return limit;
|
|
31
|
+
// A fresh error, not one derived from the check: the result then carries nothing of the password.
|
|
32
|
+
if (!(await isCurrentPassword(ctx, input.userId, input.password)))
|
|
33
|
+
return err("privacy.password_invalid");
|
|
34
|
+
const erased = await eraseUserData(ctx, input.userId);
|
|
35
|
+
return erased.ok ? ok() : erased;
|
|
36
|
+
}
|
|
37
|
+
//# sourceMappingURL=self-service.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"self-service.js","sourceRoot":"","sources":["../../src/server/self-service.ts"],"names":[],"mappings":"AAAA,+FAA+F;AAC/F,yFAAyF;AACzF,wDAAwD;AACxD,OAAO,EAAE,GAAG,EAAE,EAAE,EAAqB,MAAM,kBAAkB,CAAC;AAC9D,OAAO,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAE5D,OAAO,EAAE,gBAAgB,EAAE,MAAM,6BAA6B,CAAC;AAC/D,OAAO,EAAE,eAAe,EAA8B,MAAM,cAAc,CAAC;AAE3E,OAAO,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAC3C,OAAO,EAAE,oBAAoB,EAAE,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AASjF,2FAA2F;AAC3F,MAAM,CAAC,KAAK,UAAU,aAAa,CAAC,GAAmB,EAAE,KAAyB;IAChF,oBAAoB,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;IACjC,MAAM,KAAK,GAAG,MAAM,gBAAgB,CAAC,GAAG,EAAE,EAAE,MAAM,EAAE,OAAO,CAAC,MAAM,EAAE,GAAG,EAAE,cAAc,CAAC,KAAK,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;IACzG,IAAI,CAAC,KAAK,CAAC,EAAE;QAAE,OAAO,KAAK,CAAC;IAC5B,OAAO,eAAe,CAAC,GAAG,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;AAC5C,CAAC;AAeD;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,gBAAgB,CAAC,GAAmB,EAAE,KAA4B;IACtF,oBAAoB,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;IACjC,IAAI,CAAC,KAAK,CAAC,WAAW;QAAE,OAAO,GAAG,CAAC,+BAA+B,CAAC,CAAC;IAEpE,MAAM,KAAK,GAAG,MAAM,gBAAgB,CAAC,GAAG,EAAE,EAAE,MAAM,EAAE,OAAO,CAAC,MAAM,EAAE,GAAG,EAAE,cAAc,CAAC,KAAK,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;IACzG,IAAI,CAAC,KAAK,CAAC,EAAE;QAAE,OAAO,KAAK,CAAC;IAE5B,kGAAkG;IAClG,IAAI,CAAC,CAAC,MAAM,iBAAiB,CAAC,GAAG,EAAE,KAAK,CAAC,MAAM,EAAE,KAAK,CAAC,QAAQ,CAAC,CAAC;QAAE,OAAO,GAAG,CAAC,0BAA0B,CAAC,CAAC;IAE1G,MAAM,MAAM,GAAG,MAAM,aAAa,CAAC,GAAG,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IACtD,OAAO,MAAM,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC;AACnC,CAAC"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { Locale } from "@softure-ai/core";
|
|
2
|
+
import { type ClassNames } from "@softure-ai/ui";
|
|
3
|
+
import { type DeleteAccountFormState } from "../contract.js";
|
|
4
|
+
import { type PrivacyMessages } from "../messages/index.js";
|
|
5
|
+
export type DeleteAccountAction = (previous: DeleteAccountFormState, formData: FormData) => Promise<DeleteAccountFormState>;
|
|
6
|
+
export type DeleteAccountFormSlot = "root" | "description" | "form";
|
|
7
|
+
export interface DeleteAccountFormProps {
|
|
8
|
+
readonly action: DeleteAccountAction;
|
|
9
|
+
readonly messages: PrivacyMessages;
|
|
10
|
+
/** Locale of the built-in copy of the ui primitives. */
|
|
11
|
+
readonly locale?: Locale;
|
|
12
|
+
readonly classNames?: ClassNames<DeleteAccountFormSlot>;
|
|
13
|
+
readonly unstyled?: boolean;
|
|
14
|
+
}
|
|
15
|
+
export declare function DeleteAccountForm({ action, messages, locale, classNames, unstyled }: DeleteAccountFormProps): import("react").JSX.Element;
|
|
16
|
+
//# sourceMappingURL=delete-account-form.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"delete-account-form.d.ts","sourceRoot":"","sources":["../../src/ui/delete-account-form.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAC/C,OAAO,EAAoB,KAAK,UAAU,EAAmD,MAAM,gBAAgB,CAAC;AAEpH,OAAO,EAA2B,KAAK,sBAAsB,EAAgC,MAAM,gBAAgB,CAAC;AACpH,OAAO,EAA0B,KAAK,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAOpF,MAAM,MAAM,mBAAmB,GAAG,CAAC,QAAQ,EAAE,sBAAsB,EAAE,QAAQ,EAAE,QAAQ,KAAK,OAAO,CAAC,sBAAsB,CAAC,CAAC;AAE5H,MAAM,MAAM,qBAAqB,GAAG,MAAM,GAAG,aAAa,GAAG,MAAM,CAAC;AAEpE,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,MAAM,EAAE,mBAAmB,CAAC;IACrC,QAAQ,CAAC,QAAQ,EAAE,eAAe,CAAC;IACnC,wDAAwD;IACxD,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,UAAU,CAAC,EAAE,UAAU,CAAC,qBAAqB,CAAC,CAAC;IACxD,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAC;CAC7B;AAQD,wBAAgB,iBAAiB,CAAC,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,UAAU,EAAE,QAAQ,EAAE,EAAE,sBAAsB,+BAyB3G"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { Button, Checkbox, createSlotClassGetter, FormError, PasswordField } from "@softure-ai/ui";
|
|
4
|
+
import { useActionState } from "react";
|
|
5
|
+
import { INITIAL_DELETE_ACCOUNT_STATE } from "../contract.js";
|
|
6
|
+
import { getPrivacyErrorMessage } from "../messages/index.js";
|
|
7
|
+
const DEFAULT_CLASSES = {
|
|
8
|
+
root: "sft:flex sft:flex-col sft:gap-3 sft:font-sans",
|
|
9
|
+
description: "sft:m-0 sft:text-sm sft:text-muted",
|
|
10
|
+
form: "sft:flex sft:flex-col sft:gap-3",
|
|
11
|
+
};
|
|
12
|
+
export function DeleteAccountForm({ action, messages, locale, classNames, unstyled }) {
|
|
13
|
+
const [state, formAction, isPending] = useActionState(action, INITIAL_DELETE_ACCOUNT_STATE);
|
|
14
|
+
const slot = createSlotClassGetter({ defaults: DEFAULT_CLASSES, classNames, unstyled });
|
|
15
|
+
const message = state.error === undefined ? undefined : getPrivacyErrorMessage(messages, state.error);
|
|
16
|
+
const errorAt = (field) => (state.field === field ? message : undefined);
|
|
17
|
+
return (_jsxs("div", { className: slot("root"), children: [_jsx("p", { className: slot("description"), children: messages.delete.description }), _jsxs("form", { action: formAction, className: slot("form"), children: [_jsx(PasswordField, { name: "password", label: messages.delete.password, autoComplete: "current-password", error: errorAt("password"), unstyled: unstyled, locale: locale }), _jsx(Checkbox, { name: "confirm", label: messages.delete.confirm, required: true, error: errorAt("confirm"), unstyled: unstyled }), _jsx(FormError, { message: state.field === undefined ? message : undefined, unstyled: unstyled }), _jsx(Button, { type: "submit", variant: "danger", fullWidth: true, pending: isPending, unstyled: unstyled, children: isPending ? messages.delete.pending : messages.delete.submit })] })] }));
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=delete-account-form.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"delete-account-form.js","sourceRoot":"","sources":["../../src/ui/delete-account-form.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAC;;AAGb,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAmB,qBAAqB,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AACpH,OAAO,EAAE,cAAc,EAAE,MAAM,OAAO,CAAC;AACvC,OAAO,EAAwD,4BAA4B,EAAE,MAAM,gBAAgB,CAAC;AACpH,OAAO,EAAE,sBAAsB,EAAwB,MAAM,sBAAsB,CAAC;AAoBpF,MAAM,eAAe,GAAoD;IACvE,IAAI,EAAE,+CAA+C;IACrD,WAAW,EAAE,oCAAoC;IACjD,IAAI,EAAE,iCAAiC;CACxC,CAAC;AAEF,MAAM,UAAU,iBAAiB,CAAC,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,UAAU,EAAE,QAAQ,EAA0B;IAC1G,MAAM,CAAC,KAAK,EAAE,UAAU,EAAE,SAAS,CAAC,GAAG,cAAc,CAAC,MAAM,EAAE,4BAA4B,CAAC,CAAC;IAC5F,MAAM,IAAI,GAAG,qBAAqB,CAAC,EAAE,QAAQ,EAAE,eAAe,EAAE,UAAU,EAAE,QAAQ,EAAE,CAAC,CAAC;IACxF,MAAM,OAAO,GAAG,KAAK,CAAC,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,sBAAsB,CAAC,QAAQ,EAAE,KAAK,CAAC,KAAK,CAAC,CAAC;IACtG,MAAM,OAAO,GAAG,CAAC,KAAyB,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,KAAK,KAAK,KAAK,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC;IAC7F,OAAO,CACL,eAAK,SAAS,EAAE,IAAI,CAAC,MAAM,CAAC,aAC1B,YAAG,SAAS,EAAE,IAAI,CAAC,aAAa,CAAC,YAAG,QAAQ,CAAC,MAAM,CAAC,WAAW,GAAK,EACpE,gBAAM,MAAM,EAAE,UAAU,EAAE,SAAS,EAAE,IAAI,CAAC,MAAM,CAAC,aAC/C,KAAC,aAAa,IACZ,IAAI,EAAC,UAAU,EACf,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAC,QAAQ,EAC/B,YAAY,EAAC,kBAAkB,EAC/B,KAAK,EAAE,OAAO,CAAC,UAAU,CAAC,EAC1B,QAAQ,EAAE,QAAQ,EAClB,MAAM,EAAE,MAAM,GACd,EACF,KAAC,QAAQ,IAAC,IAAI,EAAC,SAAS,EAAC,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAC,OAAO,EAAE,QAAQ,QAAC,KAAK,EAAE,OAAO,CAAC,SAAS,CAAC,EAAE,QAAQ,EAAE,QAAQ,GAAI,EACnH,KAAC,SAAS,IAAC,OAAO,EAAE,KAAK,CAAC,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,SAAS,EAAE,QAAQ,EAAE,QAAQ,GAAI,EAC3F,KAAC,MAAM,IAAC,IAAI,EAAC,QAAQ,EAAC,OAAO,EAAC,QAAQ,EAAC,SAAS,QAAC,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,QAAQ,YACpF,SAAS,CAAC,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,MAAM,GACtD,IACJ,IACH,CACP,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { DeleteAccountForm, type DeleteAccountAction, type DeleteAccountFormProps, type DeleteAccountFormSlot, } from "./delete-account-form.js";
|
|
2
|
+
export { formatLegalDate, LegalDocument, LegalSection, type LegalChange, type LegalDocumentProps, type LegalDocumentSlot, type LegalSectionContent, type LegalSectionProps, type LegalSectionSlot, } from "./legal-document.js";
|
|
3
|
+
export { LegalFooter, type LegalFooterLink, type LegalFooterProps, type LegalFooterSlot } from "./legal-footer.js";
|
|
4
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/ui/index.ts"],"names":[],"mappings":"AAGA,OAAO,EACL,iBAAiB,EACjB,KAAK,mBAAmB,EACxB,KAAK,sBAAsB,EAC3B,KAAK,qBAAqB,GAC3B,MAAM,0BAA0B,CAAC;AAClC,OAAO,EACL,eAAe,EACf,aAAa,EACb,YAAY,EACZ,KAAK,WAAW,EAChB,KAAK,kBAAkB,EACvB,KAAK,iBAAiB,EACtB,KAAK,mBAAmB,EACxB,KAAK,iBAAiB,EACtB,KAAK,gBAAgB,GACtB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAE,WAAW,EAAE,KAAK,eAAe,EAAE,KAAK,gBAAgB,EAAE,KAAK,eAAe,EAAE,MAAM,mBAAmB,CAAC"}
|
package/dist/ui/index.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// The privacy forms and the legal document shell, to compose into the app's own pages. The form
|
|
2
|
+
// takes the server action from `@softure-ai/privacy/next` as a prop: no `next/*` import here
|
|
3
|
+
// (docs/02-module-standard.md §5).
|
|
4
|
+
export { DeleteAccountForm, } from "./delete-account-form.js";
|
|
5
|
+
export { formatLegalDate, LegalDocument, LegalSection, } from "./legal-document.js";
|
|
6
|
+
export { LegalFooter } from "./legal-footer.js";
|
|
7
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/ui/index.ts"],"names":[],"mappings":"AAAA,gGAAgG;AAChG,6FAA6F;AAC7F,mCAAmC;AACnC,OAAO,EACL,iBAAiB,GAIlB,MAAM,0BAA0B,CAAC;AAClC,OAAO,EACL,eAAe,EACf,aAAa,EACb,YAAY,GAOb,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAE,WAAW,EAAqE,MAAM,mBAAmB,CAAC"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { Locale } from "@softure-ai/core";
|
|
2
|
+
import { type ClassNames } from "@softure-ai/ui";
|
|
3
|
+
import type { ReactNode } from "react";
|
|
4
|
+
import type { PrivacyMessages } from "../messages/index.js";
|
|
5
|
+
/** One section of a document: `id` is its anchor in the table of contents. */
|
|
6
|
+
export interface LegalSectionContent {
|
|
7
|
+
/** Kebab-case anchor, unique in the document, e.g. `data-we-collect`. */
|
|
8
|
+
readonly id: string;
|
|
9
|
+
readonly title: ReactNode;
|
|
10
|
+
readonly content: ReactNode;
|
|
11
|
+
}
|
|
12
|
+
/** One entry of the change history. */
|
|
13
|
+
export interface LegalChange {
|
|
14
|
+
readonly version: string;
|
|
15
|
+
/** ISO date, `YYYY-MM-DD`. */
|
|
16
|
+
readonly date: string;
|
|
17
|
+
readonly summary: ReactNode;
|
|
18
|
+
}
|
|
19
|
+
export type LegalSectionSlot = "root" | "title" | "body";
|
|
20
|
+
export interface LegalSectionProps {
|
|
21
|
+
readonly id: string;
|
|
22
|
+
readonly title: ReactNode;
|
|
23
|
+
readonly children: ReactNode;
|
|
24
|
+
readonly classNames?: ClassNames<LegalSectionSlot>;
|
|
25
|
+
readonly unstyled?: boolean;
|
|
26
|
+
}
|
|
27
|
+
/** A titled section with an anchor; `LegalDocument` renders one per entry of `sections`. */
|
|
28
|
+
export declare function LegalSection({ id, title, children, classNames, unstyled }: LegalSectionProps): import("react").JSX.Element;
|
|
29
|
+
export type LegalDocumentSlot = "root" | "header" | "title" | "meta" | "intro" | "contents" | "contentsTitle" | "contentsList" | "link" | "changes" | "changesTitle" | "changesList" | "change" | "changeMeta";
|
|
30
|
+
export interface LegalDocumentProps {
|
|
31
|
+
readonly title: ReactNode;
|
|
32
|
+
/** The version in force; read it from the config with `getLegalDocument(config, id).version`. */
|
|
33
|
+
readonly version: string;
|
|
34
|
+
/** ISO date, `YYYY-MM-DD`, from which this version applies. */
|
|
35
|
+
readonly effectiveFrom: string;
|
|
36
|
+
readonly intro?: ReactNode;
|
|
37
|
+
readonly sections: readonly LegalSectionContent[];
|
|
38
|
+
/** Newest first, as the reader expects; none hides the history. */
|
|
39
|
+
readonly changes?: readonly LegalChange[];
|
|
40
|
+
readonly messages: PrivacyMessages;
|
|
41
|
+
/** Formats the dates. */
|
|
42
|
+
readonly locale: Locale;
|
|
43
|
+
readonly classNames?: ClassNames<LegalDocumentSlot>;
|
|
44
|
+
/** Classes of every section. */
|
|
45
|
+
readonly sectionClassNames?: ClassNames<LegalSectionSlot>;
|
|
46
|
+
readonly unstyled?: boolean;
|
|
47
|
+
}
|
|
48
|
+
/** `2026-10-01` as the locale writes it (`October 1, 2026` in English); other text as given. */
|
|
49
|
+
export declare function formatLegalDate(date: string, locale: Locale): string;
|
|
50
|
+
export declare function LegalDocument({ title, version, effectiveFrom, intro, sections, changes, messages, locale, classNames, sectionClassNames, unstyled, }: LegalDocumentProps): import("react").JSX.Element;
|
|
51
|
+
//# sourceMappingURL=legal-document.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"legal-document.d.ts","sourceRoot":"","sources":["../../src/ui/legal-document.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAC/C,OAAO,EAAE,KAAK,UAAU,EAAyB,MAAM,gBAAgB,CAAC;AACxE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AACvC,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAO5D,8EAA8E;AAC9E,MAAM,WAAW,mBAAmB;IAClC,yEAAyE;IACzE,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,KAAK,EAAE,SAAS,CAAC;IAC1B,QAAQ,CAAC,OAAO,EAAE,SAAS,CAAC;CAC7B;AAED,uCAAuC;AACvC,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,8BAA8B;IAC9B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,EAAE,SAAS,CAAC;CAC7B;AAED,MAAM,MAAM,gBAAgB,GAAG,MAAM,GAAG,OAAO,GAAG,MAAM,CAAC;AAEzD,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,KAAK,EAAE,SAAS,CAAC;IAC1B,QAAQ,CAAC,QAAQ,EAAE,SAAS,CAAC;IAC7B,QAAQ,CAAC,UAAU,CAAC,EAAE,UAAU,CAAC,gBAAgB,CAAC,CAAC;IACnD,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAC;CAC7B;AAQD,4FAA4F;AAC5F,wBAAgB,YAAY,CAAC,EAAE,EAAE,EAAE,KAAK,EAAE,QAAQ,EAAE,UAAU,EAAE,QAAQ,EAAE,EAAE,iBAAiB,+BAU5F;AAED,MAAM,MAAM,iBAAiB,GACzB,MAAM,GACN,QAAQ,GACR,OAAO,GACP,MAAM,GACN,OAAO,GACP,UAAU,GACV,eAAe,GACf,cAAc,GACd,MAAM,GACN,SAAS,GACT,cAAc,GACd,aAAa,GACb,QAAQ,GACR,YAAY,CAAC;AAEjB,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,KAAK,EAAE,SAAS,CAAC;IAC1B,iGAAiG;IACjG,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,+DAA+D;IAC/D,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,KAAK,CAAC,EAAE,SAAS,CAAC;IAC3B,QAAQ,CAAC,QAAQ,EAAE,SAAS,mBAAmB,EAAE,CAAC;IAClD,mEAAmE;IACnE,QAAQ,CAAC,OAAO,CAAC,EAAE,SAAS,WAAW,EAAE,CAAC;IAC1C,QAAQ,CAAC,QAAQ,EAAE,eAAe,CAAC;IACnC,yBAAyB;IACzB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,UAAU,CAAC,EAAE,UAAU,CAAC,iBAAiB,CAAC,CAAC;IACpD,gCAAgC;IAChC,QAAQ,CAAC,iBAAiB,CAAC,EAAE,UAAU,CAAC,gBAAgB,CAAC,CAAC;IAC1D,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAC;CAC7B;AAqBD,gGAAgG;AAChG,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAQpE;AAMD,wBAAgB,aAAa,CAAC,EAC5B,KAAK,EACL,OAAO,EACP,aAAa,EACb,KAAK,EACL,QAAQ,EACR,OAAY,EACZ,QAAQ,EACR,MAAM,EACN,UAAU,EACV,iBAAiB,EACjB,QAAQ,GACT,EAAE,kBAAkB,+BAoDpB"}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { createSlotClassGetter } from "@softure-ai/ui";
|
|
3
|
+
const SECTION_CLASSES = {
|
|
4
|
+
root: "sft:flex sft:flex-col sft:gap-2 sft:scroll-mt-4",
|
|
5
|
+
title: "sft:m-0 sft:font-heading sft:text-lg sft:font-semibold sft:text-foreground",
|
|
6
|
+
body: "sft:flex sft:flex-col sft:gap-2 sft:text-sm sft:leading-relaxed sft:text-foreground",
|
|
7
|
+
};
|
|
8
|
+
/** A titled section with an anchor; `LegalDocument` renders one per entry of `sections`. */
|
|
9
|
+
export function LegalSection({ id, title, children, classNames, unstyled }) {
|
|
10
|
+
const slot = createSlotClassGetter({ defaults: SECTION_CLASSES, classNames, unstyled });
|
|
11
|
+
return (_jsxs("section", { id: id, "aria-labelledby": `${id}-title`, className: slot("root"), children: [_jsx("h2", { id: `${id}-title`, className: slot("title"), children: title }), _jsx("div", { className: slot("body"), children: children })] }));
|
|
12
|
+
}
|
|
13
|
+
const DOCUMENT_CLASSES = {
|
|
14
|
+
root: "sft:mx-auto sft:box-border sft:flex sft:w-full sft:flex-col sft:gap-4 sft:sm:max-w-3xl sft:px-4 sft:py-4 sft:font-sans",
|
|
15
|
+
header: "sft:flex sft:flex-col sft:gap-2",
|
|
16
|
+
title: "sft:m-0 sft:font-heading sft:text-2xl sft:sm:text-3xl sft:font-bold sft:tracking-tight sft:text-foreground",
|
|
17
|
+
meta: "sft:m-0 sft:text-sm sft:text-muted",
|
|
18
|
+
intro: "sft:text-sm sft:leading-relaxed sft:text-foreground",
|
|
19
|
+
contents: "sft:flex sft:flex-col sft:gap-2 sft:rounded-card sft:border sft:border-border sft:bg-surface sft:p-4",
|
|
20
|
+
contentsTitle: "sft:m-0 sft:text-base sft:font-semibold sft:text-foreground",
|
|
21
|
+
contentsList: "sft:m-0 sft:flex sft:flex-col sft:gap-1 sft:pl-8 sft:text-sm",
|
|
22
|
+
link: "sft:text-accent sft:focus-visible:outline-2 sft:focus-visible:outline-focus sft:focus-visible:outline-offset-2",
|
|
23
|
+
changes: "sft:flex sft:flex-col sft:gap-2 sft:border-t sft:border-border sft:pt-4 sft:scroll-mt-4",
|
|
24
|
+
changesTitle: "sft:m-0 sft:font-heading sft:text-lg sft:font-semibold sft:text-foreground",
|
|
25
|
+
changesList: "sft:m-0 sft:flex sft:flex-col sft:gap-2 sft:p-0 sft:list-none sft:text-sm",
|
|
26
|
+
change: "sft:flex sft:flex-col sft:gap-1",
|
|
27
|
+
changeMeta: "sft:font-medium sft:text-muted sft:tabular-nums",
|
|
28
|
+
};
|
|
29
|
+
const ISO_DATE = /^(\d{4})-(\d{2})-(\d{2})$/;
|
|
30
|
+
/** `2026-10-01` as the locale writes it (`October 1, 2026` in English); other text as given. */
|
|
31
|
+
export function formatLegalDate(date, locale) {
|
|
32
|
+
const match = ISO_DATE.exec(date);
|
|
33
|
+
if (match === null)
|
|
34
|
+
return date;
|
|
35
|
+
const [, year, month, day] = match.map(Number);
|
|
36
|
+
const value = new Date(Date.UTC(year, month - 1, day));
|
|
37
|
+
// A calendar date, so UTC: the server's time zone must not move it to the day before.
|
|
38
|
+
if (value.getUTCDate() !== day)
|
|
39
|
+
return date;
|
|
40
|
+
return new Intl.DateTimeFormat(locale, { dateStyle: "long", timeZone: "UTC" }).format(value);
|
|
41
|
+
}
|
|
42
|
+
/** The id of the change history section; the table of contents does not list it. */
|
|
43
|
+
const CHANGES_ID = "legal-changes";
|
|
44
|
+
const CONTENTS_TITLE_ID = "legal-contents-title";
|
|
45
|
+
export function LegalDocument({ title, version, effectiveFrom, intro, sections, changes = [], messages, locale, classNames, sectionClassNames, unstyled, }) {
|
|
46
|
+
const slot = createSlotClassGetter({ defaults: DOCUMENT_CLASSES, classNames, unstyled });
|
|
47
|
+
const copy = messages.legal;
|
|
48
|
+
return (_jsxs("article", { className: slot("root"), children: [_jsxs("header", { className: slot("header"), children: [_jsx("h1", { className: slot("title"), children: title }), _jsxs("p", { className: slot("meta"), children: [copy.version, " ", version, " \u00B7 ", copy.effectiveFrom, " ", formatLegalDate(effectiveFrom, locale)] }), intro === undefined ? null : _jsx("div", { className: slot("intro"), children: intro })] }), sections.length === 0 ? null : (_jsxs("nav", { "aria-labelledby": CONTENTS_TITLE_ID, className: slot("contents"), children: [_jsx("h2", { id: CONTENTS_TITLE_ID, className: slot("contentsTitle"), children: copy.contents }), _jsx("ol", { className: slot("contentsList"), children: sections.map((section) => (_jsx("li", { children: _jsx("a", { href: `#${section.id}`, className: slot("link"), children: section.title }) }, section.id))) })] })), sections.map((section) => (_jsx(LegalSection, { id: section.id, title: section.title, classNames: sectionClassNames, unstyled: unstyled, children: section.content }, section.id))), changes.length === 0 ? null : (_jsxs("section", { id: CHANGES_ID, "aria-labelledby": `${CHANGES_ID}-title`, className: slot("changes"), children: [_jsx("h2", { id: `${CHANGES_ID}-title`, className: slot("changesTitle"), children: copy.changes }), _jsx("ol", { className: slot("changesList"), children: changes.map((change) => (_jsxs("li", { className: slot("change"), children: [_jsxs("span", { className: slot("changeMeta"), children: [copy.version, " ", change.version, " \u00B7 ", formatLegalDate(change.date, locale)] }), _jsx("span", { children: change.summary })] }, change.version))) })] }))] }));
|
|
49
|
+
}
|
|
50
|
+
//# sourceMappingURL=legal-document.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"legal-document.js","sourceRoot":"","sources":["../../src/ui/legal-document.tsx"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type ClassNames } from "@softure-ai/ui";
|
|
2
|
+
import type { ReactNode } from "react";
|
|
3
|
+
import type { PrivacyMessages } from "../messages/index.js";
|
|
4
|
+
export interface LegalFooterLink {
|
|
5
|
+
readonly href: string;
|
|
6
|
+
readonly label: ReactNode;
|
|
7
|
+
}
|
|
8
|
+
export type LegalFooterSlot = "root" | "list" | "link" | "note";
|
|
9
|
+
export interface LegalFooterProps {
|
|
10
|
+
readonly links: readonly LegalFooterLink[];
|
|
11
|
+
/** A line under the links, e.g. the operator's name and address. */
|
|
12
|
+
readonly note?: ReactNode;
|
|
13
|
+
readonly messages: PrivacyMessages;
|
|
14
|
+
readonly classNames?: ClassNames<LegalFooterSlot>;
|
|
15
|
+
readonly unstyled?: boolean;
|
|
16
|
+
}
|
|
17
|
+
export declare function LegalFooter({ links, note, messages, classNames, unstyled }: LegalFooterProps): import("react").JSX.Element;
|
|
18
|
+
//# sourceMappingURL=legal-footer.d.ts.map
|