@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/dist/schema.d.ts
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
export declare const privacySchema: import("drizzle-orm/pg-core").PgSchema<"privacy">;
|
|
2
|
+
export declare const consents: import("drizzle-orm/pg-core").PgTableWithColumns<{
|
|
3
|
+
name: "consents";
|
|
4
|
+
schema: "privacy";
|
|
5
|
+
columns: {
|
|
6
|
+
id: import("drizzle-orm/pg-core").PgColumn<{
|
|
7
|
+
name: "id";
|
|
8
|
+
tableName: "consents";
|
|
9
|
+
dataType: "number";
|
|
10
|
+
columnType: "PgBigInt53";
|
|
11
|
+
data: number;
|
|
12
|
+
driverParam: string | number;
|
|
13
|
+
notNull: true;
|
|
14
|
+
hasDefault: true;
|
|
15
|
+
isPrimaryKey: true;
|
|
16
|
+
isAutoincrement: false;
|
|
17
|
+
hasRuntimeDefault: false;
|
|
18
|
+
enumValues: undefined;
|
|
19
|
+
baseColumn: never;
|
|
20
|
+
identity: "always";
|
|
21
|
+
generated: undefined;
|
|
22
|
+
}, {}, {}>;
|
|
23
|
+
userId: import("drizzle-orm/pg-core").PgColumn<{
|
|
24
|
+
name: "user_id";
|
|
25
|
+
tableName: "consents";
|
|
26
|
+
dataType: "string";
|
|
27
|
+
columnType: "PgUUID";
|
|
28
|
+
data: string;
|
|
29
|
+
driverParam: string;
|
|
30
|
+
notNull: false;
|
|
31
|
+
hasDefault: false;
|
|
32
|
+
isPrimaryKey: false;
|
|
33
|
+
isAutoincrement: false;
|
|
34
|
+
hasRuntimeDefault: false;
|
|
35
|
+
enumValues: undefined;
|
|
36
|
+
baseColumn: never;
|
|
37
|
+
identity: undefined;
|
|
38
|
+
generated: undefined;
|
|
39
|
+
}, {}, {}>;
|
|
40
|
+
emailKey: import("drizzle-orm/pg-core").PgColumn<{
|
|
41
|
+
name: "email_key";
|
|
42
|
+
tableName: "consents";
|
|
43
|
+
dataType: "string";
|
|
44
|
+
columnType: "PgText";
|
|
45
|
+
data: string;
|
|
46
|
+
driverParam: string;
|
|
47
|
+
notNull: false;
|
|
48
|
+
hasDefault: false;
|
|
49
|
+
isPrimaryKey: false;
|
|
50
|
+
isAutoincrement: false;
|
|
51
|
+
hasRuntimeDefault: false;
|
|
52
|
+
enumValues: [string, ...string[]];
|
|
53
|
+
baseColumn: never;
|
|
54
|
+
identity: undefined;
|
|
55
|
+
generated: undefined;
|
|
56
|
+
}, {}, {}>;
|
|
57
|
+
purpose: import("drizzle-orm/pg-core").PgColumn<{
|
|
58
|
+
name: "purpose";
|
|
59
|
+
tableName: "consents";
|
|
60
|
+
dataType: "string";
|
|
61
|
+
columnType: "PgText";
|
|
62
|
+
data: string;
|
|
63
|
+
driverParam: string;
|
|
64
|
+
notNull: true;
|
|
65
|
+
hasDefault: false;
|
|
66
|
+
isPrimaryKey: false;
|
|
67
|
+
isAutoincrement: false;
|
|
68
|
+
hasRuntimeDefault: false;
|
|
69
|
+
enumValues: [string, ...string[]];
|
|
70
|
+
baseColumn: never;
|
|
71
|
+
identity: undefined;
|
|
72
|
+
generated: undefined;
|
|
73
|
+
}, {}, {}>;
|
|
74
|
+
granted: import("drizzle-orm/pg-core").PgColumn<{
|
|
75
|
+
name: "granted";
|
|
76
|
+
tableName: "consents";
|
|
77
|
+
dataType: "boolean";
|
|
78
|
+
columnType: "PgBoolean";
|
|
79
|
+
data: boolean;
|
|
80
|
+
driverParam: boolean;
|
|
81
|
+
notNull: true;
|
|
82
|
+
hasDefault: false;
|
|
83
|
+
isPrimaryKey: false;
|
|
84
|
+
isAutoincrement: false;
|
|
85
|
+
hasRuntimeDefault: false;
|
|
86
|
+
enumValues: undefined;
|
|
87
|
+
baseColumn: never;
|
|
88
|
+
identity: undefined;
|
|
89
|
+
generated: undefined;
|
|
90
|
+
}, {}, {}>;
|
|
91
|
+
documentId: import("drizzle-orm/pg-core").PgColumn<{
|
|
92
|
+
name: "document_id";
|
|
93
|
+
tableName: "consents";
|
|
94
|
+
dataType: "string";
|
|
95
|
+
columnType: "PgText";
|
|
96
|
+
data: string;
|
|
97
|
+
driverParam: string;
|
|
98
|
+
notNull: false;
|
|
99
|
+
hasDefault: false;
|
|
100
|
+
isPrimaryKey: false;
|
|
101
|
+
isAutoincrement: false;
|
|
102
|
+
hasRuntimeDefault: false;
|
|
103
|
+
enumValues: [string, ...string[]];
|
|
104
|
+
baseColumn: never;
|
|
105
|
+
identity: undefined;
|
|
106
|
+
generated: undefined;
|
|
107
|
+
}, {}, {}>;
|
|
108
|
+
documentVersion: import("drizzle-orm/pg-core").PgColumn<{
|
|
109
|
+
name: "document_version";
|
|
110
|
+
tableName: "consents";
|
|
111
|
+
dataType: "string";
|
|
112
|
+
columnType: "PgText";
|
|
113
|
+
data: string;
|
|
114
|
+
driverParam: string;
|
|
115
|
+
notNull: false;
|
|
116
|
+
hasDefault: false;
|
|
117
|
+
isPrimaryKey: false;
|
|
118
|
+
isAutoincrement: false;
|
|
119
|
+
hasRuntimeDefault: false;
|
|
120
|
+
enumValues: [string, ...string[]];
|
|
121
|
+
baseColumn: never;
|
|
122
|
+
identity: undefined;
|
|
123
|
+
generated: undefined;
|
|
124
|
+
}, {}, {}>;
|
|
125
|
+
source: import("drizzle-orm/pg-core").PgColumn<{
|
|
126
|
+
name: "source";
|
|
127
|
+
tableName: "consents";
|
|
128
|
+
dataType: "string";
|
|
129
|
+
columnType: "PgText";
|
|
130
|
+
data: string;
|
|
131
|
+
driverParam: string;
|
|
132
|
+
notNull: true;
|
|
133
|
+
hasDefault: false;
|
|
134
|
+
isPrimaryKey: false;
|
|
135
|
+
isAutoincrement: false;
|
|
136
|
+
hasRuntimeDefault: false;
|
|
137
|
+
enumValues: [string, ...string[]];
|
|
138
|
+
baseColumn: never;
|
|
139
|
+
identity: undefined;
|
|
140
|
+
generated: undefined;
|
|
141
|
+
}, {}, {}>;
|
|
142
|
+
recordedAt: import("drizzle-orm/pg-core").PgColumn<{
|
|
143
|
+
name: "recorded_at";
|
|
144
|
+
tableName: "consents";
|
|
145
|
+
dataType: "date";
|
|
146
|
+
columnType: "PgTimestamp";
|
|
147
|
+
data: Date;
|
|
148
|
+
driverParam: string;
|
|
149
|
+
notNull: true;
|
|
150
|
+
hasDefault: false;
|
|
151
|
+
isPrimaryKey: false;
|
|
152
|
+
isAutoincrement: false;
|
|
153
|
+
hasRuntimeDefault: false;
|
|
154
|
+
enumValues: undefined;
|
|
155
|
+
baseColumn: never;
|
|
156
|
+
identity: undefined;
|
|
157
|
+
generated: undefined;
|
|
158
|
+
}, {}, {}>;
|
|
159
|
+
};
|
|
160
|
+
dialect: "pg";
|
|
161
|
+
}>;
|
|
162
|
+
//# sourceMappingURL=schema.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../src/schema.ts"],"names":[],"mappings":"AAKA,eAAO,MAAM,aAAa,mDAAsB,CAAC;AAEjD,eAAO,MAAM,QAAQ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAUnB,CAAC"}
|
package/dist/schema.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// Drizzle view of the module's table (migrations/0001_create_consents.sql). The migration is the
|
|
2
|
+
// source of truth; this file only types the queries.
|
|
3
|
+
import { users } from "@softure-ai/auth";
|
|
4
|
+
import { bigint, boolean, pgSchema, text, timestamp, uuid } from "drizzle-orm/pg-core";
|
|
5
|
+
export const privacySchema = pgSchema("privacy");
|
|
6
|
+
export const consents = privacySchema.table("consents", {
|
|
7
|
+
id: bigint("id", { mode: "number" }).primaryKey().generatedAlwaysAsIdentity(),
|
|
8
|
+
userId: uuid("user_id").references(() => users.id, { onDelete: "cascade" }),
|
|
9
|
+
emailKey: text("email_key"),
|
|
10
|
+
purpose: text("purpose").notNull(),
|
|
11
|
+
granted: boolean("granted").notNull(),
|
|
12
|
+
documentId: text("document_id"),
|
|
13
|
+
documentVersion: text("document_version"),
|
|
14
|
+
source: text("source").notNull(),
|
|
15
|
+
recordedAt: timestamp("recorded_at", { withTimezone: true }).notNull(),
|
|
16
|
+
});
|
|
17
|
+
//# sourceMappingURL=schema.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"schema.js","sourceRoot":"","sources":["../src/schema.ts"],"names":[],"mappings":"AAAA,iGAAiG;AACjG,qDAAqD;AACrD,OAAO,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAC;AACzC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,qBAAqB,CAAC;AAEvF,MAAM,CAAC,MAAM,aAAa,GAAG,QAAQ,CAAC,SAAS,CAAC,CAAC;AAEjD,MAAM,CAAC,MAAM,QAAQ,GAAG,aAAa,CAAC,KAAK,CAAC,UAAU,EAAE;IACtD,EAAE,EAAE,MAAM,CAAC,IAAI,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC,CAAC,UAAU,EAAE,CAAC,yBAAyB,EAAE;IAC7E,MAAM,EAAE,IAAI,CAAC,SAAS,CAAC,CAAC,UAAU,CAAC,GAAG,EAAE,CAAC,KAAK,CAAC,EAAE,EAAE,EAAE,QAAQ,EAAE,SAAS,EAAE,CAAC;IAC3E,QAAQ,EAAE,IAAI,CAAC,WAAW,CAAC;IAC3B,OAAO,EAAE,IAAI,CAAC,SAAS,CAAC,CAAC,OAAO,EAAE;IAClC,OAAO,EAAE,OAAO,CAAC,SAAS,CAAC,CAAC,OAAO,EAAE;IACrC,UAAU,EAAE,IAAI,CAAC,aAAa,CAAC;IAC/B,eAAe,EAAE,IAAI,CAAC,kBAAkB,CAAC;IACzC,MAAM,EAAE,IAAI,CAAC,QAAQ,CAAC,CAAC,OAAO,EAAE;IAChC,UAAU,EAAE,SAAS,CAAC,aAAa,EAAE,EAAE,YAAY,EAAE,IAAI,EAAE,CAAC,CAAC,OAAO,EAAE;CACvE,CAAC,CAAC"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type Err, type Ok } from "@softure-ai/core";
|
|
2
|
+
import type { PrivacyExport } from "../contract.js";
|
|
3
|
+
import type { PrivacyContext } from "./context.js";
|
|
4
|
+
export interface CollectedUserData {
|
|
5
|
+
readonly document: PrivacyExport;
|
|
6
|
+
/** The document as JSON, the bytes a download sends; measured against `export.maxBytes`. */
|
|
7
|
+
readonly json: string;
|
|
8
|
+
}
|
|
9
|
+
export type CollectUserDataResult = Ok<CollectedUserData> | Err<"privacy.export_failed" | "privacy.export_too_large">;
|
|
10
|
+
/**
|
|
11
|
+
* Collects every exporting contributor's part for `userId`. A contributor's `Err` fails the whole
|
|
12
|
+
* export (`privacy.export_failed`, logged with the contributor's id and code): a file with a part
|
|
13
|
+
* missing would look complete. Thrown errors (a database failure) propagate after the rollback.
|
|
14
|
+
*/
|
|
15
|
+
export declare function collectUserData(ctx: PrivacyContext, userId: string): Promise<CollectUserDataResult>;
|
|
16
|
+
//# sourceMappingURL=collect.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"collect.d.ts","sourceRoot":"","sources":["../../src/server/collect.ts"],"names":[],"mappings":"AAEA,OAAO,EAAW,KAAK,GAAG,EAAE,KAAK,EAAE,EAAE,MAAM,kBAAkB,CAAC;AAC9D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AACpD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAInD,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,QAAQ,EAAE,aAAa,CAAC;IACjC,4FAA4F;IAC5F,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,MAAM,qBAAqB,GAAG,EAAE,CAAC,iBAAiB,CAAC,GAAG,GAAG,CAAC,uBAAuB,GAAG,0BAA0B,CAAC,CAAC;AAEtH;;;;GAIG;AACH,wBAAsB,eAAe,CAAC,GAAG,EAAE,cAAc,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,qBAAqB,CAAC,CAoCzG"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// The export: every contributor's part of one user's data, read in one transaction so the parts
|
|
2
|
+
// agree with each other, and bounded in size before it is handed to a response.
|
|
3
|
+
import { err, ok } from "@softure-ai/core";
|
|
4
|
+
import { getExportingContributors } from "./contributors.js";
|
|
5
|
+
import { getPrivacyOptions } from "./options.js";
|
|
6
|
+
/**
|
|
7
|
+
* Collects every exporting contributor's part for `userId`. A contributor's `Err` fails the whole
|
|
8
|
+
* export (`privacy.export_failed`, logged with the contributor's id and code): a file with a part
|
|
9
|
+
* missing would look complete. Thrown errors (a database failure) propagate after the rollback.
|
|
10
|
+
*/
|
|
11
|
+
export async function collectUserData(ctx, userId) {
|
|
12
|
+
const contributors = getExportingContributors(ctx.config);
|
|
13
|
+
const exportedAt = ctx.clock.now();
|
|
14
|
+
const collected = await ctx.db.transaction(async (tx) => {
|
|
15
|
+
const txCtx = { ...ctx, db: tx };
|
|
16
|
+
const data = {};
|
|
17
|
+
for (const contributor of contributors) {
|
|
18
|
+
const part = await contributor.exportUserData(txCtx, userId);
|
|
19
|
+
if (!part.ok) {
|
|
20
|
+
console.error(`@softure-ai/privacy: contributor "${contributor.id}" failed the export: ${part.error}`);
|
|
21
|
+
return err("privacy.export_failed");
|
|
22
|
+
}
|
|
23
|
+
data[contributor.id] = part.value;
|
|
24
|
+
}
|
|
25
|
+
return ok(data);
|
|
26
|
+
}, { isolationLevel: "repeatable read", accessMode: "read only" });
|
|
27
|
+
if (!collected.ok)
|
|
28
|
+
return collected;
|
|
29
|
+
const document = {
|
|
30
|
+
format: "softure.privacy-export",
|
|
31
|
+
version: 1,
|
|
32
|
+
userId,
|
|
33
|
+
exportedAt: exportedAt.toISOString(),
|
|
34
|
+
data: collected.value,
|
|
35
|
+
};
|
|
36
|
+
const json = JSON.stringify(document, null, 2);
|
|
37
|
+
const { maxBytes } = getPrivacyOptions(ctx.config).export;
|
|
38
|
+
if (Buffer.byteLength(json, "utf8") > maxBytes) {
|
|
39
|
+
console.error(`@softure-ai/privacy: an export exceeded export.maxBytes (${String(maxBytes)})`);
|
|
40
|
+
return err("privacy.export_too_large");
|
|
41
|
+
}
|
|
42
|
+
return ok({ document, json });
|
|
43
|
+
}
|
|
44
|
+
//# sourceMappingURL=collect.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"collect.js","sourceRoot":"","sources":["../../src/server/collect.ts"],"names":[],"mappings":"AAAA,gGAAgG;AAChG,gFAAgF;AAChF,OAAO,EAAE,GAAG,EAAE,EAAE,EAAqB,MAAM,kBAAkB,CAAC;AAG9D,OAAO,EAAE,wBAAwB,EAAE,MAAM,mBAAmB,CAAC;AAC7D,OAAO,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AAUjD;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CAAC,GAAmB,EAAE,MAAc;IACvE,MAAM,YAAY,GAAG,wBAAwB,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;IAC1D,MAAM,UAAU,GAAG,GAAG,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC;IAEnC,MAAM,SAAS,GAAG,MAAM,GAAG,CAAC,EAAE,CAAC,WAAW,CACxC,KAAK,EAAE,EAAE,EAAuE,EAAE;QAChF,MAAM,KAAK,GAAmB,EAAE,GAAG,GAAG,EAAE,EAAE,EAAE,EAAE,EAAE,CAAC;QACjD,MAAM,IAAI,GAA4B,EAAE,CAAC;QACzC,KAAK,MAAM,WAAW,IAAI,YAAY,EAAE,CAAC;YACvC,MAAM,IAAI,GAAG,MAAM,WAAW,CAAC,cAAc,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC;YAC7D,IAAI,CAAC,IAAI,CAAC,EAAE,EAAE,CAAC;gBACb,OAAO,CAAC,KAAK,CAAC,qCAAqC,WAAW,CAAC,EAAE,wBAAwB,IAAI,CAAC,KAAK,EAAE,CAAC,CAAC;gBACvG,OAAO,GAAG,CAAC,uBAAuB,CAAC,CAAC;YACtC,CAAC;YACD,IAAI,CAAC,WAAW,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC;QACpC,CAAC;QACD,OAAO,EAAE,CAAC,IAAI,CAAC,CAAC;IAClB,CAAC,EACD,EAAE,cAAc,EAAE,iBAAiB,EAAE,UAAU,EAAE,WAAW,EAAE,CAC/D,CAAC;IACF,IAAI,CAAC,SAAS,CAAC,EAAE;QAAE,OAAO,SAAS,CAAC;IAEpC,MAAM,QAAQ,GAAkB;QAC9B,MAAM,EAAE,wBAAwB;QAChC,OAAO,EAAE,CAAC;QACV,MAAM;QACN,UAAU,EAAE,UAAU,CAAC,WAAW,EAAE;QACpC,IAAI,EAAE,SAAS,CAAC,KAAK;KACtB,CAAC;IACF,MAAM,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC;IAC/C,MAAM,EAAE,QAAQ,EAAE,GAAG,iBAAiB,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC;IAC1D,IAAI,MAAM,CAAC,UAAU,CAAC,IAAI,EAAE,MAAM,CAAC,GAAG,QAAQ,EAAE,CAAC;QAC/C,OAAO,CAAC,KAAK,CAAC,4DAA4D,MAAM,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC;QAC/F,OAAO,GAAG,CAAC,0BAA0B,CAAC,CAAC;IACzC,CAAC;IACD,OAAO,EAAE,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC;AAChC,CAAC"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type ModuleContext, type Ok, type PrivacyContributor } from "@softure-ai/core";
|
|
2
|
+
import type { ConsentRecord } from "../contract.js";
|
|
3
|
+
/** What privacy holds about one user, as it appears in their export. */
|
|
4
|
+
export interface PrivacyUserData {
|
|
5
|
+
/** Every consent given or withdrawn, oldest first; `subject` says whether it names the account or its email. */
|
|
6
|
+
readonly consents: readonly (ConsentRecord & {
|
|
7
|
+
readonly subject: "account" | "email";
|
|
8
|
+
})[];
|
|
9
|
+
}
|
|
10
|
+
export declare function exportConsentUserData(context: ModuleContext, userId: string): Promise<Ok<PrivacyUserData>>;
|
|
11
|
+
/** Deletes the user's consents: with the data gone there is nothing left to prove consent for. */
|
|
12
|
+
export declare function deleteConsentUserData(context: ModuleContext, userId: string): Promise<Ok<undefined>>;
|
|
13
|
+
export declare const privacyConsentsContributor: PrivacyContributor;
|
|
14
|
+
//# sourceMappingURL=consents-contributor.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"consents-contributor.d.ts","sourceRoot":"","sources":["../../src/server/consents-contributor.ts"],"names":[],"mappings":"AAIA,OAAO,EAAM,KAAK,aAAa,EAAE,KAAK,EAAE,EAAE,KAAK,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AAG5F,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAIpD,wEAAwE;AACxE,MAAM,WAAW,eAAe;IAC9B,gHAAgH;IAChH,QAAQ,CAAC,QAAQ,EAAE,SAAS,CAAC,aAAa,GAAG;QAAE,QAAQ,CAAC,OAAO,EAAE,SAAS,GAAG,OAAO,CAAA;KAAE,CAAC,EAAE,CAAC;CAC3F;AAYD,wBAAsB,qBAAqB,CAAC,OAAO,EAAE,aAAa,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,EAAE,CAAC,eAAe,CAAC,CAAC,CAOhH;AAED,kGAAkG;AAClG,wBAAsB,qBAAqB,CAAC,OAAO,EAAE,aAAa,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,EAAE,CAAC,SAAS,CAAC,CAAC,CAK1G;AAED,eAAO,MAAM,0BAA0B,EAAE,kBAGxC,CAAC"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// The privacy module's own part of a GDPR export and deletion: the consent ledger. A user's
|
|
2
|
+
// consents are the rows of their account and the rows of their email address recorded before
|
|
3
|
+
// the account existed (a waitlist sign-up).
|
|
4
|
+
import { users } from "@softure-ai/auth";
|
|
5
|
+
import { ok } from "@softure-ai/core";
|
|
6
|
+
import { asc, eq, or } from "drizzle-orm";
|
|
7
|
+
import { consents } from "../schema.js";
|
|
8
|
+
import { getEmailKey, toConsentRecord } from "./consents.js";
|
|
9
|
+
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
10
|
+
/** The rows of the account and of its email; null when no account has this id. */
|
|
11
|
+
async function matchUserConsents(db, userId) {
|
|
12
|
+
if (!UUID.test(userId))
|
|
13
|
+
return null;
|
|
14
|
+
const [account] = await db.select({ email: users.email }).from(users).where(eq(users.id, userId));
|
|
15
|
+
if (account === undefined)
|
|
16
|
+
return eq(consents.userId, userId);
|
|
17
|
+
return or(eq(consents.userId, userId), eq(consents.emailKey, getEmailKey(account.email))) ?? null;
|
|
18
|
+
}
|
|
19
|
+
export async function exportConsentUserData(context, userId) {
|
|
20
|
+
// Core types `db` as unknown; privacy passes the @softure-ai/db handle or its transaction.
|
|
21
|
+
const db = context.db;
|
|
22
|
+
const condition = await matchUserConsents(db, userId);
|
|
23
|
+
if (condition === null)
|
|
24
|
+
return ok({ consents: [] });
|
|
25
|
+
const rows = await db.select().from(consents).where(condition).orderBy(asc(consents.recordedAt), asc(consents.id));
|
|
26
|
+
return ok({ consents: rows.map((row) => ({ ...toConsentRecord(row), subject: row.userId === null ? "email" : "account" })) });
|
|
27
|
+
}
|
|
28
|
+
/** Deletes the user's consents: with the data gone there is nothing left to prove consent for. */
|
|
29
|
+
export async function deleteConsentUserData(context, userId) {
|
|
30
|
+
const db = context.db;
|
|
31
|
+
const condition = await matchUserConsents(db, userId);
|
|
32
|
+
if (condition !== null)
|
|
33
|
+
await db.delete(consents).where(condition);
|
|
34
|
+
return ok();
|
|
35
|
+
}
|
|
36
|
+
export const privacyConsentsContributor = {
|
|
37
|
+
exportUserData: exportConsentUserData,
|
|
38
|
+
deleteUserData: deleteConsentUserData,
|
|
39
|
+
};
|
|
40
|
+
//# sourceMappingURL=consents-contributor.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"consents-contributor.js","sourceRoot":"","sources":["../../src/server/consents-contributor.ts"],"names":[],"mappings":"AAAA,4FAA4F;AAC5F,6FAA6F;AAC7F,4CAA4C;AAC5C,OAAO,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAC;AACzC,OAAO,EAAE,EAAE,EAAwD,MAAM,kBAAkB,CAAC;AAE5F,OAAO,EAAE,GAAG,EAAE,EAAE,EAAE,EAAE,EAAY,MAAM,aAAa,CAAC;AAEpD,OAAO,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC;AACxC,OAAO,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAQ7D,MAAM,IAAI,GAAG,iEAAiE,CAAC;AAE/E,kFAAkF;AAClF,KAAK,UAAU,iBAAiB,CAAC,EAAa,EAAE,MAAc;IAC5D,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC;QAAE,OAAO,IAAI,CAAC;IACpC,MAAM,CAAC,OAAO,CAAC,GAAG,MAAM,EAAE,CAAC,MAAM,CAAC,EAAE,KAAK,EAAE,KAAK,CAAC,KAAK,EAAE,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,EAAE,CAAC,KAAK,CAAC,EAAE,EAAE,MAAM,CAAC,CAAC,CAAC;IAClG,IAAI,OAAO,KAAK,SAAS;QAAE,OAAO,EAAE,CAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC9D,OAAO,EAAE,CAAC,EAAE,CAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,EAAE,CAAC,QAAQ,CAAC,QAAQ,EAAE,WAAW,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC;AACpG,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,qBAAqB,CAAC,OAAsB,EAAE,MAAc;IAChF,2FAA2F;IAC3F,MAAM,EAAE,GAAG,OAAO,CAAC,EAAe,CAAC;IACnC,MAAM,SAAS,GAAG,MAAM,iBAAiB,CAAC,EAAE,EAAE,MAAM,CAAC,CAAC;IACtD,IAAI,SAAS,KAAK,IAAI;QAAE,OAAO,EAAE,CAAC,EAAE,QAAQ,EAAE,EAAE,EAAE,CAAC,CAAC;IACpD,MAAM,IAAI,GAAG,MAAM,EAAE,CAAC,MAAM,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,UAAU,CAAC,EAAE,GAAG,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,CAAC;IACnH,OAAO,EAAE,CAAC,EAAE,QAAQ,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,EAAE,GAAG,eAAe,CAAC,GAAG,CAAC,EAAE,OAAO,EAAE,GAAG,CAAC,MAAM,KAAK,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,SAAS,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC;AAChI,CAAC;AAED,kGAAkG;AAClG,MAAM,CAAC,KAAK,UAAU,qBAAqB,CAAC,OAAsB,EAAE,MAAc;IAChF,MAAM,EAAE,GAAG,OAAO,CAAC,EAAe,CAAC;IACnC,MAAM,SAAS,GAAG,MAAM,iBAAiB,CAAC,EAAE,EAAE,MAAM,CAAC,CAAC;IACtD,IAAI,SAAS,KAAK,IAAI;QAAE,MAAM,EAAE,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC;IACnE,OAAO,EAAE,EAAE,CAAC;AACd,CAAC;AAED,MAAM,CAAC,MAAM,0BAA0B,GAAuB;IAC5D,cAAc,EAAE,qBAAqB;IACrC,cAAc,EAAE,qBAAqB;CACtC,CAAC"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { type Err, type Ok } from "@softure-ai/core";
|
|
2
|
+
import type { ConsentRecord, ConsentState, ConsentSubject } from "../contract.js";
|
|
3
|
+
import { consents } from "../schema.js";
|
|
4
|
+
import type { PrivacyContext } from "./context.js";
|
|
5
|
+
export interface RecordConsentInput {
|
|
6
|
+
readonly subject: ConsentSubject;
|
|
7
|
+
/** What the consent is for, kebab-case, e.g. `terms` or `newsletter`. */
|
|
8
|
+
readonly purpose: string;
|
|
9
|
+
/** True to give consent, false to withdraw it (a new row; the earlier one stays as evidence). */
|
|
10
|
+
readonly granted: boolean;
|
|
11
|
+
/** The id of a document declared in `privacy({ documents })`; its configured version is recorded. */
|
|
12
|
+
readonly document?: string;
|
|
13
|
+
/** Where the consent was given, kebab-case, e.g. `registration`, `waitlist`, `account`. */
|
|
14
|
+
readonly source: string;
|
|
15
|
+
}
|
|
16
|
+
export type RecordConsentResult = Ok<ConsentRecord> | Err<"privacy.consent_invalid" | "privacy.document_unknown">;
|
|
17
|
+
export interface ConsentQuery {
|
|
18
|
+
readonly subject: ConsentSubject;
|
|
19
|
+
readonly purpose: string;
|
|
20
|
+
}
|
|
21
|
+
/** The key an email subject is stored under: base64url SHA-256 of the trimmed, lowercased address. */
|
|
22
|
+
export declare function getEmailKey(email: string): string;
|
|
23
|
+
type ConsentRow = typeof consents.$inferSelect;
|
|
24
|
+
export declare function toConsentRecord(row: ConsentRow): ConsentRecord;
|
|
25
|
+
/**
|
|
26
|
+
* Records a consent at `recordedAt`. For `recordConsent` (the clock's now) and the registration
|
|
27
|
+
* hook (the account's creation time).
|
|
28
|
+
*/
|
|
29
|
+
export declare function insertConsent(ctx: PrivacyContext, input: RecordConsentInput, recordedAt: Date): Promise<RecordConsentResult>;
|
|
30
|
+
/**
|
|
31
|
+
* Records that `subject` gave (or withdrew) consent to `purpose`, now. With `document`, the
|
|
32
|
+
* document's configured version is recorded with it. Database errors propagate.
|
|
33
|
+
*/
|
|
34
|
+
export declare function recordConsent(ctx: PrivacyContext, input: RecordConsentInput): Promise<RecordConsentResult>;
|
|
35
|
+
/** The current state of one purpose for one subject (its latest record), or null when none was recorded. */
|
|
36
|
+
export declare function getConsent(ctx: PrivacyContext, query: ConsentQuery): Promise<ConsentState | null>;
|
|
37
|
+
/** Whether the latest record of the purpose grants it, for the document version in force. */
|
|
38
|
+
export declare function hasConsent(ctx: PrivacyContext, query: ConsentQuery): Promise<boolean>;
|
|
39
|
+
/** Every record of the subject, oldest first: the evidence of what was given and withdrawn, when. */
|
|
40
|
+
export declare function listConsents(ctx: PrivacyContext, subject: ConsentSubject): Promise<ConsentRecord[]>;
|
|
41
|
+
export {};
|
|
42
|
+
//# sourceMappingURL=consents.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"consents.d.ts","sourceRoot":"","sources":["../../src/server/consents.ts"],"names":[],"mappings":"AAIA,OAAO,EAAW,KAAK,GAAG,EAAE,KAAK,EAAE,EAAE,MAAM,kBAAkB,CAAC;AAG9D,OAAO,KAAK,EAAE,aAAa,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAElF,OAAO,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC;AACxC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAUnD,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,OAAO,EAAE,cAAc,CAAC;IACjC,yEAAyE;IACzE,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,iGAAiG;IACjG,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,qGAAqG;IACrG,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,2FAA2F;IAC3F,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,MAAM,mBAAmB,GAAG,EAAE,CAAC,aAAa,CAAC,GAAG,GAAG,CAAC,yBAAyB,GAAG,0BAA0B,CAAC,CAAC;AAElH,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,OAAO,EAAE,cAAc,CAAC;IACjC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B;AAED,sGAAsG;AACtG,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAEjD;AAmBD,KAAK,UAAU,GAAG,OAAO,QAAQ,CAAC,YAAY,CAAC;AAE/C,wBAAgB,eAAe,CAAC,GAAG,EAAE,UAAU,GAAG,aAAa,CAQ9D;AAED;;;GAGG;AACH,wBAAsB,aAAa,CAAC,GAAG,EAAE,cAAc,EAAE,KAAK,EAAE,kBAAkB,EAAE,UAAU,EAAE,IAAI,GAAG,OAAO,CAAC,mBAAmB,CAAC,CA0BlI;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,cAAc,EAAE,KAAK,EAAE,kBAAkB,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAE1G;AAED,4GAA4G;AAC5G,wBAAsB,UAAU,CAAC,GAAG,EAAE,cAAc,EAAE,KAAK,EAAE,YAAY,GAAG,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC,CAavG;AAED,6FAA6F;AAC7F,wBAAsB,UAAU,CAAC,GAAG,EAAE,cAAc,EAAE,KAAK,EAAE,YAAY,GAAG,OAAO,CAAC,OAAO,CAAC,CAG3F;AAED,qGAAqG;AACrG,wBAAsB,YAAY,CAAC,GAAG,EAAE,cAAc,EAAE,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,aAAa,EAAE,CAAC,CAKzG"}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
// The consent ledger: recording a consent or its withdrawal, and reading the current state and the
|
|
2
|
+
// history of a subject. Rows are only ever inserted (the table refuses UPDATE); the current state
|
|
3
|
+
// of a purpose is its latest row.
|
|
4
|
+
import { createHash } from "node:crypto";
|
|
5
|
+
import { err, ok } from "@softure-ai/core";
|
|
6
|
+
import { and, asc, desc, eq } from "drizzle-orm";
|
|
7
|
+
import { z } from "zod";
|
|
8
|
+
import { CONTRIBUTOR_ID_PATTERN } from "../options.js";
|
|
9
|
+
import { consents } from "../schema.js";
|
|
10
|
+
import { findLegalDocument } from "./legal-documents.js";
|
|
11
|
+
const MAX_NAME_LENGTH = 64;
|
|
12
|
+
const MAX_EMAIL_LENGTH = 254;
|
|
13
|
+
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
14
|
+
/** A SHA-256 digest in unpadded base64url, the shape of every stored email key. */
|
|
15
|
+
const EMAIL_KEY = /^[A-Za-z0-9_-]{43}$/;
|
|
16
|
+
const emailSchema = z.email();
|
|
17
|
+
/** The key an email subject is stored under: base64url SHA-256 of the trimmed, lowercased address. */
|
|
18
|
+
export function getEmailKey(email) {
|
|
19
|
+
return createHash("sha256").update(email.trim().toLowerCase()).digest("base64url");
|
|
20
|
+
}
|
|
21
|
+
const isName = (value) => value.length <= MAX_NAME_LENGTH && CONTRIBUTOR_ID_PATTERN.test(value);
|
|
22
|
+
/** The subject's column values, or null when it cannot name anyone (not a UUID, an address or a key). */
|
|
23
|
+
function toSubjectColumns(subject) {
|
|
24
|
+
if ("userId" in subject)
|
|
25
|
+
return UUID.test(subject.userId) ? { userId: subject.userId, emailKey: null } : null;
|
|
26
|
+
if ("emailKey" in subject)
|
|
27
|
+
return EMAIL_KEY.test(subject.emailKey) ? { userId: null, emailKey: subject.emailKey } : null;
|
|
28
|
+
const email = subject.email.trim().toLowerCase();
|
|
29
|
+
if (email.length > MAX_EMAIL_LENGTH || !emailSchema.safeParse(email).success)
|
|
30
|
+
return null;
|
|
31
|
+
return { userId: null, emailKey: getEmailKey(email) };
|
|
32
|
+
}
|
|
33
|
+
function matchSubject(subject) {
|
|
34
|
+
const columns = toSubjectColumns(subject);
|
|
35
|
+
if (columns === null)
|
|
36
|
+
return null;
|
|
37
|
+
return columns.userId === null ? eq(consents.emailKey, columns.emailKey) : eq(consents.userId, columns.userId);
|
|
38
|
+
}
|
|
39
|
+
export function toConsentRecord(row) {
|
|
40
|
+
return {
|
|
41
|
+
purpose: row.purpose,
|
|
42
|
+
granted: row.granted,
|
|
43
|
+
document: row.documentId === null || row.documentVersion === null ? null : { id: row.documentId, version: row.documentVersion },
|
|
44
|
+
source: row.source,
|
|
45
|
+
recordedAt: row.recordedAt,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Records a consent at `recordedAt`. For `recordConsent` (the clock's now) and the registration
|
|
50
|
+
* hook (the account's creation time).
|
|
51
|
+
*/
|
|
52
|
+
export async function insertConsent(ctx, input, recordedAt) {
|
|
53
|
+
const subject = toSubjectColumns(input.subject);
|
|
54
|
+
if (subject === null || !isName(input.purpose) || !isName(input.source))
|
|
55
|
+
return err("privacy.consent_invalid");
|
|
56
|
+
let document = null;
|
|
57
|
+
if (input.document !== undefined) {
|
|
58
|
+
const declared = findLegalDocument(ctx.config, input.document);
|
|
59
|
+
if (declared === undefined)
|
|
60
|
+
return err("privacy.document_unknown");
|
|
61
|
+
document = { id: declared.id, version: declared.version };
|
|
62
|
+
}
|
|
63
|
+
const [row] = await ctx.db
|
|
64
|
+
.insert(consents)
|
|
65
|
+
.values({
|
|
66
|
+
...subject,
|
|
67
|
+
purpose: input.purpose,
|
|
68
|
+
granted: input.granted,
|
|
69
|
+
documentId: document?.id ?? null,
|
|
70
|
+
documentVersion: document?.version ?? null,
|
|
71
|
+
source: input.source,
|
|
72
|
+
recordedAt,
|
|
73
|
+
})
|
|
74
|
+
.returning();
|
|
75
|
+
// An INSERT … RETURNING without a conflict clause returns its row or throws.
|
|
76
|
+
if (row === undefined)
|
|
77
|
+
throw new Error("@softure-ai/privacy: recording a consent returned no row");
|
|
78
|
+
return ok(toConsentRecord(row));
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Records that `subject` gave (or withdrew) consent to `purpose`, now. With `document`, the
|
|
82
|
+
* document's configured version is recorded with it. Database errors propagate.
|
|
83
|
+
*/
|
|
84
|
+
export function recordConsent(ctx, input) {
|
|
85
|
+
return insertConsent(ctx, input, ctx.clock.now());
|
|
86
|
+
}
|
|
87
|
+
/** The current state of one purpose for one subject (its latest record), or null when none was recorded. */
|
|
88
|
+
export async function getConsent(ctx, query) {
|
|
89
|
+
const subject = matchSubject(query.subject);
|
|
90
|
+
if (subject === null)
|
|
91
|
+
return null;
|
|
92
|
+
const [row] = await ctx.db
|
|
93
|
+
.select()
|
|
94
|
+
.from(consents)
|
|
95
|
+
.where(and(subject, eq(consents.purpose, query.purpose)))
|
|
96
|
+
.orderBy(desc(consents.recordedAt), desc(consents.id))
|
|
97
|
+
.limit(1);
|
|
98
|
+
if (row === undefined)
|
|
99
|
+
return null;
|
|
100
|
+
const record = toConsentRecord(row);
|
|
101
|
+
const isCurrentVersion = record.document === null || findLegalDocument(ctx.config, record.document.id)?.version === record.document.version;
|
|
102
|
+
return { ...record, isCurrentVersion };
|
|
103
|
+
}
|
|
104
|
+
/** Whether the latest record of the purpose grants it, for the document version in force. */
|
|
105
|
+
export async function hasConsent(ctx, query) {
|
|
106
|
+
const state = await getConsent(ctx, query);
|
|
107
|
+
return state !== null && state.granted && state.isCurrentVersion;
|
|
108
|
+
}
|
|
109
|
+
/** Every record of the subject, oldest first: the evidence of what was given and withdrawn, when. */
|
|
110
|
+
export async function listConsents(ctx, subject) {
|
|
111
|
+
const condition = matchSubject(subject);
|
|
112
|
+
if (condition === null)
|
|
113
|
+
return [];
|
|
114
|
+
const rows = await ctx.db.select().from(consents).where(condition).orderBy(asc(consents.recordedAt), asc(consents.id));
|
|
115
|
+
return rows.map(toConsentRecord);
|
|
116
|
+
}
|
|
117
|
+
//# sourceMappingURL=consents.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"consents.js","sourceRoot":"","sources":["../../src/server/consents.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { ModuleContext } from "@softure-ai/core";
|
|
2
|
+
import type { Queryable } from "@softure-ai/db";
|
|
3
|
+
/** What the privacy server functions receive; contributors get the same with `db` as a transaction. */
|
|
4
|
+
export type PrivacyContext = ModuleContext<Queryable>;
|
|
5
|
+
//# sourceMappingURL=context.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../../src/server/context.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AACtD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAEhD,uGAAuG;AACvG,MAAM,MAAM,cAAc,GAAG,aAAa,CAAC,SAAS,CAAC,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"context.js","sourceRoot":"","sources":["../../src/server/context.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { PrivacyContributor, SoftureConfig } from "@softure-ai/core";
|
|
2
|
+
export interface RegisteredContributor extends PrivacyContributor {
|
|
3
|
+
/** A module id or an app contributor id; the key of its part of the export. */
|
|
4
|
+
readonly id: string;
|
|
5
|
+
readonly source: "module" | "app";
|
|
6
|
+
}
|
|
7
|
+
export type ExportingContributor = RegisteredContributor & Required<Pick<PrivacyContributor, "exportUserData">>;
|
|
8
|
+
export type DeletingContributor = RegisteredContributor & Required<Pick<PrivacyContributor, "deleteUserData">>;
|
|
9
|
+
/**
|
|
10
|
+
* Every contributor in export order: the modules in the config's dependency order (dependencies
|
|
11
|
+
* first), then the app's in the order it lists them. Throws when an app contributor takes the id of
|
|
12
|
+
* an enabled module: both parts would land under one key.
|
|
13
|
+
*/
|
|
14
|
+
export declare function getPrivacyContributors(config: SoftureConfig): readonly RegisteredContributor[];
|
|
15
|
+
/** The contributors with an export, in export order. */
|
|
16
|
+
export declare function getExportingContributors(config: SoftureConfig): readonly ExportingContributor[];
|
|
17
|
+
/**
|
|
18
|
+
* The contributors with a deletion, in deletion order: the reverse of the export order. The app's
|
|
19
|
+
* own data goes first, then each module before the modules it depends on, so a row is always
|
|
20
|
+
* deleted before the rows it references, whatever the foreign key's ON DELETE action.
|
|
21
|
+
*/
|
|
22
|
+
export declare function getDeletingContributors(config: SoftureConfig): readonly DeletingContributor[];
|
|
23
|
+
//# sourceMappingURL=contributors.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"contributors.d.ts","sourceRoot":"","sources":["../../src/server/contributors.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,kBAAkB,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAG1E,MAAM,WAAW,qBAAsB,SAAQ,kBAAkB;IAC/D,+EAA+E;IAC/E,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,MAAM,EAAE,QAAQ,GAAG,KAAK,CAAC;CACnC;AAED,MAAM,MAAM,oBAAoB,GAAG,qBAAqB,GAAG,QAAQ,CAAC,IAAI,CAAC,kBAAkB,EAAE,gBAAgB,CAAC,CAAC,CAAC;AAChH,MAAM,MAAM,mBAAmB,GAAG,qBAAqB,GAAG,QAAQ,CAAC,IAAI,CAAC,kBAAkB,EAAE,gBAAgB,CAAC,CAAC,CAAC;AAI/G;;;;GAIG;AACH,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,aAAa,GAAG,SAAS,qBAAqB,EAAE,CAkB9F;AAED,wDAAwD;AACxD,wBAAgB,wBAAwB,CAAC,MAAM,EAAE,aAAa,GAAG,SAAS,oBAAoB,EAAE,CAE/F;AAED;;;;GAIG;AACH,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,aAAa,GAAG,SAAS,mBAAmB,EAAE,CAI7F"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { getPrivacyOptions } from "./options.js";
|
|
2
|
+
const registries = new WeakMap();
|
|
3
|
+
/**
|
|
4
|
+
* Every contributor in export order: the modules in the config's dependency order (dependencies
|
|
5
|
+
* first), then the app's in the order it lists them. Throws when an app contributor takes the id of
|
|
6
|
+
* an enabled module: both parts would land under one key.
|
|
7
|
+
*/
|
|
8
|
+
export function getPrivacyContributors(config) {
|
|
9
|
+
const cached = registries.get(config);
|
|
10
|
+
if (cached !== undefined)
|
|
11
|
+
return cached;
|
|
12
|
+
const modules = config.modules.flatMap((module) => module.privacy === null ? [] : [{ ...module.privacy, id: module.id, source: "module" }]);
|
|
13
|
+
const moduleIds = new Set(config.modules.map((module) => module.id));
|
|
14
|
+
const app = getPrivacyOptions(config).contributors.map((contributor) => {
|
|
15
|
+
if (moduleIds.has(contributor.id)) {
|
|
16
|
+
throw new Error(`@softure-ai/privacy: app contributor "${contributor.id}" has the id of an enabled module; give it another id`);
|
|
17
|
+
}
|
|
18
|
+
return { ...contributor, source: "app" };
|
|
19
|
+
});
|
|
20
|
+
const registry = Object.freeze([...modules, ...app]);
|
|
21
|
+
registries.set(config, registry);
|
|
22
|
+
return registry;
|
|
23
|
+
}
|
|
24
|
+
/** The contributors with an export, in export order. */
|
|
25
|
+
export function getExportingContributors(config) {
|
|
26
|
+
return getPrivacyContributors(config).filter((contributor) => contributor.exportUserData !== undefined);
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* The contributors with a deletion, in deletion order: the reverse of the export order. The app's
|
|
30
|
+
* own data goes first, then each module before the modules it depends on, so a row is always
|
|
31
|
+
* deleted before the rows it references, whatever the foreign key's ON DELETE action.
|
|
32
|
+
*/
|
|
33
|
+
export function getDeletingContributors(config) {
|
|
34
|
+
return getPrivacyContributors(config)
|
|
35
|
+
.filter((contributor) => contributor.deleteUserData !== undefined)
|
|
36
|
+
.toReversed();
|
|
37
|
+
}
|
|
38
|
+
//# sourceMappingURL=contributors.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"contributors.js","sourceRoot":"","sources":["../../src/server/contributors.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AAWjD,MAAM,UAAU,GAAG,IAAI,OAAO,EAAmD,CAAC;AAElF;;;;GAIG;AACH,MAAM,UAAU,sBAAsB,CAAC,MAAqB;IAC1D,MAAM,MAAM,GAAG,UAAU,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;IACtC,IAAI,MAAM,KAAK,SAAS;QAAE,OAAO,MAAM,CAAC;IAExC,MAAM,OAAO,GAA4B,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,MAAM,EAAE,EAAE,CACzE,MAAM,CAAC,OAAO,KAAK,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,GAAG,MAAM,CAAC,OAAO,EAAE,EAAE,EAAE,MAAM,CAAC,EAAE,EAAE,MAAM,EAAE,QAAiB,EAAE,CAAC,CACjG,CAAC;IACF,MAAM,SAAS,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,CAAC;IACrE,MAAM,GAAG,GAA4B,iBAAiB,CAAC,MAAM,CAAC,CAAC,YAAY,CAAC,GAAG,CAAC,CAAC,WAAW,EAAE,EAAE;QAC9F,IAAI,SAAS,CAAC,GAAG,CAAC,WAAW,CAAC,EAAE,CAAC,EAAE,CAAC;YAClC,MAAM,IAAI,KAAK,CAAC,yCAAyC,WAAW,CAAC,EAAE,uDAAuD,CAAC,CAAC;QAClI,CAAC;QACD,OAAO,EAAE,GAAG,WAAW,EAAE,MAAM,EAAE,KAAc,EAAE,CAAC;IACpD,CAAC,CAAC,CAAC;IAEH,MAAM,QAAQ,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC,GAAG,OAAO,EAAE,GAAG,GAAG,CAAC,CAAC,CAAC;IACrD,UAAU,CAAC,GAAG,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;IACjC,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED,wDAAwD;AACxD,MAAM,UAAU,wBAAwB,CAAC,MAAqB;IAC5D,OAAO,sBAAsB,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,CAAC,WAAW,EAAuC,EAAE,CAAC,WAAW,CAAC,cAAc,KAAK,SAAS,CAAC,CAAC;AAC/I,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,uBAAuB,CAAC,MAAqB;IAC3D,OAAO,sBAAsB,CAAC,MAAM,CAAC;SAClC,MAAM,CAAC,CAAC,WAAW,EAAsC,EAAE,CAAC,WAAW,CAAC,cAAc,KAAK,SAAS,CAAC;SACrG,UAAU,EAAE,CAAC;AAClB,CAAC"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type Err, type Ok } from "@softure-ai/core";
|
|
2
|
+
import type { PrivacyContext } from "./context.js";
|
|
3
|
+
export type EraseUserDataResult = Ok<undefined> | Err<"privacy.deletion_refused">;
|
|
4
|
+
/**
|
|
5
|
+
* Deletes everything the app holds about `userId`. A contributor that must keep data (legal
|
|
6
|
+
* retention) returns an `Err`: nothing is deleted, the result is `privacy.deletion_refused`, and
|
|
7
|
+
* the contributor's id and code are logged. Thrown errors propagate after the rollback.
|
|
8
|
+
*/
|
|
9
|
+
export declare function eraseUserData(ctx: PrivacyContext, userId: string): Promise<EraseUserDataResult>;
|
|
10
|
+
//# sourceMappingURL=erase.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"erase.d.ts","sourceRoot":"","sources":["../../src/server/erase.ts"],"names":[],"mappings":"AAGA,OAAO,EAAW,KAAK,GAAG,EAAkB,KAAK,EAAE,EAAE,MAAM,kBAAkB,CAAC;AAC9E,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAGnD,MAAM,MAAM,mBAAmB,GAAG,EAAE,CAAC,SAAS,CAAC,GAAG,GAAG,CAAC,0BAA0B,CAAC,CAAC;AAalF;;;;GAIG;AACH,wBAAsB,aAAa,CAAC,GAAG,EAAE,cAAc,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAgBrG"}
|