@krizaka/auth 0.5.0 → 0.7.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.
- package/README.md +23 -2
- package/dist/chunk-H7AKE2OH.js +131 -0
- package/dist/chunk-H7AKE2OH.js.map +1 -0
- package/dist/identity-links-C5AG7Fbp.d.ts +239 -0
- package/dist/index.d.ts +72 -126
- package/dist/index.js +109 -9
- package/dist/index.js.map +1 -1
- package/dist/testkit.d.ts +15 -0
- package/dist/testkit.js +101 -0
- package/dist/testkit.js.map +1 -0
- package/package.json +13 -1
package/dist/index.d.ts
CHANGED
|
@@ -1,133 +1,54 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
* 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
|
|
4
|
-
* service, no runtime dependency between products.
|
|
5
|
-
*/
|
|
6
|
-
type Theme = "light" | "dark" | "system";
|
|
7
|
-
interface AccountProfile {
|
|
8
|
-
id: string;
|
|
9
|
-
email: string;
|
|
10
|
-
name: string | null;
|
|
11
|
-
locale: "en" | "fr";
|
|
12
|
-
/** The theme the person chose; null = follow the device. */
|
|
13
|
-
theme: Theme | null;
|
|
14
|
-
/** A seed of the default avatars (the UI draws it); null = derived from the e-mail address. */
|
|
15
|
-
avatarSeed: string | null;
|
|
16
|
-
emailVerified: boolean;
|
|
17
|
-
/** An address awaiting its verification link, if the person asked to change it. */
|
|
18
|
-
pendingEmail: string | null;
|
|
19
|
-
}
|
|
20
|
-
interface ProfilePatch {
|
|
21
|
-
name?: string | null;
|
|
22
|
-
locale?: "en" | "fr";
|
|
23
|
-
theme?: Theme | null;
|
|
24
|
-
avatarSeed?: string | null;
|
|
25
|
-
}
|
|
26
|
-
/** A sign-in of the person: where from and when. Never the token. */
|
|
27
|
-
interface SessionSummary {
|
|
28
|
-
id: string;
|
|
29
|
-
createdAt: Date;
|
|
30
|
-
expiresAt: Date;
|
|
31
|
-
ip: string | null;
|
|
32
|
-
userAgent: string | null;
|
|
33
|
-
current: boolean;
|
|
34
|
-
}
|
|
35
|
-
interface AccountService {
|
|
36
|
-
profile(accountId: string): Promise<AccountProfile>;
|
|
37
|
-
updateProfile(accountId: string, patch: ProfilePatch): Promise<AccountProfile>;
|
|
38
|
-
/** Asks to change the address: the password is checked, a link is mailed to the NEW address. Answers the same whether or not it is already taken (no enumeration). */
|
|
39
|
-
requestEmailChange(accountId: string, input: {
|
|
40
|
-
newEmail: string;
|
|
41
|
-
password: string;
|
|
42
|
-
}, now: Date): Promise<void>;
|
|
43
|
-
/** Applies the change from the mailed link (single use, expires); the new address is verified by that very act. */
|
|
44
|
-
confirmEmailChange(token: string, now: Date): Promise<"CHANGED" | "INVALID">;
|
|
45
|
-
/** Changes the password (the current one is checked) and signs every other session out; `keepSessionId` stays. */
|
|
46
|
-
changePassword(accountId: string, input: {
|
|
47
|
-
current: string;
|
|
48
|
-
next: string;
|
|
49
|
-
}, keepSessionId?: string): Promise<void>;
|
|
50
|
-
/** Asks for a reset link, mailed (in the person's language) when an account with a password has this address; answers the same either way (no enumeration). */
|
|
51
|
-
requestPasswordReset(email: string, now: Date): Promise<void>;
|
|
52
|
-
/** Sets a new password from the mailed link — single use, expires — and signs every session out. The new password is checked before the link is used up. */
|
|
53
|
-
resetPassword(token: string, next: string, now: Date): Promise<"CHANGED" | "INVALID">;
|
|
54
|
-
/** Checks the password. Throws `AccountError(403, "WRONG_PASSWORD")`. */
|
|
55
|
-
confirmPassword(accountId: string, password: string): Promise<void>;
|
|
56
|
-
/** The person's live sign-ins, newest first; `currentSessionId` is flagged. */
|
|
57
|
-
sessions(accountId: string, now: Date, currentSessionId?: string): Promise<SessionSummary[]>;
|
|
58
|
-
/** Signs every other session out (and returns how many); `keepSessionId` stays. */
|
|
59
|
-
signOutOthers(accountId: string, keepSessionId: string): Promise<number>;
|
|
60
|
-
/** What the product keeps about the person, as one JSON-able document: the profile, the live sign-ins, and what `extra` adds. */
|
|
61
|
-
exportAccount(accountId: string, now: Date, extra?: () => Promise<Record<string, unknown>>): Promise<Record<string, unknown>>;
|
|
62
|
-
/** Deletes the identity: the account, its sessions and everything that hangs on it (the product erases its own data first). */
|
|
63
|
-
deleteAccount(accountId: string): Promise<void>;
|
|
64
|
-
}
|
|
65
|
-
declare class AccountError extends Error {
|
|
66
|
-
readonly status: number;
|
|
67
|
-
readonly code: string;
|
|
68
|
-
constructor(status: number, code: string, message: string);
|
|
69
|
-
}
|
|
1
|
+
import { E as ExternalIdentity, I as IdentityLinkStore, a as IdentityProvider } from './identity-links-C5AG7Fbp.js';
|
|
2
|
+
export { A as AccountDeps, b as AccountError, c as AccountProfile, d as AccountService, C as CompleteOptions, e as EMAIL_CHANGE_TTL_HOURS, F as FEDERATED_PROVIDERS, f as FEDERATION_CODE, g as FederatedProvider, h as FederationCode, i as FederationError, j as IdentityLinkError, M as Mail, k as MailLink, P as PASSWORD_RESET_MAX_MINUTES, l as PASSWORD_RESET_MIN_MINUTES, m as PASSWORD_RESET_TTL_MINUTES, n as ProfilePatch, Q as Query, S as SessionSummary, o as StartOptions, p as StartedSignIn, T as Theme, q as dbAccounts, r as dbIdentityLinks, s as emailChangeMail, t as emailChangeToken, u as identityLinksMigration, v as isEmailChangeToken, w as isFederatedProvider, x as isLinkToken, y as isLocalReturn, z as linkToken, B as memoryIdentityLinks, D as memoryIdentityProvider, G as passwordResetMail, H as pkceChallenge, J as postgresAccounts } from './identity-links-C5AG7Fbp.js';
|
|
70
3
|
|
|
71
|
-
/**
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
4
|
+
/** What federated sign-in needs from the product's accounts: no second user table, just a look-up and, when registration is open, a creation. */
|
|
5
|
+
interface FederatedAccounts {
|
|
6
|
+
/** The account that holds this address (lower case), and whether the product has verified it; null when none. */
|
|
7
|
+
findByEmail(email: string): Promise<{
|
|
8
|
+
id: string;
|
|
9
|
+
emailVerified: boolean;
|
|
10
|
+
} | null>;
|
|
11
|
+
/** Creates an account for a person the provider vouches for (address verified, no password). Absent when registration is closed. */
|
|
12
|
+
create?(identity: ExternalIdentity & {
|
|
13
|
+
email: string;
|
|
14
|
+
}): Promise<string>;
|
|
79
15
|
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
16
|
+
/** Why a sign-in was refused: a stable code the product maps to its words. */
|
|
17
|
+
declare const FEDERATED_REFUSAL: {
|
|
18
|
+
/** The provider did not verify the address (or gave none): nothing is linked, nothing is created. */
|
|
19
|
+
readonly emailNotVerified: "EMAIL_NOT_VERIFIED";
|
|
20
|
+
/** An account holds the address but the product never verified it: linking would let a stranger take it over. */
|
|
21
|
+
readonly accountEmailUnverified: "ACCOUNT_EMAIL_UNVERIFIED";
|
|
22
|
+
/** No account, and registration is closed. */
|
|
23
|
+
readonly noAccount: "NO_ACCOUNT";
|
|
24
|
+
};
|
|
25
|
+
type FederatedRefusal = (typeof FEDERATED_REFUSAL)[keyof typeof FEDERATED_REFUSAL];
|
|
26
|
+
type FederatedSignInResult =
|
|
27
|
+
/** `via`: `link` already linked, `linked` attached to the account of that address now, `created` a new account. */
|
|
28
|
+
{
|
|
29
|
+
status: "signed-in";
|
|
30
|
+
accountId: string;
|
|
31
|
+
via: "link" | "linked" | "created";
|
|
32
|
+
} | {
|
|
33
|
+
status: "refused";
|
|
34
|
+
code: FederatedRefusal;
|
|
35
|
+
};
|
|
36
|
+
interface FederatedSignInDeps {
|
|
37
|
+
links: IdentityLinkStore;
|
|
38
|
+
accounts: FederatedAccounts;
|
|
39
|
+
clock: {
|
|
40
|
+
now(): Date;
|
|
99
41
|
};
|
|
100
|
-
/** How a token is kept: the product's own keyed hash, the one its sessions use. */
|
|
101
|
-
tokenHash: (token: string) => string;
|
|
102
|
-
/**
|
|
103
|
-
* How long a password reset link lives, in minutes: a whole number from 5 to 1440 (`PASSWORD_RESET_MIN_MINUTES`–`PASSWORD_RESET_MAX_MINUTES`),
|
|
104
|
-
* default `PASSWORD_RESET_TTL_MINUTES` (30). The product reads it from its settings; the expiry and the mailed text both use it.
|
|
105
|
-
* Out of range, `postgresAccounts` throws.
|
|
106
|
-
*/
|
|
107
|
-
passwordResetTtlMinutes?: number;
|
|
108
|
-
/** Test seam: the token to issue. */
|
|
109
|
-
token?: () => string;
|
|
110
42
|
}
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
declare const isLinkToken: (prefix: string, value: string | undefined | null) => value is string;
|
|
119
|
-
declare const emailChangeToken: (prefix: string) => string;
|
|
120
|
-
declare const isEmailChangeToken: (prefix: string, value: string | undefined | null) => value is string;
|
|
121
|
-
declare function passwordResetMail(product: string, locale: "en" | "fr", url: string, ttlMinutes?: number): {
|
|
122
|
-
subject: string;
|
|
123
|
-
text: string;
|
|
124
|
-
};
|
|
125
|
-
declare function emailChangeMail(product: string, locale: "en" | "fr", url: string): {
|
|
126
|
-
subject: string;
|
|
127
|
-
text: string;
|
|
43
|
+
/**
|
|
44
|
+
* From a verified external identity to an account of the product. The rule that matters: an identity attaches to an EXISTING account
|
|
45
|
+
* only when the address is verified on both sides — by the provider and by the product. Otherwise a stranger who registers at
|
|
46
|
+
* Facebook with your address would sign in as you.
|
|
47
|
+
*/
|
|
48
|
+
declare function createFederatedSignIn({ links, accounts, clock }: FederatedSignInDeps): {
|
|
49
|
+
signIn(identity: ExternalIdentity): Promise<FederatedSignInResult>;
|
|
128
50
|
};
|
|
129
|
-
|
|
130
|
-
declare function postgresAccounts(deps: AccountDeps): AccountService;
|
|
51
|
+
type FederatedSignIn = ReturnType<typeof createFederatedSignIn>;
|
|
131
52
|
|
|
132
53
|
/**
|
|
133
54
|
* What the account adapter needs from a product's database, safe to run on a database that already has `accounts` and `sessions`:
|
|
@@ -250,4 +171,29 @@ declare function createRoles<Role extends string, Permission extends string>(dep
|
|
|
250
171
|
};
|
|
251
172
|
type Roles<Role extends string = string, Permission extends string = string> = ReturnType<typeof createRoles<Role, Permission>>;
|
|
252
173
|
|
|
253
|
-
|
|
174
|
+
/** The `fetch` the adapter calls krizaka-users with (injected: no global, no network in tests). */
|
|
175
|
+
type Fetcher = (url: string, init: {
|
|
176
|
+
method: "POST";
|
|
177
|
+
headers: Record<string, string>;
|
|
178
|
+
body: string;
|
|
179
|
+
}) => Promise<{
|
|
180
|
+
ok: boolean;
|
|
181
|
+
status: number;
|
|
182
|
+
json(): Promise<unknown>;
|
|
183
|
+
}>;
|
|
184
|
+
interface UsersFederationOptions {
|
|
185
|
+
/** The tenant host of the product: `https://auth.dev.akinaza.com`. */
|
|
186
|
+
baseUrl: string;
|
|
187
|
+
/** The product's public address: the start URL sends the person back to it, so `returnTo` is absolute from it. */
|
|
188
|
+
appUrl: string;
|
|
189
|
+
fetch: Fetcher;
|
|
190
|
+
}
|
|
191
|
+
/** Reads krizaka-users' answer into an identity; anything that is not a complete identity is refused. */
|
|
192
|
+
declare function parseExternalIdentity(body: unknown): ExternalIdentity;
|
|
193
|
+
/**
|
|
194
|
+
* The adapter for krizaka-users (`auth.<product>` tenant host, krizaka/krizaka-users#31): `start` builds the tenant's start address,
|
|
195
|
+
* `complete` exchanges the one-time code (and the PKCE verifier of a native app) for the verified identity.
|
|
196
|
+
*/
|
|
197
|
+
declare function usersIdentityProvider({ baseUrl, appUrl, fetch }: UsersFederationOptions): IdentityProvider;
|
|
198
|
+
|
|
199
|
+
export { ExternalIdentity, FEDERATED_REFUSAL, type FederatedAccounts, type FederatedRefusal, type FederatedSignIn, type FederatedSignInDeps, type FederatedSignInResult, type Fetcher, IdentityLinkStore, IdentityProvider, type Invitation, type Member, ROLES_CODE, type RoleModel, type Roles, type RolesCode, type RolesDeps, RolesError, type RolesQuery, type UsersFederationOptions, accountsMigration, can, createFederatedSignIn, createRoles, defineRoles, parseExternalIdentity, rolesMigration, usersIdentityProvider };
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
|
+
import {
|
|
2
|
+
FEDERATED_PROVIDERS,
|
|
3
|
+
FEDERATION_CODE,
|
|
4
|
+
FederationError,
|
|
5
|
+
IdentityLinkError,
|
|
6
|
+
dbIdentityLinks,
|
|
7
|
+
identityLinksMigration,
|
|
8
|
+
isFederatedProvider,
|
|
9
|
+
isLocalReturn,
|
|
10
|
+
memoryIdentityLinks,
|
|
11
|
+
memoryIdentityProvider,
|
|
12
|
+
pkceChallenge
|
|
13
|
+
} from "./chunk-H7AKE2OH.js";
|
|
14
|
+
|
|
1
15
|
// src/accounts.ts
|
|
2
|
-
import
|
|
16
|
+
import crypto2 from "crypto";
|
|
3
17
|
import { addHours, addMinutes } from "date-fns";
|
|
4
18
|
|
|
5
19
|
// src/port.ts
|
|
@@ -29,7 +43,7 @@ var NAME_MAX = 80;
|
|
|
29
43
|
var PASSWORD_MIN = 12;
|
|
30
44
|
var PASSWORD_MAX = 256;
|
|
31
45
|
var THEMES = ["light", "dark", "system"];
|
|
32
|
-
var linkToken = (prefix) => `${prefix}1.${
|
|
46
|
+
var linkToken = (prefix) => `${prefix}1.${crypto2.randomBytes(32).toString("base64url")}`;
|
|
33
47
|
var isLinkToken = (prefix, value) => !!value && new RegExp(`^${prefix}1\\.[A-Za-z0-9_-]{43}$`).test(value);
|
|
34
48
|
var emailChangeToken = linkToken;
|
|
35
49
|
var isEmailChangeToken = isLinkToken;
|
|
@@ -77,7 +91,7 @@ var cleanName = (raw) => {
|
|
|
77
91
|
if (name.length > NAME_MAX) throw new AccountError(400, "BAD_REQUEST", `A name is at most ${NAME_MAX} characters`);
|
|
78
92
|
return name;
|
|
79
93
|
};
|
|
80
|
-
function
|
|
94
|
+
function dbAccounts(deps) {
|
|
81
95
|
const { query } = deps;
|
|
82
96
|
const resetTtlMinutes = checkResetTtl(deps.passwordResetTtlMinutes ?? PASSWORD_RESET_TTL_MINUTES);
|
|
83
97
|
const transaction = deps.transaction ?? ((work) => work(query));
|
|
@@ -120,7 +134,7 @@ function postgresAccounts(deps) {
|
|
|
120
134
|
const token = (deps.token ?? (() => emailChangeToken(deps.tokenPrefix)))();
|
|
121
135
|
await query("INSERT INTO email_changes (account_id, new_email, token_hash, expires_at) VALUES ($1, $2, $3, $4)", [id, address, deps.tokenHash(token), addHours(now, EMAIL_CHANGE_TTL_HOURS)]);
|
|
122
136
|
const url = `${deps.baseUrl.replace(/\/$/, "")}/${profile.locale}/confirm-email?token=${encodeURIComponent(token)}`;
|
|
123
|
-
await deps.mail({ to: address, locale: profile.locale, ...emailChangeMail(deps.product, profile.locale, url) });
|
|
137
|
+
await deps.mail({ to: address, locale: profile.locale, ...emailChangeMail(deps.product, profile.locale, url), link: { kind: "emailChange", url, hours: EMAIL_CHANGE_TTL_HOURS } });
|
|
124
138
|
},
|
|
125
139
|
async confirmEmailChange(token, now) {
|
|
126
140
|
if (!isEmailChangeToken(deps.tokenPrefix, token)) return "INVALID";
|
|
@@ -154,7 +168,7 @@ function postgresAccounts(deps) {
|
|
|
154
168
|
});
|
|
155
169
|
if (!issued) return;
|
|
156
170
|
const url = `${deps.baseUrl.replace(/\/$/, "")}/${issued.locale}/reset-password?token=${encodeURIComponent(issued.token)}`;
|
|
157
|
-
await deps.mail({ to: address, locale: issued.locale, ...passwordResetMail(deps.product, issued.locale, url, resetTtlMinutes) });
|
|
171
|
+
await deps.mail({ to: address, locale: issued.locale, ...passwordResetMail(deps.product, issued.locale, url, resetTtlMinutes), link: { kind: "passwordReset", url, minutes: resetTtlMinutes } });
|
|
158
172
|
},
|
|
159
173
|
async resetPassword(token, next, now) {
|
|
160
174
|
if (next.length < PASSWORD_MIN) throw new AccountError(400, "PASSWORD_TOO_SHORT", `At least ${PASSWORD_MIN} characters`);
|
|
@@ -186,6 +200,37 @@ function postgresAccounts(deps) {
|
|
|
186
200
|
}
|
|
187
201
|
};
|
|
188
202
|
}
|
|
203
|
+
var postgresAccounts = dbAccounts;
|
|
204
|
+
|
|
205
|
+
// src/federated-sign-in.ts
|
|
206
|
+
var FEDERATED_REFUSAL = {
|
|
207
|
+
/** The provider did not verify the address (or gave none): nothing is linked, nothing is created. */
|
|
208
|
+
emailNotVerified: "EMAIL_NOT_VERIFIED",
|
|
209
|
+
/** An account holds the address but the product never verified it: linking would let a stranger take it over. */
|
|
210
|
+
accountEmailUnverified: "ACCOUNT_EMAIL_UNVERIFIED",
|
|
211
|
+
/** No account, and registration is closed. */
|
|
212
|
+
noAccount: "NO_ACCOUNT"
|
|
213
|
+
};
|
|
214
|
+
function createFederatedSignIn({ links, accounts, clock }) {
|
|
215
|
+
return {
|
|
216
|
+
async signIn(identity) {
|
|
217
|
+
const known = await links.find(identity.issuer, identity.subject);
|
|
218
|
+
if (known) return { status: "signed-in", accountId: known, via: "link" };
|
|
219
|
+
const email = identity.email?.trim().toLowerCase();
|
|
220
|
+
if (!email || !identity.emailVerified) return { status: "refused", code: FEDERATED_REFUSAL.emailNotVerified };
|
|
221
|
+
const existing = await accounts.findByEmail(email);
|
|
222
|
+
if (existing) {
|
|
223
|
+
if (!existing.emailVerified) return { status: "refused", code: FEDERATED_REFUSAL.accountEmailUnverified };
|
|
224
|
+
await links.link(existing.id, identity.issuer, identity.subject, clock.now());
|
|
225
|
+
return { status: "signed-in", accountId: existing.id, via: "linked" };
|
|
226
|
+
}
|
|
227
|
+
if (!accounts.create) return { status: "refused", code: FEDERATED_REFUSAL.noAccount };
|
|
228
|
+
const accountId = await accounts.create({ ...identity, email });
|
|
229
|
+
await links.link(accountId, identity.issuer, identity.subject, clock.now());
|
|
230
|
+
return { status: "signed-in", accountId, via: "created" };
|
|
231
|
+
}
|
|
232
|
+
};
|
|
233
|
+
}
|
|
189
234
|
|
|
190
235
|
// src/migration.ts
|
|
191
236
|
function accountsMigration(options = {}) {
|
|
@@ -248,7 +293,7 @@ CREATE INDEX IF NOT EXISTS invitations_scope ON invitations(scope_id) WHERE used
|
|
|
248
293
|
}
|
|
249
294
|
|
|
250
295
|
// src/roles.ts
|
|
251
|
-
import
|
|
296
|
+
import crypto3 from "crypto";
|
|
252
297
|
import { addHours as addHours2 } from "date-fns";
|
|
253
298
|
var defineRoles = (model) => model;
|
|
254
299
|
var can = (model, role, permission) => !!role && model.permissions[role].includes(permission);
|
|
@@ -279,7 +324,7 @@ function createRoles(deps) {
|
|
|
279
324
|
if (!known(role)) throw new RolesError(ROLES_CODE.unknownRole);
|
|
280
325
|
return role;
|
|
281
326
|
};
|
|
282
|
-
const newToken = deps.token ?? (() => `${deps.tokenPrefix}1.${
|
|
327
|
+
const newToken = deps.token ?? (() => `${deps.tokenPrefix}1.${crypto3.randomBytes(32).toString("base64url")}`);
|
|
283
328
|
const owners = (scopeId) => query("SELECT account_id FROM memberships WHERE scope_id = $1 AND role = $2", [scopeId, ownerRole()]);
|
|
284
329
|
const ownerRole = () => model.roles.find((r) => model.roles.every((x) => model.manages[r].includes(x) || x === r)) ?? model.roles[0];
|
|
285
330
|
async function roleOf(accountId, scopeId) {
|
|
@@ -345,7 +390,7 @@ function createRoles(deps) {
|
|
|
345
390
|
const token = newToken();
|
|
346
391
|
const [row] = await query(
|
|
347
392
|
"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",
|
|
348
|
-
[
|
|
393
|
+
[crypto3.randomUUID(), scopeId, email, role, deps.tokenHash(token), actor.id, now, addHours2(now, deps.inviteTtlHours)]
|
|
349
394
|
);
|
|
350
395
|
return { token, invitation: { id: row.id, email, role, createdAt: row.created_at, expiresAt: row.expires_at } };
|
|
351
396
|
},
|
|
@@ -376,9 +421,53 @@ function createRoles(deps) {
|
|
|
376
421
|
}
|
|
377
422
|
};
|
|
378
423
|
}
|
|
424
|
+
|
|
425
|
+
// src/users-federation.ts
|
|
426
|
+
var START = (base, provider) => `${base}/api/v1/auth/web/${provider}/start`;
|
|
427
|
+
var EXCHANGE = (base) => `${base}/api/v1/auth/web/exchange`;
|
|
428
|
+
var text = (value) => typeof value === "string" && value.length > 0 ? value : null;
|
|
429
|
+
function parseExternalIdentity(body) {
|
|
430
|
+
const o = body ?? {};
|
|
431
|
+
const issuer = text(o.issuer);
|
|
432
|
+
const subject = text(o.subject);
|
|
433
|
+
if (!issuer || !subject) throw new FederationError(FEDERATION_CODE.unavailable, "krizaka-users answered without an issuer and a subject");
|
|
434
|
+
return { issuer, subject, email: text(o.email), emailVerified: o.email_verified === true, name: text(o.name) };
|
|
435
|
+
}
|
|
436
|
+
function usersIdentityProvider({ baseUrl, appUrl, fetch }) {
|
|
437
|
+
const base = baseUrl.replace(/\/+$/, "");
|
|
438
|
+
const app = appUrl.replace(/\/+$/, "");
|
|
439
|
+
return {
|
|
440
|
+
async start(provider, returnTo, options = {}) {
|
|
441
|
+
if (!isFederatedProvider(provider)) throw new FederationError(FEDERATION_CODE.unknownProvider, `Unknown provider ${String(provider)}`);
|
|
442
|
+
if (!isLocalReturn(returnTo)) throw new FederationError(FEDERATION_CODE.badReturn, "That return address is not allowed");
|
|
443
|
+
const state = crypto.randomUUID();
|
|
444
|
+
const query = new URLSearchParams({ return_to: `${app}${returnTo}`, state });
|
|
445
|
+
if (options.codeChallenge) {
|
|
446
|
+
query.set("code_challenge", options.codeChallenge);
|
|
447
|
+
query.set("code_challenge_method", "S256");
|
|
448
|
+
}
|
|
449
|
+
return { url: `${START(base, provider)}?${query}`, state };
|
|
450
|
+
},
|
|
451
|
+
async complete(code, options = {}) {
|
|
452
|
+
const response = await fetch(EXCHANGE(base), {
|
|
453
|
+
method: "POST",
|
|
454
|
+
headers: { "content-type": "application/json" },
|
|
455
|
+
body: JSON.stringify({ code, ...options.codeVerifier ? { code_verifier: options.codeVerifier } : {} })
|
|
456
|
+
});
|
|
457
|
+
if (response.status === 400 || response.status === 401 || response.status === 404 || response.status === 410) throw new FederationError(FEDERATION_CODE.invalidCode, "That code is not valid");
|
|
458
|
+
if (!response.ok) throw new FederationError(FEDERATION_CODE.unavailable, `krizaka-users answered ${response.status}`);
|
|
459
|
+
return parseExternalIdentity(await response.json());
|
|
460
|
+
}
|
|
461
|
+
};
|
|
462
|
+
}
|
|
379
463
|
export {
|
|
380
464
|
AccountError,
|
|
381
465
|
EMAIL_CHANGE_TTL_HOURS,
|
|
466
|
+
FEDERATED_PROVIDERS,
|
|
467
|
+
FEDERATED_REFUSAL,
|
|
468
|
+
FEDERATION_CODE,
|
|
469
|
+
FederationError,
|
|
470
|
+
IdentityLinkError,
|
|
382
471
|
PASSWORD_RESET_MAX_MINUTES,
|
|
383
472
|
PASSWORD_RESET_MIN_MINUTES,
|
|
384
473
|
PASSWORD_RESET_TTL_MINUTES,
|
|
@@ -386,15 +475,26 @@ export {
|
|
|
386
475
|
RolesError,
|
|
387
476
|
accountsMigration,
|
|
388
477
|
can,
|
|
478
|
+
createFederatedSignIn,
|
|
389
479
|
createRoles,
|
|
480
|
+
dbAccounts,
|
|
481
|
+
dbIdentityLinks,
|
|
390
482
|
defineRoles,
|
|
391
483
|
emailChangeMail,
|
|
392
484
|
emailChangeToken,
|
|
485
|
+
identityLinksMigration,
|
|
393
486
|
isEmailChangeToken,
|
|
487
|
+
isFederatedProvider,
|
|
394
488
|
isLinkToken,
|
|
489
|
+
isLocalReturn,
|
|
395
490
|
linkToken,
|
|
491
|
+
memoryIdentityLinks,
|
|
492
|
+
memoryIdentityProvider,
|
|
493
|
+
parseExternalIdentity,
|
|
396
494
|
passwordResetMail,
|
|
495
|
+
pkceChallenge,
|
|
397
496
|
postgresAccounts,
|
|
398
|
-
rolesMigration
|
|
497
|
+
rolesMigration,
|
|
498
|
+
usersIdentityProvider
|
|
399
499
|
};
|
|
400
500
|
//# sourceMappingURL=index.js.map
|