dpdp-erasure-cli 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (155) hide show
  1. package/.env.example +55 -0
  2. package/Dockerfile +33 -0
  3. package/compliance.worker.yaml +64 -0
  4. package/package.json +41 -0
  5. package/src/constants/index.ts +1 -0
  6. package/src/errors/fail.ts +110 -0
  7. package/src/errors/index.ts +4 -0
  8. package/src/errors/inferer.ts +166 -0
  9. package/src/errors/registry.ts +122 -0
  10. package/src/errors/types.ts +65 -0
  11. package/src/errors/worker.ts +161 -0
  12. package/src/index.ts +328 -0
  13. package/src/lib/crypto/digest.ts +22 -0
  14. package/src/lib/crypto/encoding.ts +78 -0
  15. package/src/lib/crypto/index.ts +2 -0
  16. package/src/lib/index.ts +1 -0
  17. package/src/modules/bootstrap/index.ts +2 -0
  18. package/src/modules/bootstrap/integrity.ts +38 -0
  19. package/src/modules/bootstrap/preflight.ts +296 -0
  20. package/src/modules/cli/check-integrity.ts +48 -0
  21. package/src/modules/cli/dry-run.ts +90 -0
  22. package/src/modules/cli/graph.ts +87 -0
  23. package/src/modules/cli/index.ts +184 -0
  24. package/src/modules/cli/init.ts +115 -0
  25. package/src/modules/cli/inspect.ts +86 -0
  26. package/src/modules/cli/introspector.ts +117 -0
  27. package/src/modules/cli/keygen.ts +38 -0
  28. package/src/modules/cli/scan.ts +126 -0
  29. package/src/modules/cli/sign.ts +50 -0
  30. package/src/modules/cli/ui.ts +61 -0
  31. package/src/modules/cli/verify-schema.ts +31 -0
  32. package/src/modules/cli/verify.ts +85 -0
  33. package/src/modules/config/compatibility.ts +271 -0
  34. package/src/modules/config/index.ts +4 -0
  35. package/src/modules/config/reader.ts +149 -0
  36. package/src/modules/config/signature.ts +69 -0
  37. package/src/modules/config/validation.ts +658 -0
  38. package/src/modules/crypto/aes.ts +158 -0
  39. package/src/modules/crypto/envelope.ts +48 -0
  40. package/src/modules/crypto/hmac.ts +60 -0
  41. package/src/modules/crypto/index.ts +3 -0
  42. package/src/modules/db/drift.ts +36 -0
  43. package/src/modules/db/graph.ts +203 -0
  44. package/src/modules/db/index.ts +4 -0
  45. package/src/modules/db/migrations.ts +254 -0
  46. package/src/modules/db/sql-debug.ts +61 -0
  47. package/src/modules/engine/blob/index.ts +3 -0
  48. package/src/modules/engine/blob/s3.ts +455 -0
  49. package/src/modules/engine/blob/store.ts +236 -0
  50. package/src/modules/engine/blob/types.ts +44 -0
  51. package/src/modules/engine/helpers/identity.ts +47 -0
  52. package/src/modules/engine/helpers/index.ts +4 -0
  53. package/src/modules/engine/helpers/outbox.ts +118 -0
  54. package/src/modules/engine/helpers/runtime.ts +115 -0
  55. package/src/modules/engine/helpers/types.ts +61 -0
  56. package/src/modules/engine/index.ts +6 -0
  57. package/src/modules/engine/notifier/config.ts +147 -0
  58. package/src/modules/engine/notifier/dispatcher.ts +300 -0
  59. package/src/modules/engine/notifier/index.ts +3 -0
  60. package/src/modules/engine/notifier/payload.ts +51 -0
  61. package/src/modules/engine/notifier/reservation.ts +153 -0
  62. package/src/modules/engine/notifier/types.ts +38 -0
  63. package/src/modules/engine/shredder.ts +254 -0
  64. package/src/modules/engine/types.ts +146 -0
  65. package/src/modules/engine/vault/compiled-targets.ts +562 -0
  66. package/src/modules/engine/vault/context.ts +254 -0
  67. package/src/modules/engine/vault/dry-run.ts +94 -0
  68. package/src/modules/engine/vault/execution.ts +485 -0
  69. package/src/modules/engine/vault/index.ts +3 -0
  70. package/src/modules/engine/vault/purge.ts +82 -0
  71. package/src/modules/engine/vault/retention.ts +124 -0
  72. package/src/modules/engine/vault/satellite-mutation.ts +193 -0
  73. package/src/modules/engine/vault/satellite.ts +103 -0
  74. package/src/modules/engine/vault/shadow.ts +36 -0
  75. package/src/modules/engine/vault/static-plan.ts +116 -0
  76. package/src/modules/engine/vault/store.ts +34 -0
  77. package/src/modules/engine/vault/vault.ts +84 -0
  78. package/src/modules/introspector/classifier.ts +502 -0
  79. package/src/modules/introspector/dag.ts +276 -0
  80. package/src/modules/introspector/index.ts +7 -0
  81. package/src/modules/introspector/naming.ts +75 -0
  82. package/src/modules/introspector/report.ts +153 -0
  83. package/src/modules/introspector/run.ts +123 -0
  84. package/src/modules/introspector/s3-sampler.ts +227 -0
  85. package/src/modules/introspector/types.ts +131 -0
  86. package/src/modules/introspector/yaml.ts +101 -0
  87. package/src/modules/network/api/control-plane.ts +275 -0
  88. package/src/modules/network/api/index.ts +1 -0
  89. package/src/modules/network/api/validation.ts +71 -0
  90. package/src/modules/network/index.ts +4 -0
  91. package/src/modules/network/object-store/aws/client.ts +444 -0
  92. package/src/modules/network/object-store/aws/credentials.ts +271 -0
  93. package/src/modules/network/object-store/aws/index.ts +2 -0
  94. package/src/modules/network/object-store/aws/sigv4.ts +190 -0
  95. package/src/modules/network/object-store/aws/type.ts +6 -0
  96. package/src/modules/network/object-store/index.ts +1 -0
  97. package/src/modules/network/outbox/dispatcher.ts +183 -0
  98. package/src/modules/network/outbox/index.ts +3 -0
  99. package/src/modules/network/outbox/process.ts +133 -0
  100. package/src/modules/network/outbox/shared.ts +56 -0
  101. package/src/modules/network/outbox/store.ts +346 -0
  102. package/src/modules/network/outbox/types.ts +54 -0
  103. package/src/modules/network/request-signing.ts +61 -0
  104. package/src/modules/worker/index.ts +2 -0
  105. package/src/modules/worker/tasks.ts +58 -0
  106. package/src/modules/worker/types.ts +89 -0
  107. package/src/modules/worker/worker.ts +243 -0
  108. package/src/secrets/index.ts +4 -0
  109. package/src/secrets/kms/index.ts +2 -0
  110. package/src/secrets/kms/signature.ts +82 -0
  111. package/src/secrets/kms/validation.ts +64 -0
  112. package/src/secrets/reader.ts +42 -0
  113. package/src/secrets/repository/crypto.ts +89 -0
  114. package/src/secrets/repository/index.ts +2 -0
  115. package/src/secrets/repository/methods.ts +37 -0
  116. package/src/secrets/resolvers.ts +247 -0
  117. package/src/secrets/signature.ts +78 -0
  118. package/src/types/index.ts +1 -0
  119. package/src/types/types.ts +23 -0
  120. package/src/utils/identifiers.ts +48 -0
  121. package/src/utils/index.ts +3 -0
  122. package/src/utils/json.ts +35 -0
  123. package/src/utils/logger.ts +161 -0
  124. package/src/validation/zod.ts +70 -0
  125. package/tests/adversarial.test.ts +464 -0
  126. package/tests/blob-s3.test.ts +216 -0
  127. package/tests/config.test.ts +395 -0
  128. package/tests/control-plane-client.test.ts +108 -0
  129. package/tests/crypto.test.ts +106 -0
  130. package/tests/errors.test.ts +69 -0
  131. package/tests/fetch-dispatcher.test.ts +213 -0
  132. package/tests/graph.test.ts +84 -0
  133. package/tests/helpers/index.ts +101 -0
  134. package/tests/index-preflight.test.ts +168 -0
  135. package/tests/introspector-classifier.test.ts +62 -0
  136. package/tests/introspector-report.test.ts +85 -0
  137. package/tests/introspector.test.ts +394 -0
  138. package/tests/kms.test.ts +124 -0
  139. package/tests/logger.test.ts +61 -0
  140. package/tests/notifier.test.ts +303 -0
  141. package/tests/outbox.test.ts +478 -0
  142. package/tests/purge-policy.test.ts +124 -0
  143. package/tests/retention.test.ts +103 -0
  144. package/tests/s3-client.test.ts +110 -0
  145. package/tests/satellite.test.ts +119 -0
  146. package/tests/schema-compatibility.test.ts +237 -0
  147. package/tests/schema-integrity.test.ts +64 -0
  148. package/tests/shredder.test.ts +163 -0
  149. package/tests/vault.compiled-targets.test.ts +243 -0
  150. package/tests/vault.replica.test.ts +59 -0
  151. package/tests/vault.test.ts +279 -0
  152. package/tests/worker.retry.test.ts +291 -0
  153. package/tests/worker.test.ts +200 -0
  154. package/tsconfig.json +19 -0
  155. package/vitest.config.ts +13 -0
@@ -0,0 +1,254 @@
1
+ import type { BlobTarget, MutationRule, RootPiiColumns, SatelliteTarget } from "@modules/config";
2
+ import { generateHMACWithKey } from "@modules/crypto";
3
+ import { fail } from "@/errors";
4
+ import { assertIdentifier, quoteQualifiedIdentifier } from "@/utils";
5
+ import type { DryRunPlan, VaultUserOptions } from "../types";
6
+
7
+ /**
8
+ * Root-table mutation configuration resolved from worker runtime options.
9
+ */
10
+ export interface RootMutationContext {
11
+ rootTable: string;
12
+ rootIdColumn: string;
13
+ rootPiiColumns: RootPiiColumns;
14
+ satelliteTargets: SatelliteTarget[];
15
+ blobTargets: BlobTarget[];
16
+ }
17
+
18
+ /**
19
+ * Normalizes an arbitrary row value into the string form used by hashing and mutation logic.
20
+ *
21
+ * @param value - Database cell value.
22
+ * @returns String form or `null` when the source is absent.
23
+ */
24
+ export function normalizeRootRowValue(value: unknown): string | null {
25
+ if (value === null || value === undefined) {
26
+ return null;
27
+ }
28
+
29
+ return String(value);
30
+ }
31
+
32
+ /**
33
+ * Produces a human-readable dry-run plan describing the exact vault mutation sequence.
34
+ *
35
+ * @param appSchema - Client application schema.
36
+ * @param engineSchema - Worker engine schema.
37
+ * @param subjectId - Root subject identifier.
38
+ * @param rootContext - Validated root-table mutation config.
39
+ * @param userHash - Deterministic worker-side subject hash.
40
+ * @param dependencyCount - Count of discovered dependencies.
41
+ * @param retentionExpiry - Calculated retention expiry timestamp.
42
+ * @param notificationDueAt - Calculated notice dispatch timestamp.
43
+ * @param appliedRuleName - Retention rule selected for the subject.
44
+ * @returns Dry-run plan explaining intended crypto and SQL actions.
45
+ */
46
+ export function buildVaultDryRunPlan(
47
+ appSchema: string,
48
+ engineSchema: string,
49
+ subjectId: string | number,
50
+ rootContext: RootMutationContext,
51
+ userHash: string,
52
+ dependencyCount: number,
53
+ retentionExpiry: Date,
54
+ notificationDueAt: Date,
55
+ appliedRuleName: string
56
+ ): DryRunPlan {
57
+ const rootTable = quoteQualifiedIdentifier(appSchema, rootContext.rootTable);
58
+ const vaultTable = quoteQualifiedIdentifier(engineSchema, "pii_vault");
59
+ const keyTable = quoteQualifiedIdentifier(engineSchema, "user_keys");
60
+ const outboxTable = quoteQualifiedIdentifier(engineSchema, "outbox");
61
+ const mutationColumns = Object.keys(rootContext.rootPiiColumns).join(", ");
62
+ const action = dependencyCount === 0 ? "hard delete" : "vault";
63
+
64
+ return {
65
+ mode: "dry-run",
66
+ summary: `Would ${action} root row ${subjectId} in ${appSchema}.${rootContext.rootTable} with worker hash ${userHash}.`,
67
+ checks: [
68
+ `Validate ${appSchema} and ${engineSchema} as trusted schema identifiers.`,
69
+ `Load the DPO-attested static execution plan rooted at ${rootTable}.`,
70
+ `Evaluate retention evidence and select rule ${appliedRuleName}.`,
71
+ `Lock the target row in ${rootTable} before mutating it.`,
72
+ "Write the outbox event atomically with the primary data mutation.",
73
+ ],
74
+ cryptoSteps:
75
+ dependencyCount === 0
76
+ ? ["No vaulting cryptography required because the root table has no dependent tables."]
77
+ : [
78
+ "Generate a one-time 32-byte DEK for the root entity.",
79
+ "Encrypt the configured root PII payload with AES-256-GCM.",
80
+ "Wrap the DEK with the worker KEK using envelope encryption.",
81
+ "Mutate configured root PII columns with rule-driven masking/HMAC/nullification.",
82
+ ],
83
+ sqlSteps:
84
+ dependencyCount === 0
85
+ ? [
86
+ "BEGIN ISOLATION LEVEL REPEATABLE READ;",
87
+ `SELECT ... FROM ${rootTable} WHERE ${rootContext.rootIdColumn} = '${String(subjectId)}' FOR UPDATE;`,
88
+ `DELETE FROM ${rootTable} WHERE ${rootContext.rootIdColumn} = '${String(subjectId)}';`,
89
+ `INSERT INTO ${outboxTable} (...) VALUES (...);`,
90
+ "COMMIT;",
91
+ ]
92
+ : [
93
+ "BEGIN ISOLATION LEVEL REPEATABLE READ;",
94
+ `SELECT ... FROM ${rootTable} WHERE ${rootContext.rootIdColumn} = '${String(subjectId)}' FOR UPDATE;`,
95
+ `INSERT INTO ${vaultTable} (... retention_expiry='${retentionExpiry.toISOString()}', notification_due_at='${notificationDueAt.toISOString()}', applied_rule_name='${appliedRuleName}');`,
96
+ `INSERT INTO ${keyTable} (...);`,
97
+ `UPDATE ${rootTable} SET {${mutationColumns}} = <rule-driven values> WHERE ${rootContext.rootIdColumn} = '${String(subjectId)}';`,
98
+ `INSERT INTO ${outboxTable} (...) VALUES (...);`,
99
+ "COMMIT;",
100
+ ],
101
+ };
102
+ }
103
+
104
+ /**
105
+ * Resolves and validate root mutation config from worker options.
106
+ *
107
+ * @param options - Runtime vault options.
108
+ * @returns Trusted root-table context ready for dynamic SQL interactions.
109
+ * @throws Invokes `fail()` When mandatory config is missing or identifiers are unsafe.
110
+ */
111
+ export function resolveRootContext(options: VaultUserOptions): RootMutationContext {
112
+ if (!options.rootTable) {
113
+ fail({
114
+ code: "VAULT_ROOT_TABLE_MISSING",
115
+ title: "Missing root table configuration",
116
+ detail: "rootTable is required.",
117
+ category: "configuration",
118
+ retryable: false,
119
+ fatal: true,
120
+ })
121
+ }
122
+
123
+ if (!options.rootIdColumn) {
124
+ fail({
125
+ code: "VAULT_ROOT_ID_COLUMN_MISSING",
126
+ title: "Missing root identifier configuration",
127
+ detail: "rootIdColumn is required.",
128
+ category: "configuration",
129
+ retryable: false,
130
+ fatal: true,
131
+ });
132
+ }
133
+
134
+ if (!options.rootPiiColumns || Object.keys(options.rootPiiColumns).length === 0) {
135
+ fail({
136
+ code: "VAULT_ROOT_PII_COLUMNS_MISSING",
137
+ title: "Missing root PII column mapping",
138
+ detail: "rootPiiColumns is required and must contain at least one mutation rule.",
139
+ category: "configuration",
140
+ retryable: false,
141
+ fatal: true,
142
+ });
143
+ }
144
+
145
+ const rootTable = assertIdentifier(options.rootTable, "graph root table");
146
+ const rootIdColumn = assertIdentifier(options.rootIdColumn, "graph root id column");
147
+
148
+ const rootPiiColumns: RootPiiColumns = {};
149
+ for (const [column, mutation] of Object.entries(options.rootPiiColumns)) {
150
+ rootPiiColumns[assertIdentifier(column, "graph root pii column")] = mutation;
151
+ }
152
+
153
+ const satelliteTargets = (options.satelliteTargets ?? []).map((target) => ({
154
+ ...target,
155
+ table: assertIdentifier(target.table, "satellite table name"),
156
+ lookup_column: assertIdentifier(target.lookup_column, "satellite lookup column"),
157
+ }));
158
+ const blobTargets = (options.blobTargets ?? []).map((target) => ({
159
+ ...target,
160
+ table: assertIdentifier(target.table, "blob target table name"),
161
+ column: assertIdentifier(target.column, "blob target column name"),
162
+ lookup_column: target.lookup_column
163
+ ? assertIdentifier(target.lookup_column, "blob target lookup column")
164
+ : undefined,
165
+ }));
166
+
167
+ return {
168
+ rootTable,
169
+ rootIdColumn,
170
+ rootPiiColumns,
171
+ satelliteTargets,
172
+ blobTargets,
173
+ };
174
+ }
175
+
176
+ /**
177
+ * Builds a deterministic worker idempotency key for `USER_VAULTED`.
178
+ *
179
+ * @param options - Runtime vault options.
180
+ * @param appSchema - Client application schema.
181
+ * @param rootTable - Root table name.
182
+ * @param rootIdColumn - Root identifier column.
183
+ * @param subjectId - Subject identifier.
184
+ * @returns Stable idempotency key for the vault event.
185
+ */
186
+ export function buildHardDeleteEventIdempotencyKey(
187
+ options: VaultUserOptions,
188
+ appSchema: string,
189
+ rootTable: string,
190
+ rootIdColumn: string,
191
+ subjectId: string | number
192
+ ): string {
193
+ return options.requestId
194
+ ? `hard-delete:${options.requestId}`
195
+ : `hard-delete:${appSchema}:${rootTable}:${rootIdColumn}:${String(subjectId)}`;
196
+ }
197
+
198
+ /**
199
+ * Calculates the replacement value for one configured root PII column.
200
+ *
201
+ * @param mutation - Mutation rule to apply.
202
+ * @param originalValue - Existing root-row value.
203
+ * @param appSchema - Client application schema.
204
+ * @param rootTable - Root table name.
205
+ * @param column - Column being mutated.
206
+ * @param hmacKey - Pre-imported worker HMAC key.
207
+ * @returns Replacement scalar persisted back to the root row.
208
+ */
209
+ export async function computeMutationValue(
210
+ mutation: MutationRule,
211
+ originalValue: unknown,
212
+ appSchema: string,
213
+ rootTable: string,
214
+ column: string,
215
+ hmacKey: CryptoKey
216
+ ): Promise<string | null> {
217
+ if (mutation === "STATIC_MASK") {
218
+ return "[REDACTED]";
219
+ }
220
+
221
+ if (mutation === "NULLIFY") {
222
+ return null;
223
+ }
224
+
225
+ const normalizedValue = normalizeRootRowValue(originalValue);
226
+ if (normalizedValue === null) {
227
+ return null;
228
+ }
229
+
230
+ return generateHMACWithKey(`${appSchema}:${rootTable}:${column}:${normalizedValue}`, hmacKey);
231
+ };
232
+
233
+ /**
234
+ * Builds a deterministic worker idempotency key for `USER_VAULTED`.
235
+ *
236
+ * @param options - Runtime vault options.
237
+ * @param appSchema - Client application schema.
238
+ * @param rootTable - Root table name.
239
+ * @param rootIdColumn - Root identifier column.
240
+ * @param subjectId - Subject identifier.
241
+ * @returns Stable idempotency key for the vault event.
242
+ */
243
+ export function buildVaultEventIdempotencyKey(
244
+ options: VaultUserOptions,
245
+ appSchema: string,
246
+ rootTable: string,
247
+ rootIdColumn: string,
248
+ subjectId: string | number
249
+ ): string {
250
+ return options.requestId
251
+ ? `vault:${options.requestId}`
252
+ : `vault:${appSchema}:${rootTable}:${rootIdColumn}:${String(subjectId)}`;
253
+ }
254
+
@@ -0,0 +1,94 @@
1
+ import type { Sql } from "@/types";
2
+ import type { CompiledExecutionTargetInput } from "@modules/config";
3
+ import type { VaultUserResult } from "../types";
4
+ import { buildVaultDryRunPlan, type RootMutationContext } from "./context";
5
+ import { resolveStaticExecutionPlan } from "./static-plan";
6
+ import { resolveRetentionWindow } from "./retention";
7
+ import { getVaultRecordByUserId } from "./store";
8
+
9
+ /**
10
+ * Prepared inputs shared across vault dry-run evaluation.
11
+ */
12
+ export interface PreparedVaultDryRunContext {
13
+ appSchema: string;
14
+ engineSchema: string;
15
+ rootContext: RootMutationContext;
16
+ defaultRetentionYears: number;
17
+ noticeWindowHours: number;
18
+ now: Date;
19
+ tenantId?: string;
20
+ userHash: string;
21
+ compiledTargets?: CompiledExecutionTargetInput[];
22
+ }
23
+
24
+ /**
25
+ * Executes the vault dry-run path without mutating state.
26
+ *
27
+ * @param sql - Primary SQL handle used for time math and optional vault lookup.
28
+ * @param sqlReplica - Deprecated replica handle retained for API compatibility.
29
+ * @param subjectId - Root subject identifier.
30
+ * @param context - Prepared dry-run context.
31
+ * @returns Dry-run vault result with the computed execution plan.
32
+ */
33
+ export async function runVaultDryRun(
34
+ sql: Sql,
35
+ _sqlReplica: Sql | undefined,
36
+ subjectId: string | number,
37
+ context: PreparedVaultDryRunContext
38
+ ): Promise<VaultUserResult> {
39
+ const staticPlan = resolveStaticExecutionPlan(
40
+ context.appSchema,
41
+ context.rootContext,
42
+ { compiledTargets: context.compiledTargets }
43
+ );
44
+
45
+ const dependencyCount = staticPlan.dependencyCount;
46
+ const retention = {
47
+ retentionYears: context.defaultRetentionYears,
48
+ appliedRuleName: "DEFAULT",
49
+ appliedRuleCitation: "Configured defaut_retention_years policy"
50
+ };
51
+ const { retentionExpiry, notificationDueAt } = await resolveRetentionWindow(
52
+ sql,
53
+ context.now,
54
+ retention.retentionYears,
55
+ context.noticeWindowHours
56
+ )
57
+
58
+ const existingVault =
59
+ typeof sql === "function"
60
+ ? await getVaultRecordByUserId(
61
+ sql,
62
+ context.engineSchema,
63
+ context.appSchema,
64
+ subjectId,
65
+ context.rootContext.rootTable,
66
+ context.tenantId
67
+ )
68
+ : null;
69
+
70
+ return {
71
+ action: "dry_run",
72
+ userHash: context.userHash,
73
+ dryRun: true,
74
+ dependencyCount,
75
+ retentionYears: dependencyCount === 0 ? null : retention.retentionYears,
76
+ appliedRuleName: dependencyCount === 0 ? null : retention.appliedRuleName,
77
+ appliedRuleCitation: dependencyCount === 0 ? null : retention.appliedRuleCitation,
78
+ retentionExpiry: dependencyCount === 0 ? null : retentionExpiry.toISOString(),
79
+ notificationDueAt: dependencyCount === 0 ? null : notificationDueAt.toISOString(),
80
+ pseudonym: existingVault?.pseudonym ?? null,
81
+ outboxEventType: dependencyCount === 0 ? "USER_HARD_DELETED" : "USER_VAULTED",
82
+ plan: buildVaultDryRunPlan(
83
+ context.appSchema,
84
+ context.engineSchema,
85
+ subjectId,
86
+ context.rootContext,
87
+ context.userHash,
88
+ dependencyCount,
89
+ retentionExpiry,
90
+ notificationDueAt,
91
+ retention.appliedRuleName
92
+ ),
93
+ };
94
+ }