@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,135 @@
1
+ /** Typed contract shared by identity policy, planning, and execution stages. */
2
+ export declare const SUPPORTED_IDENTITY_PROVIDERS: readonly ["email", "github", "google"];
3
+ export type JsonObject = Record<string, unknown>;
4
+ export type IdentityProvider = (typeof SUPPORTED_IDENTITY_PROVIDERS)[number];
5
+ export type SubjectIdentityProvider = Exclude<IdentityProvider, "email">;
6
+ export type IdentityStrategy = "transfer" | "preserve-audit";
7
+ export type PathValueType = "text" | "jsonb";
8
+ export interface Relation {
9
+ readonly schema: string;
10
+ readonly table: string;
11
+ }
12
+ export interface PatternDefaultMatcher {
13
+ readonly kind: "pattern";
14
+ readonly pattern: string;
15
+ }
16
+ export interface ExactDefaultMatcher {
17
+ readonly kind: "exact";
18
+ readonly value: unknown;
19
+ }
20
+ export type SignupDefaultMatcher = PatternDefaultMatcher | ExactDefaultMatcher;
21
+ export interface VerifiedEmailMatcher {
22
+ readonly type: "verified-email";
23
+ readonly providers: readonly IdentityProvider[];
24
+ readonly emailEnvironmentVariable: string;
25
+ readonly approvedEmailSha256: string;
26
+ }
27
+ export interface ProviderSubjectMatcher {
28
+ readonly type: "provider-subject";
29
+ readonly provider: SubjectIdentityProvider;
30
+ readonly subjectEnvironmentVariable: string;
31
+ readonly approvedSubjectSha256: string;
32
+ }
33
+ export type IdentityMatcher = VerifiedEmailMatcher | ProviderSubjectMatcher;
34
+ export interface IdentityReference extends Relation {
35
+ readonly column: string;
36
+ readonly required: boolean;
37
+ readonly strategy: IdentityStrategy;
38
+ }
39
+ export interface JsonIdentityReference extends Relation {
40
+ readonly column: string;
41
+ readonly path: readonly string[];
42
+ }
43
+ export interface SignupDefaultDeclaration {
44
+ readonly table: Relation;
45
+ readonly identityColumn: string;
46
+ readonly ignoredColumns: readonly string[];
47
+ readonly values: Readonly<Record<string, SignupDefaultMatcher>>;
48
+ }
49
+ export interface PathIdentityReference extends Relation {
50
+ readonly column: string;
51
+ readonly valueType: PathValueType;
52
+ }
53
+ export interface IdentityAsset {
54
+ readonly bucket: string;
55
+ readonly prefix: string;
56
+ readonly rewritePath: boolean;
57
+ }
58
+ export interface IdentityTokenHook {
59
+ readonly function: Readonly<{
60
+ schema: string;
61
+ name: string;
62
+ }>;
63
+ readonly expectedClaims: JsonObject;
64
+ }
65
+ export interface IdentityDeclaration {
66
+ readonly name: string;
67
+ readonly matcher: IdentityMatcher;
68
+ readonly placeholderUserId: string;
69
+ readonly references: readonly IdentityReference[];
70
+ readonly jsonReferences: readonly JsonIdentityReference[];
71
+ readonly signupDefaults: readonly SignupDefaultDeclaration[];
72
+ readonly pathReferences: readonly PathIdentityReference[];
73
+ readonly assets: readonly IdentityAsset[];
74
+ readonly claims: JsonObject;
75
+ readonly tokenHook: IdentityTokenHook | null;
76
+ }
77
+ export interface IdentityPolicy {
78
+ readonly identityVersion: 1;
79
+ readonly identities: readonly IdentityDeclaration[];
80
+ }
81
+ export interface IdentityClaimReview {
82
+ readonly planVersion: 3;
83
+ readonly operation: "claim-local-identity";
84
+ readonly name: string;
85
+ readonly matcher: IdentityMatcher;
86
+ readonly placeholderUserId: string;
87
+ readonly references: readonly IdentityReference[];
88
+ readonly jsonReferences: readonly JsonIdentityReference[];
89
+ readonly signupDefaults: readonly Readonly<{
90
+ table: Relation;
91
+ identityColumn: string;
92
+ ignoredColumns: readonly string[];
93
+ valuesSha256: string;
94
+ }>[];
95
+ readonly pathReferences: readonly PathIdentityReference[];
96
+ readonly assets: readonly IdentityAsset[];
97
+ readonly claimKeys: readonly string[];
98
+ readonly claimsSha256: string;
99
+ readonly tokenHook: Readonly<{
100
+ function: Readonly<{
101
+ schema: string;
102
+ name: string;
103
+ }>;
104
+ expectedClaimsSha256: string;
105
+ }> | null;
106
+ }
107
+ export interface IdentityClaimPlan {
108
+ readonly identity: IdentityDeclaration;
109
+ readonly review: Readonly<IdentityClaimReview>;
110
+ readonly digest: string;
111
+ }
112
+ export interface QueryResult<Row extends JsonObject = JsonObject> {
113
+ readonly rows: Row[];
114
+ }
115
+ export interface IdentityDatabaseClient {
116
+ connect?(): Promise<void>;
117
+ end?(): Promise<void>;
118
+ query<Row extends JsonObject = JsonObject>(sql: string, parameters?: readonly unknown[]): Promise<QueryResult<Row>>;
119
+ }
120
+ export type IdentityClientFactory = (connectionString: string) => IdentityDatabaseClient | Promise<IdentityDatabaseClient>;
121
+ export interface StorageCopy {
122
+ readonly bucket: string;
123
+ readonly source: string;
124
+ readonly destination: string;
125
+ }
126
+ export interface IdentityClaimResult {
127
+ readonly name: string;
128
+ readonly provider: IdentityProvider;
129
+ readonly claimed: true;
130
+ readonly removedSignupDefaults: number;
131
+ readonly tokenHookVerified: boolean;
132
+ readonly placeholderRetainedForAudit: boolean;
133
+ readonly storageObjectsTransferred: number;
134
+ readonly idempotent: boolean;
135
+ }
@@ -0,0 +1,6 @@
1
+ /** Typed contract shared by identity policy, planning, and execution stages. */
2
+ export const SUPPORTED_IDENTITY_PROVIDERS = Object.freeze([
3
+ "email",
4
+ "github",
5
+ "google",
6
+ ]);
@@ -0,0 +1,10 @@
1
+ /** Execute and verify one confirmed local identity claim transaction. */
2
+ import type { IdentityClaimPlan, IdentityClaimResult, IdentityClientFactory } from "./claim_contract.mjs";
3
+ import type { SupabaseStorageTransfer } from "./storage.mjs";
4
+ export declare const applyIdentityClaim: ({ plan, confirmation, connectionString, clientFactory, storageTransfer, }: {
5
+ plan: IdentityClaimPlan;
6
+ confirmation: string;
7
+ connectionString: string;
8
+ clientFactory?: IdentityClientFactory;
9
+ storageTransfer?: SupabaseStorageTransfer;
10
+ }) => Promise<IdentityClaimResult>;
@@ -0,0 +1,358 @@
1
+ /** Execute and verify one confirmed local identity claim transaction. */
2
+ import pg from "pg";
3
+ import { IDENTITY_CLAIM_RECEIPT_SQL } from "../runtime/policy.mjs";
4
+ import { countIdentityRows, discoverPublicAuthReferences, findVerifiedLocalIdentity, } from "./claim_identity_lookup.mjs";
5
+ import { getIdentityPlanMatchValue } from "./claim_plan.mjs";
6
+ import { planJsonReferenceTransfer, planPathReferenceCount, planPathReferenceTransfer, planRelationalReferenceTransfer, planRequiredReferenceCheck, planSignupDefaultInspection, planSignupDefaultRemoval, } from "./claim_sql.mjs";
7
+ import { containsJson, inspectDeclaredDefault, valueKind, } from "./claim_verification.mjs";
8
+ const { Client } = pg;
9
+ const IDENTIFIER = /^[a-z][a-z0-9_]{0,62}$/u;
10
+ 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;
11
+ const groupStorageCopies = (copies) => {
12
+ const grouped = new Map();
13
+ for (const copy of copies) {
14
+ const bucketCopies = grouped.get(copy.bucket);
15
+ if (bucketCopies)
16
+ bucketCopies.push(copy);
17
+ else
18
+ grouped.set(copy.bucket, [copy]);
19
+ }
20
+ return grouped;
21
+ };
22
+ const isObject = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
23
+ const id = (value, label) => {
24
+ if (typeof value !== "string" || !IDENTIFIER.test(value))
25
+ throw new Error(`${label} is not a safe identifier.`);
26
+ return value;
27
+ };
28
+ const qid = (value) => `"${id(value, "SQL identifier")}"`;
29
+ export const applyIdentityClaim = async ({ plan, confirmation, connectionString, clientFactory, storageTransfer, }) => {
30
+ if (confirmation !== plan.digest) {
31
+ throw new Error(`Identity claim confirmation does not match. Expected ${plan.digest}.`);
32
+ }
33
+ const matchValue = getIdentityPlanMatchValue(plan);
34
+ if (!matchValue) {
35
+ throw new Error("Identity claim plans must be created from the reviewed local matcher environment.");
36
+ }
37
+ const target = new URL(connectionString);
38
+ if (!["127.0.0.1", "::1", "localhost"].includes(target.hostname)) {
39
+ throw new Error("Identity claims may target only the local Rehearsal database.");
40
+ }
41
+ const client = clientFactory
42
+ ? await clientFactory(connectionString)
43
+ : new Client({
44
+ connectionString,
45
+ application_name: "rehearsal-identity-claim",
46
+ });
47
+ await client.connect?.();
48
+ let removedSignupDefaults = 0;
49
+ let tokenHookVerified = false;
50
+ let placeholderRetainedForAudit = false;
51
+ const stagedStorageCopies = [];
52
+ let committed = false;
53
+ let storageObjectsTransferred = 0;
54
+ try {
55
+ await client.query("begin");
56
+ const localIdentity = await findVerifiedLocalIdentity({
57
+ client,
58
+ plan,
59
+ matchValue,
60
+ });
61
+ const localUserId = localIdentity.id;
62
+ if (!UUID.test(localUserId))
63
+ throw new Error("Matched local identity is invalid.");
64
+ const placeholderUser = await client.query("select 1 from auth.users where id = $1::uuid limit 1", [plan.identity.placeholderUserId]);
65
+ const placeholderReferenceCounts = [];
66
+ if (localUserId !== plan.identity.placeholderUserId) {
67
+ for (const reference of plan.identity.references) {
68
+ placeholderReferenceCounts.push(await countIdentityRows({
69
+ client,
70
+ reference,
71
+ userId: plan.identity.placeholderUserId,
72
+ }));
73
+ }
74
+ }
75
+ const transferReferences = plan.identity.references.filter((reference) => reference.strategy === "transfer");
76
+ const preservedReferences = plan.identity.references.filter((reference) => reference.strategy === "preserve-audit");
77
+ const transferPlaceholderCounts = transferReferences.map((reference) => {
78
+ const index = plan.identity.references.indexOf(reference);
79
+ return placeholderReferenceCounts[index] ?? 0;
80
+ });
81
+ const transferLocalCounts = [];
82
+ if (localUserId !== plan.identity.placeholderUserId &&
83
+ preservedReferences.length > 0) {
84
+ for (const reference of transferReferences) {
85
+ transferLocalCounts.push(await countIdentityRows({ client, reference, userId: localUserId }));
86
+ }
87
+ }
88
+ const alreadyClaimed = localUserId !== plan.identity.placeholderUserId &&
89
+ transferPlaceholderCounts.every((count) => count === 0) &&
90
+ (preservedReferences.length > 0
91
+ ? transferLocalCounts.some((count) => count > 0)
92
+ : placeholderUser.rows.length === 0);
93
+ const publicAuthReferences = localUserId === plan.identity.placeholderUserId
94
+ ? []
95
+ : await discoverPublicAuthReferences(client);
96
+ if (localUserId !== plan.identity.placeholderUserId) {
97
+ const placeholderIdentity = await client.query("select 1 from auth.identities where user_id = $1::uuid limit 1", [plan.identity.placeholderUserId]);
98
+ if (placeholderIdentity.rows.length > 0) {
99
+ throw new Error("The copied placeholder already belongs to a conflicting local identity.");
100
+ }
101
+ }
102
+ for (const reference of alreadyClaimed ? [] : publicAuthReferences) {
103
+ const declared = plan.identity.references.find((candidate) => candidate.schema === reference.schema &&
104
+ candidate.table === reference.table &&
105
+ candidate.column === reference.column);
106
+ if (declared?.strategy === "preserve-audit")
107
+ continue;
108
+ const localRows = await countIdentityRows({
109
+ client,
110
+ reference,
111
+ userId: localUserId,
112
+ });
113
+ if (localRows === 0)
114
+ continue;
115
+ const declaredDefault = plan.identity.signupDefaults.find((declaration) => declaration.table.schema === reference.schema &&
116
+ declaration.table.table === reference.table &&
117
+ declaration.identityColumn === reference.column);
118
+ if (!declaredDefault) {
119
+ throw new Error("The local account already contains independent application data; nothing was changed.");
120
+ }
121
+ }
122
+ if (localUserId !== plan.identity.placeholderUserId && !alreadyClaimed) {
123
+ for (const declaration of plan.identity.signupDefaults) {
124
+ const rows = await client.query(planSignupDefaultInspection(declaration), [localUserId, plan.identity.placeholderUserId]);
125
+ const localRows = rows.rows.filter((row) => row.identity === localUserId);
126
+ const placeholderRows = rows.rows.filter((row) => row.identity === plan.identity.placeholderUserId);
127
+ if (localRows.length > 1 || placeholderRows.length > 1) {
128
+ throw new Error("The declared signup-default relation is ambiguous; nothing was changed.");
129
+ }
130
+ if (localRows.length === 1 && placeholderRows.length === 1) {
131
+ const localRow = localRows[0];
132
+ if (!localRow) {
133
+ throw new Error("The declared signup-default relation is ambiguous; nothing was changed.");
134
+ }
135
+ const defaultInspection = inspectDeclaredDefault({
136
+ row: localRow.row,
137
+ declaration,
138
+ });
139
+ if (!defaultInspection.matches) {
140
+ const declaredColumns = Object.keys(declaration.values).sort();
141
+ const mismatchKinds = defaultInspection.mismatchedColumns.map((column) => `${column}(${valueKind(isObject(localRow.row) ? localRow.row[column] : undefined)}/${declaration.values[column]?.kind === "exact" ? valueKind(declaration.values[column].value) : "pattern"})`);
142
+ throw new Error(`The local signup row was edited or does not match its complete declared default; nothing was changed. Declared columns: ${declaredColumns.join(", ")}. Actual columns: ${defaultInspection.actualColumns.join(", ") || "none"}. Mismatched column shapes: ${mismatchKinds.join(", ") || "none"}.`);
143
+ }
144
+ await client.query(planSignupDefaultRemoval(declaration), [
145
+ localUserId,
146
+ ]);
147
+ removedSignupDefaults += 1;
148
+ }
149
+ }
150
+ }
151
+ for (const reference of plan.identity.references) {
152
+ const expectedUserId = reference.strategy === "preserve-audit"
153
+ ? plan.identity.placeholderUserId
154
+ : localUserId;
155
+ if (reference.strategy === "transfer") {
156
+ await client.query(planRelationalReferenceTransfer(reference), [
157
+ localUserId,
158
+ plan.identity.placeholderUserId,
159
+ ]);
160
+ }
161
+ if (reference.required) {
162
+ const required = await client.query(planRequiredReferenceCheck(reference), [expectedUserId]);
163
+ if (required.rows.length !== 1) {
164
+ throw new Error("A required local identity reference is missing; nothing was changed.");
165
+ }
166
+ }
167
+ }
168
+ for (const reference of plan.identity.jsonReferences) {
169
+ await client.query(planJsonReferenceTransfer(reference), [
170
+ localUserId,
171
+ plan.identity.placeholderUserId,
172
+ reference.path,
173
+ ]);
174
+ }
175
+ const sourcePathPrefix = `${plan.identity.placeholderUserId}/`;
176
+ let sourcePathReferenceCount = 0;
177
+ if (plan.identity.assets.some((asset) => asset.rewritePath)) {
178
+ for (const reference of plan.identity.pathReferences) {
179
+ const result = await client.query(planPathReferenceCount(reference), [sourcePathPrefix]);
180
+ const count = Number(result.rows[0]?.count);
181
+ if (!Number.isSafeInteger(count) || count < 0) {
182
+ throw new Error("A Storage path reference returned an invalid count.");
183
+ }
184
+ sourcePathReferenceCount += count;
185
+ }
186
+ }
187
+ for (const asset of plan.identity.assets) {
188
+ const sourceObjects = await client.query(`select name, owner_id
189
+ from storage.objects
190
+ where bucket_id = $1
191
+ and name like $2::text || '%'
192
+ order by name`, [asset.bucket, asset.prefix]);
193
+ if (sourceObjects.rows.some(({ owner_id: ownerId }) => ownerId !== null &&
194
+ ownerId !== plan.identity.placeholderUserId &&
195
+ !(alreadyClaimed && ownerId === localUserId))) {
196
+ throw new Error("A Storage object in the reviewed placeholder path belongs to another identity; nothing was changed.");
197
+ }
198
+ if (sourceObjects.rows.length === 0)
199
+ continue;
200
+ if (!asset.rewritePath) {
201
+ await client.query(`update storage.objects
202
+ set owner_id = $1::text
203
+ where bucket_id = $2 and name = any($3::text[])`, [
204
+ localUserId,
205
+ asset.bucket,
206
+ sourceObjects.rows.map(({ name }) => name),
207
+ ]);
208
+ continue;
209
+ }
210
+ if (!storageTransfer) {
211
+ throw new Error("Storage path transfer requires the local Supabase Storage service; nothing was changed.");
212
+ }
213
+ const copies = sourceObjects.rows.map(({ name }) => {
214
+ if (typeof name !== "string" || !name.startsWith(asset.prefix)) {
215
+ throw new Error("A Storage object escaped its reviewed identity scope.");
216
+ }
217
+ return {
218
+ bucket: asset.bucket,
219
+ source: name,
220
+ destination: `${localUserId}/${name.slice(asset.prefix.length)}`,
221
+ };
222
+ });
223
+ const collisions = await client.query(`select name
224
+ from storage.objects
225
+ where bucket_id = $1 and name = any($2::text[])
226
+ limit 1`, [asset.bucket, copies.map((copy) => copy.destination)]);
227
+ if (collisions.rows.length > 0) {
228
+ throw new Error("A destination Storage path already exists; nothing was changed.");
229
+ }
230
+ for (const copy of copies) {
231
+ await storageTransfer.copyAndVerify(copy);
232
+ stagedStorageCopies.push(copy);
233
+ }
234
+ await client.query(`update storage.objects
235
+ set owner_id = $1::text
236
+ where bucket_id = $2 and name = any($3::text[])`, [localUserId, asset.bucket, copies.map((copy) => copy.destination)]);
237
+ const transferred = await client.query(`select count(*)::integer as count
238
+ from storage.objects
239
+ where owner_id = $1::text
240
+ and bucket_id = $2
241
+ and name = any($3::text[])`, [localUserId, asset.bucket, copies.map((copy) => copy.destination)]);
242
+ if (Number(transferred.rows[0]?.count) !== copies.length) {
243
+ throw new Error("Storage ownership did not match the verified physical copies; nothing was changed.");
244
+ }
245
+ storageObjectsTransferred += copies.length;
246
+ }
247
+ if (sourcePathReferenceCount > 0 && storageObjectsTransferred === 0) {
248
+ throw new Error("Copied records reference Storage paths, but no matching objects were available; nothing was changed.");
249
+ }
250
+ const targetPathPrefix = `${localUserId}/`;
251
+ for (const reference of plan.identity.pathReferences) {
252
+ await client.query(planPathReferenceTransfer(reference), [
253
+ targetPathPrefix,
254
+ sourcePathPrefix,
255
+ ]);
256
+ }
257
+ await client.query(`update auth.users
258
+ set raw_app_meta_data = coalesce(raw_app_meta_data, '{}'::jsonb) || $2::jsonb,
259
+ updated_at = now()
260
+ where id = $1::uuid`, [localUserId, JSON.stringify(plan.identity.claims)]);
261
+ if (plan.identity.tokenHook) {
262
+ const hook = plan.identity.tokenHook;
263
+ const tokenResult = await client.query(`select ${qid(hook.function.schema)}.${qid(hook.function.name)}(
264
+ jsonb_build_object('user_id', $1::text, 'claims', '{}'::jsonb)
265
+ ) as event`, [localUserId]);
266
+ const event = tokenResult.rows[0]?.event;
267
+ const claims = isObject(event) ? event.claims : undefined;
268
+ if (!containsJson(claims, hook.expectedClaims)) {
269
+ throw new Error("The configured local token hook did not return the declared claims; nothing was changed.");
270
+ }
271
+ tokenHookVerified = true;
272
+ }
273
+ await client.query(IDENTITY_CLAIM_RECEIPT_SQL);
274
+ await client.query(`insert into rehearsal_internal.identity_claims (
275
+ identity_name, placeholder_user_id, local_user_id
276
+ ) values ($1, $2::uuid, $3::uuid)
277
+ on conflict (identity_name) do nothing`, [plan.identity.name, plan.identity.placeholderUserId, localUserId]);
278
+ const claimReceipt = await client.query(`select 1
279
+ from rehearsal_internal.identity_claims
280
+ where identity_name = $1
281
+ and placeholder_user_id = $2::uuid
282
+ and local_user_id = $3::uuid`, [plan.identity.name, plan.identity.placeholderUserId, localUserId]);
283
+ if (claimReceipt.rows.length !== 1) {
284
+ throw new Error("The local identity claim conflicts with an existing association receipt; nothing was changed.");
285
+ }
286
+ if (localUserId !== plan.identity.placeholderUserId) {
287
+ for (const reference of publicAuthReferences) {
288
+ const remaining = await countIdentityRows({
289
+ client,
290
+ reference,
291
+ userId: plan.identity.placeholderUserId,
292
+ });
293
+ if (remaining === 0)
294
+ continue;
295
+ const declared = preservedReferences.find((candidate) => candidate.schema === reference.schema &&
296
+ candidate.table === reference.table &&
297
+ candidate.column === reference.column);
298
+ if (!declared) {
299
+ throw new Error("The identity policy omitted a copied application reference; nothing was changed.");
300
+ }
301
+ placeholderRetainedForAudit = true;
302
+ }
303
+ if (!placeholderRetainedForAudit) {
304
+ await client.query(`delete from auth.users
305
+ where id = $1::uuid
306
+ and not exists (select 1 from auth.identities where user_id = $1::uuid)`, [plan.identity.placeholderUserId]);
307
+ }
308
+ }
309
+ await client.query("commit");
310
+ committed = true;
311
+ const copiesByBucket = groupStorageCopies(stagedStorageCopies);
312
+ for (const [bucket, copies] of copiesByBucket) {
313
+ if (!storageTransfer) {
314
+ throw new Error("Storage transfer became unavailable after commit.");
315
+ }
316
+ await storageTransfer.remove({
317
+ bucket,
318
+ paths: copies.map((copy) => copy.source),
319
+ });
320
+ }
321
+ return {
322
+ name: plan.identity.name,
323
+ provider: localIdentity.provider,
324
+ claimed: true,
325
+ removedSignupDefaults,
326
+ tokenHookVerified,
327
+ placeholderRetainedForAudit,
328
+ storageObjectsTransferred,
329
+ idempotent: alreadyClaimed,
330
+ };
331
+ }
332
+ catch (error) {
333
+ if (committed) {
334
+ throw new Error("The identity claim committed and the new Storage copies are usable, but old Storage cleanup failed.", { cause: error });
335
+ }
336
+ await client.query("rollback").catch(() => undefined);
337
+ if (stagedStorageCopies.length > 0) {
338
+ const cleanupErrors = [];
339
+ if (!storageTransfer)
340
+ throw error;
341
+ for (const [bucket, copies] of groupStorageCopies(stagedStorageCopies)) {
342
+ await storageTransfer
343
+ .remove({
344
+ bucket,
345
+ paths: copies.map((copy) => copy.destination),
346
+ })
347
+ .catch((cleanupError) => cleanupErrors.push(cleanupError));
348
+ }
349
+ if (cleanupErrors.length > 0) {
350
+ throw new AggregateError([error, ...cleanupErrors], "The identity claim rolled back, but copied Storage cleanup needs attention.");
351
+ }
352
+ }
353
+ throw error;
354
+ }
355
+ finally {
356
+ await client.end?.();
357
+ }
358
+ };
@@ -0,0 +1,23 @@
1
+ /** Read-only identity discovery and reference inspection. */
2
+ import type { IdentityClaimPlan, IdentityDatabaseClient, IdentityProvider, IdentityReference } from "./claim_contract.mjs";
3
+ export declare const discoverPublicAuthReferences: (client: IdentityDatabaseClient) => Promise<IdentityReference[]>;
4
+ export declare const countIdentityRows: ({ client, reference, userId, }: {
5
+ client: IdentityDatabaseClient;
6
+ reference: Pick<IdentityReference, "schema" | "table" | "column">;
7
+ userId: string;
8
+ }) => Promise<number>;
9
+ export interface VerifiedLocalIdentity {
10
+ readonly id: string;
11
+ readonly provider: IdentityProvider;
12
+ }
13
+ export declare class IdentityLookupError extends Error {
14
+ readonly reason: "none" | "ambiguous" | "conflict" | "invalid";
15
+ constructor(reason: "none" | "ambiguous" | "conflict" | "invalid", message: string);
16
+ }
17
+ export declare const isIdentityNotFoundError: (error: unknown) => error is IdentityLookupError;
18
+ export declare const findVerifiedLocalIdentity: ({ client, plan, matchValue, lock, }: {
19
+ client: IdentityDatabaseClient;
20
+ plan: IdentityClaimPlan;
21
+ matchValue: string;
22
+ lock?: boolean;
23
+ }) => Promise<VerifiedLocalIdentity>;
@@ -0,0 +1,134 @@
1
+ /** Read-only identity discovery and reference inspection. */
2
+ import { createHash } from "node:crypto";
3
+ const IDENTIFIER = /^[a-z][a-z0-9_]{0,62}$/u;
4
+ const id = (value, label) => {
5
+ if (typeof value !== "string" || !IDENTIFIER.test(value)) {
6
+ throw new Error(`${label} is not a safe identifier.`);
7
+ }
8
+ return value;
9
+ };
10
+ const qid = (value) => `"${id(value, "SQL identifier")}"`;
11
+ const hash = (value) => createHash("sha256").update(value).digest("hex");
12
+ export const discoverPublicAuthReferences = async (client) => {
13
+ const result = await client.query(`select distinct
14
+ source_namespace.nspname as schema,
15
+ source_table.relname as table,
16
+ source_column.attname as column
17
+ from pg_constraint constraint_record
18
+ join pg_class source_table on source_table.oid = constraint_record.conrelid
19
+ join pg_namespace source_namespace on source_namespace.oid = source_table.relnamespace
20
+ join unnest(constraint_record.conkey) with ordinality source_key(attnum, position) on true
21
+ join unnest(constraint_record.confkey) with ordinality target_key(attnum, position)
22
+ on target_key.position = source_key.position
23
+ join pg_attribute source_column
24
+ on source_column.attrelid = constraint_record.conrelid
25
+ and source_column.attnum = source_key.attnum
26
+ join pg_attribute target_column
27
+ on target_column.attrelid = constraint_record.confrelid
28
+ and target_column.attnum = target_key.attnum
29
+ where constraint_record.contype = 'f'
30
+ and constraint_record.confrelid = 'auth.users'::regclass
31
+ and target_column.attname = 'id'
32
+ and source_namespace.nspname = 'public'`);
33
+ return result.rows.map((reference, index) => ({
34
+ schema: id(reference.schema, `discoveredReference[${index}].schema`),
35
+ table: id(reference.table, `discoveredReference[${index}].table`),
36
+ column: id(reference.column, `discoveredReference[${index}].column`),
37
+ required: false,
38
+ strategy: "transfer",
39
+ }));
40
+ };
41
+ export const countIdentityRows = async ({ client, reference, userId, }) => {
42
+ const result = await client.query(`select count(*)::integer as count
43
+ from ${qid(reference.schema)}.${qid(reference.table)}
44
+ where ${qid(reference.column)} = $1::uuid`, [userId]);
45
+ const count = Number(result.rows[0]?.count);
46
+ if (!Number.isSafeInteger(count) || count < 0) {
47
+ throw new Error("A local identity reference returned an invalid count.");
48
+ }
49
+ return count;
50
+ };
51
+ export class IdentityLookupError extends Error {
52
+ reason;
53
+ constructor(reason, message) {
54
+ super(message);
55
+ this.name = "IdentityLookupError";
56
+ this.reason = reason;
57
+ }
58
+ }
59
+ export const isIdentityNotFoundError = (error) => error instanceof IdentityLookupError && error.reason === "none";
60
+ export const findVerifiedLocalIdentity = async ({ client, plan, matchValue, lock = true, }) => {
61
+ const matcher = plan.identity.matcher;
62
+ if (matcher.type === "verified-email") {
63
+ const match = await client.query(`select u.id::text, i.provider
64
+ from auth.users u
65
+ join auth.identities i on i.user_id = u.id
66
+ where lower(coalesce(i.identity_data->>'email', u.email)) = $1
67
+ and i.provider = any($2::text[])
68
+ and (
69
+ coalesce((i.identity_data->>'email_verified')::boolean, false)
70
+ or u.email_confirmed_at is not null
71
+ )
72
+ ${lock ? "for update of u" : ""}`, [matchValue, matcher.providers]);
73
+ if (match.rows.length !== 1) {
74
+ throw new IdentityLookupError(match.rows.length === 0 ? "none" : "ambiguous", match.rows.length === 0
75
+ ? "No verified local identity matches the reviewed email and provider allowlist."
76
+ : "More than one verified local identity matches the reviewed email and provider allowlist.");
77
+ }
78
+ const matchedRow = match.rows[0];
79
+ if (!matchedRow)
80
+ throw new IdentityLookupError("invalid", "Matched local identity is invalid.");
81
+ const matchedProvider = matchedRow.provider ??
82
+ (matcher.providers.length === 1 ? matcher.providers[0] : null);
83
+ if (typeof matchedProvider !== "string" ||
84
+ !matcher.providers.includes(matchedProvider)) {
85
+ throw new IdentityLookupError("invalid", "Matched local identity provider is invalid.");
86
+ }
87
+ if (typeof matchedRow.id !== "string") {
88
+ throw new IdentityLookupError("invalid", "Matched local identity is invalid.");
89
+ }
90
+ return {
91
+ id: matchedRow.id,
92
+ provider: matchedProvider,
93
+ };
94
+ }
95
+ const candidates = await client.query(`select u.id::text,
96
+ i.provider,
97
+ i.provider_id::text,
98
+ i.identity_data->>'sub' as subject
99
+ from auth.users u
100
+ join auth.identities i on i.user_id = u.id
101
+ where i.provider = $1
102
+ and (
103
+ coalesce((i.identity_data->>'email_verified')::boolean, false)
104
+ or u.email_confirmed_at is not null
105
+ )
106
+ ${lock ? "for update of u" : ""}`, [matcher.provider]);
107
+ const matches = [];
108
+ for (const candidate of candidates.rows) {
109
+ const subjects = [candidate.provider_id, candidate.subject]
110
+ .filter((value) => typeof value === "string" && Boolean(value.trim()))
111
+ .map((value) => value.trim());
112
+ const uniqueSubjects = [...new Set(subjects)];
113
+ if (uniqueSubjects.some((subject) => hash(subject) === matcher.approvedSubjectSha256)) {
114
+ if (uniqueSubjects.length !== 1) {
115
+ throw new IdentityLookupError("conflict", "The matched local provider identity has conflicting stable subjects.");
116
+ }
117
+ if (typeof candidate.id !== "string" ||
118
+ candidate.provider !== matcher.provider) {
119
+ throw new IdentityLookupError("invalid", "Matched local identity provider is invalid.");
120
+ }
121
+ matches.push({ id: candidate.id, provider: matcher.provider });
122
+ }
123
+ }
124
+ if (matches.length !== 1) {
125
+ throw new IdentityLookupError(matches.length === 0 ? "none" : "ambiguous", matches.length === 0
126
+ ? "No verified local identity matches the reviewed provider subject."
127
+ : "More than one verified local identity matches the reviewed provider subject.");
128
+ }
129
+ const match = matches[0];
130
+ if (!match || match.provider !== matcher.provider) {
131
+ throw new IdentityLookupError("invalid", "Matched local identity provider is invalid.");
132
+ }
133
+ return match;
134
+ };