@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) => !["phase", "code"].includes(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
- const b = object(input);
2943
- exact(b, ["username", "password"]);
2944
- const name = username(b.username), p = password(b.password);
2945
- if (await this.setupStatus() !== "ready")
2946
- fail("initialization_required", 409);
2947
- const prior = users(await this.records()).find((u) => u.username === name);
2948
- if (!prior?.passwordHash)
2949
- fail("invalid_credentials", 401);
2950
- if (prior.username !== name || prior.disabled || !await Bun.password.verify(p, prior.passwordHash))
2951
- fail("invalid_credentials", 401);
2952
- const latest = (await this.readAdministrator(prior.id)).value;
2953
- if (!latest || latest.id !== prior.id || latest.generation !== prior.generation || latest.username !== prior.username || latest.disabled)
2954
- fail("invalid_credentials", 401);
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, ["setupCode", "username", "password", "passwordConfirmation"]);
2987
- await this.reserve(["source:" + digest(this.source(request))]);
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, (v) => {
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(), setup = setupRecord(records);
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
- return { initialized: setup.value.phase === "ready" };
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
- async function prepareInitialization(context) {
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(), fresh = !records.length;
3799
- if (!fresh)
3800
- await readManagementSetup(state);
3801
- const setup = fresh ? undefined : setupRecord(records);
3802
- if (setup?.value.phase === "ready")
3803
- fail("already_initialized", 409);
3804
- const clock = fresh ? { version: 0, value: wallTime(now) } : await observeClock(state, now), code = randomBytes2(32).toString("base64url"), expiresAt = clock.value + 30 * 60000;
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([{ key: "setup", expectedVersion: setup?.version ?? 0, value: asJson({ schema: ACCOUNT_RECORD_SCHEMA, setup: { phase: "uninitialized", code: { digest: digest(code), expiresAt } } }) }, clockMutation(clock)]);
3807
- return { code, expiresAt };
3808
- } catch (e) {
3809
- if (!isDurableStateConflictError(e))
3810
- throw e;
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, hash = await Bun.password.hash(p, { algorithm: "argon2id", memoryCost: 65536, timeCost: 3 });
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
- if (r.value?.schema !== ACCOUNT_RECORD_SCHEMA)
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 s = settings(records), turn = turnstile(records), clockRecord = records.find((r) => r.key === "clock");
3854
- let observed = 0;
3855
- try {
3856
- observed = clockValue(clockRecord);
3857
- } catch (error) {
3858
- if (!(error instanceof AccountError))
3859
- throw error;
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 at = Math.max(observed, wallTime(now)), user = { id, username: name, displayName: prior?.displayName ?? name, disabled: false, generation: next(prior?.generation ?? 0) }, credential = records.find((r) => r.key === "credential:" + id);
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 control_default = { createControl, readManagementSetup, offlineRecovery: {
3887
- kind: "identity",
3888
- prepareInitialization,
3889
- prompts: [{ name: "username", label: "Recovery username", secret: false }, { name: "password", label: "New password", secret: true }, { name: "passwordConfirmation", label: "Confirm password", secret: true }, { name: "reason", label: "Recovery reason", secret: false }, { name: "disableTurnstile", label: "Disable Turnstile? Type yes to disable; default no", secret: false }],
3890
- recover: recoverIdentity
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=5FEFB6DCD014AEAE64756E2164756E21
4058
+ //# debugId=2F381703039117D664756E2164756E21