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,193 @@
1
+ import type { Tsql } from "@/types";
2
+ import { fail } from "@/errors";
3
+ import { quoteQualifiedIdentifier } from "@/utils";
4
+ import { generateHMACWithKey } from "@modules/crypto";
5
+ import type { SatelliteTarget } from "@modules/config";
6
+ import { normalizeRootRowValue, type RootMutationContext } from "./context";
7
+ import { redactSatelliteTable } from "./satellite";
8
+
9
+ const DEFAULT_SATELLITE_BATCH_SIZE = 1000;
10
+
11
+ /**
12
+ * Summary of a single satellite-table mutation.
13
+ */
14
+ export interface SatelliteMutationResult {
15
+ table: string;
16
+ action: "redact" | "hard_delete";
17
+ affectedRows: number;
18
+ }
19
+
20
+ /**
21
+ * Yields the Bun event loop between large mutation batches to keep heartbeats responsive.
22
+ */
23
+ export async function yieldWorkerEventLoop(): Promise<void> {
24
+ if (typeof globalThis.Bun !== "undefined" && typeof globalThis.Bun.sleep === "function") {
25
+ await globalThis.Bun.sleep(0);
26
+ return;
27
+ }
28
+
29
+ await new Promise<void>((resolve) => setTimeout(resolve, 0));
30
+ }
31
+
32
+ async function hardDeleteSatelliteRows(
33
+ tx: Tsql,
34
+ appSchema: string,
35
+ tableName: string,
36
+ lookupColumn: string,
37
+ lookupValue: string,
38
+ tenantId?: string,
39
+ batchSize: number = DEFAULT_SATELLITE_BATCH_SIZE
40
+ ): Promise<number> {
41
+ if (!Number.isInteger(batchSize) || batchSize < 1) {
42
+ fail({
43
+ code: "SATELLITE_BATCH_SIZE_INVALID",
44
+ title: "Invalid satellite batch size",
45
+ detail: "satellite batchSize must be an integer greater than 0.",
46
+ category: "validation",
47
+ retryable: false,
48
+ });
49
+ }
50
+
51
+ let totalDeleted = 0;
52
+
53
+ while (true) {
54
+ const tenantFilter = tenantId ? tx` AND tenant_id = ${tenantId}` : tx``;
55
+ const deletedRows = await tx<{ id: string | number }[]>`
56
+ WITH batch AS (
57
+ SELECT id
58
+ FROM ${tx(appSchema)}.${tx(tableName)}
59
+ WHERE ${tx(lookupColumn)} = ${lookupValue}
60
+ ${tenantFilter}
61
+ LIMIT ${batchSize}
62
+ FOR UPDATE SKIP LOCKED
63
+ )
64
+ DELETE FROM ${tx(appSchema)}.${tx(tableName)}
65
+ WHERE id IN (SELECT id FROM batch)
66
+ RETURNING id
67
+ `;
68
+
69
+ if (deletedRows.length === 0) {
70
+ break;
71
+ }
72
+
73
+ totalDeleted += deletedRows.length;
74
+ await yieldWorkerEventLoop();
75
+ }
76
+
77
+ return totalDeleted;
78
+ }
79
+
80
+ async function mutateSatelliteTarget(
81
+ tx: Tsql,
82
+ appSchema: string,
83
+ target: SatelliteTarget,
84
+ lockedRootRow: Record<string, unknown>,
85
+ hmacKey: CryptoKey,
86
+ tenantId?: string
87
+ ): Promise<SatelliteMutationResult> {
88
+ const lookupValue = normalizeRootRowValue(lockedRootRow[target.lookup_column]);
89
+ if (lookupValue === null) {
90
+ return {
91
+ table: `${appSchema}.${target.table}`,
92
+ action: target.action,
93
+ affectedRows: 0,
94
+ };
95
+ }
96
+
97
+ const tableCheck = quoteQualifiedIdentifier(appSchema, target.table);
98
+ const colCheck = await tx.unsafe(`SELECT * FROM ${tableCheck} LIMIT 0`);
99
+ const existingCols = new Set((colCheck.columns ?? []).map((c) => c.name));
100
+ if (!existingCols.has(target.lookup_column)) {
101
+ fail({
102
+ code: "SATELLITE_COLUMN_MISSING",
103
+ title: "Satellite lookup column missing from database schema",
104
+ detail: `Lookup column "${target.lookup_column}" does not exist in target table "${appSchema}.${target.table}".`,
105
+ category: "database",
106
+ retryable: false,
107
+ fatal: false,
108
+ });
109
+ }
110
+
111
+ if (target.action === "redact") {
112
+ const newHmacValue = await generateHMACWithKey(
113
+ `${appSchema}:${target.table}:${target.lookup_column}:${lookupValue}`,
114
+ hmacKey
115
+ );
116
+ const affectedRows = await redactSatelliteTable(
117
+ tx,
118
+ `${appSchema}.${target.table}`,
119
+ target.lookup_column,
120
+ lookupValue,
121
+ newHmacValue,
122
+ DEFAULT_SATELLITE_BATCH_SIZE,
123
+ tenantId
124
+ );
125
+
126
+ return {
127
+ table: `${appSchema}.${target.table}`,
128
+ action: target.action,
129
+ affectedRows,
130
+ };
131
+ }
132
+
133
+ const affectedRows = await hardDeleteSatelliteRows(
134
+ tx,
135
+ appSchema,
136
+ target.table,
137
+ target.lookup_column,
138
+ lookupValue,
139
+ tenantId
140
+ );
141
+
142
+ return {
143
+ table: `${appSchema}.${target.table}`,
144
+ action: target.action,
145
+ affectedRows,
146
+ };
147
+ }
148
+
149
+ /**
150
+ * Applies all configured satellite mutations inside the active vault transaction.
151
+ *
152
+ * @param tx - Active repeatable-read transaction.
153
+ * @param appSchema - Client application schema.
154
+ * @param rootContext - Validated root and satellite configuration.
155
+ * @param lockedRootRow - Locked root row snapshot.
156
+ * @param hmacKey - Pre-imported worker HMAC key.
157
+ * @param tenantId - Optional tenant discriminator.
158
+ * @returns One result entry per configured satellite target.
159
+ */
160
+ export async function mutateSatelliteTargets(
161
+ tx: Tsql,
162
+ appSchema: string,
163
+ rootContext: RootMutationContext,
164
+ lockedRootRow: Record<string, unknown>,
165
+ hmacKey: CryptoKey,
166
+ tenantId?: string
167
+ ): Promise<SatelliteMutationResult[]> {
168
+ const settled = await Promise.allSettled(
169
+ rootContext.satelliteTargets.map(async (target) => {
170
+ const result = await mutateSatelliteTarget(
171
+ tx,
172
+ appSchema,
173
+ target,
174
+ lockedRootRow,
175
+ hmacKey,
176
+ tenantId
177
+ );
178
+ await yieldWorkerEventLoop();
179
+ return result;
180
+ })
181
+ );
182
+
183
+ const rejected = settled.find(
184
+ (result): result is PromiseRejectedResult => result.status === "rejected"
185
+ );
186
+ if (rejected) {
187
+ throw rejected.reason;
188
+ }
189
+
190
+ return settled.map(
191
+ (result) => (result as PromiseFulfilledResult<SatelliteMutationResult>).value
192
+ );
193
+ }
@@ -0,0 +1,103 @@
1
+ import { fail } from "@/errors";
2
+ import type { Tsql } from "@/types";
3
+ import { assertIdentifier } from "@/utils";
4
+
5
+ interface SatelliteRowId {
6
+ id: number;
7
+ }
8
+
9
+ async function yieldWorkerEventLoop(): Promise<void> {
10
+ if (typeof globalThis.Bun !== "undefined" && typeof globalThis.Bun.sleep === "function") {
11
+ await globalThis.Bun.sleep(0);
12
+ return;
13
+ }
14
+
15
+ await new Promise<void>((resolve) => setTimeout(resolve, 0));
16
+ }
17
+
18
+ function parseQualifiedTableName(tableName: string) {
19
+ const [schema, table, ...rest] = tableName.split(".");
20
+ if (!schema || !table || rest.length > 0) {
21
+ fail({
22
+ code: "SATELLITE_TABLE_INVALID",
23
+ title: "Invalid satellite table name",
24
+ detail: `Invalid table name "${tableName}". Expected "schema.table".`,
25
+ category: "validation",
26
+ retryable: false,
27
+ context: { tableName },
28
+ });
29
+ }
30
+
31
+ return {
32
+ schema: assertIdentifier(schema, "schema name"),
33
+ table: assertIdentifier(table, "table name"),
34
+ };
35
+ }
36
+
37
+ /**
38
+ * Redacts satellite table rows in cursor-sized batches using `FOR UPDATE SKIP LOCKED`.
39
+ *
40
+ * The function is designed for large tables and concurrent workers:
41
+ * each iteration locks and updates only one batch, yields back to Bun's event loop, then
42
+ * continues until no rows remain.
43
+ *
44
+ * @param tx - Active worker transaction.
45
+ * @param tableName - Qualified table name in `schema.table` form.
46
+ * @param lookupColumn - Column used to locate rows that reference the root subject.
47
+ * @param lookupValue - Value to match in `lookupColumn`.
48
+ * @param newHmacValue - Replacement value written during redaction.
49
+ * @param batchSize - Maximum rows processed per loop iteration.
50
+ * @param tenantId - Optional tenant discriminator.
51
+ * @returns Total number of rows redacted.
52
+ * @throws {WorkerError} When identifiers are invalid or batch sizing is unsafe.
53
+ */
54
+ export async function redactSatelliteTable(
55
+ tx: Tsql,
56
+ tableName: string,
57
+ lookupColumn: string,
58
+ lookupValue: string,
59
+ newHmacValue: string,
60
+ batchSize: number = 1000,
61
+ tenantId?: string
62
+ ): Promise<number> {
63
+ if (!Number.isInteger(batchSize) || batchSize < 1) {
64
+ fail({
65
+ code: "SATELLITE_BATCH_SIZE_INVALID",
66
+ title: "Invalid satellite batch size",
67
+ detail: "batchSize must be an integer greater than 0.",
68
+ category: "validation",
69
+ retryable: false,
70
+ });
71
+ }
72
+
73
+ const { schema, table } = parseQualifiedTableName(tableName);
74
+ const safeLookupColumn = assertIdentifier(lookupColumn, "lookup column");
75
+ let totalRedacted = 0;
76
+
77
+ while (true) {
78
+ const tenantFilter = tenantId ? tx` AND tenant_id = ${tenantId}` : tx``;
79
+ const updatedRows = await tx<SatelliteRowId[]>`
80
+ WITH batch AS (
81
+ SELECT id
82
+ FROM ${tx(schema)}.${tx(table)}
83
+ WHERE ${tx(safeLookupColumn)} = ${lookupValue}
84
+ ${tenantFilter}
85
+ LIMIT ${batchSize}
86
+ FOR UPDATE SKIP LOCKED
87
+ )
88
+ UPDATE ${tx(schema)}.${tx(table)}
89
+ SET ${tx(safeLookupColumn)} = ${newHmacValue}
90
+ WHERE id IN (SELECT id FROM batch)
91
+ RETURNING id
92
+ `;
93
+
94
+ if (updatedRows.length === 0) {
95
+ break;
96
+ }
97
+
98
+ totalRedacted += updatedRows.length;
99
+ await yieldWorkerEventLoop();
100
+ }
101
+
102
+ return totalRedacted;
103
+ }
@@ -0,0 +1,36 @@
1
+ import type { VaultUserResult } from "../types";
2
+
3
+ /**
4
+ * Internal control-flow error used to force rollback during `shadowMode`.
5
+ *
6
+ * The result payload survives the rollback and is returned to the caller so the full pipeline
7
+ * can be validated without persisting any mutation.
8
+ */
9
+ export class ShadowModeRollback extends Error {
10
+ readonly result: VaultUserResult;
11
+
12
+ constructor(result: VaultUserResult) {
13
+ super("Shadow mode rollback.");
14
+ this.name = "ShadowModeRollback";
15
+ this.result = result;
16
+ }
17
+ }
18
+
19
+ /**
20
+ * Returns a vault result or throws a rollback sentinel when shadow mode is enabled.
21
+ *
22
+ * @param result - Completed vault result.
23
+ * @param shadowMode - Shadow-mode flag from runtime options.
24
+ * @returns Result when shadow mode is disabled.
25
+ * @throws {ShadowModeRollback} When shadow mode is enabled.
26
+ */
27
+ export function finalizeVaultResult(
28
+ result: VaultUserResult,
29
+ shadowMode: boolean
30
+ ): VaultUserResult {
31
+ if (shadowMode) {
32
+ throw new ShadowModeRollback(result);
33
+ }
34
+
35
+ return result;
36
+ }
@@ -0,0 +1,116 @@
1
+ import type { CompiledExecutionTargetInput } from "@modules/config";
2
+ import { assertIdentifier } from "@/utils";
3
+ import type { RootMutationContext } from "./context";
4
+ import type { VaultUserOptions } from "../types";
5
+
6
+ interface QualifiedTarget {
7
+ schema: string;
8
+ table: string;
9
+ }
10
+
11
+ /**
12
+ * Runtime summary of the DPO-attested static execution plan.
13
+ */
14
+ export interface StaticExecutionPlan {
15
+ targets: CompiledExecutionTargetInput[];
16
+ dependencyCount: number;
17
+ source: "compiled" | "legacy_config";
18
+ }
19
+
20
+ function parseTargetTable(
21
+ value: string,
22
+ defaultSchema: string
23
+ ): QualifiedTarget {
24
+ const parts = value.split('.');
25
+ if (parts.length === 1) {
26
+ return {
27
+ schema: defaultSchema,
28
+ table: assertIdentifier(parts[0]!, "compiled DAG target table")
29
+ };
30
+ }
31
+
32
+ if (parts.length === 2) {
33
+ return {
34
+ schema: assertIdentifier(parts[0] as string, "compiled DAG target schema"),
35
+ table: assertIdentifier(parts[1] as string, "compiled DAG target table")
36
+ };
37
+ }
38
+
39
+ assertIdentifier(value, "compile DAG target table");
40
+ throw new Error("Unreadable compiled DAG target parser branch");
41
+ }
42
+
43
+ function targetKey(target: QualifiedTarget): string {
44
+ return `${target.schema}.${target.table}`;
45
+ };
46
+
47
+ function configuredMutationTargetKeys(
48
+ appSchema: string,
49
+ rootContext: RootMutationContext
50
+ ): Set<string> {
51
+ const keys = new Set<string>();
52
+ for (const target of rootContext.satelliteTargets) {
53
+ keys.add(targetKey({ schema: appSchema, table: target.table }));
54
+ }
55
+
56
+ for (const target of rootContext.blobTargets) {
57
+ keys.add(targetKey({ schema: appSchema, table: target.table }));
58
+ }
59
+
60
+ return keys;
61
+ };
62
+
63
+ /**
64
+ * Resolves the bounded runtime plan used by live vault execution.
65
+ *
66
+ * The preferred source is the Introspector-generated `rules[].targets` manifest. If an older
67
+ * manifest has not yet adopted the compiled DAG block, the worker falls back to the explicitly
68
+ * configured satellite/blob target lists. It never runs recursive FK discovery inside the vault
69
+ * transaction.
70
+ *
71
+ * @param appSchema - Client application Schema.
72
+ * @param rootContext - Validated root and mutation target config.
73
+ * @param options - Vault options containing optional compiled targets.
74
+ * @returns Static dependency boundary used for dry-runs, vaulting, and hard-delete decisions.
75
+ */
76
+ export function resolveStaticExecutionPlan(
77
+ appSchema: string,
78
+ rootContext: RootMutationContext,
79
+ options: Pick<VaultUserOptions, "compiledTargets">
80
+ ): StaticExecutionPlan {
81
+ const rootKey = targetKey({ schema: appSchema, table: rootContext.rootTable });
82
+ const explicitTargets = options.compiledTargets ?? [];
83
+
84
+ if (explicitTargets.length > 0) {
85
+ const seen = new Set<string>();
86
+ let dependencyCount = 0;
87
+
88
+ for (const target of explicitTargets) {
89
+ const key = targetKey(parseTargetTable(target.table, appSchema));
90
+ if (seen.has(key)) {
91
+ continue;
92
+ }
93
+
94
+ seen.add(key);
95
+
96
+ if (key !== rootKey) {
97
+ dependencyCount += 1;
98
+ }
99
+ };
100
+
101
+ return {
102
+ targets: explicitTargets,
103
+ dependencyCount,
104
+ source: "compiled"
105
+ };
106
+ }
107
+
108
+ const fallBackTargetKeys = configuredMutationTargetKeys(appSchema, rootContext);
109
+ fallBackTargetKeys.delete(rootKey);
110
+
111
+ return {
112
+ targets: [],
113
+ dependencyCount: fallBackTargetKeys.size,
114
+ source: "legacy_config"
115
+ }
116
+ }
@@ -0,0 +1,34 @@
1
+ import type { SqlExecutor } from "@/types";
2
+ import type { VaultRecord } from "../helpers";
3
+
4
+ /**
5
+ * Fetches a vault row by root identity tuple.
6
+ *
7
+ * Lookup uses `(root_schema, root_table, root_id, tenant_id)` to avoid cross-tenant collisions.
8
+ *
9
+ * @param sql - Postgres pool or transaction.
10
+ * @param engineSchema - Worker engine schema.
11
+ * @param appSchema - Source application schema.
12
+ * @param userId - Source root identifier.
13
+ * @param rootTable - Source root table name.
14
+ * @param tenantId - Optional tenant discriminator.
15
+ * @returns Matching vault row or `null` when not yet vaulted.
16
+ */
17
+ export async function getVaultRecordByUserId(
18
+ sql: SqlExecutor,
19
+ engineSchema: string,
20
+ appSchema: string,
21
+ userId: string | number,
22
+ rootTable: string = "users",
23
+ tenantId?: string
24
+ ): Promise<VaultRecord | null> {
25
+ const rows = await sql<VaultRecord[]>`
26
+ SELECT *
27
+ FROM ${sql(engineSchema)}.pii_vault
28
+ WHERE root_schema = ${appSchema}
29
+ AND root_table = ${rootTable}
30
+ AND root_id = ${userId.toString()}
31
+ AND tenant_id = ${tenantId ?? ""}
32
+ `;
33
+ return rows[0] ?? null;
34
+ }
@@ -0,0 +1,84 @@
1
+ import type { Sql } from "@/types";
2
+ import { CODE, fail } from "@/errors";
3
+ import {
4
+ assertWorkerSecrets,
5
+ createUserHash,
6
+ resolveNoticeWindowHours,
7
+ resolveRetentionYears,
8
+ resolveSchemas
9
+ } from "../helpers";
10
+ import type { VaultUserOptions, VaultUserResult, WorkerSecrets } from "../types";
11
+ import { resolveRootContext } from "./context";
12
+ import { runVaultDryRun } from "./dry-run";
13
+ import { runVaultMutation } from "./execution";
14
+
15
+ /**
16
+ * Vaults or hard-deletes a configured root entity under repeatable-read guarantees.
17
+ *
18
+ * @param sql - Primary Postgres pool used for transactional writes.
19
+ * @param subjectId - Root identifier for the subject.
20
+ * @param secrets - Worker KEK/HMAC key material.
21
+ * @param options - Vault execution options, including graph, retention, tenancy, and dry-run flags.
22
+ * @returns Vault execution result with lifecycle timestamps and outbox classification.
23
+ * @throws {WorkerError} When validation, integrity, concurrency, or crypto preconditions fail.
24
+ */
25
+ export async function vaultUser(
26
+ sql: Sql,
27
+ subjectId: string | number,
28
+ secrets: WorkerSecrets,
29
+ options: VaultUserOptions = {}
30
+ ): Promise<VaultUserResult> {
31
+ if (
32
+ (typeof subjectId !== "string" && typeof subjectId !== "number") ||
33
+ String(subjectId).trim().length === 0
34
+ ) {
35
+ fail({
36
+ code: `VAULT_${CODE.USER_ID_INVALID}`
37
+ });
38
+ }
39
+
40
+ const { appSchema, engineSchema } = resolveSchemas(options);
41
+ const rootContext = resolveRootContext(options);
42
+ const { kek, hmacKey } = assertWorkerSecrets(secrets);
43
+ const defaultRetentionYears = resolveRetentionYears(options.defaultRetentionYears);
44
+ const noticeWindowHours = resolveNoticeWindowHours(options.noticeWindowHours);
45
+ const now = options.now ? new Date(options.now) : new Date();
46
+ const tenantId = options.tenantId;
47
+ const normalizedSubjectId = String(subjectId);
48
+ const userHash = await createUserHash(
49
+ subjectId,
50
+ appSchema,
51
+ rootContext.rootTable,
52
+ hmacKey,
53
+ tenantId
54
+ );
55
+
56
+ if (options.dryRun) {
57
+ return runVaultDryRun(sql, options.sqlReplica, subjectId, {
58
+ appSchema,
59
+ engineSchema,
60
+ rootContext,
61
+ defaultRetentionYears,
62
+ noticeWindowHours,
63
+ now,
64
+ tenantId,
65
+ userHash,
66
+ compiledTargets: options.compiledTargets,
67
+ });
68
+ }
69
+
70
+ return runVaultMutation(sql, subjectId, {
71
+ appSchema,
72
+ engineSchema,
73
+ rootContext,
74
+ defaultRetentionYears,
75
+ noticeWindowHours,
76
+ now,
77
+ tenantId,
78
+ normalizedSubjectId,
79
+ userHash,
80
+ kek,
81
+ hmacKey,
82
+ options,
83
+ });
84
+ }