@jeffusion/bungee-core 6.0.4 → 6.1.1
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,33 +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());
|
|
3788
3791
|
if (!records.length)
|
|
3789
3792
|
return { initialized: false };
|
|
3793
|
+
const setup = validateState(records);
|
|
3794
|
+
return { initialized: setup.value.phase === "ready" };
|
|
3795
|
+
}
|
|
3796
|
+
function validateState(records) {
|
|
3790
3797
|
const setup = setupRecord(records);
|
|
3791
3798
|
slots(records, "session", MAX_SESSIONS, validateSession);
|
|
3792
3799
|
slots(records, "failure", MAX_FAILURES, validateFailure);
|
|
3800
|
+
slots(records, "flow", MAX_FLOWS, validateFlow);
|
|
3793
3801
|
const policy = records.find((r) => r.key === "session-policy");
|
|
3794
3802
|
if (policy)
|
|
3795
3803
|
validateSessionPolicy(envelope(policy.value, "policy"), "corrupt_state", 503);
|
|
3796
|
-
|
|
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;
|
|
3797
3811
|
}
|
|
3798
|
-
|
|
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");
|
|
3799
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 });
|
|
3800
3880
|
for (let i = 0;i < 64; i++) {
|
|
3801
|
-
const records = await state.list()
|
|
3802
|
-
if (
|
|
3803
|
-
|
|
3804
|
-
const
|
|
3805
|
-
if (
|
|
3806
|
-
fail("
|
|
3807
|
-
|
|
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)) }));
|
|
3808
3897
|
try {
|
|
3809
|
-
await state.transact(
|
|
3810
|
-
return {
|
|
3811
|
-
} catch (
|
|
3812
|
-
if (!isDurableStateConflictError(
|
|
3813
|
-
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;
|
|
3814
3937
|
}
|
|
3815
3938
|
}
|
|
3816
3939
|
return fail("version_conflict", 409);
|
|
@@ -3825,7 +3948,9 @@ async function recoverIdentity(input, context) {
|
|
|
3825
3948
|
fail("password_confirmation");
|
|
3826
3949
|
if (typeof b.reason !== "string" || !b.reason.trim() || b.reason.length > 512)
|
|
3827
3950
|
fail("invalid_reason");
|
|
3828
|
-
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 });
|
|
3829
3954
|
for (let i = 0;i < 64; i++) {
|
|
3830
3955
|
const records = await state.list();
|
|
3831
3956
|
validateKeys(records);
|
|
@@ -3837,9 +3962,11 @@ async function recoverIdentity(input, context) {
|
|
|
3837
3962
|
fail("corrupt_state", 503);
|
|
3838
3963
|
anchorId = id;
|
|
3839
3964
|
}
|
|
3840
|
-
for (const r of records)
|
|
3841
|
-
|
|
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)
|
|
3842
3968
|
fail("corrupt_state", 503);
|
|
3969
|
+
}
|
|
3843
3970
|
const id = anchorId ?? randomUUID(), record = records.find((r) => r.key === "user:" + id);
|
|
3844
3971
|
let prior;
|
|
3845
3972
|
if (record) {
|
|
@@ -3853,15 +3980,27 @@ async function recoverIdentity(input, context) {
|
|
|
3853
3980
|
const other = records.filter((r) => r.key.startsWith("user:") && r.key !== "user:" + id).map((r) => validateUser(envelope(r.value, "user")));
|
|
3854
3981
|
if (other.some((u) => u.username === name))
|
|
3855
3982
|
fail("username_exists", 409);
|
|
3856
|
-
const
|
|
3857
|
-
|
|
3858
|
-
|
|
3859
|
-
|
|
3860
|
-
|
|
3861
|
-
|
|
3862
|
-
|
|
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);
|
|
3863
4001
|
}
|
|
3864
|
-
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));
|
|
3865
4004
|
const failures = slots(records, "failure", MAX_FAILURES, validateFailure);
|
|
3866
4005
|
slots(records, "session", MAX_SESSIONS, validateSession);
|
|
3867
4006
|
connections(records);
|
|
@@ -3886,29 +4025,34 @@ async function recoverIdentity(input, context) {
|
|
|
3886
4025
|
}
|
|
3887
4026
|
return fail("version_conflict", 409);
|
|
3888
4027
|
}
|
|
3889
|
-
var
|
|
3890
|
-
|
|
3891
|
-
|
|
3892
|
-
|
|
3893
|
-
|
|
3894
|
-
|
|
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
|
+
} } };
|
|
3895
4038
|
export {
|
|
3896
4039
|
ACCOUNT_RECORD_SCHEMA,
|
|
3897
4040
|
LoginProviders,
|
|
3898
4041
|
MAX_FAILURES,
|
|
3899
4042
|
MAX_SESSIONS,
|
|
3900
4043
|
UserAuthControl,
|
|
4044
|
+
addUserOffline,
|
|
3901
4045
|
api,
|
|
3902
4046
|
controlApi,
|
|
3903
4047
|
controlRpc,
|
|
3904
4048
|
createControl,
|
|
3905
4049
|
control_default as default,
|
|
3906
4050
|
parseProvider,
|
|
3907
|
-
prepareInitialization,
|
|
3908
4051
|
providerUrl,
|
|
3909
4052
|
readManagementSetup,
|
|
3910
4053
|
recoverIdentity,
|
|
4054
|
+
resetUserPasswordOffline,
|
|
3911
4055
|
rpc
|
|
3912
4056
|
};
|
|
3913
4057
|
|
|
3914
|
-
//# debugId=
|
|
4058
|
+
//# debugId=2F381703039117D664756E2164756E21
|