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