@chttp/auth 0.0.0-stage → 0.2.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.
@@ -0,0 +1,160 @@
1
+ // JWT — JWS compact serialization only. ~120 LoC over node:crypto; no JWE,
2
+ // no JWKS machinery, no EdDSA in v1 (follow-up tickets).
3
+ //
4
+ // - alg comes from the key type: string => HS256 secret, KeyObject =>
5
+ // ES256 P-256 (the spec's v2 path). Overridable via opts.alg; verify
6
+ // enforces the expected alg, so there is no alg-confusion window and
7
+ // "none" is hard-rejected.
8
+ // - ES256 IS RECOGNIZED BUT NOT LOWERABLE TODAY: scriptc 0.3.3's static
9
+ // node:crypto slice covers hashing, randomness and secret KeyObjects —
10
+ // createSign/createVerify/createPrivateKey/createPublicKey have no
11
+ // lowering (asymmetric ops need a public-key stack the runtime lacks).
12
+ // signJwt({ alg: "ES256" }) throws E_BAD_ALG and verifyJwt rejects ES256
13
+ // tokens with null; the `string | KeyObject` signature stays so the
14
+ // feature slots in upstream without a breaking change. When it lands it
15
+ // must sign/verify with dsaEncoding "ieee-p1363" (JWS carries raw R||S,
16
+ // not DER — the classic interop bug; RFC 7515 A.3 vector is in tests).
17
+ // - Verify is null-on-invalid for ANY failure: bad format, wrong alg, bad
18
+ // signature, expired/nbf, wrong issuer/audience. Never throws — callers
19
+ // branch on null instead of try/catching attacker input. Sign throws on
20
+ // caller bugs (missing/wrong key) because those are programmer errors.
21
+ import { createHmac, timingSafeEqual } from "node:crypto";
22
+ import { authError } from "./errors.js";
23
+ import { b64urlDecode, b64urlEncode } from "./encoding.js";
24
+ import { runSchema } from "./types.js";
25
+ function inferAlg(key) {
26
+ return typeof key === "string" ? "HS256" : "ES256";
27
+ }
28
+ export function signJwt(payload, key, opts) {
29
+ const o = opts === undefined || opts === null ? {} : opts;
30
+ const alg = o.alg === undefined ? inferAlg(key) : o.alg;
31
+ if (alg === "ES256") {
32
+ throw authError("E_BAD_ALG", "ES256 needs node:crypto asymmetric ops, which scriptc 0.3.3 does not lower");
33
+ }
34
+ if (alg !== "HS256") {
35
+ throw authError("E_BAD_ALG", "signJwt supports HS256 only");
36
+ }
37
+ if (typeof key !== "string") {
38
+ throw authError("E_NO_KEY", "HS256 requires a string secret");
39
+ }
40
+ const now = Math.floor(Date.now() / 1000);
41
+ const claims = {};
42
+ const keys = Object.keys(payload);
43
+ for (let i = 0; i < keys.length; i++) {
44
+ claims[keys[i]] = payload[keys[i]];
45
+ }
46
+ claims["iat"] = now;
47
+ if (o.expiresInSeconds !== undefined) {
48
+ claims["exp"] = now + o.expiresInSeconds;
49
+ }
50
+ if (o.issuer !== undefined) {
51
+ claims["iss"] = o.issuer;
52
+ }
53
+ if (o.audience !== undefined) {
54
+ claims["aud"] = o.audience;
55
+ }
56
+ const header = { alg: alg, typ: "JWT" };
57
+ const input = b64urlEncode(Buffer.from(JSON.stringify(header), "utf8")) +
58
+ "." +
59
+ b64urlEncode(Buffer.from(JSON.stringify(claims), "utf8"));
60
+ const sig = createHmac("sha256", key).update(input).digest();
61
+ return input + "." + b64urlEncode(sig);
62
+ }
63
+ function audMatches(aud, expected) {
64
+ if (typeof aud === "string") {
65
+ return aud === expected;
66
+ }
67
+ if (Array.isArray(aud)) {
68
+ for (let i = 0; i < aud.length; i++) {
69
+ if (aud[i] === expected) {
70
+ return true;
71
+ }
72
+ }
73
+ }
74
+ return false;
75
+ }
76
+ // null on ANY failure: bad format, wrong alg, bad signature, expired, wrong
77
+ // issuer/audience. alg "none" always rejected. Never throws.
78
+ export function verifyJwt(token, key, opts) {
79
+ const o = opts === undefined || opts === null ? {} : opts;
80
+ try {
81
+ const parts = token.split(".");
82
+ if (parts.length !== 3) {
83
+ return null;
84
+ }
85
+ const expectedAlg = o.alg === undefined ? inferAlg(key) : o.alg;
86
+ const headerRaw = JSON.parse(b64urlDecode(parts[0]).toString("utf8"));
87
+ const alg = headerRaw["alg"];
88
+ if (alg !== expectedAlg) {
89
+ // Covers "none", alg-confusion, and any unsupported alg.
90
+ return null;
91
+ }
92
+ if (alg !== "HS256" || typeof key !== "string") {
93
+ // ES256 (or anything non-HS256) is unverifiable in this build; a
94
+ // KeyObject key has no usable signing surface either.
95
+ return null;
96
+ }
97
+ const input = parts[0] + "." + parts[1];
98
+ const sigBytes = b64urlDecode(parts[2]);
99
+ const expected = createHmac("sha256", key).update(input).digest();
100
+ // timingSafeEqual throws on length mismatch — length pre-check.
101
+ if (expected.length !== sigBytes.length) {
102
+ return null;
103
+ }
104
+ if (!timingSafeEqual(expected, sigBytes)) {
105
+ return null;
106
+ }
107
+ const claims = JSON.parse(b64urlDecode(parts[1]).toString("utf8"));
108
+ if (claims === null || typeof claims !== "object" || Array.isArray(claims)) {
109
+ return null;
110
+ }
111
+ const tol = o.clockToleranceSeconds === undefined ? 0 : o.clockToleranceSeconds;
112
+ const now = Math.floor(Date.now() / 1000);
113
+ const exp = claims["exp"];
114
+ if (exp !== undefined) {
115
+ if (typeof exp !== "number") {
116
+ return null;
117
+ }
118
+ if (now - tol >= exp) {
119
+ return null; // expired
120
+ }
121
+ }
122
+ const nbf = claims["nbf"];
123
+ if (nbf !== undefined) {
124
+ if (typeof nbf !== "number") {
125
+ return null;
126
+ }
127
+ if (now + tol < nbf) {
128
+ return null; // not yet valid
129
+ }
130
+ }
131
+ if (o.issuer !== undefined) {
132
+ if (claims["iss"] !== o.issuer) {
133
+ return null;
134
+ }
135
+ }
136
+ if (o.audience !== undefined) {
137
+ if (!audMatches(claims["aud"], o.audience)) {
138
+ return null;
139
+ }
140
+ }
141
+ return claims;
142
+ }
143
+ catch {
144
+ return null;
145
+ }
146
+ }
147
+ // val-first twin of verifyJwt: verifies, then parses the claims through any
148
+ // StandardSchemaV1 (@chttp/val, valibot, ...). Returns the typed claims or
149
+ // null — the positional-schema convention (`prepare(sql, schema)`).
150
+ export async function verifyJwtWith(token, key, schema, opts) {
151
+ const claims = verifyJwt(token, key, opts);
152
+ if (claims === null) {
153
+ return null;
154
+ }
155
+ const out = await runSchema(schema, claims);
156
+ if (out === null) {
157
+ return null;
158
+ }
159
+ return out;
160
+ }
@@ -0,0 +1,16 @@
1
+ import type { SessionRecordData } from "./session.ts";
2
+ export declare class MemorySessionStoreCore {
3
+ _records: Map<string, SessionRecordData>;
4
+ _now: () => number;
5
+ constructor(now?: () => number);
6
+ _sweep(key: string, record: SessionRecordData): SessionRecordData | null;
7
+ get(key: string): SessionRecordData | null;
8
+ set(record: SessionRecordData): void;
9
+ updateExpiry(key: string, expiresAt: number): void;
10
+ delete(key: string): void;
11
+ deleteAllForUser(userId: string): void;
12
+ get size(): number;
13
+ }
14
+ export interface MemorySessionStore<_A> extends MemorySessionStoreCore {
15
+ }
16
+ export declare const MemorySessionStore: typeof MemorySessionStoreCore;
@@ -0,0 +1,59 @@
1
+ // MemorySessionStore — dev/test SessionStore over a Map (the 0.3.3 probe
2
+ // lifted the Map/Set ban; same exception @chttp/cache uses).
3
+ //
4
+ // NOT for production. Process memory loses sessions on restart and does not
5
+ // share them across replicas — ship a real adapter (the @cutils/sqlite
6
+ // `key TEXT PRIMARY KEY` shape) instead. Dev only.
7
+ export class MemorySessionStoreCore {
8
+ _records;
9
+ _now;
10
+ // `now` is a constructor-only test seam (fake clock), deliberately NOT an
11
+ // options-record member — function values inside records degrade
12
+ // instantiation under scriptc.
13
+ constructor(now) {
14
+ this._records = new Map();
15
+ this._now = now === undefined ? Date.now : now;
16
+ }
17
+ _sweep(key, record) {
18
+ if (this._now() >= record.expiresAt) {
19
+ this._records.delete(key);
20
+ return null;
21
+ }
22
+ return record;
23
+ }
24
+ get(key) {
25
+ const r = this._records.get(key);
26
+ if (r === undefined) {
27
+ return null;
28
+ }
29
+ return this._sweep(key, r);
30
+ }
31
+ set(record) {
32
+ this._records.set(record.key, record);
33
+ }
34
+ updateExpiry(key, expiresAt) {
35
+ const r = this._records.get(key);
36
+ if (r === undefined) {
37
+ return;
38
+ }
39
+ r.expiresAt = expiresAt;
40
+ }
41
+ delete(key) {
42
+ this._records.delete(key);
43
+ }
44
+ deleteAllForUser(userId) {
45
+ const doomed = [];
46
+ for (const e of this._records.entries()) {
47
+ if (e[1].userId === userId) {
48
+ doomed.push(e[0]);
49
+ }
50
+ }
51
+ for (let i = 0; i < doomed.length; i++) {
52
+ this._records.delete(doomed[i]);
53
+ }
54
+ }
55
+ get size() {
56
+ return this._records.size;
57
+ }
58
+ }
59
+ export const MemorySessionStore = MemorySessionStoreCore;
@@ -0,0 +1,42 @@
1
+ import type { StandardSchemaV1 } from "./types.ts";
2
+ export interface OAuthProvider {
3
+ authorizationEndpoint: string;
4
+ tokenEndpoint: string;
5
+ userInfoEndpoint: string | null;
6
+ scope: string[];
7
+ usePkce: boolean;
8
+ }
9
+ export declare function generateState(): string;
10
+ export declare function generateCodeVerifier(): string;
11
+ export declare function codeChallenge(verifier: string): string;
12
+ export interface AuthorizeParams {
13
+ state: string;
14
+ codeVerifier: string | null;
15
+ extraQuery?: {
16
+ [k: string]: string;
17
+ };
18
+ }
19
+ export declare function createAuthorizationURL(provider: OAuthProvider, clientId: string, redirectURI: string, params: AuthorizeParams): string;
20
+ export interface TokenSet {
21
+ access_token: string;
22
+ token_type: string;
23
+ refresh_token: string | null;
24
+ expires_in: number | null;
25
+ scope: string | null;
26
+ id_token: string | null;
27
+ raw: {
28
+ [k: string]: unknown;
29
+ };
30
+ }
31
+ export interface ExchangeParams {
32
+ code: string;
33
+ clientId: string;
34
+ clientSecret: string | null;
35
+ redirectURI: string;
36
+ codeVerifier: string | null;
37
+ }
38
+ export declare function exchangeCodeForTokens(provider: OAuthProvider, params: ExchangeParams): Promise<TokenSet | null>;
39
+ export declare function fetchUserInfo(provider: OAuthProvider, accessToken: string): Promise<{
40
+ [k: string]: unknown;
41
+ } | null>;
42
+ export declare function fetchUserInfoWith<Schema extends StandardSchemaV1>(provider: OAuthProvider, accessToken: string, schema: Schema): Promise<StandardSchemaV1.InferOutput<Schema> | null>;
@@ -0,0 +1,184 @@
1
+ // OAuth2 authorization-code + PKCE (RFC 6749 + RFC 7636). Providers are
2
+ // plain records — no registry, no plugin bus; adding one is a 15-LoC record
3
+ // in user code. The token exchange is a POST x-www-form-urlencoded with
4
+ // HTTP-basic client auth; responses parse as JSON OR form-urlencoded (RFC
5
+ // 6749 permits both). Exchange/userinfo return null on non-2xx or missing
6
+ // fields — never throw, never surface provider error bodies as exceptions.
7
+ import { createHash, randomBytes } from "node:crypto";
8
+ import { b64urlEncode } from "./encoding.js";
9
+ import { runSchema } from "./types.js";
10
+ export function generateState() {
11
+ return b64urlEncode(randomBytes(32));
12
+ }
13
+ // RFC 7636: verifier is 43-128 chars of unreserved; base64url(32 bytes)
14
+ // yields exactly 43.
15
+ export function generateCodeVerifier() {
16
+ return b64urlEncode(randomBytes(32));
17
+ }
18
+ // S256: base64url(sha256(verifier)), no padding (Node's base64url is
19
+ // already unpadded).
20
+ export function codeChallenge(verifier) {
21
+ return createHash("sha256").update(verifier).digest().toString("base64url");
22
+ }
23
+ export function createAuthorizationURL(provider, clientId, redirectURI, params) {
24
+ const url = new URL(provider.authorizationEndpoint);
25
+ url.searchParams.set("response_type", "code");
26
+ url.searchParams.set("client_id", clientId);
27
+ url.searchParams.set("redirect_uri", redirectURI);
28
+ url.searchParams.set("scope", provider.scope.join(" "));
29
+ url.searchParams.set("state", params.state);
30
+ if (provider.usePkce && params.codeVerifier !== null) {
31
+ url.searchParams.set("code_challenge", codeChallenge(params.codeVerifier));
32
+ url.searchParams.set("code_challenge_method", "S256");
33
+ }
34
+ if (params.extraQuery !== undefined && params.extraQuery !== null) {
35
+ const keys = Object.keys(params.extraQuery);
36
+ for (let i = 0; i < keys.length; i++) {
37
+ url.searchParams.set(keys[i], params.extraQuery[keys[i]]);
38
+ }
39
+ }
40
+ return url.toString();
41
+ }
42
+ function strOrNull(v) {
43
+ return typeof v === "string" ? v : null;
44
+ }
45
+ function numOrNull(v) {
46
+ return typeof v === "number" ? v : null;
47
+ }
48
+ // Parses a token-endpoint body by content-type: JSON, else
49
+ // x-www-form-urlencoded (RFC 6749 permits both).
50
+ function parseTokenBody(contentType, text) {
51
+ if (contentType.indexOf("json") >= 0) {
52
+ try {
53
+ const parsed = JSON.parse(text);
54
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
55
+ return null;
56
+ }
57
+ return parsed;
58
+ }
59
+ catch {
60
+ return null;
61
+ }
62
+ }
63
+ const params = new URLSearchParams(text);
64
+ const raw = {};
65
+ for (const name of params.keys()) {
66
+ const v = params.get(name);
67
+ if (v !== null) {
68
+ raw[name] = v;
69
+ }
70
+ }
71
+ if (raw["access_token"] === undefined) {
72
+ return null;
73
+ }
74
+ return raw;
75
+ }
76
+ export async function exchangeCodeForTokens(provider, params) {
77
+ const body = new URLSearchParams();
78
+ body.set("grant_type", "authorization_code");
79
+ body.set("code", params.code);
80
+ body.set("redirect_uri", params.redirectURI);
81
+ body.set("client_id", params.clientId);
82
+ if (params.codeVerifier !== null) {
83
+ body.set("code_verifier", params.codeVerifier);
84
+ }
85
+ const headers = {};
86
+ headers["content-type"] = "application/x-www-form-urlencoded";
87
+ headers["accept"] = "application/json";
88
+ if (params.clientSecret !== null) {
89
+ // RFC 6749 §2.3.1: client auth via HTTP basic.
90
+ body.set("client_secret", params.clientSecret);
91
+ headers["authorization"] =
92
+ "Basic " +
93
+ Buffer.from(params.clientId + ":" + params.clientSecret, "utf8").toString("base64");
94
+ }
95
+ let res;
96
+ try {
97
+ res = await fetch(provider.tokenEndpoint, {
98
+ method: "POST",
99
+ headers: headers,
100
+ body: body.toString(),
101
+ });
102
+ }
103
+ catch {
104
+ return null;
105
+ }
106
+ if (!res.ok) {
107
+ return null;
108
+ }
109
+ let text;
110
+ let contentType = "";
111
+ try {
112
+ const ct = res.headers.get("content-type");
113
+ contentType = ct === null ? "" : ct;
114
+ text = await res.text();
115
+ }
116
+ catch {
117
+ return null;
118
+ }
119
+ const raw = parseTokenBody(contentType, text);
120
+ if (raw === null) {
121
+ return null;
122
+ }
123
+ const accessToken = raw["access_token"];
124
+ if (typeof accessToken !== "string" || accessToken === "") {
125
+ return null;
126
+ }
127
+ const out = {
128
+ access_token: accessToken,
129
+ token_type: strOrNull(raw["token_type"]) === null ? "" : strOrNull(raw["token_type"]),
130
+ refresh_token: strOrNull(raw["refresh_token"]),
131
+ expires_in: numOrNull(raw["expires_in"]),
132
+ scope: strOrNull(raw["scope"]),
133
+ id_token: strOrNull(raw["id_token"]),
134
+ raw: raw,
135
+ };
136
+ return out;
137
+ }
138
+ export async function fetchUserInfo(provider, accessToken) {
139
+ if (provider.userInfoEndpoint === null) {
140
+ return null;
141
+ }
142
+ const headers = {};
143
+ headers["authorization"] = "Bearer " + accessToken;
144
+ headers["accept"] = "application/json";
145
+ let res;
146
+ try {
147
+ res = await fetch(provider.userInfoEndpoint, { headers: headers });
148
+ }
149
+ catch {
150
+ return null;
151
+ }
152
+ if (!res.ok) {
153
+ return null;
154
+ }
155
+ let text;
156
+ try {
157
+ text = await res.text();
158
+ }
159
+ catch {
160
+ return null;
161
+ }
162
+ try {
163
+ const parsed = JSON.parse(text);
164
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
165
+ return null;
166
+ }
167
+ return parsed;
168
+ }
169
+ catch {
170
+ return null;
171
+ }
172
+ }
173
+ // Schema-parsed userinfo — same StandardSchemaV1 seam as verifyJwtWith.
174
+ export async function fetchUserInfoWith(provider, accessToken, schema) {
175
+ const info = await fetchUserInfo(provider, accessToken);
176
+ if (info === null) {
177
+ return null;
178
+ }
179
+ const out = await runSchema(schema, info);
180
+ if (out === null) {
181
+ return null;
182
+ }
183
+ return out;
184
+ }
@@ -0,0 +1,7 @@
1
+ export interface PasswordOpts {
2
+ N?: number;
3
+ r?: number;
4
+ p?: number;
5
+ }
6
+ export declare function hashPassword(password: string, opts?: PasswordOpts): string;
7
+ export declare function verifyPassword(password: string, stored: string): boolean;
@@ -0,0 +1,103 @@
1
+ // scrypt password hashing, self-describing format:
2
+ // "scrypt:N:r:p:saltB64:hashB64"
3
+ // verifyPassword reads the parameters from the string, so parameter upgrades
4
+ // don't invalidate old hashes; hashPassword writes today's OWASP defaults
5
+ // (N=2^17, r=8, p=1, 64-byte key, 16-byte salt). The format prefix leaves
6
+ // room for a future memory-hard successor (e.g. argon2) to slot in.
7
+ import { randomBytes, scryptSync, timingSafeEqual } from "node:crypto";
8
+ const DEFAULT_N = 131072; // 2^17 — OWASP Password Storage Cheat Sheet
9
+ const DEFAULT_R = 8;
10
+ const DEFAULT_P = 1;
11
+ const KEYLEN = 64;
12
+ const SALT_BYTES = 16;
13
+ // scrypt memory is ~128*N*r bytes: 2^17*8*128 = 128 MiB, past Node's 32 MiB
14
+ // default maxmem — raised explicitly.
15
+ const MAXMEM = 256 * 1024 * 1024;
16
+ export function hashPassword(password, opts) {
17
+ let N = DEFAULT_N;
18
+ let r = DEFAULT_R;
19
+ let p = DEFAULT_P;
20
+ if (opts !== undefined && opts !== null) {
21
+ if (opts.N !== undefined) {
22
+ N = opts.N;
23
+ }
24
+ if (opts.r !== undefined) {
25
+ r = opts.r;
26
+ }
27
+ if (opts.p !== undefined) {
28
+ p = opts.p;
29
+ }
30
+ }
31
+ const salt = randomBytes(SALT_BYTES);
32
+ const hash = scryptSync(password, salt, KEYLEN, {
33
+ N: N,
34
+ r: r,
35
+ p: p,
36
+ maxmem: MAXMEM,
37
+ });
38
+ return ("scrypt:" +
39
+ N +
40
+ ":" +
41
+ r +
42
+ ":" +
43
+ p +
44
+ ":" +
45
+ salt.toString("base64") +
46
+ ":" +
47
+ hash.toString("base64"));
48
+ }
49
+ // Burns one scrypt at default parameters so malformed/corrupt stored rows
50
+ // cost the same as a real verify — flattens the oracle between "row doesn't
51
+ // parse" and "password wrong".
52
+ function dummyHash(password) {
53
+ scryptSync(password, Buffer.from("cutils-auth-dummy-salt", "utf8"), KEYLEN, {
54
+ N: DEFAULT_N,
55
+ r: DEFAULT_R,
56
+ p: DEFAULT_P,
57
+ maxmem: MAXMEM,
58
+ });
59
+ }
60
+ function parseParam(s) {
61
+ if (!/^[0-9]+$/.test(s)) {
62
+ return -1;
63
+ }
64
+ return Number(s);
65
+ }
66
+ export function verifyPassword(password, stored) {
67
+ const parts = stored.split(":");
68
+ if (parts.length !== 6 || parts[0] !== "scrypt") {
69
+ dummyHash(password);
70
+ return false;
71
+ }
72
+ const N = parseParam(parts[1]);
73
+ const r = parseParam(parts[2]);
74
+ const p = parseParam(parts[3]);
75
+ if (N <= 0 || r <= 0 || p <= 0) {
76
+ dummyHash(password);
77
+ return false;
78
+ }
79
+ const salt = Buffer.from(parts[4], "base64");
80
+ const expected = Buffer.from(parts[5], "base64");
81
+ if (salt.length === 0 || expected.length === 0) {
82
+ dummyHash(password);
83
+ return false;
84
+ }
85
+ let hash;
86
+ try {
87
+ hash = scryptSync(password, salt, expected.length, {
88
+ N: N,
89
+ r: r,
90
+ p: p,
91
+ maxmem: MAXMEM,
92
+ });
93
+ }
94
+ catch {
95
+ dummyHash(password);
96
+ return false;
97
+ }
98
+ // timingSafeEqual throws on length mismatch — length pre-check.
99
+ if (hash.length !== expected.length) {
100
+ return false;
101
+ }
102
+ return timingSafeEqual(hash, expected);
103
+ }
@@ -0,0 +1,4 @@
1
+ import type { OAuthProvider } from "./oauth.ts";
2
+ export declare const googleProvider: OAuthProvider;
3
+ export declare const githubProvider: OAuthProvider;
4
+ export declare const discordProvider: OAuthProvider;
@@ -0,0 +1,27 @@
1
+ // Provider presets: plain records, ~15 LoC each — google, github, discord,
2
+ // the top three. A new provider is a 15-LoC record in user code; the
3
+ // package deliberately grows no registry (the better-auth trap).
4
+ // OIDC: openid email profile; PKCE on.
5
+ export const googleProvider = {
6
+ authorizationEndpoint: "https://accounts.google.com/o/oauth2/v2/auth",
7
+ tokenEndpoint: "https://oauth2.googleapis.com/token",
8
+ userInfoEndpoint: "https://openidconnect.googleapis.com/v1/userinfo",
9
+ scope: ["openid", "email", "profile"],
10
+ usePkce: true,
11
+ };
12
+ // Non-OIDC /user endpoint; PKCE is tolerated (ignored) by GitHub.
13
+ export const githubProvider = {
14
+ authorizationEndpoint: "https://github.com/login/oauth/authorize",
15
+ tokenEndpoint: "https://github.com/login/oauth/access_token",
16
+ userInfoEndpoint: "https://api.github.com/user",
17
+ scope: ["read:user", "user:email"],
18
+ usePkce: true,
19
+ };
20
+ // Discord supports PKCE for public clients (clientSecret may be null).
21
+ export const discordProvider = {
22
+ authorizationEndpoint: "https://discord.com/oauth2/authorize",
23
+ tokenEndpoint: "https://discord.com/api/oauth2/token",
24
+ userInfoEndpoint: "https://discord.com/api/users/@me",
25
+ scope: ["identify", "email"],
26
+ usePkce: true,
27
+ };
@@ -0,0 +1,29 @@
1
+ export interface SessionRecordData {
2
+ key: string;
3
+ userId: string;
4
+ expiresAt: number;
5
+ attributes: unknown;
6
+ }
7
+ export interface SessionRecord<A> extends SessionRecordData {
8
+ attributes: A | null;
9
+ }
10
+ export interface SessionStoreCore {
11
+ get(key: string): unknown;
12
+ set(record: SessionRecordData): unknown;
13
+ updateExpiry(key: string, expiresAt: number): unknown;
14
+ delete(key: string): unknown;
15
+ deleteAllForUser(userId: string): unknown;
16
+ }
17
+ export interface SessionStore<_A> extends SessionStoreCore {
18
+ }
19
+ export interface SessionOpts {
20
+ ttlSeconds?: number;
21
+ extensionThresholdSeconds?: number;
22
+ nowMs?: number;
23
+ }
24
+ export declare function generateSessionToken(): string;
25
+ export declare function hashToken(token: string): string;
26
+ export declare function createSession(store: SessionStoreCore, token: string, userId: string, attributes: unknown, opts?: SessionOpts): Promise<SessionRecordData>;
27
+ export declare function validateSessionToken(store: SessionStoreCore, token: string, opts?: SessionOpts): Promise<SessionRecordData | null>;
28
+ export declare function invalidateSession(store: SessionStoreCore, token: string): Promise<void>;
29
+ export declare function invalidateAllUserSessions(store: SessionStoreCore, userId: string): Promise<void>;