@krizaka/auth 0.13.1 → 0.13.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/contract.js CHANGED
@@ -4,7 +4,7 @@ import {
4
4
  meAccountSchema,
5
5
  meResponse,
6
6
  meSchema
7
- } from "./chunk-G46LWJPQ.js";
7
+ } from "./chunk-3MIBOYLS.js";
8
8
  export {
9
9
  ME_HEADERS,
10
10
  ME_PATH,
package/dist/index.d.ts CHANGED
@@ -1,12 +1,12 @@
1
1
  import { Query, ModuleSchema } from '@krizaka/db';
2
2
  export { Query } from '@krizaka/db';
3
- import { E as ExternalIdentity, a as IdentityLinkStore, S as SessionStore, N as NewSession, I as IdentityProvider } from './sessions-DZXjHjzd.js';
4
- export { C as CompleteOptions, F as FEDERATED_PROVIDERS, b as FEDERATION_CODE, c as FederatedProvider, d as FederationCode, e as FederationError, f as IdentityLinkError, g as Session, h as SessionCookie, i as Sessions, j as SessionsDeps, k as StartOptions, l as StartedSignIn, m as clearedSessionCookie, n as createSessions, o as dbIdentityLinks, p as identityLinksMigration, q as isFederatedProvider, r as isLocalReturn, s as memoryIdentityLinks, t as memoryIdentityProvider, u as pkceChallenge, v as sessionCookie, w as sessionTokenOf } from './sessions-DZXjHjzd.js';
3
+ import { E as ExternalIdentity, a as IdentityLinkStore, S as SessionStore, N as NewSession, I as IdentityProvider } from './sessions-DscdTKEJ.js';
4
+ export { C as CompleteOptions, F as FEDERATED_PROVIDERS, b as FEDERATION_CODE, c as FederatedProvider, d as FederationCode, e as FederationError, f as IdentityLinkError, g as Session, h as SessionCookie, i as Sessions, j as SessionsDeps, k as StartOptions, l as StartedSignIn, m as clearedSessionCookie, n as createSessions, o as dbIdentityLinks, p as identityLinksMigration, q as isFederatedProvider, r as isLocalReturn, s as memoryIdentityLinks, t as memoryIdentityProvider, u as pkceChallenge, v as sessionCookie, w as sessionTokenOf } from './sessions-DscdTKEJ.js';
5
5
  import { a as RoleModel } from './roles-model-CSCe8O57.js';
6
6
  export { R as ROLES_CODE, b as RolesCode, c as RolesError, d as can, e as defineRoles } from './roles-model-CSCe8O57.js';
7
7
 
8
8
  /**
9
- * The account PORT (ADR-0008 of Yomizaka, now shared). A product's account — name, language, theme, avatar, e-mail change with
9
+ * The account PORT (ADR-0008 of Verikora, now shared). A product's account — name, language, theme, avatar, e-mail change with
10
10
  * verification, password (changed when signed in, or reset by a mailed link), sessions, export, deletion — behind one interface. Every product keeps its OWN accounts: no central
11
11
  * service, no runtime dependency between products.
12
12
  */
@@ -106,11 +106,11 @@ interface AccountDeps {
106
106
  */
107
107
  transaction?: <T>(work: (query: Query) => Promise<T>) => Promise<T>;
108
108
  mail: (mail: Mail) => Promise<void>;
109
- /** `https://www.akinaza.com`: the e-mail change link is `<baseUrl>/<locale>/confirm-email?token=…`, the reset link `<baseUrl>/<locale>/reset-password?token=…`. */
109
+ /** `https://www.folioviv.com`: the e-mail change link is `<baseUrl>/<locale>/confirm-email?token=…`, the reset link `<baseUrl>/<locale>/reset-password?token=…`. */
110
110
  baseUrl: string;
111
- /** The product's name, in the words of the mail (« Akinaza »). */
111
+ /** The product's name, in the words of the mail (« Folioviv »). */
112
112
  product: string;
113
- /** The product's token prefix, two or three letters (`ye1.` for Yomizaka: `ye`). The tokens are `<prefix>1.<43 base64url>`. */
113
+ /** The product's token prefix, two or three letters (`ye1.` for Verikora: `ye`). The tokens are `<prefix>1.<43 base64url>`. */
114
114
  tokenPrefix: string;
115
115
  passwords: {
116
116
  hash: (password: string) => Promise<string>;
@@ -279,14 +279,14 @@ type FederatedSignIn = ReturnType<typeof createFederatedSignIn>;
279
279
  declare const SESSION_HINT_COOKIE = "kz-session";
280
280
  /** Where a signed-in visitor of the landing route goes. */
281
281
  interface LandingRule {
282
- /** The product's session cookie name (`yomizaka_session`). */
282
+ /** The product's session cookie name (`verikora_session`). */
283
283
  cookie: string;
284
284
  /** The landing routes (`["/", "/fr", "/en"]`): exact paths, a trailing slash tolerated. */
285
285
  landing: readonly string[];
286
286
  /** The signed-in start page, or a function of the request (the language of the address). */
287
287
  home: string | ((request: Request) => string);
288
288
  /**
289
- * The product's own public origin (`https://dev.yomizaka.com`), from its declared environment (APP_URL), NEVER from the
289
+ * The product's own public origin (`https://dev.verikora.com`), from its declared environment (APP_URL), NEVER from the
290
290
  * request: `Host` and `x-forwarded-*` are client-controlled. With it the Location is absolute on that origin (what a
291
291
  * Next.js proxy accepts). Only the origin of the value is used. Optional outside production (the Location is then
292
292
  * relative); REQUIRED in production, where a missing origin throws instead of falling back to the request's headers.
@@ -494,7 +494,7 @@ type Fetcher = (url: string, init: {
494
494
  json(): Promise<unknown>;
495
495
  }>;
496
496
  interface UsersFederationOptions {
497
- /** The tenant host of the product: `https://auth.dev.akinaza.com`. */
497
+ /** The tenant host of the product: `https://auth.dev.folioviv.com`. */
498
498
  baseUrl: string;
499
499
  /** The product's public address: the start URL sends the person back to it, so `returnTo` is absolute from it. */
500
500
  appUrl: string;
@@ -503,7 +503,8 @@ interface UsersFederationOptions {
503
503
  /** Reads krizaka-users' answer into an identity; anything that is not a complete identity is refused. */
504
504
  declare function parseExternalIdentity(body: unknown): ExternalIdentity;
505
505
  /**
506
- * The adapter for krizaka-users (`auth.<product>` tenant host, krizaka/krizaka-users#31): `start` builds the tenant's start address,
506
+ * The adapter for the users service (`auth.<product>` tenant host), which lives in krizaka/krizaka-java `krizaka-users/` since
507
+ * ADR-021 (the federation landed as krizaka/krizaka-users#31, the old split): `start` builds the tenant's start address,
507
508
  * `complete` exchanges the one-time code (and the PKCE verifier of a native app) for the verified identity.
508
509
  */
509
510
  declare function usersIdentityProvider({ baseUrl, appUrl, fetch }: UsersFederationOptions): IdentityProvider;
package/dist/index.js CHANGED
@@ -14,7 +14,7 @@ import {
14
14
  pkceChallenge,
15
15
  rolesSchema,
16
16
  sessionsSchema
17
- } from "./chunk-5CFOMWBN.js";
17
+ } from "./chunk-2QUHBEBL.js";
18
18
  import {
19
19
  AccountError,
20
20
  EMAIL_CHANGE_TTL_HOURS,
@@ -29,13 +29,13 @@ import {
29
29
  linkToken,
30
30
  passwordResetMail,
31
31
  postgresAccounts
32
- } from "./chunk-3CLSDLXB.js";
32
+ } from "./chunk-73MO6V5M.js";
33
33
  import {
34
34
  ROLES_CODE,
35
35
  RolesError,
36
36
  can,
37
37
  defineRoles
38
- } from "./chunk-2PWX2B4I.js";
38
+ } from "./chunk-RWKBQE6N.js";
39
39
 
40
40
  // src/admin-gate.ts
41
41
  var ADMIN_REFUSAL = {
@@ -136,14 +136,18 @@ function landingRedirect(request, rule) {
136
136
  const path = `${target.pathname}${target.search}${target.hash}`;
137
137
  const base = rule.origin ? new URL(rule.origin).origin : null;
138
138
  const location = base && new URL(home, base).origin === base ? new URL(home, base).href : path;
139
- return new Response(null, { status: 307, headers: { Location: location, "Cache-Control": "private, no-store", Vary: "Cookie" } });
139
+ return new Response(null, {
140
+ status: 307,
141
+ headers: { Location: location, "Cache-Control": "private, no-store", Vary: "Cookie" }
142
+ });
140
143
  }
141
144
  function sessionHint(response, request, options) {
142
145
  const session = Boolean(cookieValue(request.headers, options.cookie));
143
146
  const flagged = cookieValue(request.headers, SESSION_HINT_COOKIE) === "1";
144
147
  const base = `${SESSION_HINT_COOKIE}=${session ? "1" : ""}; Path=/; SameSite=Lax${options.secure ? "; Secure" : ""}`;
145
148
  if (session && !flagged) response.headers.append("Set-Cookie", base);
146
- else if (!session && cookieValue(request.headers, SESSION_HINT_COOKIE) !== void 0) response.headers.append("Set-Cookie", `${base}; Max-Age=0`);
149
+ else if (!session && cookieValue(request.headers, SESSION_HINT_COOKIE) !== void 0)
150
+ response.headers.append("Set-Cookie", `${base}; Max-Age=0`);
147
151
  return response;
148
152
  }
149
153
 
@@ -157,8 +161,14 @@ function accountsMigration(options = {}) {
157
161
  return accountsSchema(options).migrations[0].sql({ schema: "public", tables: { accounts: "accounts" } });
158
162
  }
159
163
  function rolesMigration(roles, options = {}) {
160
- const tables = { owner: ident(options.ownerTable ?? "accounts"), ...options.scopeTable ? { scope: ident(options.scopeTable) } : {} };
161
- return rolesSchema(roles, { scoped: options.scopeTable !== void 0 }).migrations[0].sql({ schema: "public", tables });
164
+ const tables = {
165
+ owner: ident(options.ownerTable ?? "accounts"),
166
+ ...options.scopeTable ? { scope: ident(options.scopeTable) } : {}
167
+ };
168
+ return rolesSchema(roles, { scoped: options.scopeTable !== void 0 }).migrations[0].sql({
169
+ schema: "public",
170
+ tables
171
+ });
162
172
  }
163
173
 
164
174
  // src/password.ts
@@ -188,7 +198,12 @@ async function verifyPassword(password, stored) {
188
198
  const [scheme, n, r, p, salt, hash] = stored.split("$");
189
199
  if (scheme !== "scrypt" || !salt || !hash || password.length > PASSWORD_MAX_LENGTH) return false;
190
200
  const expected = Buffer.from(hash, "base64url");
191
- const given = await scrypt(password, Buffer.from(salt, "base64url"), expected.length, { N: Number(n), r: Number(r), p: Number(p), maxmem: MAXMEM });
201
+ const given = await scrypt(password, Buffer.from(salt, "base64url"), expected.length, {
202
+ N: Number(n),
203
+ r: Number(r),
204
+ p: Number(p),
205
+ maxmem: MAXMEM
206
+ });
192
207
  return given.length === expected.length && crypto2.timingSafeEqual(given, expected);
193
208
  }
194
209
  var decoy;
@@ -205,10 +220,16 @@ function createRoles(deps) {
205
220
  return role;
206
221
  };
207
222
  const newToken = deps.token ?? (() => `${deps.tokenPrefix}1.${crypto3.randomBytes(32).toString("base64url")}`);
208
- const owners = (scopeId) => query("SELECT account_id FROM memberships WHERE scope_id = $1 AND role = $2", [scopeId, ownerRole()]);
223
+ const owners = (scopeId) => query("SELECT account_id FROM memberships WHERE scope_id = $1 AND role = $2", [
224
+ scopeId,
225
+ ownerRole()
226
+ ]);
209
227
  const ownerRole = () => model.roles.find((r) => model.roles.every((x) => model.manages[r].includes(x) || x === r)) ?? model.roles[0];
210
228
  async function roleOf(accountId, scopeId) {
211
- const [row] = await query("SELECT role FROM memberships WHERE scope_id = $1 AND account_id = $2", [scopeId, accountId]);
229
+ const [row] = await query("SELECT role FROM memberships WHERE scope_id = $1 AND account_id = $2", [
230
+ scopeId,
231
+ accountId
232
+ ]);
212
233
  return row?.role ?? null;
213
234
  }
214
235
  return {
@@ -216,7 +237,10 @@ function createRoles(deps) {
216
237
  roleOf,
217
238
  /** Every scope the person belongs to, with their role in it. */
218
239
  async scopesOf(accountId) {
219
- return (await query("SELECT scope_id, role FROM memberships WHERE account_id = $1 ORDER BY created_at", [accountId])).map((r) => ({ scopeId: r.scope_id, role: r.role }));
240
+ return (await query(
241
+ "SELECT scope_id, role FROM memberships WHERE account_id = $1 ORDER BY created_at",
242
+ [accountId]
243
+ )).map((r) => ({ scopeId: r.scope_id, role: r.role }));
220
244
  },
221
245
  /** The person's role in the scope if it holds the permission; otherwise `NOT_A_MEMBER` (404: nothing tells the scope exists) or `FORBIDDEN`. */
222
246
  async authorize(accountId, scopeId, permission) {
@@ -238,7 +262,13 @@ function createRoles(deps) {
238
262
  "SELECT m.account_id, a.email, a.name, m.role, m.created_at FROM memberships m JOIN accounts a ON a.id = m.account_id WHERE m.scope_id = $1 ORDER BY m.created_at",
239
263
  [scopeId]
240
264
  );
241
- return rows.map((r) => ({ accountId: r.account_id, email: r.email, name: r.name, role: r.role, since: r.created_at }));
265
+ return rows.map((r) => ({
266
+ accountId: r.account_id,
267
+ email: r.email,
268
+ name: r.name,
269
+ role: r.role,
270
+ since: r.created_at
271
+ }));
242
272
  },
243
273
  /** Changes someone's role. The actor must manage both the current and the new role; the last owner can never be demoted. */
244
274
  async setRole(scopeId, actor, target, role) {
@@ -246,8 +276,10 @@ function createRoles(deps) {
246
276
  const mine = await roleOf(actor.id, scopeId);
247
277
  const current = await roleOf(target, scopeId);
248
278
  if (!mine || !current) throw new RolesError(ROLES_CODE.notMember);
249
- if (!model.manages[mine].includes(current) || !model.manages[mine].includes(role)) throw new RolesError(ROLES_CODE.notAllowed);
250
- if (current === ownerRole() && role !== current && (await owners(scopeId)).length <= 1) throw new RolesError(ROLES_CODE.lastOwner);
279
+ if (!model.manages[mine].includes(current) || !model.manages[mine].includes(role))
280
+ throw new RolesError(ROLES_CODE.notAllowed);
281
+ if (current === ownerRole() && role !== current && (await owners(scopeId)).length <= 1)
282
+ throw new RolesError(ROLES_CODE.lastOwner);
251
283
  await query("UPDATE memberships SET role = $3 WHERE scope_id = $1 AND account_id = $2", [scopeId, target, role]);
252
284
  },
253
285
  /** Removes someone from the scope (or leaves it, when the target is the actor). The last owner cannot go. */
@@ -266,22 +298,50 @@ function createRoles(deps) {
266
298
  if (!mine) throw new RolesError(ROLES_CODE.notMember);
267
299
  if (!model.manages[mine].includes(role)) throw new RolesError(ROLES_CODE.notAllowed);
268
300
  const email = input.email.trim().toLowerCase();
269
- await query("UPDATE invitations SET used_at = $3 WHERE scope_id = $1 AND email = $2 AND used_at IS NULL", [scopeId, email, now]);
301
+ await query("UPDATE invitations SET used_at = $3 WHERE scope_id = $1 AND email = $2 AND used_at IS NULL", [
302
+ scopeId,
303
+ email,
304
+ now
305
+ ]);
270
306
  const token = newToken();
271
307
  const [row] = await query(
272
308
  "INSERT INTO invitations (id, scope_id, email, role, token_hash, invited_by, created_at, expires_at) VALUES ($1, $2, $3, $4, $5, $6, $7, $8) RETURNING id, created_at, expires_at",
273
- [crypto3.randomUUID(), scopeId, email, role, deps.tokenHash(token), actor.id, now, addHours(now, deps.inviteTtlHours)]
309
+ [
310
+ crypto3.randomUUID(),
311
+ scopeId,
312
+ email,
313
+ role,
314
+ deps.tokenHash(token),
315
+ actor.id,
316
+ now,
317
+ addHours(now, deps.inviteTtlHours)
318
+ ]
274
319
  );
275
- return { token, invitation: { id: row.id, email, role, createdAt: row.created_at, expiresAt: row.expires_at } };
320
+ return {
321
+ token,
322
+ invitation: { id: row.id, email, role, createdAt: row.created_at, expiresAt: row.expires_at }
323
+ };
276
324
  },
277
325
  async pendingInvitations(scopeId, now) {
278
- const rows = await query("SELECT id, email, role, created_at, expires_at FROM invitations WHERE scope_id = $1 AND used_at IS NULL AND expires_at > $2 ORDER BY created_at", [scopeId, now]);
279
- return rows.map((r) => ({ id: r.id, email: r.email, role: r.role, createdAt: r.created_at, expiresAt: r.expires_at }));
326
+ const rows = await query(
327
+ "SELECT id, email, role, created_at, expires_at FROM invitations WHERE scope_id = $1 AND used_at IS NULL AND expires_at > $2 ORDER BY created_at",
328
+ [scopeId, now]
329
+ );
330
+ return rows.map((r) => ({
331
+ id: r.id,
332
+ email: r.email,
333
+ role: r.role,
334
+ createdAt: r.created_at,
335
+ expiresAt: r.expires_at
336
+ }));
280
337
  },
281
338
  async revokeInvitation(scopeId, actor, invitationId, now) {
282
339
  const mine = await roleOf(actor.id, scopeId);
283
340
  if (!mine) throw new RolesError(ROLES_CODE.notMember);
284
- const [row] = await query("SELECT role FROM invitations WHERE id = $1 AND scope_id = $2 AND used_at IS NULL", [invitationId, scopeId]);
341
+ const [row] = await query(
342
+ "SELECT role FROM invitations WHERE id = $1 AND scope_id = $2 AND used_at IS NULL",
343
+ [invitationId, scopeId]
344
+ );
285
345
  if (!row) throw new RolesError(ROLES_CODE.inviteInvalid);
286
346
  if (!model.manages[mine].includes(row.role)) throw new RolesError(ROLES_CODE.notAllowed);
287
347
  await query("UPDATE invitations SET used_at = $2 WHERE id = $1", [invitationId, now]);
@@ -296,8 +356,15 @@ function createRoles(deps) {
296
356
  if (row.email !== account.email.trim().toLowerCase()) throw new RolesError(ROLES_CODE.inviteWrongAccount);
297
357
  await query("UPDATE invitations SET used_at = $2 WHERE id = $1", [row.id, now]);
298
358
  const current = await roleOf(account.id, row.scope_id);
299
- if (!current || model.manages[current].length < model.manages[row.role].length) await query("INSERT INTO memberships (scope_id, account_id, role, invited_by, created_at) VALUES ($1, $2, $3, $4, $5) ON CONFLICT (scope_id, account_id) DO UPDATE SET role = excluded.role", [row.scope_id, account.id, row.role, row.invited_by, now]);
300
- return { scopeId: row.scope_id, role: !current || model.manages[current].length < model.manages[row.role].length ? row.role : current };
359
+ if (!current || model.manages[current].length < model.manages[row.role].length)
360
+ await query(
361
+ "INSERT INTO memberships (scope_id, account_id, role, invited_by, created_at) VALUES ($1, $2, $3, $4, $5) ON CONFLICT (scope_id, account_id) DO UPDATE SET role = excluded.role",
362
+ [row.scope_id, account.id, row.role, row.invited_by, now]
363
+ );
364
+ return {
365
+ scopeId: row.scope_id,
366
+ role: !current || model.manages[current].length < model.manages[row.role].length ? row.role : current
367
+ };
301
368
  }
302
369
  };
303
370
  }
@@ -308,7 +375,8 @@ var BODY = /^[A-Za-z0-9_-]{43}$/;
308
375
  var PREFIX = /^[a-z]{1,4}$/;
309
376
  var USER_AGENT_MAX = 300;
310
377
  function createSessions(deps) {
311
- if (!PREFIX.test(deps.tokenPrefix)) throw new RangeError(`tokenPrefix must be 1 to 4 lower-case letters, got ${deps.tokenPrefix}`);
378
+ if (!PREFIX.test(deps.tokenPrefix))
379
+ throw new RangeError(`tokenPrefix must be 1 to 4 lower-case letters, got ${deps.tokenPrefix}`);
312
380
  const head = `${deps.tokenPrefix}1.`;
313
381
  const secret = () => typeof deps.secret === "function" ? deps.secret() : deps.secret;
314
382
  const isToken = (value) => !!value && value.startsWith(head) && BODY.test(value.slice(head.length));
@@ -320,12 +388,20 @@ function createSessions(deps) {
320
388
  tokenHash,
321
389
  async create(accountId, meta) {
322
390
  const days = typeof deps.sessionDays === "function" ? await deps.sessionDays() : deps.sessionDays;
323
- if (!Number.isInteger(days) || days < 1) throw new RangeError(`sessionDays must be a whole number of days from 1, got ${days}`);
391
+ if (!Number.isInteger(days) || days < 1)
392
+ throw new RangeError(`sessionDays must be a whole number of days from 1, got ${days}`);
324
393
  const token = newToken();
325
394
  const createdAt = deps.clock.now();
326
395
  const expires = new Date(createdAt);
327
396
  expires.setUTCHours(expires.getUTCHours() + days * 24);
328
- await deps.store.insert({ accountId, tokenHash: tokenHash(token), createdAt, expiresAt: expires, ip: meta.ip, userAgent: meta.userAgent?.slice(0, USER_AGENT_MAX) ?? null });
397
+ await deps.store.insert({
398
+ accountId,
399
+ tokenHash: tokenHash(token),
400
+ createdAt,
401
+ expiresAt: expires,
402
+ ip: meta.ip,
403
+ userAgent: meta.userAgent?.slice(0, USER_AGENT_MAX) ?? null
404
+ });
329
405
  return { token, expires };
330
406
  },
331
407
  async resolve(token) {
@@ -339,7 +415,15 @@ function createSessions(deps) {
339
415
  };
340
416
  }
341
417
  function sessionCookie(token, expires, options) {
342
- return { name: options.name, value: token, httpOnly: true, secure: options.secure, sameSite: "lax", path: "/", expires };
418
+ return {
419
+ name: options.name,
420
+ value: token,
421
+ httpOnly: true,
422
+ secure: options.secure,
423
+ sameSite: "lax",
424
+ path: "/",
425
+ expires
426
+ };
343
427
  }
344
428
  function clearedSessionCookie(options, now) {
345
429
  return sessionCookie("", now, options);
@@ -385,7 +469,10 @@ function dbSessionStore(query, options) {
385
469
  const active = options.active ? ` AND a.${ident2(options.active.column)} = $3` : "";
386
470
  return {
387
471
  async insert(session) {
388
- await query(`INSERT INTO ${sessions} (account_id, token_hash, created_at, expires_at, ip, user_agent) VALUES ($1, $2, $3, $4, $5, $6)`, [session.accountId, session.tokenHash, session.createdAt, session.expiresAt, session.ip, session.userAgent]);
472
+ await query(
473
+ `INSERT INTO ${sessions} (account_id, token_hash, created_at, expires_at, ip, user_agent) VALUES ($1, $2, $3, $4, $5, $6)`,
474
+ [session.accountId, session.tokenHash, session.createdAt, session.expiresAt, session.ip, session.userAgent]
475
+ );
389
476
  },
390
477
  async find(tokenHash, now) {
391
478
  const params = [tokenHash, now];
@@ -412,7 +499,8 @@ function parseExternalIdentity(body) {
412
499
  const o = body ?? {};
413
500
  const issuer = text(o.issuer);
414
501
  const subject = text(o.subject);
415
- if (!issuer || !subject) throw new FederationError(FEDERATION_CODE.unavailable, "krizaka-users answered without an issuer and a subject");
502
+ if (!issuer || !subject)
503
+ throw new FederationError(FEDERATION_CODE.unavailable, "krizaka-users answered without an issuer and a subject");
416
504
  return { issuer, subject, email: text(o.email), emailVerified: o.email_verified === true, name: text(o.name) };
417
505
  }
418
506
  function usersIdentityProvider({ baseUrl, appUrl, fetch }) {
@@ -420,8 +508,10 @@ function usersIdentityProvider({ baseUrl, appUrl, fetch }) {
420
508
  const app = appUrl.replace(/\/+$/, "");
421
509
  return {
422
510
  async start(provider, returnTo, options = {}) {
423
- if (!isFederatedProvider(provider)) throw new FederationError(FEDERATION_CODE.unknownProvider, `Unknown provider ${String(provider)}`);
424
- if (!isLocalReturn(returnTo)) throw new FederationError(FEDERATION_CODE.badReturn, "That return address is not allowed");
511
+ if (!isFederatedProvider(provider))
512
+ throw new FederationError(FEDERATION_CODE.unknownProvider, `Unknown provider ${String(provider)}`);
513
+ if (!isLocalReturn(returnTo))
514
+ throw new FederationError(FEDERATION_CODE.badReturn, "That return address is not allowed");
425
515
  const state = crypto.randomUUID();
426
516
  const query = new URLSearchParams({ return_to: `${app}${returnTo}`, state });
427
517
  if (options.codeChallenge) {
@@ -436,8 +526,10 @@ function usersIdentityProvider({ baseUrl, appUrl, fetch }) {
436
526
  headers: { "content-type": "application/json" },
437
527
  body: JSON.stringify({ code, ...options.codeVerifier ? { code_verifier: options.codeVerifier } : {} })
438
528
  });
439
- if (response.status === 400 || response.status === 401 || response.status === 404 || response.status === 410) throw new FederationError(FEDERATION_CODE.invalidCode, "That code is not valid");
440
- if (!response.ok) throw new FederationError(FEDERATION_CODE.unavailable, `krizaka-users answered ${response.status}`);
529
+ if (response.status === 400 || response.status === 401 || response.status === 404 || response.status === 410)
530
+ throw new FederationError(FEDERATION_CODE.invalidCode, "That code is not valid");
531
+ if (!response.ok)
532
+ throw new FederationError(FEDERATION_CODE.unavailable, `krizaka-users answered ${response.status}`);
441
533
  return parseExternalIdentity(await response.json());
442
534
  }
443
535
  };