@rehearsal-db/core 0.1.0-beta.14 → 0.1.0-beta.16

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 (236) hide show
  1. package/CHANGELOG.md +51 -1
  2. package/README.md +1 -0
  3. package/dist/package.json +101 -0
  4. package/dist/scripts/install/create_root_config.d.mts +3 -0
  5. package/dist/scripts/install/create_root_config.mjs +13 -0
  6. package/dist/scripts/runtime/manage_database.d.mts +6 -0
  7. package/{scripts → dist/scripts}/runtime/manage_database.mjs +6 -8
  8. package/dist/src/application/session.d.mts +96 -0
  9. package/dist/src/application/session.mjs +380 -0
  10. package/dist/src/baseline/artifact.d.mts +119 -0
  11. package/dist/src/baseline/artifact.mjs +522 -0
  12. package/dist/src/baseline/builder.d.mts +19 -0
  13. package/dist/src/baseline/builder.mjs +142 -0
  14. package/dist/src/baseline/input_discovery.d.mts +13 -0
  15. package/dist/src/baseline/input_discovery.mjs +149 -0
  16. package/dist/src/baseline/policy_review.d.mts +56 -0
  17. package/dist/src/baseline/policy_review.mjs +73 -0
  18. package/dist/src/baseline/preparation.d.mts +63 -0
  19. package/dist/src/baseline/preparation.mjs +295 -0
  20. package/dist/src/baseline/privacy_contract.d.mts +147 -0
  21. package/dist/src/baseline/privacy_contract.mjs +2 -0
  22. package/dist/src/baseline/privacy_dependency_graph.d.mts +7 -0
  23. package/dist/src/baseline/privacy_dependency_graph.mjs +110 -0
  24. package/dist/src/baseline/privacy_engine.d.mts +5 -0
  25. package/dist/src/baseline/privacy_engine.mjs +4 -0
  26. package/dist/src/baseline/privacy_key.d.mts +7 -0
  27. package/dist/src/baseline/privacy_key.mjs +42 -0
  28. package/dist/src/baseline/privacy_pipeline.d.mts +7 -0
  29. package/dist/src/baseline/privacy_pipeline.mjs +127 -0
  30. package/dist/src/baseline/privacy_policy.d.mts +6 -0
  31. package/dist/src/baseline/privacy_policy.mjs +237 -0
  32. package/dist/src/baseline/privacy_recipe_validation.d.mts +5 -0
  33. package/dist/src/baseline/privacy_recipe_validation.mjs +289 -0
  34. package/dist/src/baseline/privacy_rule_registry.d.mts +18 -0
  35. package/dist/src/baseline/privacy_rule_registry.mjs +143 -0
  36. package/dist/src/baseline/privacy_structured_json.d.mts +66 -0
  37. package/dist/src/baseline/privacy_structured_json.mjs +464 -0
  38. package/dist/src/baseline/privacy_transforms.d.mts +18 -0
  39. package/dist/src/baseline/privacy_transforms.mjs +142 -0
  40. package/dist/src/baseline/privacy_validation_shared.d.mts +31 -0
  41. package/dist/src/baseline/privacy_validation_shared.mjs +127 -0
  42. package/dist/src/baseline/sanitization_policy.d.mts +69 -0
  43. package/dist/src/baseline/sanitization_policy.mjs +227 -0
  44. package/dist/src/baseline/schema_snapshot.d.mts +19 -0
  45. package/dist/src/baseline/schema_snapshot.mjs +79 -0
  46. package/dist/src/cli/application_command_handlers.d.mts +8 -0
  47. package/dist/src/cli/application_command_handlers.mjs +52 -0
  48. package/dist/src/cli/arguments.d.mts +38 -0
  49. package/dist/src/cli/arguments.mjs +128 -0
  50. package/dist/src/cli/basic_command_handlers.d.mts +4 -0
  51. package/dist/src/cli/basic_command_handlers.mjs +56 -0
  52. package/dist/src/cli/cli_commands.d.mts +14 -0
  53. package/dist/src/cli/cli_commands.mjs +32 -0
  54. package/dist/src/cli/command_contract.d.mts +57 -0
  55. package/dist/src/cli/command_contract.mjs +39 -0
  56. package/dist/src/cli/command_output.d.mts +12 -0
  57. package/dist/src/cli/command_output.mjs +20 -0
  58. package/dist/src/cli/command_registry.d.mts +7 -0
  59. package/dist/src/cli/command_registry.mjs +14 -0
  60. package/dist/src/cli/guided.d.mts +53 -0
  61. package/dist/src/cli/guided.mjs +719 -0
  62. package/dist/src/cli/identity_command_handlers.d.mts +8 -0
  63. package/dist/src/cli/identity_command_handlers.mjs +43 -0
  64. package/dist/src/cli/init_command.d.mts +21 -0
  65. package/dist/src/cli/init_command.mjs +90 -0
  66. package/dist/src/cli/inspection_command_handlers.d.mts +3 -0
  67. package/dist/src/cli/inspection_command_handlers.mjs +79 -0
  68. package/dist/src/cli/project_command_handlers.d.mts +11 -0
  69. package/dist/src/cli/project_command_handlers.mjs +92 -0
  70. package/dist/src/cli/project_commands.d.mts +159 -0
  71. package/dist/src/cli/project_commands.mjs +183 -0
  72. package/dist/src/cli/rehearsal.d.mts +7 -0
  73. package/dist/src/cli/rehearsal.mjs +190 -0
  74. package/dist/src/cli/renderers.d.mts +299 -0
  75. package/dist/src/cli/renderers.mjs +554 -0
  76. package/dist/src/cli/runtime_action_handlers.d.mts +12 -0
  77. package/dist/src/cli/runtime_action_handlers.mjs +201 -0
  78. package/dist/src/cli/runtime_commands.d.mts +269 -0
  79. package/dist/src/cli/runtime_commands.mjs +601 -0
  80. package/dist/src/cli/source_command_handlers.d.mts +8 -0
  81. package/dist/src/cli/source_command_handlers.mjs +62 -0
  82. package/dist/src/cli/source_commands.d.mts +436 -0
  83. package/dist/src/cli/source_commands.mjs +301 -0
  84. package/dist/src/cli/terminal.d.mts +59 -0
  85. package/dist/src/cli/terminal.mjs +230 -0
  86. package/dist/src/identity/claim.d.mts +5 -0
  87. package/dist/src/identity/claim.mjs +4 -0
  88. package/dist/src/identity/claim_contract.d.mts +135 -0
  89. package/dist/src/identity/claim_contract.mjs +6 -0
  90. package/dist/src/identity/claim_executor.d.mts +10 -0
  91. package/dist/src/identity/claim_executor.mjs +358 -0
  92. package/dist/src/identity/claim_identity_lookup.d.mts +17 -0
  93. package/dist/src/identity/claim_identity_lookup.mjs +125 -0
  94. package/dist/src/identity/claim_plan.d.mts +8 -0
  95. package/dist/src/identity/claim_plan.mjs +61 -0
  96. package/dist/src/identity/claim_policy.d.mts +3 -0
  97. package/dist/src/identity/claim_policy.mjs +377 -0
  98. package/dist/src/identity/claim_sql.d.mts +10 -0
  99. package/dist/src/identity/claim_sql.mjs +46 -0
  100. package/dist/src/identity/claim_verification.d.mts +13 -0
  101. package/dist/src/identity/claim_verification.mjs +35 -0
  102. package/dist/src/identity/provider_configuration.d.mts +37 -0
  103. package/dist/src/identity/provider_configuration.mjs +132 -0
  104. package/dist/src/identity/storage.d.mts +21 -0
  105. package/dist/src/identity/storage.mjs +122 -0
  106. package/dist/src/index.d.mts +4 -0
  107. package/dist/src/index.mjs +3 -0
  108. package/dist/src/project/configuration.d.mts +13 -0
  109. package/dist/src/project/configuration.mjs +9 -0
  110. package/dist/src/project/configuration_contract.d.mts +125 -0
  111. package/dist/src/project/configuration_contract.mjs +27 -0
  112. package/dist/src/project/configuration_discovery.d.mts +7 -0
  113. package/dist/src/project/configuration_discovery.mjs +37 -0
  114. package/dist/src/project/configuration_normalization.d.mts +107 -0
  115. package/dist/src/project/configuration_normalization.mjs +302 -0
  116. package/dist/src/project/configuration_rendering.d.mts +14 -0
  117. package/dist/src/project/configuration_rendering.mjs +224 -0
  118. package/dist/src/project/configuration_resolution.d.mts +125 -0
  119. package/dist/src/project/configuration_resolution.mjs +81 -0
  120. package/dist/src/project/configuration_schema.d.mts +22 -0
  121. package/dist/src/project/configuration_schema.mjs +160 -0
  122. package/dist/src/project/configuration_validation.d.mts +22 -0
  123. package/dist/src/project/configuration_validation.mjs +136 -0
  124. package/dist/src/project/install_scaffold.d.mts +42 -0
  125. package/dist/src/project/install_scaffold.mjs +263 -0
  126. package/dist/src/project/project_detection.d.mts +14 -0
  127. package/dist/src/project/project_detection.mjs +47 -0
  128. package/dist/src/project/setup.d.mts +87 -0
  129. package/dist/src/project/setup.mjs +414 -0
  130. package/dist/src/project/support_report.d.mts +88 -0
  131. package/dist/src/project/support_report.mjs +83 -0
  132. package/dist/src/runtime/cleanup.d.mts +85 -0
  133. package/dist/src/runtime/cleanup.mjs +311 -0
  134. package/dist/src/runtime/lifecycle_engine.d.mts +63 -0
  135. package/dist/src/runtime/lifecycle_engine.mjs +114 -0
  136. package/dist/src/runtime/migration_history.d.mts +51 -0
  137. package/dist/src/runtime/migration_history.mjs +170 -0
  138. package/dist/src/runtime/plan.d.mts +134 -0
  139. package/dist/src/runtime/plan.mjs +637 -0
  140. package/dist/src/runtime/policy.d.mts +66 -0
  141. package/dist/src/runtime/policy.mjs +369 -0
  142. package/dist/src/runtime/restore.d.mts +22 -0
  143. package/dist/src/runtime/restore.mjs +276 -0
  144. package/dist/src/runtime/service_environment.d.mts +11 -0
  145. package/dist/src/runtime/service_environment.mjs +58 -0
  146. package/dist/src/runtime/topology.d.mts +51 -0
  147. package/dist/src/runtime/topology.mjs +128 -0
  148. package/dist/src/shared/diagnostics.d.mts +97 -0
  149. package/dist/src/shared/diagnostics.mjs +170 -0
  150. package/dist/src/shared/human_output.d.mts +2 -0
  151. package/{src → dist/src}/shared/human_output.mjs +1 -3
  152. package/dist/src/shared/node_runtime.d.mts +14 -0
  153. package/dist/src/shared/node_runtime.mjs +25 -0
  154. package/dist/src/shared/operation_guard.d.mts +22 -0
  155. package/dist/src/shared/operation_guard.mjs +150 -0
  156. package/dist/src/shared/process_environment.d.mts +15 -0
  157. package/dist/src/shared/process_environment.mjs +50 -0
  158. package/dist/src/source/access.d.mts +165 -0
  159. package/dist/src/source/access.mjs +602 -0
  160. package/dist/src/source/asset_transfer.d.mts +27 -0
  161. package/dist/src/source/asset_transfer.mjs +202 -0
  162. package/dist/src/source/baseline.d.mts +66 -0
  163. package/dist/src/source/baseline.mjs +372 -0
  164. package/dist/src/source/external_reader.d.mts +42 -0
  165. package/dist/src/source/external_reader.mjs +278 -0
  166. package/dist/src/source/postgresql_access.d.mts +86 -0
  167. package/dist/src/source/postgresql_access.mjs +454 -0
  168. package/dist/src/source/privacy_paths.d.mts +10 -0
  169. package/dist/src/source/privacy_paths.mjs +27 -0
  170. package/dist/src/targets/driver.d.mts +2 -0
  171. package/dist/src/targets/driver.mjs +13 -0
  172. package/dist/src/targets/postgresql.d.mts +5 -0
  173. package/dist/src/targets/postgresql.mjs +620 -0
  174. package/dist/src/targets/supabase.d.mts +8 -0
  175. package/dist/src/targets/supabase.mjs +627 -0
  176. package/dist/src/targets/supabase_environment.d.mts +49 -0
  177. package/dist/src/targets/supabase_environment.mjs +162 -0
  178. package/dist/src/targets/target.d.mts +33 -0
  179. package/dist/src/targets/target.mjs +39 -0
  180. package/docs/README.md +1 -0
  181. package/docs/architecture.md +133 -10
  182. package/docs/configuration.md +9 -3
  183. package/docs/index.md +1 -0
  184. package/docs/releasing.md +8 -3
  185. package/docs/typescript.md +134 -0
  186. package/package.json +36 -29
  187. package/types/configuration.d.mts +5 -0
  188. package/scripts/install/create_root_config.mjs +0 -19
  189. package/src/README.md +0 -17
  190. package/src/application/session.mjs +0 -435
  191. package/src/baseline/artifact.mjs +0 -684
  192. package/src/baseline/builder.mjs +0 -164
  193. package/src/baseline/input_discovery.mjs +0 -155
  194. package/src/baseline/policy_review.mjs +0 -92
  195. package/src/baseline/preparation.mjs +0 -323
  196. package/src/baseline/privacy_engine.mjs +0 -1399
  197. package/src/baseline/privacy_structured_json.mjs +0 -554
  198. package/src/baseline/sanitization_policy.mjs +0 -299
  199. package/src/baseline/schema_snapshot.mjs +0 -113
  200. package/src/cli/arguments.mjs +0 -126
  201. package/src/cli/guided.mjs +0 -814
  202. package/src/cli/init_command.mjs +0 -94
  203. package/src/cli/rehearsal.mjs +0 -972
  204. package/src/cli/renderers.mjs +0 -638
  205. package/src/cli/runtime_commands.mjs +0 -707
  206. package/src/cli/source_commands.mjs +0 -355
  207. package/src/cli/terminal.mjs +0 -275
  208. package/src/identity/claim.mjs +0 -1165
  209. package/src/identity/provider_configuration.mjs +0 -177
  210. package/src/identity/storage.mjs +0 -165
  211. package/src/project/configuration.d.mts +0 -164
  212. package/src/project/configuration.mjs +0 -1337
  213. package/src/project/install_scaffold.mjs +0 -325
  214. package/src/project/setup.mjs +0 -474
  215. package/src/project/support_report.mjs +0 -105
  216. package/src/runtime/cleanup.mjs +0 -381
  217. package/src/runtime/migration_history.mjs +0 -220
  218. package/src/runtime/plan.mjs +0 -722
  219. package/src/runtime/policy.mjs +0 -438
  220. package/src/runtime/restore.mjs +0 -337
  221. package/src/runtime/service_environment.mjs +0 -82
  222. package/src/runtime/topology.mjs +0 -177
  223. package/src/shared/diagnostics.mjs +0 -207
  224. package/src/shared/node_runtime.mjs +0 -31
  225. package/src/shared/operation_guard.mjs +0 -162
  226. package/src/shared/process_environment.mjs +0 -68
  227. package/src/source/access.mjs +0 -798
  228. package/src/source/asset_transfer.mjs +0 -259
  229. package/src/source/baseline.mjs +0 -482
  230. package/src/source/external_reader.mjs +0 -368
  231. package/src/source/postgresql_access.mjs +0 -638
  232. package/src/source/privacy_paths.mjs +0 -38
  233. package/src/targets/postgresql.mjs +0 -808
  234. package/src/targets/supabase.mjs +0 -840
  235. package/src/targets/supabase_environment.mjs +0 -229
  236. package/src/targets/target.mjs +0 -66
@@ -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,37 @@
1
+ /** Render reviewed local OAuth declarations into the disposable Supabase runtime. */
2
+ export interface SupabaseAuthenticationProvider {
3
+ name: "google" | "github";
4
+ clientIdEnvironmentVariable: string;
5
+ clientSecretEnvironmentVariable: string;
6
+ skipNonceCheck?: boolean;
7
+ emailOptional?: boolean;
8
+ }
9
+ export interface SupabaseAuthentication {
10
+ providers: readonly SupabaseAuthenticationProvider[];
11
+ }
12
+ interface TomlSection {
13
+ name: string;
14
+ start: number;
15
+ end: number;
16
+ }
17
+ interface ProviderReplacementSection extends TomlSection {
18
+ provider: string;
19
+ }
20
+ interface ProviderTransition {
21
+ readonly replacedProviders: readonly string[];
22
+ readonly replacementSections: readonly ProviderReplacementSection[];
23
+ }
24
+ export declare const inspectSupabaseAuthenticationProviderTransition: ({ source, authentication, }: {
25
+ source: string;
26
+ authentication: SupabaseAuthentication | null;
27
+ }) => Readonly<ProviderTransition>;
28
+ export declare const renderSupabaseAuthenticationProviders: ({ authentication, apiPort, }: {
29
+ authentication: SupabaseAuthentication | null;
30
+ apiPort: number;
31
+ }) => string;
32
+ export declare const applySupabaseAuthenticationProviders: ({ source, authentication, apiPort, }: {
33
+ source: string;
34
+ authentication: SupabaseAuthentication | null;
35
+ apiPort: number;
36
+ }) => string;
37
+ export {};