@rehearsal-db/core 0.1.0-beta.9 → 0.1.0-rc.2

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 (247) hide show
  1. package/CHANGELOG.md +319 -1
  2. package/COMPATIBILITY.md +15 -0
  3. package/README.md +73 -16
  4. package/dist/package.json +100 -0
  5. package/dist/scripts/install/create_root_config.d.mts +3 -0
  6. package/dist/scripts/install/create_root_config.mjs +13 -0
  7. package/dist/scripts/runtime/manage_database.d.mts +6 -0
  8. package/{scripts → dist/scripts}/runtime/manage_database.mjs +6 -8
  9. package/dist/src/application/interactive_task.d.mts +30 -0
  10. package/dist/src/application/interactive_task.mjs +83 -0
  11. package/dist/src/application/session.d.mts +97 -0
  12. package/dist/src/application/session.mjs +380 -0
  13. package/dist/src/baseline/artifact.d.mts +119 -0
  14. package/dist/src/baseline/artifact.mjs +522 -0
  15. package/dist/src/baseline/builder.d.mts +19 -0
  16. package/dist/src/baseline/builder.mjs +142 -0
  17. package/dist/src/baseline/input_discovery.d.mts +13 -0
  18. package/dist/src/baseline/input_discovery.mjs +149 -0
  19. package/dist/src/baseline/policy_review.d.mts +56 -0
  20. package/dist/src/baseline/policy_review.mjs +73 -0
  21. package/dist/src/baseline/preparation.d.mts +63 -0
  22. package/dist/src/baseline/preparation.mjs +295 -0
  23. package/dist/src/baseline/privacy_contract.d.mts +147 -0
  24. package/dist/src/baseline/privacy_contract.mjs +2 -0
  25. package/dist/src/baseline/privacy_dependency_graph.d.mts +7 -0
  26. package/dist/src/baseline/privacy_dependency_graph.mjs +110 -0
  27. package/dist/src/baseline/privacy_engine.d.mts +5 -0
  28. package/dist/src/baseline/privacy_engine.mjs +4 -0
  29. package/dist/src/baseline/privacy_key.d.mts +7 -0
  30. package/dist/src/baseline/privacy_key.mjs +42 -0
  31. package/dist/src/baseline/privacy_pipeline.d.mts +7 -0
  32. package/dist/src/baseline/privacy_pipeline.mjs +127 -0
  33. package/dist/src/baseline/privacy_policy.d.mts +6 -0
  34. package/dist/src/baseline/privacy_policy.mjs +237 -0
  35. package/dist/src/baseline/privacy_recipe_validation.d.mts +5 -0
  36. package/dist/src/baseline/privacy_recipe_validation.mjs +289 -0
  37. package/dist/src/baseline/privacy_rule_registry.d.mts +18 -0
  38. package/dist/src/baseline/privacy_rule_registry.mjs +143 -0
  39. package/dist/src/baseline/privacy_structured_json.d.mts +66 -0
  40. package/dist/src/baseline/privacy_structured_json.mjs +464 -0
  41. package/dist/src/baseline/privacy_transforms.d.mts +18 -0
  42. package/dist/src/baseline/privacy_transforms.mjs +142 -0
  43. package/dist/src/baseline/privacy_validation_shared.d.mts +31 -0
  44. package/dist/src/baseline/privacy_validation_shared.mjs +127 -0
  45. package/dist/src/baseline/sanitization_policy.d.mts +69 -0
  46. package/dist/src/baseline/sanitization_policy.mjs +227 -0
  47. package/dist/src/baseline/schema_snapshot.d.mts +19 -0
  48. package/dist/src/baseline/schema_snapshot.mjs +79 -0
  49. package/dist/src/cli/application_command_handlers.d.mts +8 -0
  50. package/dist/src/cli/application_command_handlers.mjs +52 -0
  51. package/dist/src/cli/arguments.d.mts +38 -0
  52. package/dist/src/cli/arguments.mjs +130 -0
  53. package/dist/src/cli/basic_command_handlers.d.mts +4 -0
  54. package/dist/src/cli/basic_command_handlers.mjs +57 -0
  55. package/dist/src/cli/cli_commands.d.mts +14 -0
  56. package/dist/src/cli/cli_commands.mjs +34 -0
  57. package/dist/src/cli/command_contract.d.mts +57 -0
  58. package/dist/src/cli/command_contract.mjs +39 -0
  59. package/dist/src/cli/command_output.d.mts +12 -0
  60. package/dist/src/cli/command_output.mjs +20 -0
  61. package/dist/src/cli/command_registry.d.mts +7 -0
  62. package/dist/src/cli/command_registry.mjs +14 -0
  63. package/dist/src/cli/guided.d.mts +53 -0
  64. package/dist/src/cli/guided.mjs +727 -0
  65. package/dist/src/cli/identity_command_handlers.d.mts +12 -0
  66. package/dist/src/cli/identity_command_handlers.mjs +239 -0
  67. package/dist/src/cli/identity_renderer.d.mts +8 -0
  68. package/dist/src/cli/identity_renderer.mjs +33 -0
  69. package/dist/src/cli/init_command.d.mts +21 -0
  70. package/dist/src/cli/init_command.mjs +90 -0
  71. package/dist/src/cli/inspection_command_handlers.d.mts +3 -0
  72. package/dist/src/cli/inspection_command_handlers.mjs +79 -0
  73. package/dist/src/cli/project_command_handlers.d.mts +11 -0
  74. package/dist/src/cli/project_command_handlers.mjs +92 -0
  75. package/dist/src/cli/project_commands.d.mts +283 -0
  76. package/dist/src/cli/project_commands.mjs +192 -0
  77. package/dist/src/cli/rehearsal.d.mts +7 -0
  78. package/dist/src/cli/rehearsal.mjs +190 -0
  79. package/dist/src/cli/renderers.d.mts +299 -0
  80. package/dist/src/cli/renderers.mjs +554 -0
  81. package/dist/src/cli/runtime_action_handlers.d.mts +12 -0
  82. package/dist/src/cli/runtime_action_handlers.mjs +201 -0
  83. package/dist/src/cli/runtime_commands.d.mts +294 -0
  84. package/dist/src/cli/runtime_commands.mjs +619 -0
  85. package/dist/src/cli/source_command_handlers.d.mts +8 -0
  86. package/dist/src/cli/source_command_handlers.mjs +62 -0
  87. package/dist/src/cli/source_commands.d.mts +436 -0
  88. package/dist/src/cli/source_commands.mjs +301 -0
  89. package/dist/src/cli/terminal.d.mts +59 -0
  90. package/dist/src/cli/terminal.mjs +230 -0
  91. package/dist/src/identity/claim.d.mts +5 -0
  92. package/dist/src/identity/claim.mjs +4 -0
  93. package/dist/src/identity/claim_contract.d.mts +135 -0
  94. package/dist/src/identity/claim_contract.mjs +6 -0
  95. package/dist/src/identity/claim_executor.d.mts +10 -0
  96. package/dist/src/identity/claim_executor.mjs +358 -0
  97. package/dist/src/identity/claim_identity_lookup.d.mts +23 -0
  98. package/dist/src/identity/claim_identity_lookup.mjs +134 -0
  99. package/dist/src/identity/claim_plan.d.mts +8 -0
  100. package/dist/src/identity/claim_plan.mjs +61 -0
  101. package/dist/src/identity/claim_policy.d.mts +3 -0
  102. package/dist/src/identity/claim_policy.mjs +377 -0
  103. package/dist/src/identity/claim_sql.d.mts +10 -0
  104. package/dist/src/identity/claim_sql.mjs +46 -0
  105. package/dist/src/identity/claim_verification.d.mts +13 -0
  106. package/dist/src/identity/claim_verification.mjs +35 -0
  107. package/dist/src/identity/connect.d.mts +30 -0
  108. package/dist/src/identity/connect.mjs +111 -0
  109. package/dist/src/identity/provider_configuration.d.mts +37 -0
  110. package/dist/src/identity/provider_configuration.mjs +132 -0
  111. package/dist/src/identity/storage.d.mts +21 -0
  112. package/dist/src/identity/storage.mjs +122 -0
  113. package/dist/src/index.d.mts +4 -0
  114. package/dist/src/index.mjs +3 -0
  115. package/dist/src/project/configuration.d.mts +13 -0
  116. package/dist/src/project/configuration.mjs +9 -0
  117. package/dist/src/project/configuration_contract.d.mts +125 -0
  118. package/dist/src/project/configuration_contract.mjs +27 -0
  119. package/dist/src/project/configuration_discovery.d.mts +7 -0
  120. package/dist/src/project/configuration_discovery.mjs +37 -0
  121. package/dist/src/project/configuration_normalization.d.mts +107 -0
  122. package/dist/src/project/configuration_normalization.mjs +302 -0
  123. package/dist/src/project/configuration_rendering.d.mts +14 -0
  124. package/dist/src/project/configuration_rendering.mjs +228 -0
  125. package/dist/src/project/configuration_resolution.d.mts +125 -0
  126. package/dist/src/project/configuration_resolution.mjs +81 -0
  127. package/dist/src/project/configuration_schema.d.mts +22 -0
  128. package/dist/src/project/configuration_schema.mjs +160 -0
  129. package/dist/src/project/configuration_validation.d.mts +22 -0
  130. package/dist/src/project/configuration_validation.mjs +136 -0
  131. package/dist/src/project/install_scaffold.d.mts +42 -0
  132. package/dist/src/project/install_scaffold.mjs +263 -0
  133. package/dist/src/project/project_detection.d.mts +14 -0
  134. package/dist/src/project/project_detection.mjs +47 -0
  135. package/dist/src/project/setup.d.mts +87 -0
  136. package/dist/src/project/setup.mjs +414 -0
  137. package/dist/src/project/support_report.d.mts +88 -0
  138. package/dist/src/project/support_report.mjs +83 -0
  139. package/dist/src/runtime/cleanup.d.mts +85 -0
  140. package/dist/src/runtime/cleanup.mjs +311 -0
  141. package/dist/src/runtime/lifecycle_engine.d.mts +63 -0
  142. package/dist/src/runtime/lifecycle_engine.mjs +114 -0
  143. package/dist/src/runtime/migration_history.d.mts +51 -0
  144. package/dist/src/runtime/migration_history.mjs +170 -0
  145. package/dist/src/runtime/plan.d.mts +134 -0
  146. package/dist/src/runtime/plan.mjs +640 -0
  147. package/dist/src/runtime/policy.d.mts +66 -0
  148. package/dist/src/runtime/policy.mjs +369 -0
  149. package/dist/src/runtime/restore.d.mts +22 -0
  150. package/dist/src/runtime/restore.mjs +276 -0
  151. package/dist/src/runtime/service_environment.d.mts +11 -0
  152. package/dist/src/runtime/service_environment.mjs +58 -0
  153. package/dist/src/runtime/topology.d.mts +51 -0
  154. package/dist/src/runtime/topology.mjs +128 -0
  155. package/dist/src/shared/diagnostics.d.mts +97 -0
  156. package/dist/src/shared/diagnostics.mjs +170 -0
  157. package/dist/src/shared/human_output.d.mts +2 -0
  158. package/{src → dist/src}/shared/human_output.mjs +1 -3
  159. package/dist/src/shared/node_runtime.d.mts +14 -0
  160. package/dist/src/shared/node_runtime.mjs +25 -0
  161. package/dist/src/shared/operation_guard.d.mts +22 -0
  162. package/dist/src/shared/operation_guard.mjs +150 -0
  163. package/dist/src/shared/process_environment.d.mts +15 -0
  164. package/dist/src/shared/process_environment.mjs +50 -0
  165. package/dist/src/source/access.d.mts +165 -0
  166. package/dist/src/source/access.mjs +602 -0
  167. package/dist/src/source/asset_transfer.d.mts +27 -0
  168. package/dist/src/source/asset_transfer.mjs +202 -0
  169. package/dist/src/source/baseline.d.mts +66 -0
  170. package/dist/src/source/baseline.mjs +372 -0
  171. package/dist/src/source/external_reader.d.mts +42 -0
  172. package/dist/src/source/external_reader.mjs +278 -0
  173. package/dist/src/source/postgresql_access.d.mts +86 -0
  174. package/dist/src/source/postgresql_access.mjs +454 -0
  175. package/dist/src/source/privacy_paths.d.mts +10 -0
  176. package/dist/src/source/privacy_paths.mjs +27 -0
  177. package/dist/src/targets/driver.d.mts +2 -0
  178. package/dist/src/targets/driver.mjs +13 -0
  179. package/dist/src/targets/postgresql.d.mts +5 -0
  180. package/dist/src/targets/postgresql.mjs +620 -0
  181. package/dist/src/targets/supabase.d.mts +8 -0
  182. package/dist/src/targets/supabase.mjs +629 -0
  183. package/dist/src/targets/supabase_environment.d.mts +54 -0
  184. package/dist/src/targets/supabase_environment.mjs +184 -0
  185. package/dist/src/targets/target.d.mts +33 -0
  186. package/dist/src/targets/target.mjs +39 -0
  187. package/docs/README.md +35 -16
  188. package/docs/architecture.md +135 -10
  189. package/docs/commands.md +53 -15
  190. package/docs/configuration.md +186 -31
  191. package/docs/getting-started.md +66 -9
  192. package/docs/index.md +67 -0
  193. package/docs/production-source.md +175 -17
  194. package/docs/releasing.md +24 -12
  195. package/docs/roadmap.md +3 -2
  196. package/docs/runtime-lifecycle.md +99 -0
  197. package/docs/runtime-policies.md +80 -7
  198. package/docs/sanitization.md +572 -4
  199. package/docs/security-model.md +8 -0
  200. package/docs/stability.md +70 -0
  201. package/docs/standalone-workflow.md +36 -13
  202. package/docs/troubleshooting.md +82 -10
  203. package/docs/tutorial.md +7 -1
  204. package/docs/typescript.md +134 -0
  205. package/package.json +44 -29
  206. package/types/configuration.d.mts +5 -0
  207. package/src/README.md +0 -17
  208. package/src/application/session.mjs +0 -313
  209. package/src/baseline/artifact.mjs +0 -661
  210. package/src/baseline/builder.mjs +0 -164
  211. package/src/baseline/input_discovery.mjs +0 -155
  212. package/src/baseline/policy_review.mjs +0 -92
  213. package/src/baseline/preparation.mjs +0 -323
  214. package/src/baseline/privacy_engine.mjs +0 -413
  215. package/src/baseline/sanitization_policy.mjs +0 -299
  216. package/src/baseline/schema_snapshot.mjs +0 -113
  217. package/src/cli/arguments.mjs +0 -120
  218. package/src/cli/guided.mjs +0 -807
  219. package/src/cli/rehearsal.mjs +0 -933
  220. package/src/cli/renderers.mjs +0 -584
  221. package/src/cli/runtime_commands.mjs +0 -553
  222. package/src/cli/source_commands.mjs +0 -326
  223. package/src/cli/terminal.mjs +0 -275
  224. package/src/identity/claim.mjs +0 -975
  225. package/src/identity/storage.mjs +0 -165
  226. package/src/project/configuration.d.mts +0 -153
  227. package/src/project/configuration.mjs +0 -1197
  228. package/src/project/setup.mjs +0 -379
  229. package/src/project/support_report.mjs +0 -105
  230. package/src/runtime/cleanup.mjs +0 -381
  231. package/src/runtime/migration_history.mjs +0 -220
  232. package/src/runtime/plan.mjs +0 -685
  233. package/src/runtime/policy.mjs +0 -438
  234. package/src/runtime/restore.mjs +0 -329
  235. package/src/runtime/service_environment.mjs +0 -82
  236. package/src/runtime/topology.mjs +0 -177
  237. package/src/shared/diagnostics.mjs +0 -199
  238. package/src/shared/operation_guard.mjs +0 -162
  239. package/src/shared/process_environment.mjs +0 -68
  240. package/src/source/access.mjs +0 -578
  241. package/src/source/asset_transfer.mjs +0 -177
  242. package/src/source/baseline.mjs +0 -446
  243. package/src/source/postgresql_access.mjs +0 -480
  244. package/src/targets/postgresql.mjs +0 -808
  245. package/src/targets/supabase.mjs +0 -830
  246. package/src/targets/supabase_environment.mjs +0 -229
  247. package/src/targets/target.mjs +0 -66
@@ -0,0 +1,8 @@
1
+ /** Deterministic identity claim review, digest, and local matcher binding. */
2
+ import type { IdentityClaimPlan } from "./claim_contract.mjs";
3
+ export declare const createIdentityClaimPlan: ({ policy, name, environment, }: {
4
+ policy: unknown;
5
+ name: string;
6
+ environment?: Readonly<Record<string, string | undefined>>;
7
+ }) => IdentityClaimPlan;
8
+ export declare const getIdentityPlanMatchValue: (plan: IdentityClaimPlan) => string | undefined;
@@ -0,0 +1,61 @@
1
+ /** Deterministic identity claim review, digest, and local matcher binding. */
2
+ import { createHash } from "node:crypto";
3
+ import { validateIdentityPolicy } from "./claim_policy.mjs";
4
+ const canonical = (value) => `${JSON.stringify(value, null, "\t")}\n`;
5
+ const hash = (value) => createHash("sha256").update(value).digest("hex");
6
+ const identityPlanValues = new WeakMap();
7
+ export const createIdentityClaimPlan = ({ policy, name, environment = process.env, }) => {
8
+ const normalized = validateIdentityPolicy(policy);
9
+ const identity = normalized.identities.find((entry) => entry.name === name);
10
+ if (!identity)
11
+ throw new Error(`Identity policy does not declare ${name}.`);
12
+ const matcher = identity.matcher;
13
+ const environmentVariable = matcher.type === "verified-email"
14
+ ? matcher.emailEnvironmentVariable
15
+ : matcher.subjectEnvironmentVariable;
16
+ const rawValue = environment[environmentVariable];
17
+ const matchValue = matcher.type === "verified-email"
18
+ ? rawValue?.trim().toLowerCase()
19
+ : rawValue?.trim();
20
+ const approvedSha256 = matcher.type === "verified-email"
21
+ ? matcher.approvedEmailSha256
22
+ : matcher.approvedSubjectSha256;
23
+ if (!matchValue || hash(matchValue) !== approvedSha256) {
24
+ throw new Error(matcher.type === "verified-email"
25
+ ? "Local identity email does not match the reviewed receipt."
26
+ : "Local identity provider subject does not match the reviewed receipt.");
27
+ }
28
+ const review = {
29
+ planVersion: 3,
30
+ operation: "claim-local-identity",
31
+ name: identity.name,
32
+ matcher,
33
+ placeholderUserId: identity.placeholderUserId,
34
+ references: identity.references,
35
+ jsonReferences: identity.jsonReferences,
36
+ signupDefaults: identity.signupDefaults.map((declaration) => ({
37
+ table: declaration.table,
38
+ identityColumn: declaration.identityColumn,
39
+ ignoredColumns: declaration.ignoredColumns,
40
+ valuesSha256: hash(canonical(declaration.values)),
41
+ })),
42
+ pathReferences: identity.pathReferences,
43
+ assets: identity.assets,
44
+ claimKeys: Object.keys(identity.claims).sort(),
45
+ claimsSha256: hash(canonical(identity.claims)),
46
+ tokenHook: identity.tokenHook
47
+ ? {
48
+ function: identity.tokenHook.function,
49
+ expectedClaimsSha256: hash(canonical(identity.tokenHook.expectedClaims)),
50
+ }
51
+ : null,
52
+ };
53
+ const plan = {
54
+ identity,
55
+ review: Object.freeze(review),
56
+ digest: hash(canonical(review)),
57
+ };
58
+ identityPlanValues.set(plan, matchValue);
59
+ return Object.freeze(plan);
60
+ };
61
+ export const getIdentityPlanMatchValue = (plan) => identityPlanValues.get(plan);
@@ -0,0 +1,3 @@
1
+ /** Validate and normalize the reviewed local identity policy. */
2
+ import { type IdentityPolicy } from "./claim_contract.mjs";
3
+ export declare const validateIdentityPolicy: (policy: unknown) => IdentityPolicy;
@@ -0,0 +1,377 @@
1
+ /** Validate and normalize the reviewed local identity policy. */
2
+ import { SUPPORTED_IDENTITY_PROVIDERS, } from "./claim_contract.mjs";
3
+ const IDENTIFIER = /^[a-z][a-z0-9_]{0,62}$/u;
4
+ const LABEL = /^[a-z0-9][a-z0-9-]{1,62}$/u;
5
+ const UUID = /^[a-f0-9]{8}-[a-f0-9]{4}-[1-5][a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$/iu;
6
+ const SHA256 = /^[a-f0-9]{64}$/u;
7
+ const ENVIRONMENT_KEY = /^[A-Z][A-Z0-9_]*$/u;
8
+ const isObject = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
9
+ const id = (value, label) => {
10
+ if (typeof value !== "string" || !IDENTIFIER.test(value))
11
+ throw new Error(`${label} is not a safe identifier.`);
12
+ return value;
13
+ };
14
+ const keys = (value, allowed, label) => {
15
+ for (const key of Object.keys(value)) {
16
+ if (!allowed.includes(key))
17
+ throw new Error(`${label}.${key} is unknown.`);
18
+ }
19
+ };
20
+ const optionalArray = (value, label) => {
21
+ if (value === undefined)
22
+ return [];
23
+ if (!Array.isArray(value))
24
+ throw new Error(`${label} must be an array.`);
25
+ return value;
26
+ };
27
+ const relation = (value, label) => {
28
+ if (!isObject(value))
29
+ throw new Error(`${label} must be an object.`);
30
+ keys(value, ["schema", "table"], label);
31
+ return Object.freeze({
32
+ schema: id(value.schema, `${label}.schema`),
33
+ table: id(value.table, `${label}.table`),
34
+ });
35
+ };
36
+ const jsonValue = (value, label) => {
37
+ let serialized;
38
+ try {
39
+ serialized = JSON.stringify(value);
40
+ }
41
+ catch {
42
+ throw new Error(`${label} must be JSON-compatible.`);
43
+ }
44
+ if (serialized === undefined) {
45
+ throw new Error(`${label} must be JSON-compatible.`);
46
+ }
47
+ return JSON.parse(serialized);
48
+ };
49
+ const defaultMatcher = (value, label) => {
50
+ if (isObject(value) && value.kind === "pattern") {
51
+ keys(value, ["kind", "pattern"], label);
52
+ if (typeof value.pattern !== "string" ||
53
+ value.pattern.length > 256 ||
54
+ !value.pattern.startsWith("^") ||
55
+ !value.pattern.endsWith("$")) {
56
+ throw new Error(`${label}.pattern must be anchored and at most 256 characters.`);
57
+ }
58
+ try {
59
+ new RegExp(value.pattern, "u");
60
+ }
61
+ catch {
62
+ throw new Error(`${label}.pattern is invalid.`);
63
+ }
64
+ return Object.freeze({ kind: "pattern", pattern: value.pattern });
65
+ }
66
+ if (isObject(value) && value.kind === "exact") {
67
+ keys(value, ["kind", "value"], label);
68
+ return Object.freeze({
69
+ kind: "exact",
70
+ value: jsonValue(value.value, `${label}.value`),
71
+ });
72
+ }
73
+ return Object.freeze({ kind: "exact", value: jsonValue(value, label) });
74
+ };
75
+ function provider(value, label, { subject = false } = {}) {
76
+ if (typeof value !== "string" ||
77
+ !SUPPORTED_IDENTITY_PROVIDERS.includes(value)) {
78
+ throw new Error(`${label} is unsupported.`);
79
+ }
80
+ if (subject && value === "email") {
81
+ throw new Error(`${label} cannot use email with a provider-subject matcher.`);
82
+ }
83
+ return value;
84
+ }
85
+ const normalizeIdentityMatcher = (entry, label) => {
86
+ const legacyKeys = [
87
+ "provider",
88
+ "emailEnvironmentVariable",
89
+ "approvedEmailSha256",
90
+ ];
91
+ const hasLegacyMatcher = legacyKeys.some((key) => entry[key] !== undefined);
92
+ if (entry.matcher !== undefined && hasLegacyMatcher) {
93
+ throw new Error(`${label} cannot combine matcher with legacy provider and email fields.`);
94
+ }
95
+ if (entry.matcher === undefined) {
96
+ const legacyProvider = provider(entry.provider, `${label}.provider`);
97
+ if (typeof entry.emailEnvironmentVariable !== "string" ||
98
+ !ENVIRONMENT_KEY.test(entry.emailEnvironmentVariable)) {
99
+ throw new Error(`${label}.emailEnvironmentVariable is invalid.`);
100
+ }
101
+ if (typeof entry.approvedEmailSha256 !== "string" ||
102
+ !SHA256.test(entry.approvedEmailSha256)) {
103
+ throw new Error(`${label}.approvedEmailSha256 must be a SHA-256.`);
104
+ }
105
+ return Object.freeze({
106
+ type: "verified-email",
107
+ providers: Object.freeze([legacyProvider]),
108
+ emailEnvironmentVariable: entry.emailEnvironmentVariable,
109
+ approvedEmailSha256: entry.approvedEmailSha256,
110
+ });
111
+ }
112
+ if (!isObject(entry.matcher)) {
113
+ throw new Error(`${label}.matcher must be an object.`);
114
+ }
115
+ const matcherLabel = `${label}.matcher`;
116
+ if (entry.matcher.type === "verified-email") {
117
+ keys(entry.matcher, ["type", "providers", "emailEnvironmentVariable", "approvedEmailSha256"], matcherLabel);
118
+ if (!Array.isArray(entry.matcher.providers) ||
119
+ entry.matcher.providers.length === 0) {
120
+ throw new Error(`${matcherLabel}.providers must not be empty.`);
121
+ }
122
+ const providers = entry.matcher.providers.map((value, index) => provider(value, `${matcherLabel}.providers[${index}]`));
123
+ if (new Set(providers).size !== providers.length) {
124
+ throw new Error(`${matcherLabel}.providers must not contain duplicates.`);
125
+ }
126
+ if (typeof entry.matcher.emailEnvironmentVariable !== "string" ||
127
+ !ENVIRONMENT_KEY.test(entry.matcher.emailEnvironmentVariable)) {
128
+ throw new Error(`${matcherLabel}.emailEnvironmentVariable is invalid.`);
129
+ }
130
+ if (typeof entry.matcher.approvedEmailSha256 !== "string" ||
131
+ !SHA256.test(entry.matcher.approvedEmailSha256)) {
132
+ throw new Error(`${matcherLabel}.approvedEmailSha256 must be a SHA-256.`);
133
+ }
134
+ return Object.freeze({
135
+ type: "verified-email",
136
+ providers: Object.freeze(providers),
137
+ emailEnvironmentVariable: entry.matcher.emailEnvironmentVariable,
138
+ approvedEmailSha256: entry.matcher.approvedEmailSha256,
139
+ });
140
+ }
141
+ if (entry.matcher.type === "provider-subject") {
142
+ keys(entry.matcher, [
143
+ "type",
144
+ "provider",
145
+ "subjectEnvironmentVariable",
146
+ "approvedSubjectSha256",
147
+ ], matcherLabel);
148
+ const subjectProvider = provider(entry.matcher.provider, `${matcherLabel}.provider`, { subject: true });
149
+ if (typeof entry.matcher.subjectEnvironmentVariable !== "string" ||
150
+ !ENVIRONMENT_KEY.test(entry.matcher.subjectEnvironmentVariable)) {
151
+ throw new Error(`${matcherLabel}.subjectEnvironmentVariable is invalid.`);
152
+ }
153
+ if (typeof entry.matcher.approvedSubjectSha256 !== "string" ||
154
+ !SHA256.test(entry.matcher.approvedSubjectSha256)) {
155
+ throw new Error(`${matcherLabel}.approvedSubjectSha256 must be a SHA-256.`);
156
+ }
157
+ return Object.freeze({
158
+ type: "provider-subject",
159
+ provider: subjectProvider,
160
+ subjectEnvironmentVariable: entry.matcher.subjectEnvironmentVariable,
161
+ approvedSubjectSha256: entry.matcher.approvedSubjectSha256,
162
+ });
163
+ }
164
+ throw new Error(`${matcherLabel}.type is unsupported.`);
165
+ };
166
+ export const validateIdentityPolicy = (policy) => {
167
+ if (!isObject(policy) ||
168
+ policy.identityVersion !== 1 ||
169
+ !Array.isArray(policy.identities)) {
170
+ throw new Error("Identity policy must use identityVersion 1 and list identities.");
171
+ }
172
+ for (const key of Object.keys(policy)) {
173
+ if (!["identityVersion", "identities"].includes(key)) {
174
+ throw new Error(`identityPolicy.${key} is unknown.`);
175
+ }
176
+ }
177
+ const names = new Set();
178
+ const identities = policy.identities.map((entry, index) => {
179
+ const label = `identityPolicy.identities[${index}]`;
180
+ if (!isObject(entry))
181
+ throw new Error(`${label} must be an object.`);
182
+ for (const key of Object.keys(entry)) {
183
+ if (![
184
+ "name",
185
+ "matcher",
186
+ "provider",
187
+ "emailEnvironmentVariable",
188
+ "approvedEmailSha256",
189
+ "placeholderUserId",
190
+ "references",
191
+ "jsonReferences",
192
+ "signupDefaults",
193
+ "pathReferences",
194
+ "assets",
195
+ "claims",
196
+ "tokenHook",
197
+ ].includes(key)) {
198
+ throw new Error(`${label}.${key} is unknown.`);
199
+ }
200
+ }
201
+ if (typeof entry.name !== "string" || !LABEL.test(entry.name))
202
+ throw new Error(`${label}.name is invalid.`);
203
+ const name = entry.name;
204
+ if (names.has(name))
205
+ throw new Error(`Identity policy duplicates ${name}.`);
206
+ names.add(name);
207
+ const matcher = normalizeIdentityMatcher(entry, label);
208
+ if (typeof entry.placeholderUserId !== "string" ||
209
+ !UUID.test(entry.placeholderUserId))
210
+ throw new Error(`${label}.placeholderUserId is invalid.`);
211
+ const references = optionalArray(entry.references, `${label}.references`).map((reference, referenceIndex) => {
212
+ const referenceLabel = `${label}.references[${referenceIndex}]`;
213
+ if (!isObject(reference))
214
+ throw new Error(`${referenceLabel} must be an object.`);
215
+ keys(reference, ["schema", "table", "column", "required", "strategy"], referenceLabel);
216
+ if (reference.required !== undefined &&
217
+ typeof reference.required !== "boolean") {
218
+ throw new Error(`${referenceLabel}.required must be boolean.`);
219
+ }
220
+ if (reference.strategy !== undefined &&
221
+ !["transfer", "preserve-audit"].includes(reference.strategy)) {
222
+ throw new Error(`${referenceLabel}.strategy must be transfer or preserve-audit.`);
223
+ }
224
+ return Object.freeze({
225
+ schema: id(reference.schema, `${referenceLabel}.schema`),
226
+ table: id(reference.table, `${referenceLabel}.table`),
227
+ column: id(reference.column, `${referenceLabel}.column`),
228
+ required: reference.required === true,
229
+ strategy: (reference.strategy ?? "transfer"),
230
+ });
231
+ });
232
+ const jsonReferences = optionalArray(entry.jsonReferences, `${label}.jsonReferences`).map((reference, referenceIndex) => {
233
+ const referenceLabel = `${label}.jsonReferences[${referenceIndex}]`;
234
+ if (!isObject(reference))
235
+ throw new Error(`${referenceLabel} must be an object.`);
236
+ keys(reference, ["schema", "table", "column", "path"], referenceLabel);
237
+ if (!Array.isArray(reference.path) || reference.path.length === 0) {
238
+ throw new Error(`${referenceLabel}.path is invalid.`);
239
+ }
240
+ return {
241
+ schema: id(reference.schema, `${referenceLabel}.schema`),
242
+ table: id(reference.table, `${referenceLabel}.table`),
243
+ column: id(reference.column, `${referenceLabel}.column`),
244
+ path: reference.path.map((part, pathIndex) => id(part, `${referenceLabel}.path[${pathIndex}]`)),
245
+ };
246
+ });
247
+ const signupDefaults = optionalArray(entry.signupDefaults, `${label}.signupDefaults`).map((declaration, declarationIndex) => {
248
+ const declarationLabel = `${label}.signupDefaults[${declarationIndex}]`;
249
+ if (!isObject(declaration))
250
+ throw new Error(`${declarationLabel} must be an object.`);
251
+ keys(declaration, ["table", "identityColumn", "ignoredColumns", "values"], declarationLabel);
252
+ const table = relation(declaration.table, `${declarationLabel}.table`);
253
+ const identityColumn = id(declaration.identityColumn, `${declarationLabel}.identityColumn`);
254
+ if (!isObject(declaration.values)) {
255
+ throw new Error(`${declarationLabel}.values must be an object.`);
256
+ }
257
+ const values = Object.fromEntries(Object.entries(declaration.values).map(([column, value]) => [
258
+ id(column, `${declarationLabel}.values key`),
259
+ defaultMatcher(value, `${declarationLabel}.values.${column}`),
260
+ ]));
261
+ if (Object.keys(values).length === 0) {
262
+ throw new Error(`${declarationLabel}.values must not be empty.`);
263
+ }
264
+ const ignoredColumns = optionalArray(declaration.ignoredColumns, `${declarationLabel}.ignoredColumns`).map((column, columnIndex) => id(column, `${declarationLabel}.ignoredColumns[${columnIndex}]`));
265
+ if (new Set([identityColumn, ...ignoredColumns, ...Object.keys(values)])
266
+ .size !==
267
+ 1 + ignoredColumns.length + Object.keys(values).length) {
268
+ throw new Error(`${declarationLabel} repeats a column.`);
269
+ }
270
+ if (!references.some((reference) => reference.schema === table.schema &&
271
+ reference.table === table.table &&
272
+ reference.column === identityColumn)) {
273
+ throw new Error(`${declarationLabel} must match a declared relational reference.`);
274
+ }
275
+ return Object.freeze({
276
+ table,
277
+ identityColumn,
278
+ ignoredColumns: Object.freeze(ignoredColumns),
279
+ values: Object.freeze(values),
280
+ });
281
+ });
282
+ const pathReferences = optionalArray(entry.pathReferences, `${label}.pathReferences`).map((reference, referenceIndex) => {
283
+ const referenceLabel = `${label}.pathReferences[${referenceIndex}]`;
284
+ if (!isObject(reference))
285
+ throw new Error(`${referenceLabel} must be an object.`);
286
+ keys(reference, ["schema", "table", "column", "valueType"], referenceLabel);
287
+ if (typeof reference.valueType !== "string" ||
288
+ !["text", "jsonb"].includes(reference.valueType)) {
289
+ throw new Error(`${referenceLabel}.valueType is unsupported.`);
290
+ }
291
+ return Object.freeze({
292
+ schema: id(reference.schema, `${referenceLabel}.schema`),
293
+ table: id(reference.table, `${referenceLabel}.table`),
294
+ column: id(reference.column, `${referenceLabel}.column`),
295
+ valueType: reference.valueType,
296
+ });
297
+ });
298
+ const assets = optionalArray(entry.assets, `${label}.assets`).map((asset, assetIndex) => {
299
+ const assetLabel = `${label}.assets[${assetIndex}]`;
300
+ if (!isObject(asset))
301
+ throw new Error(`${assetLabel} must be an object.`);
302
+ keys(asset, ["bucket", "prefix", "rewritePath"], assetLabel);
303
+ if (typeof asset.bucket !== "string" ||
304
+ !/^[a-z0-9][a-z0-9.-]{0,99}$/u.test(asset.bucket)) {
305
+ throw new Error(`${assetLabel}.bucket is invalid.`);
306
+ }
307
+ if (typeof asset.prefix !== "string" ||
308
+ !asset.prefix ||
309
+ asset.prefix.startsWith("/") ||
310
+ asset.prefix
311
+ .replace(/\/$/u, "")
312
+ .split("/")
313
+ .some((part) => !part || part === "." || part === ".." || /[%_]/u.test(part))) {
314
+ throw new Error(`${assetLabel}.prefix is invalid.`);
315
+ }
316
+ if (asset.rewritePath !== undefined &&
317
+ typeof asset.rewritePath !== "boolean") {
318
+ throw new Error(`${assetLabel}.rewritePath must be boolean.`);
319
+ }
320
+ return Object.freeze({
321
+ bucket: asset.bucket,
322
+ prefix: asset.prefix,
323
+ rewritePath: asset.rewritePath === true,
324
+ });
325
+ });
326
+ for (const asset of assets) {
327
+ if (asset.rewritePath &&
328
+ asset.prefix !== `${entry.placeholderUserId.toLowerCase()}/`) {
329
+ throw new Error(`${label}.assets rewritePath requires the placeholder UUID prefix.`);
330
+ }
331
+ }
332
+ const rawClaims = entry.claims ?? {};
333
+ if (!isObject(rawClaims))
334
+ throw new Error(`${label}.claims must be an object.`);
335
+ const claims = jsonValue(rawClaims, `${label}.claims`);
336
+ if (!isObject(claims))
337
+ throw new Error(`${label}.claims must be an object.`);
338
+ let tokenHook = null;
339
+ if (entry.tokenHook != null) {
340
+ const hookLabel = `${label}.tokenHook`;
341
+ if (!isObject(entry.tokenHook))
342
+ throw new Error(`${hookLabel} must be an object.`);
343
+ keys(entry.tokenHook, ["function", "expectedClaims"], hookLabel);
344
+ if (!isObject(entry.tokenHook.function))
345
+ throw new Error(`${hookLabel}.function must be an object.`);
346
+ keys(entry.tokenHook.function, ["schema", "name"], `${hookLabel}.function`);
347
+ if (!isObject(entry.tokenHook.expectedClaims))
348
+ throw new Error(`${hookLabel}.expectedClaims must be an object.`);
349
+ const expectedClaims = jsonValue(entry.tokenHook.expectedClaims, `${hookLabel}.expectedClaims`);
350
+ if (!isObject(expectedClaims))
351
+ throw new Error(`${hookLabel}.expectedClaims must be an object.`);
352
+ tokenHook = Object.freeze({
353
+ function: Object.freeze({
354
+ schema: id(entry.tokenHook.function.schema, `${hookLabel}.function.schema`),
355
+ name: id(entry.tokenHook.function.name, `${hookLabel}.function.name`),
356
+ }),
357
+ expectedClaims: Object.freeze(expectedClaims),
358
+ });
359
+ }
360
+ return Object.freeze({
361
+ name,
362
+ matcher,
363
+ placeholderUserId: entry.placeholderUserId.toLowerCase(),
364
+ references: Object.freeze(references),
365
+ jsonReferences: Object.freeze(jsonReferences),
366
+ signupDefaults: Object.freeze(signupDefaults),
367
+ pathReferences: Object.freeze(pathReferences),
368
+ assets: Object.freeze(assets),
369
+ claims: Object.freeze(claims),
370
+ tokenHook,
371
+ });
372
+ });
373
+ return Object.freeze({
374
+ identityVersion: 1,
375
+ identities: Object.freeze(identities),
376
+ });
377
+ };
@@ -0,0 +1,10 @@
1
+ /** Pure SQL planning for reviewed identity references. No function executes SQL. */
2
+ import type { IdentityReference, JsonIdentityReference, PathIdentityReference, SignupDefaultDeclaration } from "./claim_contract.mjs";
3
+ export declare const quoteIdentityIdentifier: (value: string) => string;
4
+ export declare const planSignupDefaultInspection: (declaration: SignupDefaultDeclaration) => string;
5
+ export declare const planSignupDefaultRemoval: (declaration: SignupDefaultDeclaration) => string;
6
+ export declare const planRelationalReferenceTransfer: (reference: IdentityReference) => string;
7
+ export declare const planRequiredReferenceCheck: (reference: IdentityReference) => string;
8
+ export declare const planJsonReferenceTransfer: (reference: JsonIdentityReference) => string;
9
+ export declare const planPathReferenceCount: (reference: PathIdentityReference) => string;
10
+ export declare const planPathReferenceTransfer: (reference: PathIdentityReference) => string;
@@ -0,0 +1,46 @@
1
+ /** Pure SQL planning for reviewed identity references. No function executes SQL. */
2
+ const IDENTIFIER = /^[a-z][a-z0-9_]{0,62}$/u;
3
+ export const quoteIdentityIdentifier = (value) => {
4
+ if (!IDENTIFIER.test(value)) {
5
+ throw new Error("Identity SQL identifier is not safe.");
6
+ }
7
+ return `"${value}"`;
8
+ };
9
+ const target = ({ schema, table }) => `${quoteIdentityIdentifier(schema)}.${quoteIdentityIdentifier(table)}`;
10
+ export const planSignupDefaultInspection = (declaration) => `select candidate.${quoteIdentityIdentifier(declaration.identityColumn)}::text as identity,
11
+ to_jsonb(candidate) as row
12
+ from ${target(declaration.table)} candidate
13
+ where candidate.${quoteIdentityIdentifier(declaration.identityColumn)} in ($1::uuid, $2::uuid)
14
+ for update`;
15
+ export const planSignupDefaultRemoval = (declaration) => `delete from ${target(declaration.table)}
16
+ where ${quoteIdentityIdentifier(declaration.identityColumn)} = $1::uuid`;
17
+ export const planRelationalReferenceTransfer = (reference) => `update ${target(reference)}
18
+ set ${quoteIdentityIdentifier(reference.column)} = $1::uuid
19
+ where ${quoteIdentityIdentifier(reference.column)} = $2::uuid`;
20
+ export const planRequiredReferenceCheck = (reference) => `select 1
21
+ from ${target(reference)}
22
+ where ${quoteIdentityIdentifier(reference.column)} = $1::uuid
23
+ limit 1`;
24
+ export const planJsonReferenceTransfer = (reference) => `update ${target(reference)}
25
+ set ${quoteIdentityIdentifier(reference.column)} = jsonb_set(
26
+ ${quoteIdentityIdentifier(reference.column)}, $3::text[], to_jsonb($1::text), false
27
+ )
28
+ where ${quoteIdentityIdentifier(reference.column)} #>> $3::text[] = $2`;
29
+ export const planPathReferenceCount = (reference) => {
30
+ const relation = target(reference);
31
+ const column = quoteIdentityIdentifier(reference.column);
32
+ return reference.valueType === "text"
33
+ ? `select count(*)::integer as count from ${relation} where ${column} like $1::text || '%'`
34
+ : `select count(*)::integer as count from ${relation} where ${column}::text like '%' || $1::text || '%'`;
35
+ };
36
+ export const planPathReferenceTransfer = (reference) => {
37
+ const relation = target(reference);
38
+ const column = quoteIdentityIdentifier(reference.column);
39
+ return reference.valueType === "text"
40
+ ? `update ${relation}
41
+ set ${column} = $1::text || substring(${column} from char_length($2::text) + 1)
42
+ where ${column} like $2::text || '%'`
43
+ : `update ${relation}
44
+ set ${column} = replace(${column}::text, $2::text, $1::text)::jsonb
45
+ where ${column}::text like '%' || $2::text || '%'`;
46
+ };
@@ -0,0 +1,13 @@
1
+ /** Pure post-read checks used by the transactional identity executor. */
2
+ import type { SignupDefaultDeclaration } from "./claim_contract.mjs";
3
+ export interface DefaultInspection {
4
+ readonly matches: boolean;
5
+ readonly actualColumns: string[];
6
+ readonly mismatchedColumns: string[];
7
+ }
8
+ export declare const inspectDeclaredDefault: ({ row, declaration, }: {
9
+ row: unknown;
10
+ declaration: SignupDefaultDeclaration;
11
+ }) => DefaultInspection;
12
+ export declare const containsJson: (actual: unknown, expected: unknown) => boolean;
13
+ export declare const valueKind: (value: unknown) => string;
@@ -0,0 +1,35 @@
1
+ /** Pure post-read checks used by the transactional identity executor. */
2
+ import { isDeepStrictEqual } from "node:util";
3
+ const isObject = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
4
+ export const inspectDeclaredDefault = ({ row, declaration, }) => {
5
+ if (!isObject(row)) {
6
+ return { matches: false, actualColumns: [], mismatchedColumns: [] };
7
+ }
8
+ const comparable = { ...row };
9
+ delete comparable[declaration.identityColumn];
10
+ for (const column of declaration.ignoredColumns)
11
+ delete comparable[column];
12
+ const actualColumns = Object.keys(comparable).sort();
13
+ const declaredColumns = Object.keys(declaration.values).sort();
14
+ const mismatchedColumns = Object.entries(declaration.values)
15
+ .filter(([column, matcher]) => matcher.kind === "pattern"
16
+ ? typeof comparable[column] !== "string" ||
17
+ !new RegExp(matcher.pattern, "u").test(comparable[column])
18
+ : !isDeepStrictEqual(comparable[column], matcher.value))
19
+ .map(([column]) => column)
20
+ .sort();
21
+ return {
22
+ matches: isDeepStrictEqual(actualColumns, declaredColumns) &&
23
+ mismatchedColumns.length === 0,
24
+ actualColumns,
25
+ mismatchedColumns,
26
+ };
27
+ };
28
+ export const containsJson = (actual, expected) => {
29
+ if (!isObject(expected))
30
+ return isDeepStrictEqual(actual, expected);
31
+ if (!isObject(actual))
32
+ return false;
33
+ return Object.entries(expected).every(([key, value]) => containsJson(actual[key], value));
34
+ };
35
+ export const valueKind = (value) => value === null ? "null" : Array.isArray(value) ? "array" : typeof value;
@@ -0,0 +1,30 @@
1
+ /** Read-only discovery and redacted review for the guided account connection. */
2
+ import type { IdentityClaimPlan, IdentityClientFactory, IdentityProvider } from "./claim_contract.mjs";
3
+ export interface IdentityConnectionPreview {
4
+ readonly name: string;
5
+ readonly provider: IdentityProvider;
6
+ readonly planDigest: string;
7
+ readonly relationalGroups: number;
8
+ readonly preservedAuditGroups: number;
9
+ readonly nestedJsonGroups: number;
10
+ readonly signupDefaults: number;
11
+ readonly storagePathGroups: number;
12
+ readonly assetScopes: number;
13
+ readonly claimKeys: number;
14
+ readonly tokenHookConfigured: boolean;
15
+ readonly associated: boolean;
16
+ }
17
+ export declare const inspectIdentityConnection: ({ plan, connectionString, clientFactory, }: {
18
+ plan: IdentityClaimPlan;
19
+ connectionString: string;
20
+ clientFactory?: IdentityClientFactory;
21
+ }) => Promise<IdentityConnectionPreview>;
22
+ export declare const waitForIdentityConnection: ({ plan, connectionString, clientFactory, inspect, signal, pollIntervalMs, timeoutMs, }: {
23
+ plan: IdentityClaimPlan;
24
+ connectionString: string;
25
+ clientFactory?: IdentityClientFactory;
26
+ inspect?: typeof inspectIdentityConnection;
27
+ signal?: AbortSignal;
28
+ pollIntervalMs?: number;
29
+ timeoutMs?: number;
30
+ }) => Promise<IdentityConnectionPreview>;