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
+ /**
2
+ * Provisions the worker engine schema (`pii_vault`, `user_keys`, `outbox`) and supporting indexes.
3
+ *
4
+ * The migration is idempotent and safe to execute on every boot.
5
+ */
6
+
7
+ import { assertIdentifier, getLogger } from "@/utils";
8
+ import type { Sql } from "@/types";
9
+
10
+ const logger = getLogger({ component: "migrations" })
11
+
12
+ /**
13
+ * Applies worker schema migrations for vaulting, shredding, notification, and outbox processing.
14
+ *
15
+ * @param sql - Postgres pool connection.
16
+ * @param engineSchema - Target worker schema name.
17
+ * @returns Promise resolved once all DDL has been applied.
18
+ * @throws {WorkerError} When schema identifier validation fails.
19
+ */
20
+ export async function runMigrations(
21
+ sql: Sql,
22
+ engineSchema: string = "dpdp_engine"
23
+ ) {
24
+ const safeEngineSchema = assertIdentifier(engineSchema, "engine schema name")
25
+
26
+ logger.info({ engineSchema: safeEngineSchema }, "Provisioning DPDP engine schema");
27
+
28
+ await sql.begin(async (tx) => {
29
+ await tx`CREATE EXTENSION IF NOT EXISTS pgcrypto`;
30
+ await tx`CREATE SCHEMA IF NOT EXISTS ${tx(engineSchema)}`;
31
+
32
+ await tx`
33
+ CREATE TABLE IF NOT EXISTS ${tx(safeEngineSchema)}.pii_vault (
34
+ user_uuid_hash TEXT primary key,
35
+ request_id TEXT,
36
+ tenant_id TEXT NOT NULL DEFAULT '',
37
+ root_schema TEXT NOT NULL,
38
+ root_id TEXT NOT NULL,
39
+ pseudonym TEXT NOT NULL,
40
+ encrypted_pii JSONB NOT NULL,
41
+ salt TEXT NOT NULL,
42
+ dependency_count INTEGER NOT NULL DEFAULT 0,
43
+ trigger_source TEXT,
44
+ legal_framework TEXT,
45
+ actor_opaque_id TEXT,
46
+ applied_rule_name TEXT,
47
+ applied_rule_citation TEXT,
48
+ retention_expiry TIMESTAMPTZ NOT NULL,
49
+ notification_due_at TIMESTAMPTZ NOT NULL,
50
+ notification_sent_at TIMESTAMPTZ,
51
+ notification_lock_id UUID,
52
+ notification_lock_expires_at TIMESTAMPTZ,
53
+ shredded_at TIMESTAMPTZ,
54
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
55
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
56
+ )
57
+ `;
58
+
59
+ await tx`
60
+ ALTER TABLE ${tx(safeEngineSchema)}.pii_vault
61
+ ADD COLUMN IF NOT EXISTS request_id TEXT,
62
+ ADD COLUMN IF NOT EXISTS tenant_id TEXT NOT NULL DEFAULT '',
63
+ ADD COLUMN IF NOT EXISTS root_schema TEXT,
64
+ ADD COLUMN IF NOT EXISTS root_table TEXT,
65
+ ADD COLUMN IF NOT EXISTS root_id TEXT,
66
+ ADD COLUMN IF NOT EXISTS pseudonym TEXT,
67
+ ADD COLUMN IF NOT EXISTS dependency_count INTEGER NOT NULL DEFAULT 0,
68
+ ADD COLUMN IF NOT EXISTS trigger_source TEXT,
69
+ ADD COLUMN IF NOT EXISTS legal_framework TEXT,
70
+ ADD COLUMN IF NOT EXISTS actor_opaque_id TEXT,
71
+ ADD COLUMN IF NOT EXISTS applied_rule_name TEXT,
72
+ ADD COLUMN IF NOT EXISTS applied_rule_citation TEXT,
73
+ ADD COLUMN IF NOT EXISTS notification_due_at TIMESTAMPTZ,
74
+ ADD COLUMN IF NOT EXISTS notification_sent_at TIMESTAMPTZ,
75
+ ADD COLUMN IF NOT EXISTS notification_lock_id UUID,
76
+ ADD COLUMN IF NOT EXISTS notification_lock_expires_at TIMESTAMPTZ,
77
+ ADD COLUMN IF NOT EXISTS shredded_at TIMESTAMPTZ,
78
+ ADD COLUMN IF NOT EXISTS updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
79
+ `;
80
+
81
+ await tx`
82
+ CREATE UNIQUE INDEX IF NOT EXISTS pii_vault_root_lookup_idx
83
+ ON ${tx(safeEngineSchema)}.pii_vault (root_schema, root_table, root_id, tenant_id)
84
+ `;
85
+
86
+ await tx`
87
+ CREATE INDEX IF NOT EXISTS pii_vault_retention_idx
88
+ ON ${tx(safeEngineSchema)}.pii_vault (retention_expiry, notification_due_at)
89
+ `;
90
+
91
+ await tx`
92
+ CREATE INDEX IF NOT EXISTS pii_vault_request_idx
93
+ ON ${tx(safeEngineSchema)}.pii_vault (request_id)
94
+ `;
95
+
96
+ await tx`
97
+ CREATE TABLE IF NOT EXISTS ${tx(safeEngineSchema)}.notification_receipts (
98
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
99
+ user_uuid_hash TEXT NOT NULL REFERENCES ${tx(safeEngineSchema)}.pii_vault(user_uuid_hash) ON DELETE CASCADE,
100
+ request_id TEXT,
101
+ idempotency_key TEXT NOT NULL UNIQUE,
102
+ provider TEXT NOT NULL,
103
+ provider_message_id TEXT,
104
+ template_version TEXT NOT NULL,
105
+ template_hash TEXT NOT NULL,
106
+ sent_at TIMESTAMPTZ NOT NULL,
107
+ metadata JSONB NOT NULL DEFAULT '{}'::jsonb,
108
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
109
+ )
110
+ `;
111
+
112
+ await tx`
113
+ CREATE INDEX IF NOT EXISTS notification_receipts_request_idx
114
+ ON ${tx(safeEngineSchema)}.notification_receipts (request_id, sent_at DESC)
115
+ `;
116
+
117
+
118
+ await tx`
119
+ CREATE TABLE IF NOT EXISTS ${tx(safeEngineSchema)}.user_keys (
120
+ user_uuid_hash TEXT PRIMARY KEY REFERENCES ${tx(safeEngineSchema)}.pii_vault(user_uuid_hash) ON DELETE CASCADE,
121
+ encrypted_dek BYTEA NOT NULL,
122
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
123
+ )
124
+ `;
125
+
126
+ await tx`
127
+ CREATE TABLE IF NOT EXISTS ${tx(safeEngineSchema)}.blob_objects (
128
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
129
+ user_uuid_hash TEXT NOT NULL REFERENCES ${tx(safeEngineSchema)}.pii_vault(user_uuid_hash) ON DELETE CASCADE,
130
+ request_id TEXT,
131
+ tenant_id TEXT NOT NULL DEFAULT '',
132
+ root_schema TEXT NOT NULL,
133
+ root_table TEXT NOT NULL,
134
+ root_id TEXT NOT NULL,
135
+ source_table TEXT NOT NULL,
136
+ source_column TEXT NOT NULL,
137
+ provider TEXT NOT NULL,
138
+ action TEXT NOT NULL,
139
+ retention_mode TEXT NOT NULL DEFAULT 'governance',
140
+ region TEXT NOT NULL,
141
+ expected_bucket_owner TEXT,
142
+ bucket TEXT NOT NULL,
143
+ object_key TEXT NOT NULL,
144
+ version_id TEXT NOT NULL,
145
+ e_tag TEXT,
146
+ masked_value TEXT NOT NULL,
147
+ legal_hold_status TEXT NOT NULL DEFAULT 'ON',
148
+ legal_hold_applied_at TIMESTAMPTZ,
149
+ overwrite_status TEXT NOT NULL DEFAULT 'not_requested',
150
+ overwrite_e_tag TEXT,
151
+ overwrite_version_id TEXT,
152
+ overwrite_applied_at TIMESTAMPTZ,
153
+ shred_status TEXT NOT NULL DEFAULT 'pending',
154
+ shred_receipt JSONB,
155
+ shredded_at TIMESTAMPTZ,
156
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
157
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
158
+ CONSTRAINT blob_objects_provider_check CHECK (provider IN ('aws_s3')),
159
+ CONSTRAINT blob_objects_action_check CHECK (action IN ('versioned_hard_delete', 'hard_delete', 'overwrite', 'legal_hold_only')),
160
+ CONSTRAINT blob_objects_retention_mode_check CHECK (retention_mode IN ('governance', 'compliance')),
161
+ CONSTRAINT blob_objects_hold_status_check CHECK (legal_hold_status IN ('ON', 'OFF', 'not_supported')),
162
+ CONSTRAINT blob_objects_overwrite_status_check CHECK (overwrite_status IN ('not_requested', 'applied')),
163
+ CONSTRAINT blob_objects_shred_status_check CHECK (shred_status IN ('pending', 'purged', 'captured_version_deleted', 'retained_by_policy'))
164
+ )
165
+ `;
166
+
167
+ await tx`
168
+ CREATE UNIQUE INDEX IF NOT EXISTS blob_objects_identity_idx
169
+ ON ${tx(safeEngineSchema)}.blob_objects (
170
+ user_uuid_hash,
171
+ source_table,
172
+ source_column,
173
+ bucket,
174
+ object_key,
175
+ version_id
176
+ )
177
+ `;
178
+
179
+ await tx`
180
+ CREATE INDEX IF NOT EXISTS blob_objects_shred_idx
181
+ ON ${tx(safeEngineSchema)}.blob_objects (user_uuid_hash, shred_status)
182
+ `
183
+
184
+ await tx`
185
+ CREATE INDEX IF NOT EXISTS blob_objects_object_idx
186
+ ON ${tx(safeEngineSchema)}.blob_objects (provider, bucket, object_key, shred_status)
187
+ `
188
+
189
+ await tx`
190
+ CREATE TABLE IF NOT EXISTS ${tx(safeEngineSchema)}.outbox (
191
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
192
+ idempotency_key TEXT NOT NULL UNIQUE,
193
+ user_uuid_hash TEXT NOT NULL,
194
+ event_type VARCHAR(50) NOT NULL,
195
+ payload JSONB NOT NULL,
196
+ previous_hash TEXT NOT NULl,
197
+ current_hash TEXT NOT NULL,
198
+ chain_status VARCHAR(20) NOT NULL DEFAULT 'finalized',
199
+ status VARCHAR(20) NOT NULL DEFAULT 'pending',
200
+ attempt_count INTEGER NOT NULL DEFAULT 0,
201
+ lease_token UUID,
202
+ lease_expires_at TIMESTAMPTZ,
203
+ next_attempt_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
204
+ processed_at TIMESTAMPTZ,
205
+ last_error TEXT,
206
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
207
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
208
+ CONSTRAINT outbox_chain_status_check CHECK (chain_status IN ('pending', 'finalized')),
209
+ CONSTRAINT outbox_status_check CHECK (status IN ('pending', 'leased', 'processed', 'dead_letter'))
210
+ )
211
+ `
212
+
213
+ await tx`
214
+ ALTER TABLE ${tx(safeEngineSchema)}.outbox
215
+ ADD COLUMN IF NOT EXISTS idempotency_key TEXT,
216
+ ADD COLUMN IF NOT EXISTS previous_hash TEXT,
217
+ ADD COLUMN IF NOT EXISTS current_hash TEXT,
218
+ ADD COLUMN IF NOT EXISTS chain_status VARCHAR(20) NOT NULL DEFAULT 'finalized',
219
+ ADD COLUMN IF NOT EXISTS status VARCHAR(20) NOT NULL DEFAULT 'pending',
220
+ ADD COLUMN IF NOT EXISTS attempt_count INTEGER NOT NULL DEFAULT 0,
221
+ ADD COLUMN IF NOT EXISTS lease_token UUID,
222
+ ADD COLUMN IF NOT EXISTS lease_expires_at TIMESTAMPTZ,
223
+ ADD COLUMN IF NOT EXISTS next_attempt_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
224
+ ADD COLUMN IF NOT EXISTS processed_at TIMESTAMPTZ,
225
+ ADD COLUMN IF NOT EXISTS last_error TEXT,
226
+ ADD COLUMN IF NOT EXISTS updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
227
+ `;
228
+
229
+ await tx`
230
+ CREATE UNIQUE INDEX IF NOT EXISTS outbox_idempotency_key_idx
231
+ ON ${tx(safeEngineSchema)}.outbox (idempotency_key)
232
+ `;
233
+
234
+ await tx`
235
+ CREATE INDEX IF NOT EXISTS outbox_due_events_idx
236
+ ON ${tx(safeEngineSchema)}.outbox (status, next_attempt_at, created_at)
237
+ `;
238
+
239
+ await tx`
240
+ CREATE INDEX IF NOT EXISTS outbox_chain_tail_idx
241
+ ON ${tx(safeEngineSchema)}.outbox (created_at DESC, id DESC)
242
+ INCLUDE (current_hash)
243
+ WHERE current_hash IS NOT NULL AND chain_status = 'finalized'
244
+ `;
245
+
246
+ await tx`
247
+ CREATE INDEX IF NOT EXISTS outbox_chain_finalize_idx
248
+ ON ${tx(safeEngineSchema)}.outbox (chain_status, created_at, id)
249
+ WHERE chain_status = 'pending'
250
+ `;
251
+ });
252
+
253
+ logger.info({ engineSchema: safeEngineSchema }, "DPDP engine schema provisioned");
254
+ }
@@ -0,0 +1,61 @@
1
+ import type { Logger } from "pino";
2
+
3
+ const PARAM_REDACTION = "[REDACTED]";
4
+
5
+ function escapeRegExp(value: string): string {
6
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
7
+ }
8
+
9
+ function containsConfiguredPiiColumn(query: string, piiColumns: string[]): boolean {
10
+ return piiColumns.some((column) => new RegExp(`\\b${escapeRegExp(column)}\\b`, "i").test(query));
11
+ }
12
+
13
+ /**
14
+ * Produces redacted debug parameters for postgres.js query logging.
15
+ *
16
+ * When the SQL text references a configured PII column, every bound parameter is redacted. This
17
+ * conservative policy avoids leaking values from `WHERE email = $1` or dynamic PII updates where
18
+ * postgres.js exposes values only as a positional parameter array.
19
+ *
20
+ * @param query - SQL text produced by postgres.js.
21
+ * @param parameters - Positional query parameters supplied by postgres.js.
22
+ * @param piiColumns - Client-declared PII columns from `graph.root_pii_columns`.
23
+ * @returns Redacted parameters safe for structured debug logs.
24
+ */
25
+ export function redactSqlDebugParameters(
26
+ query: string,
27
+ parameters: readonly unknown[],
28
+ piiColumns: readonly string[]
29
+ ): unknown[] {
30
+ if (parameters.length === 0) return [];
31
+
32
+ return containsConfiguredPiiColumn(query, [...piiColumns])
33
+ ? parameters.map(() => PARAM_REDACTION)
34
+ : parameters.map((value) => {
35
+ if (typeof value === "string" && value.length > 128) {
36
+ return `${value.slice(0, 125)}...`;
37
+ }
38
+ return value;
39
+ })
40
+ }
41
+
42
+ /**
43
+ * Creates a postgres.js `debug` hook that logs SQL without leaking configured PII values.
44
+ *
45
+ * @param logger - Pino logger receiving structured query records.
46
+ * @param piiColumns - Client-declared PII columns that trigger full parameter redaction.
47
+ * @returns postgres.js-compatible debug callback.
48
+ */
49
+ export function createRedactingSqlDebugLogger(
50
+ logger: Logger,
51
+ piiColumns: readonly string[]
52
+ ) {
53
+ return (_connection: unknown, query: string, parameters: unknown[]) => {
54
+ logger.debug({
55
+ query: query.replace(/\s+/g, " ").trim(),
56
+ parameters: redactSqlDebugParameters(query, parameters, piiColumns)
57
+ },
58
+ "Postgres query executed"
59
+ );
60
+ }
61
+ }
@@ -0,0 +1,3 @@
1
+ export * from "./types";
2
+ export * from "./s3";
3
+ export * from "./store";