@krizaka/auth 0.7.0 → 0.8.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 +6 -4
- package/dist/{chunk-H7AKE2OH.js → chunk-U5SDV7YV.js} +111 -11
- package/dist/chunk-U5SDV7YV.js.map +1 -0
- package/dist/identity-links-BI3DYE2I.d.ts +97 -0
- package/dist/index.d.ts +186 -13
- package/dist/index.js +13 -53
- package/dist/index.js.map +1 -1
- package/dist/testkit.d.ts +2 -1
- package/dist/testkit.js +1 -1
- package/package.json +3 -2
- package/dist/chunk-H7AKE2OH.js.map +0 -1
- package/dist/identity-links-C5AG7Fbp.d.ts +0 -239
|
@@ -1,239 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The account PORT (ADR-0008 of Yomizaka, now shared). A product's account — name, language, theme, avatar, e-mail change with
|
|
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
|
-
}
|
|
70
|
-
|
|
71
|
-
/** A SQL executor: the rows of a statement and its parameters (the product's pool). */
|
|
72
|
-
type Query = <T = Record<string, unknown>>(sql: string, params?: unknown[]) => Promise<T[]>;
|
|
73
|
-
interface Mail {
|
|
74
|
-
to: string;
|
|
75
|
-
/** The language of the mail (the person's). */
|
|
76
|
-
locale: "en" | "fr";
|
|
77
|
-
subject: string;
|
|
78
|
-
text: string;
|
|
79
|
-
/**
|
|
80
|
-
* The link this mail carries, as data: a product that sends branded HTML (`@krizaka/email`: `passwordReset`, `emailChange`)
|
|
81
|
-
* renders it from this instead of from `text`. Absent on a mail that is not a link.
|
|
82
|
-
*/
|
|
83
|
-
link?: MailLink;
|
|
84
|
-
}
|
|
85
|
-
/** A mailed link: what it is for, where it goes, and how long it lives. */
|
|
86
|
-
interface MailLink {
|
|
87
|
-
kind: "passwordReset" | "emailChange";
|
|
88
|
-
url: string;
|
|
89
|
-
/** `passwordReset`: the lifetime in minutes. */
|
|
90
|
-
minutes?: number;
|
|
91
|
-
/** `emailChange`: the lifetime in hours. */
|
|
92
|
-
hours?: number;
|
|
93
|
-
}
|
|
94
|
-
interface AccountDeps {
|
|
95
|
-
query: Query;
|
|
96
|
-
/**
|
|
97
|
-
* Runs `work` in one transaction on one connection (BEGIN … COMMIT, rolled back when `work` throws) — the product's pool
|
|
98
|
-
* knows how. The password reset runs under it: the account row is locked, the links retired and issued, the token claimed,
|
|
99
|
-
* the password set and the sessions ended as one. Without it, the statements run one by one on `query` (one process, no
|
|
100
|
-
* concurrent requests): give it in production.
|
|
101
|
-
*/
|
|
102
|
-
transaction?: <T>(work: (query: Query) => Promise<T>) => Promise<T>;
|
|
103
|
-
mail: (mail: Mail) => Promise<void>;
|
|
104
|
-
/** `https://www.akinaza.com`: the e-mail change link is `<baseUrl>/<locale>/confirm-email?token=…`, the reset link `<baseUrl>/<locale>/reset-password?token=…`. */
|
|
105
|
-
baseUrl: string;
|
|
106
|
-
/** The product's name, in the words of the mail (« Akinaza »). */
|
|
107
|
-
product: string;
|
|
108
|
-
/** The product's token prefix, two or three letters (`ye1.` for Yomizaka: `ye`). The tokens are `<prefix>1.<43 base64url>`. */
|
|
109
|
-
tokenPrefix: string;
|
|
110
|
-
passwords: {
|
|
111
|
-
hash: (password: string) => Promise<string>;
|
|
112
|
-
verify: (password: string, stored: string) => Promise<boolean>;
|
|
113
|
-
};
|
|
114
|
-
/** How a token is kept: the product's own keyed hash, the one its sessions use. */
|
|
115
|
-
tokenHash: (token: string) => string;
|
|
116
|
-
/**
|
|
117
|
-
* How long a password reset link lives, in minutes: a whole number from 5 to 1440 (`PASSWORD_RESET_MIN_MINUTES`–`PASSWORD_RESET_MAX_MINUTES`),
|
|
118
|
-
* default `PASSWORD_RESET_TTL_MINUTES` (30). The product reads it from its settings; the expiry and the mailed text both use it.
|
|
119
|
-
* Out of range, `dbAccounts` throws.
|
|
120
|
-
*/
|
|
121
|
-
passwordResetTtlMinutes?: number;
|
|
122
|
-
/** Test seam: the token to issue. */
|
|
123
|
-
token?: () => string;
|
|
124
|
-
}
|
|
125
|
-
declare const EMAIL_CHANGE_TTL_HOURS = 24;
|
|
126
|
-
/** The default lifetime of a password reset link, in minutes (`AccountDeps.passwordResetTtlMinutes` changes it). */
|
|
127
|
-
declare const PASSWORD_RESET_TTL_MINUTES = 30;
|
|
128
|
-
declare const PASSWORD_RESET_MIN_MINUTES = 5;
|
|
129
|
-
declare const PASSWORD_RESET_MAX_MINUTES = 1440;
|
|
130
|
-
/** One format for every mailed link (an e-mail change, a password reset): the product's prefix, a version, 256 random bits. */
|
|
131
|
-
declare const linkToken: (prefix: string) => string;
|
|
132
|
-
declare const isLinkToken: (prefix: string, value: string | undefined | null) => value is string;
|
|
133
|
-
declare const emailChangeToken: (prefix: string) => string;
|
|
134
|
-
declare const isEmailChangeToken: (prefix: string, value: string | undefined | null) => value is string;
|
|
135
|
-
declare function passwordResetMail(product: string, locale: "en" | "fr", url: string, ttlMinutes?: number): {
|
|
136
|
-
subject: string;
|
|
137
|
-
text: string;
|
|
138
|
-
};
|
|
139
|
-
declare function emailChangeMail(product: string, locale: "en" | "fr", url: string): {
|
|
140
|
-
subject: string;
|
|
141
|
-
text: string;
|
|
142
|
-
};
|
|
143
|
-
/** The adapter over the product's own PostgreSQL (`accounts`, `sessions`, `email_changes`, `password_resets`; see `accountsMigration`). */
|
|
144
|
-
declare function dbAccounts(deps: AccountDeps): AccountService;
|
|
145
|
-
/** @deprecated The old name of `dbAccounts` (a name says what a thing does, not the technology). Removed in the next major. */
|
|
146
|
-
declare const postgresAccounts: typeof dbAccounts;
|
|
147
|
-
|
|
148
|
-
/**
|
|
149
|
-
* Federated sign-in (Google, Apple, Facebook, a native app's PKCE) delegated to krizaka-users, behind a PORT. The product keeps its
|
|
150
|
-
* own accounts (`postgresAccounts`); this port only answers « who is this person, according to the provider ». An external
|
|
151
|
-
* identity is never a second user table: it is a link to an account the product already has.
|
|
152
|
-
*/
|
|
153
|
-
type FederatedProvider = "google" | "apple" | "facebook";
|
|
154
|
-
/** The providers the port knows; anything else is refused before it reaches the adapter. */
|
|
155
|
-
declare const FEDERATED_PROVIDERS: readonly FederatedProvider[];
|
|
156
|
-
declare const isFederatedProvider: (value: unknown) => value is FederatedProvider;
|
|
157
|
-
/** Who the provider says the person is. `issuer` + `subject` identify them for good; the address can change. */
|
|
158
|
-
interface ExternalIdentity {
|
|
159
|
-
/** The authority that vouches (`https://accounts.google.com`, `https://appleid.apple.com`…). */
|
|
160
|
-
issuer: string;
|
|
161
|
-
/** The person's stable id at that authority. */
|
|
162
|
-
subject: string;
|
|
163
|
-
email: string | null;
|
|
164
|
-
/** The provider checked the address (a Facebook address, for one, may not be). */
|
|
165
|
-
emailVerified: boolean;
|
|
166
|
-
name: string | null;
|
|
167
|
-
}
|
|
168
|
-
/** What a sign-in is started with: a native app sends the PKCE challenge, a browser flow does not. */
|
|
169
|
-
interface StartOptions {
|
|
170
|
-
/** The S256 challenge of a PKCE flow (a native app); the matching verifier goes to `complete`. */
|
|
171
|
-
codeChallenge?: string;
|
|
172
|
-
}
|
|
173
|
-
interface StartedSignIn {
|
|
174
|
-
/** Where to send the person: the provider's consent page, through krizaka-users. */
|
|
175
|
-
url: string;
|
|
176
|
-
/** Echoed back with the code: the product checks it against the one it kept for this browser. */
|
|
177
|
-
state: string;
|
|
178
|
-
}
|
|
179
|
-
interface CompleteOptions {
|
|
180
|
-
/** The PKCE verifier of the challenge given to `start`. */
|
|
181
|
-
codeVerifier?: string;
|
|
182
|
-
}
|
|
183
|
-
interface IdentityProvider {
|
|
184
|
-
/** Starts a sign-in with `provider`; the person comes back to `returnTo` (a path or an address the adapter allows) with a one-time code. */
|
|
185
|
-
start(provider: FederatedProvider, returnTo: string, options?: StartOptions): Promise<StartedSignIn>;
|
|
186
|
-
/** Exchanges the one-time code (single use) for the verified identity. Throws `FederationError("INVALID_CODE")` for a code that is unknown, used or expired. */
|
|
187
|
-
complete(code: string, options?: CompleteOptions): Promise<ExternalIdentity>;
|
|
188
|
-
}
|
|
189
|
-
declare const FEDERATION_CODE: {
|
|
190
|
-
readonly invalidCode: "INVALID_CODE";
|
|
191
|
-
readonly unknownProvider: "UNKNOWN_PROVIDER";
|
|
192
|
-
readonly badReturn: "BAD_RETURN_TO";
|
|
193
|
-
readonly unavailable: "UNAVAILABLE";
|
|
194
|
-
};
|
|
195
|
-
type FederationCode = (typeof FEDERATION_CODE)[keyof typeof FEDERATION_CODE];
|
|
196
|
-
declare class FederationError extends Error {
|
|
197
|
-
readonly code: FederationCode;
|
|
198
|
-
constructor(code: FederationCode, message: string);
|
|
199
|
-
}
|
|
200
|
-
/** The S256 challenge of a PKCE verifier: base64url(SHA-256(verifier)). A native app computes it; the adapter and the in-memory provider check it. */
|
|
201
|
-
declare const pkceChallenge: (verifier: string) => string;
|
|
202
|
-
/** A return address stays on the product: a path (`/fr/account`), never `//host` or a scheme. */
|
|
203
|
-
declare const isLocalReturn: (returnTo: string) => boolean;
|
|
204
|
-
/** An in-memory provider: the tests' adapter, and the reference the contract is written against. */
|
|
205
|
-
declare function memoryIdentityProvider(): IdentityProvider & {
|
|
206
|
-
/** Test seam: the one-time code the provider would hand back for this identity (with the PKCE challenge it was started with, if any). */
|
|
207
|
-
issue(identity: ExternalIdentity, challenge?: string): string;
|
|
208
|
-
};
|
|
209
|
-
|
|
210
|
-
/**
|
|
211
|
-
* Which external identity belongs to which account. Never a second user table: a row is only `(issuer, subject) → account`.
|
|
212
|
-
* The product's `accounts` stays the one place a person lives.
|
|
213
|
-
*/
|
|
214
|
-
interface IdentityLinkStore {
|
|
215
|
-
/** The account this external identity is linked to, or null. */
|
|
216
|
-
find(issuer: string, subject: string): Promise<string | null>;
|
|
217
|
-
/** Links it. Idempotent for the same account; throws when the identity is already linked to another one. */
|
|
218
|
-
link(accountId: string, issuer: string, subject: string, now: Date): Promise<void>;
|
|
219
|
-
/** Removes the link (the person unlinks Google); true when there was one. */
|
|
220
|
-
unlink(accountId: string, issuer: string, subject: string): Promise<boolean>;
|
|
221
|
-
/** The identities linked to an account, oldest first. */
|
|
222
|
-
list(accountId: string): Promise<{
|
|
223
|
-
issuer: string;
|
|
224
|
-
subject: string;
|
|
225
|
-
linkedAt: Date;
|
|
226
|
-
}[]>;
|
|
227
|
-
}
|
|
228
|
-
declare class IdentityLinkError extends Error {
|
|
229
|
-
readonly code = "ALREADY_LINKED";
|
|
230
|
-
constructor();
|
|
231
|
-
}
|
|
232
|
-
/** An in-memory store: the tests' adapter, and the reference the contract is written against. */
|
|
233
|
-
declare function memoryIdentityLinks(): IdentityLinkStore;
|
|
234
|
-
/** The table of links (idempotent). `accounts` is the product's account table. */
|
|
235
|
-
declare function identityLinksMigration(): string;
|
|
236
|
-
/** The store over the product's own database (`external_identities`; see `identityLinksMigration`). */
|
|
237
|
-
declare function dbIdentityLinks(query: Query): IdentityLinkStore;
|
|
238
|
-
|
|
239
|
-
export { type AccountDeps as A, memoryIdentityLinks as B, type CompleteOptions as C, memoryIdentityProvider as D, type ExternalIdentity as E, FEDERATED_PROVIDERS as F, passwordResetMail as G, pkceChallenge as H, type IdentityLinkStore as I, postgresAccounts as J, type Mail as M, PASSWORD_RESET_MAX_MINUTES as P, type Query as Q, type SessionSummary as S, type Theme as T, type IdentityProvider as a, AccountError as b, type AccountProfile as c, type AccountService as d, EMAIL_CHANGE_TTL_HOURS as e, FEDERATION_CODE as f, type FederatedProvider as g, type FederationCode as h, FederationError as i, IdentityLinkError as j, type MailLink as k, PASSWORD_RESET_MIN_MINUTES as l, PASSWORD_RESET_TTL_MINUTES as m, type ProfilePatch as n, type StartOptions as o, type StartedSignIn as p, dbAccounts as q, dbIdentityLinks as r, emailChangeMail as s, emailChangeToken as t, identityLinksMigration as u, isEmailChangeToken as v, isFederatedProvider as w, isLinkToken as x, isLocalReturn as y, linkToken as z };
|