@rehearsal-db/core 0.1.0-beta.10 → 0.1.0-beta.11
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 +28 -1
- package/COMPATIBILITY.md +9 -0
- package/README.md +18 -7
- package/docs/README.md +25 -16
- package/docs/commands.md +10 -4
- package/docs/configuration.md +39 -9
- package/docs/getting-started.md +13 -4
- package/docs/index.md +47 -0
- package/docs/runtime-policies.md +65 -5
- package/docs/sanitization.md +104 -3
- package/docs/standalone-workflow.md +2 -0
- package/docs/troubleshooting.md +25 -0
- package/package.json +10 -2
- package/scripts/install/create_root_config.mjs +36 -0
- package/src/baseline/privacy_engine.mjs +358 -30
- package/src/cli/init_command.mjs +77 -0
- package/src/cli/rehearsal.mjs +6 -51
- package/src/cli/renderers.mjs +7 -2
- package/src/identity/claim.mjs +232 -44
- package/src/project/configuration.mjs +4 -2
- package/src/project/install_scaffold.mjs +291 -0
- package/src/project/setup.mjs +157 -52
package/src/cli/rehearsal.mjs
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { createHash } from "node:crypto";
|
|
9
|
-
import { readFile
|
|
9
|
+
import { readFile } from "node:fs/promises";
|
|
10
10
|
import { join, relative } from "node:path";
|
|
11
11
|
import { performance } from "node:perf_hooks";
|
|
12
12
|
import { parseEnv } from "node:util";
|
|
@@ -18,12 +18,7 @@ import {
|
|
|
18
18
|
renderHumanError,
|
|
19
19
|
serializeRehearsalError,
|
|
20
20
|
} from "../shared/diagnostics.mjs";
|
|
21
|
-
import {
|
|
22
|
-
inspectDetectedProject,
|
|
23
|
-
findRehearsalConfigPath,
|
|
24
|
-
loadRehearsalConfig,
|
|
25
|
-
renderDetectedConfig,
|
|
26
|
-
} from "../project/configuration.mjs";
|
|
21
|
+
import { loadRehearsalConfig } from "../project/configuration.mjs";
|
|
27
22
|
import {
|
|
28
23
|
inspectRehearsalBaseline,
|
|
29
24
|
inspectRehearsalMigrations,
|
|
@@ -113,6 +108,7 @@ import {
|
|
|
113
108
|
renderSourceRetirement,
|
|
114
109
|
renderSupportReport,
|
|
115
110
|
} from "./renderers.mjs";
|
|
111
|
+
import { runRehearsalInit } from "./init_command.mjs";
|
|
116
112
|
|
|
117
113
|
const packageRoot = fileURLToPath(new URL("../..", import.meta.url));
|
|
118
114
|
const projectRoot = process.cwd();
|
|
@@ -148,54 +144,13 @@ const emit = ({ command, data, flags, render, status = "success" }) => {
|
|
|
148
144
|
else console.log(render(data, flags.verbosity));
|
|
149
145
|
};
|
|
150
146
|
|
|
151
|
-
const runInit = async ({ flags }) => {
|
|
152
|
-
const detected = await inspectDetectedProject({ projectRoot });
|
|
153
|
-
const source = renderDetectedConfig(detected);
|
|
154
|
-
const destination = join(projectRoot, "rehearsal.config.mjs");
|
|
155
|
-
let existingPath = null;
|
|
156
|
-
try {
|
|
157
|
-
existingPath = await findRehearsalConfigPath({ projectRoot });
|
|
158
|
-
} catch (error) {
|
|
159
|
-
if (
|
|
160
|
-
!String(error?.message ?? error).startsWith("No Rehearsal configuration")
|
|
161
|
-
) {
|
|
162
|
-
throw error;
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
if (existingPath) {
|
|
166
|
-
if (flags.write) {
|
|
167
|
-
throw new Error(
|
|
168
|
-
`A Rehearsal configuration already exists at ${relative(projectRoot, existingPath)}; Rehearsal will not overwrite it.`,
|
|
169
|
-
);
|
|
170
|
-
}
|
|
171
|
-
return {
|
|
172
|
-
mode: "existing",
|
|
173
|
-
destination: relative(projectRoot, existingPath),
|
|
174
|
-
detected,
|
|
175
|
-
source: "",
|
|
176
|
-
nextAction:
|
|
177
|
-
"Review the existing configuration, then run rehearsal doctor.",
|
|
178
|
-
};
|
|
179
|
-
}
|
|
180
|
-
if (flags.write)
|
|
181
|
-
await writeFile(destination, source, { flag: "wx", mode: 0o600 });
|
|
182
|
-
return {
|
|
183
|
-
mode: flags.write ? "written" : "preview",
|
|
184
|
-
destination: relative(projectRoot, destination),
|
|
185
|
-
detected,
|
|
186
|
-
source,
|
|
187
|
-
nextAction: flags.write
|
|
188
|
-
? "Review the generated safety settings and run rehearsal doctor."
|
|
189
|
-
: "Review this preview, then rerun rehearsal init --write to create it.",
|
|
190
|
-
};
|
|
191
|
-
};
|
|
192
|
-
|
|
193
147
|
const runSetup = async ({ flags, planOptions }) => {
|
|
194
148
|
const plan =
|
|
195
149
|
flags.setupPlan ??
|
|
196
150
|
(await planRehearsalSetup({
|
|
197
151
|
projectRoot,
|
|
198
|
-
|
|
152
|
+
configPath: planOptions.configPath,
|
|
153
|
+
target: flags.target,
|
|
199
154
|
}));
|
|
200
155
|
if (flags.write) await applyRehearsalSetup(plan);
|
|
201
156
|
const result = summarizeRehearsalSetup(plan, {
|
|
@@ -417,7 +372,7 @@ const executeCommand = async ({ command, flags, planOptions, guided }) => {
|
|
|
417
372
|
return;
|
|
418
373
|
}
|
|
419
374
|
if (command === "init") {
|
|
420
|
-
const data = await
|
|
375
|
+
const data = await runRehearsalInit({ projectRoot, flags });
|
|
421
376
|
emit({ command, data, flags, render: renderInit });
|
|
422
377
|
return;
|
|
423
378
|
}
|
package/src/cli/renderers.mjs
CHANGED
|
@@ -566,8 +566,13 @@ export const renderIdentityPlan = ({ mode, plan }) =>
|
|
|
566
566
|
[
|
|
567
567
|
`LOCAL IDENTITY — ${mode.toUpperCase()}`,
|
|
568
568
|
`Identity: ${plan.review.name}`,
|
|
569
|
-
`
|
|
570
|
-
|
|
569
|
+
`Matcher: ${plan.review.matcher.type}`,
|
|
570
|
+
plan.review.matcher.type === "verified-email"
|
|
571
|
+
? `Providers: ${plan.review.matcher.providers.join(", ")}`
|
|
572
|
+
: `Provider: ${plan.review.matcher.provider}`,
|
|
573
|
+
plan.review.matcher.type === "verified-email"
|
|
574
|
+
? `Approved email receipt: ${plan.review.matcher.approvedEmailSha256}`
|
|
575
|
+
: `Approved subject receipt: ${plan.review.matcher.approvedSubjectSha256}`,
|
|
571
576
|
`Relational references: ${plan.review.references.length}`,
|
|
572
577
|
`Nested JSON references: ${plan.review.jsonReferences.length}`,
|
|
573
578
|
`Signup defaults: ${plan.review.signupDefaults.length}`,
|
package/src/identity/claim.mjs
CHANGED
|
@@ -12,6 +12,12 @@ const UUID =
|
|
|
12
12
|
/^[a-f0-9]{8}-[a-f0-9]{4}-[1-5][a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$/iu;
|
|
13
13
|
const SHA256 = /^[a-f0-9]{64}$/u;
|
|
14
14
|
const ENVIRONMENT_KEY = /^[A-Z][A-Z0-9_]*$/u;
|
|
15
|
+
const SUPPORTED_IDENTITY_PROVIDERS = Object.freeze([
|
|
16
|
+
"email",
|
|
17
|
+
"github",
|
|
18
|
+
"google",
|
|
19
|
+
]);
|
|
20
|
+
const identityPlanValues = new WeakMap();
|
|
15
21
|
|
|
16
22
|
const isObject = (value) =>
|
|
17
23
|
value !== null && typeof value === "object" && !Array.isArray(value);
|
|
@@ -78,6 +84,114 @@ const defaultMatcher = (value, label) => {
|
|
|
78
84
|
return Object.freeze({ kind: "exact", value: jsonValue(value, label) });
|
|
79
85
|
};
|
|
80
86
|
|
|
87
|
+
const provider = (value, label, { subject = false } = {}) => {
|
|
88
|
+
if (!SUPPORTED_IDENTITY_PROVIDERS.includes(value)) {
|
|
89
|
+
throw new Error(`${label} is unsupported.`);
|
|
90
|
+
}
|
|
91
|
+
if (subject && value === "email") {
|
|
92
|
+
throw new Error(
|
|
93
|
+
`${label} cannot use email with a provider-subject matcher.`,
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
return value;
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const normalizeIdentityMatcher = (entry, label) => {
|
|
100
|
+
const legacyKeys = [
|
|
101
|
+
"provider",
|
|
102
|
+
"emailEnvironmentVariable",
|
|
103
|
+
"approvedEmailSha256",
|
|
104
|
+
];
|
|
105
|
+
const hasLegacyMatcher = legacyKeys.some((key) => entry[key] !== undefined);
|
|
106
|
+
if (entry.matcher !== undefined && hasLegacyMatcher) {
|
|
107
|
+
throw new Error(
|
|
108
|
+
`${label} cannot combine matcher with legacy provider and email fields.`,
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
if (entry.matcher === undefined) {
|
|
112
|
+
const legacyProvider = provider(entry.provider, `${label}.provider`);
|
|
113
|
+
if (!ENVIRONMENT_KEY.test(entry.emailEnvironmentVariable ?? "")) {
|
|
114
|
+
throw new Error(`${label}.emailEnvironmentVariable is invalid.`);
|
|
115
|
+
}
|
|
116
|
+
if (!SHA256.test(entry.approvedEmailSha256 ?? "")) {
|
|
117
|
+
throw new Error(`${label}.approvedEmailSha256 must be a SHA-256.`);
|
|
118
|
+
}
|
|
119
|
+
return Object.freeze({
|
|
120
|
+
type: "verified-email",
|
|
121
|
+
providers: Object.freeze([legacyProvider]),
|
|
122
|
+
emailEnvironmentVariable: entry.emailEnvironmentVariable,
|
|
123
|
+
approvedEmailSha256: entry.approvedEmailSha256,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
if (!isObject(entry.matcher)) {
|
|
127
|
+
throw new Error(`${label}.matcher must be an object.`);
|
|
128
|
+
}
|
|
129
|
+
const matcherLabel = `${label}.matcher`;
|
|
130
|
+
if (entry.matcher.type === "verified-email") {
|
|
131
|
+
keys(
|
|
132
|
+
entry.matcher,
|
|
133
|
+
["type", "providers", "emailEnvironmentVariable", "approvedEmailSha256"],
|
|
134
|
+
matcherLabel,
|
|
135
|
+
);
|
|
136
|
+
if (
|
|
137
|
+
!Array.isArray(entry.matcher.providers) ||
|
|
138
|
+
entry.matcher.providers.length === 0
|
|
139
|
+
) {
|
|
140
|
+
throw new Error(`${matcherLabel}.providers must not be empty.`);
|
|
141
|
+
}
|
|
142
|
+
const providers = entry.matcher.providers.map((value, index) =>
|
|
143
|
+
provider(value, `${matcherLabel}.providers[${index}]`),
|
|
144
|
+
);
|
|
145
|
+
if (new Set(providers).size !== providers.length) {
|
|
146
|
+
throw new Error(`${matcherLabel}.providers must not contain duplicates.`);
|
|
147
|
+
}
|
|
148
|
+
if (!ENVIRONMENT_KEY.test(entry.matcher.emailEnvironmentVariable ?? "")) {
|
|
149
|
+
throw new Error(`${matcherLabel}.emailEnvironmentVariable is invalid.`);
|
|
150
|
+
}
|
|
151
|
+
if (!SHA256.test(entry.matcher.approvedEmailSha256 ?? "")) {
|
|
152
|
+
throw new Error(`${matcherLabel}.approvedEmailSha256 must be a SHA-256.`);
|
|
153
|
+
}
|
|
154
|
+
return Object.freeze({
|
|
155
|
+
type: "verified-email",
|
|
156
|
+
providers: Object.freeze(providers),
|
|
157
|
+
emailEnvironmentVariable: entry.matcher.emailEnvironmentVariable,
|
|
158
|
+
approvedEmailSha256: entry.matcher.approvedEmailSha256,
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
if (entry.matcher.type === "provider-subject") {
|
|
162
|
+
keys(
|
|
163
|
+
entry.matcher,
|
|
164
|
+
[
|
|
165
|
+
"type",
|
|
166
|
+
"provider",
|
|
167
|
+
"subjectEnvironmentVariable",
|
|
168
|
+
"approvedSubjectSha256",
|
|
169
|
+
],
|
|
170
|
+
matcherLabel,
|
|
171
|
+
);
|
|
172
|
+
const subjectProvider = provider(
|
|
173
|
+
entry.matcher.provider,
|
|
174
|
+
`${matcherLabel}.provider`,
|
|
175
|
+
{ subject: true },
|
|
176
|
+
);
|
|
177
|
+
if (!ENVIRONMENT_KEY.test(entry.matcher.subjectEnvironmentVariable ?? "")) {
|
|
178
|
+
throw new Error(`${matcherLabel}.subjectEnvironmentVariable is invalid.`);
|
|
179
|
+
}
|
|
180
|
+
if (!SHA256.test(entry.matcher.approvedSubjectSha256 ?? "")) {
|
|
181
|
+
throw new Error(
|
|
182
|
+
`${matcherLabel}.approvedSubjectSha256 must be a SHA-256.`,
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
return Object.freeze({
|
|
186
|
+
type: "provider-subject",
|
|
187
|
+
provider: subjectProvider,
|
|
188
|
+
subjectEnvironmentVariable: entry.matcher.subjectEnvironmentVariable,
|
|
189
|
+
approvedSubjectSha256: entry.matcher.approvedSubjectSha256,
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
throw new Error(`${matcherLabel}.type is unsupported.`);
|
|
193
|
+
};
|
|
194
|
+
|
|
81
195
|
export const validateIdentityPolicy = (policy) => {
|
|
82
196
|
if (
|
|
83
197
|
!isObject(policy) ||
|
|
@@ -100,6 +214,7 @@ export const validateIdentityPolicy = (policy) => {
|
|
|
100
214
|
if (
|
|
101
215
|
![
|
|
102
216
|
"name",
|
|
217
|
+
"matcher",
|
|
103
218
|
"provider",
|
|
104
219
|
"emailEnvironmentVariable",
|
|
105
220
|
"approvedEmailSha256",
|
|
@@ -121,14 +236,7 @@ export const validateIdentityPolicy = (policy) => {
|
|
|
121
236
|
const name = entry.name;
|
|
122
237
|
if (names.has(name)) throw new Error(`Identity policy duplicates ${name}.`);
|
|
123
238
|
names.add(name);
|
|
124
|
-
|
|
125
|
-
throw new Error(`${label}.provider is unsupported.`);
|
|
126
|
-
if (!ENVIRONMENT_KEY.test(entry.emailEnvironmentVariable ?? "")) {
|
|
127
|
-
throw new Error(`${label}.emailEnvironmentVariable is invalid.`);
|
|
128
|
-
}
|
|
129
|
-
if (!SHA256.test(entry.approvedEmailSha256 ?? "")) {
|
|
130
|
-
throw new Error(`${label}.approvedEmailSha256 must be a SHA-256.`);
|
|
131
|
-
}
|
|
239
|
+
const matcher = normalizeIdentityMatcher(entry, label);
|
|
132
240
|
if (!UUID.test(entry.placeholderUserId ?? ""))
|
|
133
241
|
throw new Error(`${label}.placeholderUserId is invalid.`);
|
|
134
242
|
const references = (entry.references ?? []).map(
|
|
@@ -341,9 +449,7 @@ export const validateIdentityPolicy = (policy) => {
|
|
|
341
449
|
}
|
|
342
450
|
return Object.freeze({
|
|
343
451
|
name,
|
|
344
|
-
|
|
345
|
-
emailEnvironmentVariable: entry.emailEnvironmentVariable,
|
|
346
|
-
approvedEmailSha256: entry.approvedEmailSha256,
|
|
452
|
+
matcher,
|
|
347
453
|
placeholderUserId: entry.placeholderUserId.toLowerCase(),
|
|
348
454
|
references: Object.freeze(references),
|
|
349
455
|
jsonReferences: Object.freeze(jsonReferences),
|
|
@@ -368,20 +474,32 @@ export const createIdentityClaimPlan = ({
|
|
|
368
474
|
const normalized = validateIdentityPolicy(policy);
|
|
369
475
|
const identity = normalized.identities.find((entry) => entry.name === name);
|
|
370
476
|
if (!identity) throw new Error(`Identity policy does not declare ${name}.`);
|
|
371
|
-
const
|
|
372
|
-
|
|
373
|
-
.
|
|
374
|
-
|
|
477
|
+
const matcher = identity.matcher;
|
|
478
|
+
const environmentVariable =
|
|
479
|
+
matcher.type === "verified-email"
|
|
480
|
+
? matcher.emailEnvironmentVariable
|
|
481
|
+
: matcher.subjectEnvironmentVariable;
|
|
482
|
+
const rawValue = environment[environmentVariable];
|
|
483
|
+
const matchValue =
|
|
484
|
+
matcher.type === "verified-email"
|
|
485
|
+
? rawValue?.trim().toLowerCase()
|
|
486
|
+
: rawValue?.trim();
|
|
487
|
+
const approvedSha256 =
|
|
488
|
+
matcher.type === "verified-email"
|
|
489
|
+
? matcher.approvedEmailSha256
|
|
490
|
+
: matcher.approvedSubjectSha256;
|
|
491
|
+
if (!matchValue || hash(matchValue) !== approvedSha256) {
|
|
375
492
|
throw new Error(
|
|
376
|
-
|
|
493
|
+
matcher.type === "verified-email"
|
|
494
|
+
? "Local identity email does not match the reviewed receipt."
|
|
495
|
+
: "Local identity provider subject does not match the reviewed receipt.",
|
|
377
496
|
);
|
|
378
497
|
}
|
|
379
498
|
const review = {
|
|
380
|
-
planVersion:
|
|
499
|
+
planVersion: 3,
|
|
381
500
|
operation: "claim-local-identity",
|
|
382
501
|
name: identity.name,
|
|
383
|
-
|
|
384
|
-
approvedEmailSha256: identity.approvedEmailSha256,
|
|
502
|
+
matcher,
|
|
385
503
|
placeholderUserId: identity.placeholderUserId,
|
|
386
504
|
references: identity.references,
|
|
387
505
|
jsonReferences: identity.jsonReferences,
|
|
@@ -404,12 +522,13 @@ export const createIdentityClaimPlan = ({
|
|
|
404
522
|
}
|
|
405
523
|
: null,
|
|
406
524
|
};
|
|
407
|
-
|
|
525
|
+
const plan = {
|
|
408
526
|
identity,
|
|
409
|
-
email,
|
|
410
527
|
review: Object.freeze(review),
|
|
411
528
|
digest: hash(canonical(review)),
|
|
412
|
-
}
|
|
529
|
+
};
|
|
530
|
+
identityPlanValues.set(plan, matchValue);
|
|
531
|
+
return Object.freeze(plan);
|
|
413
532
|
};
|
|
414
533
|
|
|
415
534
|
const inspectDeclaredDefault = ({ row, declaration }) => {
|
|
@@ -492,6 +611,84 @@ const countIdentityRows = async ({ client, reference, userId }) => {
|
|
|
492
611
|
return count;
|
|
493
612
|
};
|
|
494
613
|
|
|
614
|
+
const findVerifiedLocalIdentity = async ({ client, plan, matchValue }) => {
|
|
615
|
+
const matcher = plan.identity.matcher;
|
|
616
|
+
if (matcher.type === "verified-email") {
|
|
617
|
+
const match = await client.query(
|
|
618
|
+
`select u.id::text, i.provider
|
|
619
|
+
from auth.users u
|
|
620
|
+
join auth.identities i on i.user_id = u.id
|
|
621
|
+
where lower(coalesce(i.identity_data->>'email', u.email)) = $1
|
|
622
|
+
and i.provider = any($2::text[])
|
|
623
|
+
and (
|
|
624
|
+
coalesce((i.identity_data->>'email_verified')::boolean, false)
|
|
625
|
+
or u.email_confirmed_at is not null
|
|
626
|
+
)
|
|
627
|
+
for update of u`,
|
|
628
|
+
[matchValue, matcher.providers],
|
|
629
|
+
);
|
|
630
|
+
if (match.rows.length !== 1) {
|
|
631
|
+
throw new Error(
|
|
632
|
+
match.rows.length === 0
|
|
633
|
+
? "No verified local identity matches the reviewed email and provider allowlist."
|
|
634
|
+
: "More than one verified local identity matches the reviewed email and provider allowlist.",
|
|
635
|
+
);
|
|
636
|
+
}
|
|
637
|
+
const matchedProvider =
|
|
638
|
+
match.rows[0].provider ??
|
|
639
|
+
(matcher.providers.length === 1 ? matcher.providers[0] : null);
|
|
640
|
+
if (!matcher.providers.includes(matchedProvider)) {
|
|
641
|
+
throw new Error("Matched local identity provider is invalid.");
|
|
642
|
+
}
|
|
643
|
+
return { id: match.rows[0].id, provider: matchedProvider };
|
|
644
|
+
}
|
|
645
|
+
const candidates = await client.query(
|
|
646
|
+
`select u.id::text,
|
|
647
|
+
i.provider,
|
|
648
|
+
i.provider_id::text,
|
|
649
|
+
i.identity_data->>'sub' as subject
|
|
650
|
+
from auth.users u
|
|
651
|
+
join auth.identities i on i.user_id = u.id
|
|
652
|
+
where i.provider = $1
|
|
653
|
+
and (
|
|
654
|
+
coalesce((i.identity_data->>'email_verified')::boolean, false)
|
|
655
|
+
or u.email_confirmed_at is not null
|
|
656
|
+
)
|
|
657
|
+
for update of u`,
|
|
658
|
+
[matcher.provider],
|
|
659
|
+
);
|
|
660
|
+
const matches = [];
|
|
661
|
+
for (const candidate of candidates.rows) {
|
|
662
|
+
const subjects = [candidate.provider_id, candidate.subject]
|
|
663
|
+
.filter((value) => typeof value === "string" && value.trim())
|
|
664
|
+
.map((value) => value.trim());
|
|
665
|
+
const uniqueSubjects = [...new Set(subjects)];
|
|
666
|
+
if (
|
|
667
|
+
uniqueSubjects.some(
|
|
668
|
+
(subject) => hash(subject) === matcher.approvedSubjectSha256,
|
|
669
|
+
)
|
|
670
|
+
) {
|
|
671
|
+
if (uniqueSubjects.length !== 1) {
|
|
672
|
+
throw new Error(
|
|
673
|
+
"The matched local provider identity has conflicting stable subjects.",
|
|
674
|
+
);
|
|
675
|
+
}
|
|
676
|
+
matches.push({ id: candidate.id, provider: candidate.provider });
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
if (matches.length !== 1) {
|
|
680
|
+
throw new Error(
|
|
681
|
+
matches.length === 0
|
|
682
|
+
? "No verified local identity matches the reviewed provider subject."
|
|
683
|
+
: "More than one verified local identity matches the reviewed provider subject.",
|
|
684
|
+
);
|
|
685
|
+
}
|
|
686
|
+
if (matches[0].provider !== matcher.provider) {
|
|
687
|
+
throw new Error("Matched local identity provider is invalid.");
|
|
688
|
+
}
|
|
689
|
+
return matches[0];
|
|
690
|
+
};
|
|
691
|
+
|
|
495
692
|
export const applyIdentityClaim = async ({
|
|
496
693
|
plan,
|
|
497
694
|
confirmation,
|
|
@@ -504,6 +701,12 @@ export const applyIdentityClaim = async ({
|
|
|
504
701
|
`Identity claim confirmation does not match. Expected ${plan.digest}.`,
|
|
505
702
|
);
|
|
506
703
|
}
|
|
704
|
+
const matchValue = identityPlanValues.get(plan);
|
|
705
|
+
if (!matchValue) {
|
|
706
|
+
throw new Error(
|
|
707
|
+
"Identity claim plans must be created from the reviewed local matcher environment.",
|
|
708
|
+
);
|
|
709
|
+
}
|
|
507
710
|
const target = new URL(connectionString);
|
|
508
711
|
if (!["127.0.0.1", "::1", "localhost"].includes(target.hostname)) {
|
|
509
712
|
throw new Error(
|
|
@@ -525,27 +728,12 @@ export const applyIdentityClaim = async ({
|
|
|
525
728
|
let storageObjectsTransferred = 0;
|
|
526
729
|
try {
|
|
527
730
|
await client.query("begin");
|
|
528
|
-
const
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
and (
|
|
535
|
-
coalesce((i.identity_data->>'email_verified')::boolean, false)
|
|
536
|
-
or u.email_confirmed_at is not null
|
|
537
|
-
)
|
|
538
|
-
for update of u`,
|
|
539
|
-
[plan.email, plan.identity.provider],
|
|
540
|
-
);
|
|
541
|
-
if (match.rows.length !== 1) {
|
|
542
|
-
throw new Error(
|
|
543
|
-
match.rows.length === 0
|
|
544
|
-
? "No single verified local provider identity matches the reviewed email."
|
|
545
|
-
: "More than one local provider identity matches the reviewed email.",
|
|
546
|
-
);
|
|
547
|
-
}
|
|
548
|
-
const localUserId = match.rows[0].id;
|
|
731
|
+
const localIdentity = await findVerifiedLocalIdentity({
|
|
732
|
+
client,
|
|
733
|
+
plan,
|
|
734
|
+
matchValue,
|
|
735
|
+
});
|
|
736
|
+
const localUserId = localIdentity.id;
|
|
549
737
|
if (!UUID.test(localUserId))
|
|
550
738
|
throw new Error("Matched local identity is invalid.");
|
|
551
739
|
const placeholderUser = await client.query(
|
|
@@ -932,7 +1120,7 @@ export const applyIdentityClaim = async ({
|
|
|
932
1120
|
}
|
|
933
1121
|
return {
|
|
934
1122
|
name: plan.identity.name,
|
|
935
|
-
provider:
|
|
1123
|
+
provider: localIdentity.provider,
|
|
936
1124
|
claimed: true,
|
|
937
1125
|
removedSignupDefaults,
|
|
938
1126
|
tokenHookVerified,
|
|
@@ -1028,7 +1028,8 @@ export default defineRehearsalConfig({
|
|
|
1028
1028
|
// },
|
|
1029
1029
|
// Optional reviewed schemas/extensions/triggers/local-only rows.
|
|
1030
1030
|
// runtimePolicy: "infrastructure/rehearsal/runtime-policy.json",
|
|
1031
|
-
// Optional
|
|
1031
|
+
// Optional copied-account association. The JSON stores only reviewed matcher hashes;
|
|
1032
|
+
// raw emails/subjects and provider credentials stay in ignored environment files.
|
|
1032
1033
|
// identityPolicy: "infrastructure/rehearsal/identity-policy.json",
|
|
1033
1034
|
|
|
1034
1035
|
// Reuse any running Docker-compatible engine; start Colima only when needed.
|
|
@@ -1136,7 +1137,8 @@ export default defineRehearsalConfig({
|
|
|
1136
1137
|
// },
|
|
1137
1138
|
// Optional reviewed schemas/extensions/triggers/local-only rows.
|
|
1138
1139
|
// runtimePolicy: "infrastructure/rehearsal/runtime-policy.json",
|
|
1139
|
-
// Optional
|
|
1140
|
+
// Optional copied-account association. The JSON stores only reviewed matcher hashes;
|
|
1141
|
+
// raw emails/subjects and provider credentials stay in ignored environment files.
|
|
1140
1142
|
// identityPolicy: "infrastructure/rehearsal/identity-policy.json",
|
|
1141
1143
|
|
|
1142
1144
|
// Reuse any running Docker-compatible engine; start Colima only when needed.
|