@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) => !["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,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
- 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;
3797
3811
  }
3798
- 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");
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(), fresh = !records.length;
3802
- if (!fresh)
3803
- await readManagementSetup(state);
3804
- const setup = fresh ? undefined : setupRecord(records);
3805
- if (setup?.value.phase === "ready")
3806
- fail("already_initialized", 409);
3807
- 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)) }));
3808
3897
  try {
3809
- 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)]);
3810
- return { code, expiresAt };
3811
- } catch (e) {
3812
- if (!isDurableStateConflictError(e))
3813
- 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;
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, 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 });
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
- 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)
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 s = settings(records), turn = turnstile(records), clockRecord = records.find((r) => r.key === "clock");
3857
- let observed = 0;
3858
- try {
3859
- observed = clockValue(clockRecord);
3860
- } catch (error) {
3861
- if (!(error instanceof AccountError))
3862
- 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);
3863
4001
  }
3864
- 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));
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 control_default = { createControl, readManagementSetup, offlineRecovery: {
3890
- kind: "identity",
3891
- prepareInitialization,
3892
- 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 }],
3893
- recover: recoverIdentity
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=6F0EF3E8D5F22B1A64756E2164756E21
4058
+ //# debugId=2F381703039117D664756E2164756E21