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
package/.env.example ADDED
@@ -0,0 +1,55 @@
1
+ # Compliance Engine Worker / Introspector
2
+ # The worker does need runtime configuration. In production, prefer Docker/Kubernetes secrets
3
+ # and compliance.worker.yml over a local .env file. This file is for local development only.
4
+
5
+ LOG_LEVEL=info
6
+
7
+ # Client application database. This is the database inside the client's VPC/VPS.
8
+ DATABASE_URL=postgres://dpdp:dpdp@127.0.0.1:55432/dpdp_local
9
+
10
+ # Control Plane worker endpoints.
11
+ API_CLIENT_ID=worker-1
12
+ API_WORKER_TOKEN=replace-with-worker-token
13
+ API_REQUEST_SIGNING_SECRET=replace-with-worker-request-signing-secret
14
+ API_SYNC_URL=http://127.0.0.1:3000/api/v1/worker/sync
15
+ API_BASE_URL=http://127.0.0.1:3000/api/v1/worker/tasks
16
+ API_OUTBOX_URL=http://127.0.0.1:3000/api/v1/worker/outbox
17
+
18
+ # Metrics and health server exposed by the worker.
19
+ METRICS_PORT=9464
20
+
21
+ # Legal notice delivery. Required for production notice dispatch.
22
+ MAILER_WEBHOOK_URL=http://127.0.0.1:18080/mail
23
+ MAILER_TIMEOUT_MS=10000
24
+
25
+ # Local crypto material. For production, use compliance.worker.yml key sources:
26
+ # env, file, aws_kms, gcp_secret_manager, or hashicorp_vault.
27
+ DPDP_MASTER_KEY=replace-with-32-byte-base64-key
28
+ DPDP_HMAC_KEY=replace-with-64-hex-or-base64-hmac-key
29
+
30
+ # Optional signed worker config verification.
31
+ DPDP_CONFIG_SIGNING_PUBLIC_KEY_SPKI_BASE64=
32
+ DPDP_CONFIG_SIGNATURE_PATH=./compliance.worker.yml.sig
33
+
34
+ # Optional AWS credentials for S3/blob purge and AWS KMS.
35
+ # Prefer ECS/EC2 IAM roles in production.
36
+ AWS_ACCESS_KEY_ID=
37
+ AWS_SECRET_ACCESS_KEY=
38
+ AWS_SESSION_TOKEN=
39
+ AWS_REGION=ap-south-1
40
+ AWS_EC2_METADATA_DISABLED=true
41
+ AWS_CONTAINER_CREDENTIALS_RELATIVE_URI=
42
+ AWS_CONTAINER_CREDENTIALS_FULL_URI=
43
+ AWS_CONTAINER_AUTHORIZATION_TOKEN=
44
+ AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE=
45
+
46
+ # Optional Google Secret Manager provider.
47
+ GCP_ACCESS_TOKEN=
48
+
49
+ # Optional HashiCorp Vault provider.
50
+ VAULT_ADDR=
51
+ VAULT_TOKEN=
52
+ VAULT_NAMESPACE=
53
+
54
+ # Test runner override.
55
+ TEST_DATABASE_URL=postgres://dpdp:dpdp@127.0.0.1:55432/dpdp_local
package/Dockerfile ADDED
@@ -0,0 +1,33 @@
1
+ # syntax=docker/dockerfile:1.7
2
+
3
+ ARG BUN_VERSION=1.3.12
4
+
5
+ FROM oven/bun:${BUN_VERSION}-slim AS deps
6
+ WORKDIR /workspace
7
+
8
+ COPY . .
9
+ RUN bun install --frozen-lockfile --production
10
+
11
+ FROM oven/bun:${BUN_VERSION}-slim AS verify
12
+ WORKDIR /workspace
13
+
14
+ COPY . .
15
+ RUN bun install --frozen-lockfile
16
+
17
+ RUN bun run worker:typecheck
18
+
19
+ FROM oven/bun:${BUN_VERSION}-slim AS runtime
20
+ WORKDIR /app
21
+
22
+ ENV NODE_ENV=production
23
+ ENV METRICS_PORT=9464
24
+
25
+ COPY --from=deps /workspace/node_modules /app/node_modules
26
+ COPY --from=deps /workspace/package.json /app/package.json
27
+ COPY --from=deps /workspace/tsconfig.json /app/tsconfig.json
28
+ COPY apps/worker /app/apps/worker
29
+
30
+ EXPOSE 9464
31
+
32
+ WORKDIR /app/apps/worker
33
+ ENTRYPOINT ["bun", "src/index.ts"]
@@ -0,0 +1,64 @@
1
+ # Example `compliance.worker.yaml`
2
+ version: "1.0"
3
+ database:
4
+ app_schema: "mock_app"
5
+ engine_schema: "dpdp_engine"
6
+
7
+ compliance_policy:
8
+ default_retention_years: 0
9
+ notice_window_hours: 48
10
+ retention_rules:
11
+ - rule_name: "PMLA_FINANCIAL"
12
+ legal_citation: "Prevention of Money Laundering Act, 2002, Sec 12"
13
+ if_has_data_in:
14
+ - "transactions"
15
+ - "invoices"
16
+ retention_years: 10
17
+ - rule_name: "RBI_KYC"
18
+ legal_citation: "RBI KYC Directions, 2016, Sec 38"
19
+ if_has_data_in:
20
+ - "kyc_documents"
21
+ retention_years: 5
22
+
23
+ graph:
24
+ root_table: "users"
25
+ root_id_column: "id"
26
+ max_depth: 32
27
+ notice_email_column: "email"
28
+ notice_name_column: "full_name"
29
+ root_pii_columns:
30
+ email: "HMAC"
31
+ full_name: "STATIC_MASK"
32
+
33
+ satellite_targets:
34
+ - table: "marketing_leads"
35
+ lookup_column: "email"
36
+ action: "redact"
37
+ masking_rules:
38
+ email: "HMAC"
39
+ name: "STATIC_MASK"
40
+ - table: "system_audit_logs"
41
+ lookup_column: "user_identifier"
42
+ action: "hard_delete"
43
+
44
+ blob_targets: []
45
+
46
+ outbox:
47
+ batch_size: 10
48
+ lease_seconds: 60
49
+ max_attempts: 10
50
+ base_backoff_ms: 1000
51
+
52
+ security:
53
+ notification_lease_seconds: 120
54
+ master_key_env: "DPDP_MASTER_KEY"
55
+ hmac_key_env: "DPDP_HMAC_KEY"
56
+
57
+ integrity:
58
+ expected_schema_hash: "0000000000000000000000000000000000000000000000000000000000000000"
59
+
60
+ legal_attestation:
61
+ dpo_identifier: "dpo-name@client.com"
62
+ configuration_version: "v1.2.0"
63
+ legal_review_date: "2026-04-20"
64
+ acknowledgment: "I confirm this configuration accurately reflects our data retention obligations under DPDP Sec 12."
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "dpdp-erasure-cli",
3
+ "version": "1.0.0",
4
+ "module": "index.ts",
5
+ "type": "module",
6
+ "bin": {
7
+ "dpdp-cli": "./src/modules/cli/index.ts"
8
+ },
9
+ "publishConfig": {
10
+ "access": "public"
11
+ },
12
+ "scripts": {
13
+ "dev": "bun src/index.ts",
14
+ "typecheck": "tsc --noEmit",
15
+ "cli": "bun src/modules/cli/index.ts",
16
+ "test": "vitest run --fileParallelism=false",
17
+ "test:ui": "vitest run --ui"
18
+ },
19
+ "devDependencies": {
20
+ "@types/bun": "latest",
21
+ "@types/js-yaml": "^4.0.9",
22
+ "@types/pino": "^7.0.5",
23
+ "@vitest/ui": "4.1.5",
24
+ "vitest": "^4.1.5"
25
+ },
26
+ "peerDependencies": {
27
+ "typescript": "^5"
28
+ },
29
+ "dependencies": {
30
+ "@inquirer/prompts": "^8.5.2",
31
+ "boxen": "^8.0.1",
32
+ "cli-table3": "^0.6.5",
33
+ "commander": "^15.0.0",
34
+ "js-yaml": "^4.1.1",
35
+ "ora": "^9.4.0",
36
+ "picocolors": "^1.1.1",
37
+ "pino": "^10.3.1",
38
+ "postgres": "^3.4.9",
39
+ "zod": "^4.4.2"
40
+ }
41
+ }
@@ -0,0 +1 @@
1
+ export const MAX_DEPTH = 32;
@@ -0,0 +1,110 @@
1
+ import { ERROR_REGISTRY, type ErrorCodeType } from "./registry";
2
+ import { normalizeErrorType, WorkerError } from "./worker";
3
+ import type { WorkerErrorContext, WorkerErrorCategory, RegistryEntry } from "./types";
4
+ import { type WorkerValidationIssue } from "@/validation/zod";
5
+
6
+ type ExtractBaseCode<T extends string> = T extends ErrorCodeType
7
+ ? T
8
+ : T extends `${string}_${infer Base extends ErrorCodeType}`
9
+ ? Base
10
+ : never;
11
+
12
+ type DetailParams<Base extends ErrorCodeType> =
13
+ typeof ERROR_REGISTRY[Base] extends { detail: (...args: infer P) => string }
14
+ ? P
15
+ : never;
16
+
17
+ type InferData<Base extends ErrorCodeType> =
18
+ DetailParams<Base> extends [infer D, ...any[]]
19
+ ? D
20
+ : never;
21
+
22
+ type IsDataRequired<Base extends ErrorCodeType> = DetailParams<Base> extends []
23
+ ? false
24
+ : DetailParams<Base> extends [never]
25
+ ? false
26
+ : true;
27
+
28
+ type FailOptions<T extends string> = {
29
+ code: T;
30
+ title?: string;
31
+ category?: WorkerErrorCategory;
32
+ retryable?: boolean;
33
+ fatal?: boolean;
34
+ cause?: unknown;
35
+ context?: WorkerErrorContext | null;
36
+ issues?: WorkerValidationIssue[] | null;
37
+ } & (
38
+ [ExtractBaseCode<T>] extends [infer Base extends ErrorCodeType]
39
+ ? (
40
+ | { detail: string; data?: InferData<Base> }
41
+ | (IsDataRequired<Base> extends true
42
+ ? { detail?: never; data: InferData<Base> }
43
+ : { detail?: never; data?: InferData<Base> })
44
+ )
45
+ : {
46
+ title: string;
47
+ detail: string;
48
+ category: WorkerErrorCategory;
49
+ });
50
+
51
+ export function fail<T extends string>(options: FailOptions<T>): never {
52
+ const { code, title, category, retryable, fatal, cause, context, issues } = options;
53
+
54
+ let meta: RegistryEntry | undefined = ERROR_REGISTRY[code as ErrorCodeType];
55
+ let baseCode = code as string;
56
+
57
+ if (!meta) {
58
+ // Isolate the longest valid registry suffix matching the prefixed string
59
+ const baseMatch = (Object.keys(ERROR_REGISTRY) as ErrorCodeType[])
60
+ .filter((k) => code.endsWith(`_${k}`))
61
+ .sort((a, b) => b.length - a.length)[0];
62
+
63
+ if (baseMatch) {
64
+ meta = ERROR_REGISTRY[baseMatch];
65
+ baseCode = baseMatch;
66
+ }
67
+ }
68
+
69
+ if (meta) {
70
+ let resolvedDetail: string;
71
+
72
+ if ('detail' in options && typeof options.detail === 'string') {
73
+ resolvedDetail = options.detail;
74
+ } else {
75
+ const targetDetail = 'detail' in meta ? meta.detail : undefined;
76
+ const inputData = (options as any).data ?? {};
77
+
78
+ resolvedDetail = typeof targetDetail === 'function'
79
+ ? (targetDetail as Function)(inputData)
80
+ : String(targetDetail);
81
+ }
82
+
83
+ throw new WorkerError({
84
+ code,
85
+ type: normalizeErrorType(baseCode),
86
+ title: title ?? meta.title,
87
+ category: category ?? meta.category,
88
+ fatal: fatal ?? meta.fatal,
89
+ retryable: retryable ?? meta.retryable,
90
+ detail: resolvedDetail,
91
+ cause: cause ?? null,
92
+ context: context ?? null,
93
+ issues: issues ?? null,
94
+ });
95
+ }
96
+
97
+ // Pure ad-hoc custom string signature layout engine
98
+ throw new WorkerError({
99
+ code,
100
+ type: normalizeErrorType(baseCode),
101
+ title: title ?? "Unhandled Application Fault",
102
+ category: category ?? "internal",
103
+ detail: (options as any).detail ?? "An unexpected runtime failure was triggered.",
104
+ retryable: retryable ?? false,
105
+ fatal: fatal ?? true,
106
+ cause: cause ?? null,
107
+ context: context ?? null,
108
+ issues: issues ?? null,
109
+ });
110
+ }
@@ -0,0 +1,4 @@
1
+ export * from "./worker";
2
+ export * from "./registry";
3
+ export * from "./fail";
4
+ export * from "./types";
@@ -0,0 +1,166 @@
1
+ import { ZodError } from "zod";
2
+ import type {
3
+ WorkerErrorCode,
4
+ WorkerErrorFallback,
5
+ WorkerErrorCategory,
6
+ } from "./types";
7
+ import { WorkerError } from "./worker";
8
+ import { summarizeZodError } from "@/validation/zod";
9
+
10
+ const RETRYABLE_POSTGRES_CODES = new Set([
11
+ "40001", // serialization_failure
12
+ "40P01", // deadlock_detected
13
+ "55P03", // lock_not_available
14
+ "57014", // query_canceled
15
+ "57P01", // admin_shutdown
16
+ "57P02", // crash_shutdown
17
+ "57P03", // cannot_connect_now
18
+ ]);
19
+
20
+ function isAbortLikeError(value: unknown): value is Error {
21
+ return value instanceof Error && (value.name === "AbortError" || value.name === "TimeoutError");
22
+ }
23
+
24
+ function isPostgresError(value: unknown): value is Error & { code: string } {
25
+ return value instanceof Error && typeof (value as { code?: unknown }).code === "string";
26
+ }
27
+
28
+ export function inferRetryability(error: unknown, fallback?: WorkerErrorFallback): boolean {
29
+ if (fallback?.retryable !== undefined) {
30
+ return fallback.retryable;
31
+ }
32
+
33
+ if (isAbortLikeError(error)) {
34
+ return true;
35
+ }
36
+
37
+ if (isPostgresError(error)) {
38
+ if (error.code.startsWith("08")) {
39
+ return true;
40
+ }
41
+
42
+ return RETRYABLE_POSTGRES_CODES.has(error.code);
43
+ }
44
+
45
+ return false;
46
+ }
47
+
48
+ export function inferCategory(error: unknown, fallback?: WorkerErrorFallback): WorkerErrorCategory {
49
+ if (fallback?.category) {
50
+ return fallback.category;
51
+ }
52
+
53
+ if (error instanceof ZodError) {
54
+ return "validation";
55
+ }
56
+
57
+ if (isAbortLikeError(error)) {
58
+ return "network";
59
+ }
60
+
61
+ if (isPostgresError(error)) {
62
+ if (error.code === "40001" || error.code === "40P01" || error.code === "55P03") {
63
+ return "concurrency";
64
+ }
65
+
66
+ if (error.code.startsWith("08") || error.code.startsWith("57")) {
67
+ return "database";
68
+ }
69
+
70
+ return "database";
71
+ }
72
+
73
+ return "internal";
74
+ }
75
+
76
+ export function inferFatal(error: unknown, fallback?: WorkerErrorFallback): boolean {
77
+ if (fallback?.fatal !== undefined) {
78
+ return fallback.fatal;
79
+ }
80
+
81
+ if (error instanceof WorkerError) {
82
+ return error.fatal;
83
+ }
84
+
85
+ return false;
86
+ }
87
+
88
+ export function inferTitle(error: unknown, fallback?: WorkerErrorFallback): string {
89
+ if (fallback?.title) {
90
+ return fallback.title;
91
+ }
92
+
93
+ if (error instanceof ZodError) {
94
+ return "Validation failed";
95
+ }
96
+
97
+ if (isAbortLikeError(error)) {
98
+ return "Network operation timed out";
99
+ }
100
+
101
+ if (isPostgresError(error) && error.code === "40001") {
102
+ return "Serialization failure";
103
+ }
104
+
105
+ if (error instanceof Error && error.name) {
106
+ return error.name;
107
+ }
108
+
109
+ return "Unexpected worker error";
110
+ }
111
+
112
+ export function inferDetail(error: unknown, fallback?: WorkerErrorFallback): string {
113
+ if (fallback?.detail) {
114
+ return fallback.detail;
115
+ }
116
+
117
+ if (error instanceof ZodError) {
118
+ return summarizeZodError(error);
119
+ }
120
+
121
+ if (error instanceof Error && error.message.trim().length > 0) {
122
+ return error.message;
123
+ }
124
+
125
+ return "An unexpected worker error occurred.";
126
+ }
127
+
128
+ export function inferCode(error: unknown, fallback?: WorkerErrorFallback): WorkerErrorCode {
129
+ if (fallback?.code) {
130
+ return fallback.code;
131
+ }
132
+
133
+ if (error instanceof WorkerError) {
134
+ return error.code;
135
+ }
136
+
137
+ if (error instanceof ZodError) {
138
+ return "VALIDATION_FAILED";
139
+ }
140
+
141
+ if (isAbortLikeError(error)) {
142
+ return "NETWORK_TIMEOUT";
143
+ }
144
+
145
+ if (isPostgresError(error)) {
146
+ if (error.code === "40001") {
147
+ return "DB_SERIALIZATION_FAILURE";
148
+ }
149
+
150
+ if (error.code === "40P01") {
151
+ return "DB_DEADLOCK_DETECTED";
152
+ }
153
+
154
+ if (error.code === "55P03") {
155
+ return "DB_LOCK_NOT_AVAILABLE";
156
+ }
157
+
158
+ if (error.code.startsWith("08")) {
159
+ return "DB_CONNECTION_ERROR";
160
+ }
161
+
162
+ return "DB_ERROR";
163
+ }
164
+
165
+ return "INTERNAL_UNEXPECTED";
166
+ }
@@ -0,0 +1,122 @@
1
+ import type { RegistryEntry, WorkerErrorCategory } from "./types";
2
+
3
+ export type ErrorCodeType = keyof typeof ERROR_REGISTRY;
4
+
5
+ export interface ErrorRegistryTypes {
6
+ title: string;
7
+ detail: (data: unknown) => string;
8
+ category: WorkerErrorCategory;
9
+ retryable: boolean;
10
+ fatal: boolean;
11
+ }
12
+
13
+ /**
14
+ * Centralized Error Registry
15
+ */
16
+ export const ERROR_REGISTRY = {
17
+ UNREGISTERED_ERROR_CODE: {
18
+ title: "Unknown Error",
19
+ detail: (data: { code: string }) => `An unregistered error occurred: ${data.code}`,
20
+ category: "internal",
21
+ retryable: false,
22
+ fatal: true,
23
+ },
24
+ CONFIG_SIGNATURE_MISSING: {
25
+ title: "Missing worker config signature",
26
+ detail: (data: { value: string }) =>
27
+ `Detached config signature ${data.value} is required when config signing is enabled.`,
28
+ category: "configuration",
29
+ retryable: false,
30
+ fatal: true,
31
+ },
32
+ CONFIG_SIGNATURE_INVALID: {
33
+ title: "Invalid worker config signature",
34
+ detail: (data: { value: string }) => `Detached config signature ${data.value} failed verification.`,
35
+ category: "integrity",
36
+ retryable: false,
37
+ fatal: true,
38
+ },
39
+ SECRET_ENV_MISSING: {
40
+ title: "Required secret is missing",
41
+ detail: (data: { keyName: string }) => `${data.keyName} is required.`,
42
+ category: "configuration",
43
+ retryable: false,
44
+ fatal: true,
45
+ },
46
+ SECRET_ENV_INVALID: {
47
+ title: "Invalid secret format",
48
+ detail: (data: { keyName: string, KEY_LENGTH: number }) =>
49
+ `${data.keyName} must resolve to exactly ${data.KEY_LENGTH} bytes. Supported formats: 64-char hex or base64.`,
50
+ category: "configuration",
51
+ retryable: false,
52
+ fatal: true,
53
+ },
54
+ KMS_SECRET_MISSING: {
55
+ title: "Runtime secret is missing",
56
+ category: "configuration",
57
+ retryable: false,
58
+ fatal: true,
59
+ },
60
+ KMS_RESPONSE_INVALID: {
61
+ title: "Key provider response invalid",
62
+ category: "external",
63
+ retryable: false,
64
+ fatal: true,
65
+ },
66
+ VAULT_NOT_FOUND: {
67
+ title: "Vault record not found",
68
+ detail: (
69
+ data: {
70
+ appSchema: string,
71
+ rootTable: string,
72
+ normalizedSubjectId: string
73
+ }
74
+ ) => `Vault record not found for ${data.appSchema}.${data.rootTable}#${data.normalizedSubjectId}.`,
75
+ category: "validation",
76
+ retryable: false,
77
+ fatal: false
78
+ },
79
+ USER_ID_INVALID: {
80
+ title: "Invalid root identifier",
81
+ detail: () => "subjectId must be a non-empty string or number.",
82
+ category: "validation",
83
+ retryable: false,
84
+ },
85
+ OUTBOX_LEASE_LOST: {
86
+ title: "Outbox lease lost",
87
+ category: "concurrency",
88
+ retryable: true,
89
+ },
90
+ BLOB_URL_INVALID: {
91
+ title: "Invalid S3 URL",
92
+ category: "validation",
93
+ retryable: false,
94
+ },
95
+ BLOB_URL_UNSUPPORTED: {
96
+ title: "Unsupported blob URL protocol",
97
+ category: "validation",
98
+ retryable: false,
99
+ },
100
+ AWS_CREDENTIALS_INVALID: {
101
+ title: "AWS credentials invalid",
102
+ category: "configuration",
103
+ retryable: false,
104
+ fatal: true,
105
+ },
106
+ AWS_CREDENTIALS_UNAVAILABLE: {
107
+ title: "AWS IMDS unavailable",
108
+ category: "configuration",
109
+ retryable: true,
110
+ fatal: false,
111
+ }
112
+ } as const satisfies Record<string, RegistryEntry>;
113
+
114
+ /**
115
+ * Type-safe map for error codes derived from @constant {ERROR_REGISTRY}
116
+ * It standardizes the code without writing it manually every time
117
+ *
118
+ * @example fail({ code: CODE.CONFIG_SIGNATURE_MISSING });
119
+ */
120
+ export const CODE = Object.fromEntries(
121
+ Object.keys(ERROR_REGISTRY).map(k => [k, k])
122
+ ) as { [k in keyof typeof ERROR_REGISTRY]: k }
@@ -0,0 +1,65 @@
1
+ import { type WorkerValidationIssue } from "@/validation/zod";
2
+
3
+ export type WorkerErrorCode = string;
4
+
5
+ export type WorkerErrorCategory =
6
+ | "configuration"
7
+ | "validation"
8
+ | "integrity"
9
+ | "concurrency"
10
+ | "database"
11
+ | "network"
12
+ | "crypto"
13
+ | "runtime"
14
+ | "external"
15
+ | "internal";
16
+
17
+ export interface WorkerErrorContext {
18
+ [key: string]: unknown
19
+ };
20
+
21
+ export interface WorkerProblemDetails {
22
+ type: string;
23
+ title: string;
24
+ detail: string;
25
+ code: WorkerErrorCode;
26
+ category: WorkerErrorCategory;
27
+ retryable: boolean;
28
+ fatal: boolean;
29
+ instance?: string;
30
+ context?: WorkerErrorContext;
31
+ issues?: WorkerValidationIssue[];
32
+ cause?: WorkerProblemDetails;
33
+ }
34
+
35
+ export interface WorkerErrorOptions {
36
+ code: WorkerErrorCode;
37
+ title: string;
38
+ detail: string;
39
+ category: WorkerErrorCategory;
40
+ retryable?: boolean;
41
+ fatal?: boolean;
42
+ context?: WorkerErrorContext | null;
43
+ issues?: WorkerValidationIssue[] | null;
44
+ cause?: unknown;
45
+ type?: string;
46
+ }
47
+
48
+ export interface WorkerErrorFallback {
49
+ code?: WorkerErrorCode;
50
+ title?: string;
51
+ detail?: string;
52
+ category?: WorkerErrorCategory;
53
+ retryable?: boolean;
54
+ fatal?: boolean;
55
+ context?: WorkerErrorContext;
56
+ issues?: WorkerValidationIssue[];
57
+ }
58
+
59
+ export interface RegistryEntry<T = any> {
60
+ title: string;
61
+ detail?: (data: T) => string;
62
+ category: WorkerErrorCategory;
63
+ retryable: boolean;
64
+ fatal?: boolean;
65
+ };