@jeffusion/bungee-core 6.0.3 → 6.1.0
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.
|
@@ -2607,6 +2607,14 @@ function validateFailure(value) {
|
|
|
2607
2607
|
fail("corrupt_state", 503);
|
|
2608
2608
|
return f;
|
|
2609
2609
|
}
|
|
2610
|
+
function validateFlow(v) {
|
|
2611
|
+
if (v === null)
|
|
2612
|
+
return null;
|
|
2613
|
+
const f = v;
|
|
2614
|
+
if (!f || Object.keys(f).some((k) => !["id", "state", "nonce", "verifierRef", "connectionId", "generation", "created", "expiresAt", "epoch", "returnTo", "status", "initiator", "subject", "issuer", "protocol"].includes(k)) || typeof f.id !== "string" || typeof f.state !== "string" || typeof f.nonce !== "string" || typeof f.verifierRef !== "string" || typeof f.connectionId !== "string" || !Number.isSafeInteger(f.generation) || !Number.isSafeInteger(f.created) || !Number.isSafeInteger(f.expiresAt) || !Number.isSafeInteger(f.epoch) || !["pending", "exchanging", "verified", "consumed"].includes(f.status) || typeof f.returnTo !== "string" || f.subject !== undefined && typeof f.subject !== "string" || f.issuer !== undefined && typeof f.issuer !== "string" || f.protocol !== undefined && !["oidc", "oauth2"].includes(f.protocol) || f.status === "verified" && (!f.subject || !f.issuer || !f.protocol) || f.initiator !== undefined && (!f.initiator || typeof f.initiator.userId !== "string" || typeof f.initiator.sessionDigest !== "string" || !Number.isSafeInteger(f.initiator.generation)))
|
|
2615
|
+
fail("corrupt_state", 503);
|
|
2616
|
+
return f;
|
|
2617
|
+
}
|
|
2610
2618
|
function slots(records, kind, limit, validate) {
|
|
2611
2619
|
const result = Array.from({ length: limit }, (_, i) => ({ key: `${kind}:${i}`, version: 0, value: null }));
|
|
2612
2620
|
const seen = new Set;
|
|
@@ -2681,11 +2689,7 @@ function setupRecord(records) {
|
|
|
2681
2689
|
if (!record)
|
|
2682
2690
|
fail("corrupt_state", 503);
|
|
2683
2691
|
const value = envelope(record.value, "setup");
|
|
2684
|
-
if (!value || !["uninitialized", "ready"].includes(value.phase) || Object.keys(value).some((k) =>
|
|
2685
|
-
fail("corrupt_state", 503);
|
|
2686
|
-
if (value.code !== undefined && (!value.code || !/^[a-f0-9]{64}$/.test(value.code.digest) || !Number.isSafeInteger(value.code.expiresAt) || value.code.expiresAt < 0 || Object.keys(value.code).some((k) => !["digest", "expiresAt"].includes(k))))
|
|
2687
|
-
fail("corrupt_state", 503);
|
|
2688
|
-
if (value.phase === "ready" && value.code !== undefined)
|
|
2692
|
+
if (!value || !["uninitialized", "ready"].includes(value.phase) || Object.keys(value).some((k) => k !== "phase"))
|
|
2689
2693
|
fail("corrupt_state", 503);
|
|
2690
2694
|
const all = users(records), anchor = records.find((r) => r.key === "recovery-anchor");
|
|
2691
2695
|
if (value.phase === "ready" !== !!all.length || value.phase === "ready" && (!anchor || typeof envelope(anchor.value, "userId") !== "string"))
|
|
@@ -2905,6 +2909,8 @@ class UserAuthControl {
|
|
|
2905
2909
|
return { version: record?.version ?? 0, policy: record ? validateSessionPolicy(envelope(record.value, "policy"), "corrupt_state", 503) : { ...DEFAULT_SESSION_POLICY } };
|
|
2906
2910
|
}
|
|
2907
2911
|
async start() {
|
|
2912
|
+
await clearSetupCode(this.state);
|
|
2913
|
+
await ensureSetup(this.state, this.now);
|
|
2908
2914
|
await readManagementSetup(this.state);
|
|
2909
2915
|
await observeClock(this.state, this.now);
|
|
2910
2916
|
await this.readSessionPolicy();
|
|
@@ -2939,19 +2945,43 @@ class UserAuthControl {
|
|
|
2939
2945
|
});
|
|
2940
2946
|
}
|
|
2941
2947
|
async bootstrap(input) {
|
|
2942
|
-
|
|
2943
|
-
|
|
2944
|
-
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
|
|
2948
|
-
|
|
2949
|
-
|
|
2950
|
-
|
|
2951
|
-
|
|
2952
|
-
|
|
2953
|
-
|
|
2954
|
-
|
|
2948
|
+
try {
|
|
2949
|
+
const b = object(input);
|
|
2950
|
+
if (b.operation === "initialize") {
|
|
2951
|
+
exact(b, ["operation", "username", "password", "passwordConfirmation"]);
|
|
2952
|
+
await this.createInitialUser(b);
|
|
2953
|
+
return;
|
|
2954
|
+
}
|
|
2955
|
+
if (b.operation !== "verify")
|
|
2956
|
+
fail("invalid_input");
|
|
2957
|
+
exact(b, ["operation", "username", "password"]);
|
|
2958
|
+
const name = username(b.username), p = password(b.password);
|
|
2959
|
+
if (await this.setupStatus() !== "ready")
|
|
2960
|
+
fail("initialization_required", 409);
|
|
2961
|
+
const prior = users(await this.records()).find((u) => u.username === name);
|
|
2962
|
+
if (!prior?.passwordHash || prior.disabled || !await Bun.password.verify(p, prior.passwordHash))
|
|
2963
|
+
fail("invalid_credentials", 401);
|
|
2964
|
+
const latest = (await this.readAdministrator(prior.id)).value;
|
|
2965
|
+
if (!latest || latest.generation !== prior.generation || latest.username !== prior.username || latest.disabled)
|
|
2966
|
+
fail("invalid_credentials", 401);
|
|
2967
|
+
} catch (error) {
|
|
2968
|
+
if (error instanceof AccountError && ["invalid_password", "password_confirmation"].includes(error.code))
|
|
2969
|
+
fail("invalid_input");
|
|
2970
|
+
throw error;
|
|
2971
|
+
}
|
|
2972
|
+
}
|
|
2973
|
+
async createInitialUser(b) {
|
|
2974
|
+
const name = username(b.username), p = this.confirmPassword(b);
|
|
2975
|
+
if (validateState(await this.records()).value.phase !== "uninitialized")
|
|
2976
|
+
fail("already_initialized", 409);
|
|
2977
|
+
const passwordHash = await this.hash(p);
|
|
2978
|
+
await this.retry(async () => {
|
|
2979
|
+
const records = await this.records();
|
|
2980
|
+
if (validateState(records).value.phase !== "uninitialized")
|
|
2981
|
+
fail("already_initialized", 409);
|
|
2982
|
+
const account = { id: randomUUID(), username: name, displayName: name, disabled: false, generation: 1 };
|
|
2983
|
+
return { mutations: initialUserMutations(records, account, passwordHash), result: undefined };
|
|
2984
|
+
});
|
|
2955
2985
|
}
|
|
2956
2986
|
source(request) {
|
|
2957
2987
|
return (this.dependencies.trustedSource ?? this.host.trustedSource)?.(request) ?? "unknown";
|
|
@@ -2983,28 +3013,8 @@ class UserAuthControl {
|
|
|
2983
3013
|
fail("method_not_allowed", 405);
|
|
2984
3014
|
origin(request, this.host.managementOrigin);
|
|
2985
3015
|
const b = await body(request);
|
|
2986
|
-
exact(b, ["
|
|
2987
|
-
await this.
|
|
2988
|
-
const check = async (time) => {
|
|
2989
|
-
const records = await this.records(), setup = setupRecord(records), now = await time();
|
|
2990
|
-
if (setup.value.phase !== "uninitialized")
|
|
2991
|
-
fail("already_initialized", 409);
|
|
2992
|
-
if (!setup.value.code || now >= setup.value.code.expiresAt || typeof b.setupCode !== "string" || !constant(digest(b.setupCode), setup.value.code.digest))
|
|
2993
|
-
fail("invalid_setup_code", 401);
|
|
2994
|
-
return { setup, records };
|
|
2995
|
-
};
|
|
2996
|
-
const initial = await this.retry(async (time) => ({ mutations: [], result: await check(time) }));
|
|
2997
|
-
const name = username(b.username), p = password(b.password);
|
|
2998
|
-
if (b.passwordConfirmation !== p)
|
|
2999
|
-
fail("password_confirmation");
|
|
3000
|
-
const passwordHash = await this.hash(p);
|
|
3001
|
-
await this.retry(async (time) => {
|
|
3002
|
-
const { setup, records } = await check(time);
|
|
3003
|
-
if (setup.version !== initial.setup.version)
|
|
3004
|
-
fail("invalid_setup_code", 401);
|
|
3005
|
-
const account = { id: randomUUID(), username: name, displayName: name, disabled: false, generation: 1 };
|
|
3006
|
-
return { mutations: [mutation(setup, "setup", { phase: "ready" }), mutation(settings(records), "settings", EMPTY_SETTINGS), mutation(turnstile(records), "turnstile", EMPTY_TURNSTILE), { key: "user:" + account.id, expectedVersion: 0, value: asJson({ schema: ACCOUNT_RECORD_SCHEMA, user: account }) }, this.credentialMutation(records, account.id, passwordHash), { key: "recovery-anchor", expectedVersion: 0, value: { schema: ACCOUNT_RECORD_SCHEMA, userId: account.id } }], result: undefined };
|
|
3007
|
-
});
|
|
3016
|
+
exact(b, ["username", "password", "passwordConfirmation"]);
|
|
3017
|
+
await this.createInitialUser(b);
|
|
3008
3018
|
return json({ initialized: true });
|
|
3009
3019
|
} catch (e) {
|
|
3010
3020
|
return safe(e);
|
|
@@ -3609,14 +3619,7 @@ class UserAuthControl {
|
|
|
3609
3619
|
return `${FLOW_COOKIE}=${token}; Path=/api/auth/login/callback/; HttpOnly; SameSite=Lax; Max-Age=${maxAge}${this.publicOrigin().startsWith("https:") ? "; Secure" : ""}`;
|
|
3610
3620
|
}
|
|
3611
3621
|
flows(records) {
|
|
3612
|
-
return slots(records, "flow", MAX_FLOWS,
|
|
3613
|
-
if (v === null)
|
|
3614
|
-
return null;
|
|
3615
|
-
const f = v;
|
|
3616
|
-
if (!f || Object.keys(f).some((k) => !["id", "state", "nonce", "verifierRef", "connectionId", "generation", "created", "expiresAt", "epoch", "returnTo", "status", "initiator", "subject", "issuer", "protocol"].includes(k)) || typeof f.id !== "string" || typeof f.state !== "string" || typeof f.nonce !== "string" || typeof f.verifierRef !== "string" || typeof f.connectionId !== "string" || !Number.isSafeInteger(f.generation) || !Number.isSafeInteger(f.created) || !Number.isSafeInteger(f.expiresAt) || !Number.isSafeInteger(f.epoch) || !["pending", "exchanging", "verified", "consumed"].includes(f.status) || typeof f.returnTo !== "string" || f.subject !== undefined && typeof f.subject !== "string" || f.issuer !== undefined && typeof f.issuer !== "string" || f.protocol !== undefined && !["oidc", "oauth2"].includes(f.protocol) || f.status === "verified" && (!f.subject || !f.issuer || !f.protocol) || f.initiator !== undefined && (!f.initiator || typeof f.initiator.userId !== "string" || typeof f.initiator.sessionDigest !== "string" || !Number.isSafeInteger(f.initiator.generation)))
|
|
3617
|
-
fail("corrupt_state", 503);
|
|
3618
|
-
return f;
|
|
3619
|
-
});
|
|
3622
|
+
return slots(records, "flow", MAX_FLOWS, validateFlow);
|
|
3620
3623
|
}
|
|
3621
3624
|
loginProviders() {
|
|
3622
3625
|
return new LoginProviders(this.dependencies.fetch ?? fetch, this.host.signal);
|
|
@@ -3784,30 +3787,153 @@ var rpc = [];
|
|
|
3784
3787
|
var controlApi = api;
|
|
3785
3788
|
var controlRpc = rpc;
|
|
3786
3789
|
async function readManagementSetup(state) {
|
|
3787
|
-
const records = await state.list()
|
|
3790
|
+
const records = withoutSetupCode(await state.list());
|
|
3791
|
+
if (!records.length)
|
|
3792
|
+
return { initialized: false };
|
|
3793
|
+
const setup = validateState(records);
|
|
3794
|
+
return { initialized: setup.value.phase === "ready" };
|
|
3795
|
+
}
|
|
3796
|
+
function validateState(records) {
|
|
3797
|
+
const setup = setupRecord(records);
|
|
3788
3798
|
slots(records, "session", MAX_SESSIONS, validateSession);
|
|
3789
3799
|
slots(records, "failure", MAX_FAILURES, validateFailure);
|
|
3800
|
+
slots(records, "flow", MAX_FLOWS, validateFlow);
|
|
3790
3801
|
const policy = records.find((r) => r.key === "session-policy");
|
|
3791
3802
|
if (policy)
|
|
3792
3803
|
validateSessionPolicy(envelope(policy.value, "policy"), "corrupt_state", 503);
|
|
3793
|
-
|
|
3804
|
+
const all = users(records);
|
|
3805
|
+
for (const record of records.filter((r) => r.key.startsWith("credential:"))) {
|
|
3806
|
+
localValue(record);
|
|
3807
|
+
if (!all.some((u) => record.key === "credential:" + u.id))
|
|
3808
|
+
fail("corrupt_state", 503);
|
|
3809
|
+
}
|
|
3810
|
+
return setup;
|
|
3794
3811
|
}
|
|
3795
|
-
|
|
3812
|
+
function withoutSetupCode(records) {
|
|
3813
|
+
const record = records.find((r) => r.key === "setup");
|
|
3814
|
+
if (!record)
|
|
3815
|
+
return records;
|
|
3816
|
+
const value = envelope(record.value, "setup");
|
|
3817
|
+
if (!value || !Object.hasOwn(value, "code"))
|
|
3818
|
+
return records;
|
|
3819
|
+
const code = value.code;
|
|
3820
|
+
if (value.phase !== "uninitialized" || Object.keys(value).some((k) => !["phase", "code"].includes(k)) || !code || typeof code.digest !== "string" || !/^[a-f0-9]{64}$/.test(code.digest) || !Number.isSafeInteger(code.expiresAt) || code.expiresAt < 0 || Object.keys(code).some((k) => !["digest", "expiresAt"].includes(k)))
|
|
3821
|
+
fail("corrupt_state", 503);
|
|
3822
|
+
return records.map((r) => r === record ? { ...r, value: { schema: ACCOUNT_RECORD_SCHEMA, setup: { phase: "uninitialized" } } } : r);
|
|
3823
|
+
}
|
|
3824
|
+
async function clearSetupCode(state) {
|
|
3825
|
+
for (let i = 0;i < 64; i++) {
|
|
3826
|
+
const records = await state.list(), normalized = withoutSetupCode(records);
|
|
3827
|
+
if (normalized === records)
|
|
3828
|
+
return;
|
|
3829
|
+
const before = records.find((r) => r.key === "setup"), after = normalized.find((r) => r.key === "setup");
|
|
3830
|
+
const clock = records.find((r) => r.key === "clock");
|
|
3831
|
+
try {
|
|
3832
|
+
await state.transact([{ key: "setup", expectedVersion: before.version, value: after.value }, clockMutation({ version: clock?.version ?? 0, value: clockValue(clock) })]);
|
|
3833
|
+
return;
|
|
3834
|
+
} catch (error) {
|
|
3835
|
+
if (!isDurableStateConflictError(error))
|
|
3836
|
+
throw error;
|
|
3837
|
+
}
|
|
3838
|
+
}
|
|
3839
|
+
fail("version_conflict", 409);
|
|
3840
|
+
}
|
|
3841
|
+
async function ensureSetup(state, now) {
|
|
3842
|
+
for (let i = 0;i < 64; i++) {
|
|
3843
|
+
if ((await state.list()).length)
|
|
3844
|
+
return;
|
|
3845
|
+
try {
|
|
3846
|
+
await state.transact([{ key: "setup", expectedVersion: 0, value: { schema: ACCOUNT_RECORD_SCHEMA, setup: { phase: "uninitialized" } } }, clockMutation({ version: 0, value: wallTime(now) })]);
|
|
3847
|
+
return;
|
|
3848
|
+
} catch (error) {
|
|
3849
|
+
if (!isDurableStateConflictError(error))
|
|
3850
|
+
throw error;
|
|
3851
|
+
}
|
|
3852
|
+
}
|
|
3853
|
+
fail("version_conflict", 409);
|
|
3854
|
+
}
|
|
3855
|
+
function initialUserMutations(records, user, hash) {
|
|
3856
|
+
return [
|
|
3857
|
+
{ key: "setup", expectedVersion: records.find((r) => r.key === "setup")?.version ?? 0, value: { schema: ACCOUNT_RECORD_SCHEMA, setup: { phase: "ready" } } },
|
|
3858
|
+
mutation(settings(records), "settings", EMPTY_SETTINGS),
|
|
3859
|
+
mutation(turnstile(records), "turnstile", EMPTY_TURNSTILE),
|
|
3860
|
+
{ key: "user:" + user.id, expectedVersion: 0, value: asJson({ schema: ACCOUNT_RECORD_SCHEMA, user }) },
|
|
3861
|
+
{ key: "credential:" + user.id, expectedVersion: 0, value: { schema: ACCOUNT_RECORD_SCHEMA, credential: { passwordHash: hash, temporary: false } } },
|
|
3862
|
+
{ key: "recovery-anchor", expectedVersion: 0, value: { schema: ACCOUNT_RECORD_SCHEMA, userId: user.id } }
|
|
3863
|
+
];
|
|
3864
|
+
}
|
|
3865
|
+
function offlinePasswordInput(input, operation) {
|
|
3866
|
+
const b = object(input);
|
|
3867
|
+
exact(b, ["operation", "username", "password", "passwordConfirmation"]);
|
|
3868
|
+
if (b.operation !== operation)
|
|
3869
|
+
fail("invalid_input");
|
|
3870
|
+
const name = username(b.username), p = password(b.password);
|
|
3871
|
+
if (b.passwordConfirmation !== p)
|
|
3872
|
+
fail("password_confirmation");
|
|
3873
|
+
return { name, p };
|
|
3874
|
+
}
|
|
3875
|
+
async function addUserOffline(input, context) {
|
|
3876
|
+
const { name, p } = offlinePasswordInput(input, "add-user");
|
|
3796
3877
|
const state = context.durableState, now = context.now ?? Date.now;
|
|
3878
|
+
await clearSetupCode(state);
|
|
3879
|
+
const hash = await Bun.password.hash(p, { algorithm: "argon2id", memoryCost: 65536, timeCost: 3 });
|
|
3797
3880
|
for (let i = 0;i < 64; i++) {
|
|
3798
|
-
const records = await state.list()
|
|
3799
|
-
if (
|
|
3800
|
-
|
|
3801
|
-
const
|
|
3802
|
-
if (
|
|
3803
|
-
fail("
|
|
3804
|
-
|
|
3881
|
+
const records = await state.list();
|
|
3882
|
+
if (records.length)
|
|
3883
|
+
validateState(records);
|
|
3884
|
+
const all = users(records);
|
|
3885
|
+
if (all.some((u) => u.username === name))
|
|
3886
|
+
fail("username_exists", 409);
|
|
3887
|
+
if (all.length >= MAX_USERS)
|
|
3888
|
+
fail("user_limit", 409);
|
|
3889
|
+
const user = { id: randomUUID(), username: name, displayName: name, disabled: false, generation: 1 };
|
|
3890
|
+
const pending = !records.length || setupRecord(records).value.phase === "uninitialized";
|
|
3891
|
+
const changes = pending ? initialUserMutations(records, user, hash) : [
|
|
3892
|
+
{ key: "user:" + user.id, expectedVersion: 0, value: asJson({ schema: ACCOUNT_RECORD_SCHEMA, user }) },
|
|
3893
|
+
{ key: "credential:" + user.id, expectedVersion: 0, value: asJson({ schema: ACCOUNT_RECORD_SCHEMA, credential: { passwordHash: hash, temporary: false } }) }
|
|
3894
|
+
];
|
|
3895
|
+
const clock = records.find((r) => r.key === "clock");
|
|
3896
|
+
changes.push(clockMutation({ version: clock?.version ?? 0, value: Math.max(clock ? clockValue(clock) : 0, wallTime(now)) }));
|
|
3805
3897
|
try {
|
|
3806
|
-
await state.transact(
|
|
3807
|
-
return {
|
|
3808
|
-
} catch (
|
|
3809
|
-
if (!isDurableStateConflictError(
|
|
3810
|
-
throw
|
|
3898
|
+
await state.transact(changes);
|
|
3899
|
+
return dto({ ...user, passwordHash: hash, temporary: false });
|
|
3900
|
+
} catch (error) {
|
|
3901
|
+
if (!isDurableStateConflictError(error))
|
|
3902
|
+
throw error;
|
|
3903
|
+
}
|
|
3904
|
+
}
|
|
3905
|
+
return fail("version_conflict", 409);
|
|
3906
|
+
}
|
|
3907
|
+
async function resetUserPasswordOffline(input, context) {
|
|
3908
|
+
const { name, p } = offlinePasswordInput(input, "reset-password");
|
|
3909
|
+
const state = context.durableState, now = context.now ?? Date.now;
|
|
3910
|
+
await clearSetupCode(state);
|
|
3911
|
+
const initial = await state.list();
|
|
3912
|
+
if (initial.length)
|
|
3913
|
+
validateState(initial);
|
|
3914
|
+
if (!users(initial).some((u) => u.username === name))
|
|
3915
|
+
fail("not_found", 404);
|
|
3916
|
+
const hash = await Bun.password.hash(p, { algorithm: "argon2id", memoryCost: 65536, timeCost: 3 });
|
|
3917
|
+
for (let i = 0;i < 64; i++) {
|
|
3918
|
+
const records = await state.list();
|
|
3919
|
+
validateState(records);
|
|
3920
|
+
const target = users(records).find((u) => u.username === name);
|
|
3921
|
+
if (!target)
|
|
3922
|
+
fail("not_found", 404);
|
|
3923
|
+
const record = records.find((r) => r.key === "user:" + target.id), credential = records.find((r) => r.key === "credential:" + target.id);
|
|
3924
|
+
const { passwordHash: _, temporary: __, ...prior } = target, user = { ...prior, generation: next(prior.generation) };
|
|
3925
|
+
const s = settings(records), failures = slots(records, "failure", MAX_FAILURES, validateFailure), clock = records.find((r) => r.key === "clock");
|
|
3926
|
+
try {
|
|
3927
|
+
await state.transact([
|
|
3928
|
+
{ key: record.key, expectedVersion: record.version, value: asJson({ schema: ACCOUNT_RECORD_SCHEMA, user }) },
|
|
3929
|
+
{ key: "credential:" + user.id, expectedVersion: credential?.version ?? 0, value: { schema: ACCOUNT_RECORD_SCHEMA, credential: { passwordHash: hash, temporary: false } } },
|
|
3930
|
+
mutation(s, "settings", { ...s.value, failureEpoch: next(Math.max(s.value.failureEpoch, ...failures.map((f) => f.value?.epoch ?? 0))) }),
|
|
3931
|
+
clockMutation({ version: clock.version, value: Math.max(clockValue(clock), wallTime(now)) })
|
|
3932
|
+
]);
|
|
3933
|
+
return dto({ ...user, passwordHash: hash, temporary: false });
|
|
3934
|
+
} catch (error) {
|
|
3935
|
+
if (!isDurableStateConflictError(error))
|
|
3936
|
+
throw error;
|
|
3811
3937
|
}
|
|
3812
3938
|
}
|
|
3813
3939
|
return fail("version_conflict", 409);
|
|
@@ -3822,7 +3948,9 @@ async function recoverIdentity(input, context) {
|
|
|
3822
3948
|
fail("password_confirmation");
|
|
3823
3949
|
if (typeof b.reason !== "string" || !b.reason.trim() || b.reason.length > 512)
|
|
3824
3950
|
fail("invalid_reason");
|
|
3825
|
-
const state = context.durableState, now = context.now ?? Date.now
|
|
3951
|
+
const state = context.durableState, now = context.now ?? Date.now;
|
|
3952
|
+
await clearSetupCode(state);
|
|
3953
|
+
const hash = await Bun.password.hash(p, { algorithm: "argon2id", memoryCost: 65536, timeCost: 3 });
|
|
3826
3954
|
for (let i = 0;i < 64; i++) {
|
|
3827
3955
|
const records = await state.list();
|
|
3828
3956
|
validateKeys(records);
|
|
@@ -3834,9 +3962,11 @@ async function recoverIdentity(input, context) {
|
|
|
3834
3962
|
fail("corrupt_state", 503);
|
|
3835
3963
|
anchorId = id;
|
|
3836
3964
|
}
|
|
3837
|
-
for (const r of records)
|
|
3838
|
-
|
|
3965
|
+
for (const r of records) {
|
|
3966
|
+
const anchorTombstone = anchorId && ["user:" + anchorId, "credential:" + anchorId].includes(r.key) && r.value === null;
|
|
3967
|
+
if (!anchorTombstone && r.value?.schema !== ACCOUNT_RECORD_SCHEMA)
|
|
3839
3968
|
fail("corrupt_state", 503);
|
|
3969
|
+
}
|
|
3840
3970
|
const id = anchorId ?? randomUUID(), record = records.find((r) => r.key === "user:" + id);
|
|
3841
3971
|
let prior;
|
|
3842
3972
|
if (record) {
|
|
@@ -3850,15 +3980,27 @@ async function recoverIdentity(input, context) {
|
|
|
3850
3980
|
const other = records.filter((r) => r.key.startsWith("user:") && r.key !== "user:" + id).map((r) => validateUser(envelope(r.value, "user")));
|
|
3851
3981
|
if (other.some((u) => u.username === name))
|
|
3852
3982
|
fail("username_exists", 409);
|
|
3853
|
-
const
|
|
3854
|
-
|
|
3855
|
-
|
|
3856
|
-
|
|
3857
|
-
|
|
3858
|
-
|
|
3859
|
-
|
|
3983
|
+
const user = { id, username: name, displayName: prior?.displayName ?? name, disabled: false, generation: next(prior?.generation ?? 0) };
|
|
3984
|
+
const credential = records.find((r) => r.key === "credential:" + id);
|
|
3985
|
+
if (records.length) {
|
|
3986
|
+
if (!anchorId && setupRecord(records).value.phase === "ready")
|
|
3987
|
+
fail("corrupt_state", 503);
|
|
3988
|
+
const repaired = records.filter((r) => !["user:" + id, "credential:" + id].includes(r.key));
|
|
3989
|
+
const setup = records.find((r) => r.key === "setup");
|
|
3990
|
+
if (!setup)
|
|
3991
|
+
fail("corrupt_state", 503);
|
|
3992
|
+
const phase = envelope(setup.value, "setup").phase;
|
|
3993
|
+
if (phase === "ready") {
|
|
3994
|
+
validateState([
|
|
3995
|
+
...repaired,
|
|
3996
|
+
{ key: "user:" + id, version: record?.version ?? 0, value: asJson({ schema: ACCOUNT_RECORD_SCHEMA, user }) },
|
|
3997
|
+
{ key: "credential:" + id, version: credential?.version ?? 0, value: { schema: ACCOUNT_RECORD_SCHEMA, credential: { passwordHash: hash, temporary: false } } }
|
|
3998
|
+
]);
|
|
3999
|
+
} else
|
|
4000
|
+
validateState(records);
|
|
3860
4001
|
}
|
|
3861
|
-
const
|
|
4002
|
+
const s = settings(records), turn = turnstile(records), clockRecord = records.find((r) => r.key === "clock");
|
|
4003
|
+
const at = Math.max(clockRecord ? clockValue(clockRecord) : 0, wallTime(now));
|
|
3862
4004
|
const failures = slots(records, "failure", MAX_FAILURES, validateFailure);
|
|
3863
4005
|
slots(records, "session", MAX_SESSIONS, validateSession);
|
|
3864
4006
|
connections(records);
|
|
@@ -3883,29 +4025,34 @@ async function recoverIdentity(input, context) {
|
|
|
3883
4025
|
}
|
|
3884
4026
|
return fail("version_conflict", 409);
|
|
3885
4027
|
}
|
|
3886
|
-
var
|
|
3887
|
-
|
|
3888
|
-
|
|
3889
|
-
|
|
3890
|
-
|
|
3891
|
-
|
|
4028
|
+
var passwordPrompts = [
|
|
4029
|
+
{ name: "username", label: "Username", secret: false },
|
|
4030
|
+
{ name: "password", label: "New password", secret: true },
|
|
4031
|
+
{ name: "passwordConfirmation", label: "Confirm password", secret: true }
|
|
4032
|
+
];
|
|
4033
|
+
var control_default = { createControl, readManagementSetup, offlineRecovery: { kind: "identity", actions: {
|
|
4034
|
+
"add-user": { prompts: passwordPrompts, run: (input, context) => addUserOffline({ ...input, operation: "add-user" }, context) },
|
|
4035
|
+
"reset-password": { prompts: passwordPrompts, run: (input, context) => resetUserPasswordOffline({ ...input, operation: "reset-password" }, context) },
|
|
4036
|
+
reset: { prompts: [{ name: "username", label: "Recovery username", secret: false }, ...passwordPrompts.slice(1), { name: "reason", label: "Recovery reason", secret: false }, { name: "disableTurnstile", label: "Disable Turnstile? Type yes to disable; default no", secret: false }], run: (input, context) => recoverIdentity({ ...input, operation: "reset" }, context) }
|
|
4037
|
+
} } };
|
|
3892
4038
|
export {
|
|
3893
4039
|
ACCOUNT_RECORD_SCHEMA,
|
|
3894
4040
|
LoginProviders,
|
|
3895
4041
|
MAX_FAILURES,
|
|
3896
4042
|
MAX_SESSIONS,
|
|
3897
4043
|
UserAuthControl,
|
|
4044
|
+
addUserOffline,
|
|
3898
4045
|
api,
|
|
3899
4046
|
controlApi,
|
|
3900
4047
|
controlRpc,
|
|
3901
4048
|
createControl,
|
|
3902
4049
|
control_default as default,
|
|
3903
4050
|
parseProvider,
|
|
3904
|
-
prepareInitialization,
|
|
3905
4051
|
providerUrl,
|
|
3906
4052
|
readManagementSetup,
|
|
3907
4053
|
recoverIdentity,
|
|
4054
|
+
resetUserPasswordOffline,
|
|
3908
4055
|
rpc
|
|
3909
4056
|
};
|
|
3910
4057
|
|
|
3911
|
-
//# debugId=
|
|
4058
|
+
//# debugId=2F381703039117D664756E2164756E21
|