@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.
- package/CHANGELOG.md +51 -1
- package/README.md +1 -0
- package/dist/package.json +101 -0
- package/dist/scripts/install/create_root_config.d.mts +3 -0
- package/dist/scripts/install/create_root_config.mjs +13 -0
- package/dist/scripts/runtime/manage_database.d.mts +6 -0
- package/{scripts → dist/scripts}/runtime/manage_database.mjs +6 -8
- package/dist/src/application/session.d.mts +96 -0
- package/dist/src/application/session.mjs +380 -0
- package/dist/src/baseline/artifact.d.mts +119 -0
- package/dist/src/baseline/artifact.mjs +522 -0
- package/dist/src/baseline/builder.d.mts +19 -0
- package/dist/src/baseline/builder.mjs +142 -0
- package/dist/src/baseline/input_discovery.d.mts +13 -0
- package/dist/src/baseline/input_discovery.mjs +149 -0
- package/dist/src/baseline/policy_review.d.mts +56 -0
- package/dist/src/baseline/policy_review.mjs +73 -0
- package/dist/src/baseline/preparation.d.mts +63 -0
- package/dist/src/baseline/preparation.mjs +295 -0
- package/dist/src/baseline/privacy_contract.d.mts +147 -0
- package/dist/src/baseline/privacy_contract.mjs +2 -0
- package/dist/src/baseline/privacy_dependency_graph.d.mts +7 -0
- package/dist/src/baseline/privacy_dependency_graph.mjs +110 -0
- package/dist/src/baseline/privacy_engine.d.mts +5 -0
- package/dist/src/baseline/privacy_engine.mjs +4 -0
- package/dist/src/baseline/privacy_key.d.mts +7 -0
- package/dist/src/baseline/privacy_key.mjs +42 -0
- package/dist/src/baseline/privacy_pipeline.d.mts +7 -0
- package/dist/src/baseline/privacy_pipeline.mjs +127 -0
- package/dist/src/baseline/privacy_policy.d.mts +6 -0
- package/dist/src/baseline/privacy_policy.mjs +237 -0
- package/dist/src/baseline/privacy_recipe_validation.d.mts +5 -0
- package/dist/src/baseline/privacy_recipe_validation.mjs +289 -0
- package/dist/src/baseline/privacy_rule_registry.d.mts +18 -0
- package/dist/src/baseline/privacy_rule_registry.mjs +143 -0
- package/dist/src/baseline/privacy_structured_json.d.mts +66 -0
- package/dist/src/baseline/privacy_structured_json.mjs +464 -0
- package/dist/src/baseline/privacy_transforms.d.mts +18 -0
- package/dist/src/baseline/privacy_transforms.mjs +142 -0
- package/dist/src/baseline/privacy_validation_shared.d.mts +31 -0
- package/dist/src/baseline/privacy_validation_shared.mjs +127 -0
- package/dist/src/baseline/sanitization_policy.d.mts +69 -0
- package/dist/src/baseline/sanitization_policy.mjs +227 -0
- package/dist/src/baseline/schema_snapshot.d.mts +19 -0
- package/dist/src/baseline/schema_snapshot.mjs +79 -0
- package/dist/src/cli/application_command_handlers.d.mts +8 -0
- package/dist/src/cli/application_command_handlers.mjs +52 -0
- package/dist/src/cli/arguments.d.mts +38 -0
- package/dist/src/cli/arguments.mjs +128 -0
- package/dist/src/cli/basic_command_handlers.d.mts +4 -0
- package/dist/src/cli/basic_command_handlers.mjs +56 -0
- package/dist/src/cli/cli_commands.d.mts +14 -0
- package/dist/src/cli/cli_commands.mjs +32 -0
- package/dist/src/cli/command_contract.d.mts +57 -0
- package/dist/src/cli/command_contract.mjs +39 -0
- package/dist/src/cli/command_output.d.mts +12 -0
- package/dist/src/cli/command_output.mjs +20 -0
- package/dist/src/cli/command_registry.d.mts +7 -0
- package/dist/src/cli/command_registry.mjs +14 -0
- package/dist/src/cli/guided.d.mts +53 -0
- package/dist/src/cli/guided.mjs +719 -0
- package/dist/src/cli/identity_command_handlers.d.mts +8 -0
- package/dist/src/cli/identity_command_handlers.mjs +43 -0
- package/dist/src/cli/init_command.d.mts +21 -0
- package/dist/src/cli/init_command.mjs +90 -0
- package/dist/src/cli/inspection_command_handlers.d.mts +3 -0
- package/dist/src/cli/inspection_command_handlers.mjs +79 -0
- package/dist/src/cli/project_command_handlers.d.mts +11 -0
- package/dist/src/cli/project_command_handlers.mjs +92 -0
- package/dist/src/cli/project_commands.d.mts +159 -0
- package/dist/src/cli/project_commands.mjs +183 -0
- package/dist/src/cli/rehearsal.d.mts +7 -0
- package/dist/src/cli/rehearsal.mjs +190 -0
- package/dist/src/cli/renderers.d.mts +299 -0
- package/dist/src/cli/renderers.mjs +554 -0
- package/dist/src/cli/runtime_action_handlers.d.mts +12 -0
- package/dist/src/cli/runtime_action_handlers.mjs +201 -0
- package/dist/src/cli/runtime_commands.d.mts +269 -0
- package/dist/src/cli/runtime_commands.mjs +601 -0
- package/dist/src/cli/source_command_handlers.d.mts +8 -0
- package/dist/src/cli/source_command_handlers.mjs +62 -0
- package/dist/src/cli/source_commands.d.mts +436 -0
- package/dist/src/cli/source_commands.mjs +301 -0
- package/dist/src/cli/terminal.d.mts +59 -0
- package/dist/src/cli/terminal.mjs +230 -0
- package/dist/src/identity/claim.d.mts +5 -0
- package/dist/src/identity/claim.mjs +4 -0
- package/dist/src/identity/claim_contract.d.mts +135 -0
- package/dist/src/identity/claim_contract.mjs +6 -0
- package/dist/src/identity/claim_executor.d.mts +10 -0
- package/dist/src/identity/claim_executor.mjs +358 -0
- package/dist/src/identity/claim_identity_lookup.d.mts +17 -0
- package/dist/src/identity/claim_identity_lookup.mjs +125 -0
- package/dist/src/identity/claim_plan.d.mts +8 -0
- package/dist/src/identity/claim_plan.mjs +61 -0
- package/dist/src/identity/claim_policy.d.mts +3 -0
- package/dist/src/identity/claim_policy.mjs +377 -0
- package/dist/src/identity/claim_sql.d.mts +10 -0
- package/dist/src/identity/claim_sql.mjs +46 -0
- package/dist/src/identity/claim_verification.d.mts +13 -0
- package/dist/src/identity/claim_verification.mjs +35 -0
- package/dist/src/identity/provider_configuration.d.mts +37 -0
- package/dist/src/identity/provider_configuration.mjs +132 -0
- package/dist/src/identity/storage.d.mts +21 -0
- package/dist/src/identity/storage.mjs +122 -0
- package/dist/src/index.d.mts +4 -0
- package/dist/src/index.mjs +3 -0
- package/dist/src/project/configuration.d.mts +13 -0
- package/dist/src/project/configuration.mjs +9 -0
- package/dist/src/project/configuration_contract.d.mts +125 -0
- package/dist/src/project/configuration_contract.mjs +27 -0
- package/dist/src/project/configuration_discovery.d.mts +7 -0
- package/dist/src/project/configuration_discovery.mjs +37 -0
- package/dist/src/project/configuration_normalization.d.mts +107 -0
- package/dist/src/project/configuration_normalization.mjs +302 -0
- package/dist/src/project/configuration_rendering.d.mts +14 -0
- package/dist/src/project/configuration_rendering.mjs +224 -0
- package/dist/src/project/configuration_resolution.d.mts +125 -0
- package/dist/src/project/configuration_resolution.mjs +81 -0
- package/dist/src/project/configuration_schema.d.mts +22 -0
- package/dist/src/project/configuration_schema.mjs +160 -0
- package/dist/src/project/configuration_validation.d.mts +22 -0
- package/dist/src/project/configuration_validation.mjs +136 -0
- package/dist/src/project/install_scaffold.d.mts +42 -0
- package/dist/src/project/install_scaffold.mjs +263 -0
- package/dist/src/project/project_detection.d.mts +14 -0
- package/dist/src/project/project_detection.mjs +47 -0
- package/dist/src/project/setup.d.mts +87 -0
- package/dist/src/project/setup.mjs +414 -0
- package/dist/src/project/support_report.d.mts +88 -0
- package/dist/src/project/support_report.mjs +83 -0
- package/dist/src/runtime/cleanup.d.mts +85 -0
- package/dist/src/runtime/cleanup.mjs +311 -0
- package/dist/src/runtime/lifecycle_engine.d.mts +63 -0
- package/dist/src/runtime/lifecycle_engine.mjs +114 -0
- package/dist/src/runtime/migration_history.d.mts +51 -0
- package/dist/src/runtime/migration_history.mjs +170 -0
- package/dist/src/runtime/plan.d.mts +134 -0
- package/dist/src/runtime/plan.mjs +637 -0
- package/dist/src/runtime/policy.d.mts +66 -0
- package/dist/src/runtime/policy.mjs +369 -0
- package/dist/src/runtime/restore.d.mts +22 -0
- package/dist/src/runtime/restore.mjs +276 -0
- package/dist/src/runtime/service_environment.d.mts +11 -0
- package/dist/src/runtime/service_environment.mjs +58 -0
- package/dist/src/runtime/topology.d.mts +51 -0
- package/dist/src/runtime/topology.mjs +128 -0
- package/dist/src/shared/diagnostics.d.mts +97 -0
- package/dist/src/shared/diagnostics.mjs +170 -0
- package/dist/src/shared/human_output.d.mts +2 -0
- package/{src → dist/src}/shared/human_output.mjs +1 -3
- package/dist/src/shared/node_runtime.d.mts +14 -0
- package/dist/src/shared/node_runtime.mjs +25 -0
- package/dist/src/shared/operation_guard.d.mts +22 -0
- package/dist/src/shared/operation_guard.mjs +150 -0
- package/dist/src/shared/process_environment.d.mts +15 -0
- package/dist/src/shared/process_environment.mjs +50 -0
- package/dist/src/source/access.d.mts +165 -0
- package/dist/src/source/access.mjs +602 -0
- package/dist/src/source/asset_transfer.d.mts +27 -0
- package/dist/src/source/asset_transfer.mjs +202 -0
- package/dist/src/source/baseline.d.mts +66 -0
- package/dist/src/source/baseline.mjs +372 -0
- package/dist/src/source/external_reader.d.mts +42 -0
- package/dist/src/source/external_reader.mjs +278 -0
- package/dist/src/source/postgresql_access.d.mts +86 -0
- package/dist/src/source/postgresql_access.mjs +454 -0
- package/dist/src/source/privacy_paths.d.mts +10 -0
- package/dist/src/source/privacy_paths.mjs +27 -0
- package/dist/src/targets/driver.d.mts +2 -0
- package/dist/src/targets/driver.mjs +13 -0
- package/dist/src/targets/postgresql.d.mts +5 -0
- package/dist/src/targets/postgresql.mjs +620 -0
- package/dist/src/targets/supabase.d.mts +8 -0
- package/dist/src/targets/supabase.mjs +627 -0
- package/dist/src/targets/supabase_environment.d.mts +49 -0
- package/dist/src/targets/supabase_environment.mjs +162 -0
- package/dist/src/targets/target.d.mts +33 -0
- package/dist/src/targets/target.mjs +39 -0
- package/docs/README.md +1 -0
- package/docs/architecture.md +133 -10
- package/docs/configuration.md +9 -3
- package/docs/index.md +1 -0
- package/docs/releasing.md +8 -3
- package/docs/typescript.md +134 -0
- package/package.json +36 -29
- package/types/configuration.d.mts +5 -0
- package/scripts/install/create_root_config.mjs +0 -19
- package/src/README.md +0 -17
- package/src/application/session.mjs +0 -435
- package/src/baseline/artifact.mjs +0 -684
- package/src/baseline/builder.mjs +0 -164
- package/src/baseline/input_discovery.mjs +0 -155
- package/src/baseline/policy_review.mjs +0 -92
- package/src/baseline/preparation.mjs +0 -323
- package/src/baseline/privacy_engine.mjs +0 -1399
- package/src/baseline/privacy_structured_json.mjs +0 -554
- package/src/baseline/sanitization_policy.mjs +0 -299
- package/src/baseline/schema_snapshot.mjs +0 -113
- package/src/cli/arguments.mjs +0 -126
- package/src/cli/guided.mjs +0 -814
- package/src/cli/init_command.mjs +0 -94
- package/src/cli/rehearsal.mjs +0 -972
- package/src/cli/renderers.mjs +0 -638
- package/src/cli/runtime_commands.mjs +0 -707
- package/src/cli/source_commands.mjs +0 -355
- package/src/cli/terminal.mjs +0 -275
- package/src/identity/claim.mjs +0 -1165
- package/src/identity/provider_configuration.mjs +0 -177
- package/src/identity/storage.mjs +0 -165
- package/src/project/configuration.d.mts +0 -164
- package/src/project/configuration.mjs +0 -1337
- package/src/project/install_scaffold.mjs +0 -325
- package/src/project/setup.mjs +0 -474
- package/src/project/support_report.mjs +0 -105
- package/src/runtime/cleanup.mjs +0 -381
- package/src/runtime/migration_history.mjs +0 -220
- package/src/runtime/plan.mjs +0 -722
- package/src/runtime/policy.mjs +0 -438
- package/src/runtime/restore.mjs +0 -337
- package/src/runtime/service_environment.mjs +0 -82
- package/src/runtime/topology.mjs +0 -177
- package/src/shared/diagnostics.mjs +0 -207
- package/src/shared/node_runtime.mjs +0 -31
- package/src/shared/operation_guard.mjs +0 -162
- package/src/shared/process_environment.mjs +0 -68
- package/src/source/access.mjs +0 -798
- package/src/source/asset_transfer.mjs +0 -259
- package/src/source/baseline.mjs +0 -482
- package/src/source/external_reader.mjs +0 -368
- package/src/source/postgresql_access.mjs +0 -638
- package/src/source/privacy_paths.mjs +0 -38
- package/src/targets/postgresql.mjs +0 -808
- package/src/targets/supabase.mjs +0 -840
- package/src/targets/supabase_environment.mjs +0 -229
- 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,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,17 @@
|
|
|
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 const findVerifiedLocalIdentity: ({ client, plan, matchValue, }: {
|
|
14
|
+
client: IdentityDatabaseClient;
|
|
15
|
+
plan: IdentityClaimPlan;
|
|
16
|
+
matchValue: string;
|
|
17
|
+
}) => Promise<VerifiedLocalIdentity>;
|
|
@@ -0,0 +1,125 @@
|
|
|
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 const findVerifiedLocalIdentity = async ({ client, plan, matchValue, }) => {
|
|
52
|
+
const matcher = plan.identity.matcher;
|
|
53
|
+
if (matcher.type === "verified-email") {
|
|
54
|
+
const match = await client.query(`select u.id::text, i.provider
|
|
55
|
+
from auth.users u
|
|
56
|
+
join auth.identities i on i.user_id = u.id
|
|
57
|
+
where lower(coalesce(i.identity_data->>'email', u.email)) = $1
|
|
58
|
+
and i.provider = any($2::text[])
|
|
59
|
+
and (
|
|
60
|
+
coalesce((i.identity_data->>'email_verified')::boolean, false)
|
|
61
|
+
or u.email_confirmed_at is not null
|
|
62
|
+
)
|
|
63
|
+
for update of u`, [matchValue, matcher.providers]);
|
|
64
|
+
if (match.rows.length !== 1) {
|
|
65
|
+
throw new Error(match.rows.length === 0
|
|
66
|
+
? "No verified local identity matches the reviewed email and provider allowlist."
|
|
67
|
+
: "More than one verified local identity matches the reviewed email and provider allowlist.");
|
|
68
|
+
}
|
|
69
|
+
const matchedRow = match.rows[0];
|
|
70
|
+
if (!matchedRow)
|
|
71
|
+
throw new Error("Matched local identity is invalid.");
|
|
72
|
+
const matchedProvider = matchedRow.provider ??
|
|
73
|
+
(matcher.providers.length === 1 ? matcher.providers[0] : null);
|
|
74
|
+
if (typeof matchedProvider !== "string" ||
|
|
75
|
+
!matcher.providers.includes(matchedProvider)) {
|
|
76
|
+
throw new Error("Matched local identity provider is invalid.");
|
|
77
|
+
}
|
|
78
|
+
if (typeof matchedRow.id !== "string") {
|
|
79
|
+
throw new Error("Matched local identity is invalid.");
|
|
80
|
+
}
|
|
81
|
+
return {
|
|
82
|
+
id: matchedRow.id,
|
|
83
|
+
provider: matchedProvider,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
const candidates = await client.query(`select u.id::text,
|
|
87
|
+
i.provider,
|
|
88
|
+
i.provider_id::text,
|
|
89
|
+
i.identity_data->>'sub' as subject
|
|
90
|
+
from auth.users u
|
|
91
|
+
join auth.identities i on i.user_id = u.id
|
|
92
|
+
where i.provider = $1
|
|
93
|
+
and (
|
|
94
|
+
coalesce((i.identity_data->>'email_verified')::boolean, false)
|
|
95
|
+
or u.email_confirmed_at is not null
|
|
96
|
+
)
|
|
97
|
+
for update of u`, [matcher.provider]);
|
|
98
|
+
const matches = [];
|
|
99
|
+
for (const candidate of candidates.rows) {
|
|
100
|
+
const subjects = [candidate.provider_id, candidate.subject]
|
|
101
|
+
.filter((value) => typeof value === "string" && Boolean(value.trim()))
|
|
102
|
+
.map((value) => value.trim());
|
|
103
|
+
const uniqueSubjects = [...new Set(subjects)];
|
|
104
|
+
if (uniqueSubjects.some((subject) => hash(subject) === matcher.approvedSubjectSha256)) {
|
|
105
|
+
if (uniqueSubjects.length !== 1) {
|
|
106
|
+
throw new Error("The matched local provider identity has conflicting stable subjects.");
|
|
107
|
+
}
|
|
108
|
+
if (typeof candidate.id !== "string" ||
|
|
109
|
+
candidate.provider !== matcher.provider) {
|
|
110
|
+
throw new Error("Matched local identity provider is invalid.");
|
|
111
|
+
}
|
|
112
|
+
matches.push({ id: candidate.id, provider: matcher.provider });
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
if (matches.length !== 1) {
|
|
116
|
+
throw new Error(matches.length === 0
|
|
117
|
+
? "No verified local identity matches the reviewed provider subject."
|
|
118
|
+
: "More than one verified local identity matches the reviewed provider subject.");
|
|
119
|
+
}
|
|
120
|
+
const match = matches[0];
|
|
121
|
+
if (!match || match.provider !== matcher.provider) {
|
|
122
|
+
throw new Error("Matched local identity provider is invalid.");
|
|
123
|
+
}
|
|
124
|
+
return match;
|
|
125
|
+
};
|
|
@@ -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;
|