@planetlogin/core 0.1.0 → 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.
- package/dist/index.d.ts +84 -4
- package/dist/index.js +139 -19
- package/dist/keygen.d.ts +1 -0
- package/dist/keygen.js +23 -0
- package/package.json +9 -2
package/dist/index.d.ts
CHANGED
|
@@ -13,7 +13,9 @@ interface SessionClaims {
|
|
|
13
13
|
name?: string;
|
|
14
14
|
locale?: Locale;
|
|
15
15
|
}
|
|
16
|
-
|
|
16
|
+
type JwtAlg = 'EdDSA' | 'RS256' | 'ES256' | 'HS256';
|
|
17
|
+
/** Public JWK Set served at GET /auth/.well-known/jwks.json. Empty for HS256
|
|
18
|
+
* (symmetric secrets are shared out of band, never published). */
|
|
17
19
|
declare function jwks(): Promise<{
|
|
18
20
|
keys: JWK[];
|
|
19
21
|
}>;
|
|
@@ -22,7 +24,7 @@ declare function signSession(claims: SessionClaims, opts?: {
|
|
|
22
24
|
audience?: string;
|
|
23
25
|
ttlSeconds?: number;
|
|
24
26
|
}): Promise<string>;
|
|
25
|
-
/** Verify a session token against our own
|
|
27
|
+
/** Verify a session token against our own key material (what a downstream does). */
|
|
26
28
|
declare function verifySession(token: string): Promise<jose.JWTPayload>;
|
|
27
29
|
declare function signMagicToken(identifier: string, ttlSeconds?: number): Promise<string>;
|
|
28
30
|
/** Returns {identifier, jti} if the token is a valid, unexpired magic token, else null. */
|
|
@@ -134,6 +136,26 @@ interface PlanetLoginConfig {
|
|
|
134
136
|
session?: {
|
|
135
137
|
store?: 'none' | 'memory' | 'redis' | 'sqlite' | 'downstream';
|
|
136
138
|
};
|
|
139
|
+
security?: {
|
|
140
|
+
cors?: {
|
|
141
|
+
origins?: string[];
|
|
142
|
+
credentials?: boolean;
|
|
143
|
+
};
|
|
144
|
+
rateLimit?: {
|
|
145
|
+
login?: {
|
|
146
|
+
limit: number;
|
|
147
|
+
windowSeconds: number;
|
|
148
|
+
};
|
|
149
|
+
magic?: {
|
|
150
|
+
limit: number;
|
|
151
|
+
windowSeconds: number;
|
|
152
|
+
};
|
|
153
|
+
totp?: {
|
|
154
|
+
limit: number;
|
|
155
|
+
windowSeconds: number;
|
|
156
|
+
};
|
|
157
|
+
};
|
|
158
|
+
};
|
|
137
159
|
}
|
|
138
160
|
declare function loadConfig(): PlanetLoginConfig;
|
|
139
161
|
/** The public subset served at GET /auth/config (no secrets). */
|
|
@@ -193,13 +215,19 @@ interface SessionStore {
|
|
|
193
215
|
delete(key: string): Promise<void>;
|
|
194
216
|
/** Atomically claim a key once: true if newly claimed, false if already present. */
|
|
195
217
|
claimOnce(key: string, ttlSeconds: number): Promise<boolean>;
|
|
218
|
+
/** Atomically increment a counter, setting its TTL on first touch; returns the
|
|
219
|
+
* new count. Used by the rate limiter (fixed window). */
|
|
220
|
+
incr(key: string, ttlSeconds: number): Promise<number>;
|
|
196
221
|
}
|
|
197
|
-
/** No store: stateless. claimOnce always allows (single-use not enforced → TTL
|
|
222
|
+
/** No store: stateless. claimOnce always allows (single-use not enforced → TTL
|
|
223
|
+
* only); incr always returns 1 → the rate limiter never blocks. Both real
|
|
224
|
+
* single-use and rate limiting need a configured store (memory/redis/…). */
|
|
198
225
|
declare class NoneStore implements SessionStore {
|
|
199
226
|
get(): Promise<null>;
|
|
200
227
|
set(): Promise<void>;
|
|
201
228
|
delete(): Promise<void>;
|
|
202
229
|
claimOnce(): Promise<boolean>;
|
|
230
|
+
incr(): Promise<number>;
|
|
203
231
|
}
|
|
204
232
|
declare class MemoryStore implements SessionStore {
|
|
205
233
|
private m;
|
|
@@ -208,6 +236,7 @@ declare class MemoryStore implements SessionStore {
|
|
|
208
236
|
set(key: string, value: string, ttlSeconds: number): Promise<void>;
|
|
209
237
|
delete(key: string): Promise<void>;
|
|
210
238
|
claimOnce(key: string, ttlSeconds: number): Promise<boolean>;
|
|
239
|
+
incr(key: string, ttlSeconds: number): Promise<number>;
|
|
211
240
|
}
|
|
212
241
|
type StoreKind = 'none' | 'memory' | 'redis' | 'sqlite' | 'downstream';
|
|
213
242
|
/** Build the store from config. Only none+memory ship in this flavor; the rest
|
|
@@ -218,6 +247,57 @@ declare const _stores: {
|
|
|
218
247
|
MemoryStore: typeof MemoryStore;
|
|
219
248
|
};
|
|
220
249
|
|
|
250
|
+
interface RateLimitRule {
|
|
251
|
+
/** Max attempts allowed within the window. */
|
|
252
|
+
limit: number;
|
|
253
|
+
/** Window length in seconds. */
|
|
254
|
+
windowSeconds: number;
|
|
255
|
+
}
|
|
256
|
+
interface RateLimitDecision {
|
|
257
|
+
ok: boolean;
|
|
258
|
+
/** Attempts left in the current window (0 when blocked). */
|
|
259
|
+
remaining: number;
|
|
260
|
+
/** Seconds the caller should wait before retrying (only meaningful when blocked). */
|
|
261
|
+
retryAfter: number;
|
|
262
|
+
limit: number;
|
|
263
|
+
}
|
|
264
|
+
/** Built-in defaults (per key, fixed window). Tuned for human use; override via
|
|
265
|
+
* config.security.rateLimit or PLANETLOGIN_RATELIMIT_* env. */
|
|
266
|
+
declare const DEFAULT_RULES: Record<string, RateLimitRule>;
|
|
267
|
+
/**
|
|
268
|
+
* Consume one attempt for `key` under `rule`. `key` should already encode the
|
|
269
|
+
* action and a client discriminator (IP and/or identifier), e.g.
|
|
270
|
+
* `login:203.0.113.7` or `magic:user@x.com`. Builders below help.
|
|
271
|
+
*/
|
|
272
|
+
declare function rateLimit(store: SessionStore, key: string, rule: RateLimitRule): Promise<RateLimitDecision>;
|
|
273
|
+
/** Resolve a rule for an action: config/env override, else built-in default. */
|
|
274
|
+
declare function ruleFor(action: keyof typeof DEFAULT_RULES, overrides?: Partial<Record<string, RateLimitRule>>): RateLimitRule;
|
|
275
|
+
/** Build a rate-limit key from an action + the client discriminators available.
|
|
276
|
+
* Both IP and identifier are optional; whatever is present is hashed into the key. */
|
|
277
|
+
declare function rlKey(action: string, parts: {
|
|
278
|
+
ip?: string | null;
|
|
279
|
+
identifier?: string | null;
|
|
280
|
+
}): string;
|
|
281
|
+
|
|
282
|
+
interface CorsConfig {
|
|
283
|
+
/** Allowed origins, or ['*'] to reflect any. */
|
|
284
|
+
origins: string[];
|
|
285
|
+
/** Send Access-Control-Allow-Credentials (cookies). Default true. */
|
|
286
|
+
credentials?: boolean;
|
|
287
|
+
}
|
|
288
|
+
/** Read the allowlist from env (flavors may merge with config). */
|
|
289
|
+
declare function corsFromEnv(): CorsConfig;
|
|
290
|
+
/** Is `origin` allowed under this config? */
|
|
291
|
+
declare function originAllowed(origin: string | null | undefined, cfg: CorsConfig): boolean;
|
|
292
|
+
/**
|
|
293
|
+
* CORS response headers for a given request origin. Returns {} when the origin
|
|
294
|
+
* isn't allowed (no CORS headers → the browser blocks the cross-origin read).
|
|
295
|
+
* Always includes `Vary: Origin` so caches don't serve the wrong ACAO.
|
|
296
|
+
*/
|
|
297
|
+
declare function corsHeaders(origin: string | null | undefined, cfg: CorsConfig): Record<string, string>;
|
|
298
|
+
/** True for a CORS preflight (OPTIONS + Access-Control-Request-Method). */
|
|
299
|
+
declare function isPreflight(method: string, requestMethodHeader: string | null | undefined): boolean;
|
|
300
|
+
|
|
221
301
|
interface ProviderConfig {
|
|
222
302
|
authorizeUrl: string;
|
|
223
303
|
tokenUrl: string;
|
|
@@ -490,4 +570,4 @@ declare function totpVerify(deps: TotpDeps, input: {
|
|
|
490
570
|
ok: boolean;
|
|
491
571
|
}>;
|
|
492
572
|
|
|
493
|
-
export { type AuthResult, type AuthVerifyDeps, Downstream, type DownstreamUser, type Locale, type MagicRequestDeps, type MagicVerifyDeps, type MagicVerifyResult, type OAuthLoginDeps, type OAuthLoginInput, type OAuthLoginResult, type OAuthStateData, type PasswordLoginDeps, type PasswordLoginInput, type PasswordLoginResult, type PlanetLoginConfig, type ProviderConfig, type RegisterResult, type RegisterVerifyDeps, type SessionClaims, type SessionStore, type StoreKind, type StoredCredential, type TotpDeps, _stores, authenticationOptions, buildAuthUrl, downstreamFromEnv, exchangeCode, fetchProfile, getProvider, getStore, hashPassword, jwks, loadConfig, newTotpSecret, oauthCallback, oauthStart, openEnc, openOAuthState, passkeyAuthVerify, passkeyRegisterVerify, passwordLogin, pkcePair, publicConfig, registrationOptions, requestMagicLink, sealEnc, sealOAuthState, signMagicToken, signSession, totpEnroll, totpKeyUri, totpVerify, verifyAuthentication, verifyMagicLink, verifyMagicToken, verifyPassword, verifyRegistration, verifySession, verifyTotp };
|
|
573
|
+
export { type AuthResult, type AuthVerifyDeps, type CorsConfig, DEFAULT_RULES, Downstream, type DownstreamUser, type JwtAlg, type Locale, type MagicRequestDeps, type MagicVerifyDeps, type MagicVerifyResult, type OAuthLoginDeps, type OAuthLoginInput, type OAuthLoginResult, type OAuthStateData, type PasswordLoginDeps, type PasswordLoginInput, type PasswordLoginResult, type PlanetLoginConfig, type ProviderConfig, type RateLimitDecision, type RateLimitRule, type RegisterResult, type RegisterVerifyDeps, type SessionClaims, type SessionStore, type StoreKind, type StoredCredential, type TotpDeps, _stores, authenticationOptions, buildAuthUrl, corsFromEnv, corsHeaders, downstreamFromEnv, exchangeCode, fetchProfile, getProvider, getStore, hashPassword, isPreflight, jwks, loadConfig, newTotpSecret, oauthCallback, oauthStart, openEnc, openOAuthState, originAllowed, passkeyAuthVerify, passkeyRegisterVerify, passwordLogin, pkcePair, publicConfig, rateLimit, registrationOptions, requestMagicLink, rlKey, ruleFor, sealEnc, sealOAuthState, signMagicToken, signSession, totpEnroll, totpKeyUri, totpVerify, verifyAuthentication, verifyMagicLink, verifyMagicToken, verifyPassword, verifyRegistration, verifySession, verifyTotp };
|
package/dist/index.js
CHANGED
|
@@ -86,47 +86,84 @@ import {
|
|
|
86
86
|
jwtVerify,
|
|
87
87
|
exportJWK,
|
|
88
88
|
generateKeyPair,
|
|
89
|
+
generateSecret,
|
|
89
90
|
importPKCS8,
|
|
90
91
|
createLocalJWKSet
|
|
91
92
|
} from "jose";
|
|
93
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
94
|
+
var ASYMMETRIC = ["EdDSA", "RS256", "ES256"];
|
|
92
95
|
var cache = null;
|
|
96
|
+
function resolveAlg() {
|
|
97
|
+
const a = process.env.PLANETLOGIN_JWT_ALG || "EdDSA";
|
|
98
|
+
if (!ASYMMETRIC.includes(a) && a !== "HS256") {
|
|
99
|
+
throw new Error(`Unsupported PLANETLOGIN_JWT_ALG "${a}" (use EdDSA|RS256|ES256|HS256)`);
|
|
100
|
+
}
|
|
101
|
+
return a;
|
|
102
|
+
}
|
|
103
|
+
function loadPrivatePem() {
|
|
104
|
+
const v = process.env.PLANETLOGIN_JWT_PRIVATE_KEY ?? process.env.PLANETLOGIN_JWT_PRIVATE_KEY_PEM;
|
|
105
|
+
if (!v) return null;
|
|
106
|
+
return v.includes("-----BEGIN") ? v : readFileSync2(v, "utf8");
|
|
107
|
+
}
|
|
108
|
+
function loadSecret() {
|
|
109
|
+
const v = process.env.PLANETLOGIN_JWT_SECRET;
|
|
110
|
+
if (!v) return null;
|
|
111
|
+
const raw = v.includes("\n") || v.length > 256 ? v : v.startsWith("/") ? readFileSync2(v, "utf8").trim() : v;
|
|
112
|
+
return new TextEncoder().encode(raw);
|
|
113
|
+
}
|
|
93
114
|
async function getKeys() {
|
|
94
115
|
if (cache) return cache;
|
|
116
|
+
const alg = resolveAlg();
|
|
117
|
+
const kid = process.env.PLANETLOGIN_JWT_KID || "dev";
|
|
118
|
+
if (alg === "HS256") {
|
|
119
|
+
let secret = loadSecret();
|
|
120
|
+
if (!secret) {
|
|
121
|
+
console.warn("[planetlogin] WARNING: no PLANETLOGIN_JWT_SECRET \u2014 using an EPHEMERAL HS256 secret. Tokens die on restart and cannot be verified across instances. Do not use in production.");
|
|
122
|
+
secret = await generateSecret("HS256", { extractable: true });
|
|
123
|
+
}
|
|
124
|
+
cache = { alg, kid, sign: secret, verify: secret, pubJwk: null };
|
|
125
|
+
return cache;
|
|
126
|
+
}
|
|
95
127
|
let priv;
|
|
96
|
-
const pem =
|
|
128
|
+
const pem = loadPrivatePem();
|
|
97
129
|
if (pem) {
|
|
98
|
-
priv = await importPKCS8(pem,
|
|
130
|
+
priv = await importPKCS8(pem, alg);
|
|
99
131
|
} else {
|
|
100
|
-
|
|
132
|
+
console.warn(`[planetlogin] WARNING: no PLANETLOGIN_JWT_PRIVATE_KEY \u2014 using an EPHEMERAL ${alg} keypair. Do not use in production.`);
|
|
133
|
+
priv = (await generateKeyPair(alg, { extractable: true })).privateKey;
|
|
101
134
|
}
|
|
102
135
|
const full = await exportJWK(priv);
|
|
103
|
-
const { d, ...pub } = full;
|
|
104
|
-
|
|
105
|
-
cache = { priv, pubJwk: { ...pub, kid, use: "sig", alg: "EdDSA" }, kid };
|
|
136
|
+
const { d, p, q, dp, dq, qi, ...pub } = full;
|
|
137
|
+
cache = { alg, kid, sign: priv, verify: priv, pubJwk: { ...pub, kid, use: "sig", alg } };
|
|
106
138
|
return cache;
|
|
107
139
|
}
|
|
108
140
|
async function jwks() {
|
|
109
141
|
const k = await getKeys();
|
|
110
|
-
return { keys: [k.pubJwk] };
|
|
142
|
+
return { keys: k.pubJwk ? [k.pubJwk] : [] };
|
|
143
|
+
}
|
|
144
|
+
async function verifier() {
|
|
145
|
+
const k = await getKeys();
|
|
146
|
+
if (k.alg === "HS256") return k.verify;
|
|
147
|
+
return createLocalJWKSet({ keys: [k.pubJwk] });
|
|
111
148
|
}
|
|
112
149
|
async function signSession(claims, opts = {}) {
|
|
113
150
|
const k = await getKeys();
|
|
114
|
-
return new SignJWT({ email: claims.email, name: claims.name, locale: claims.locale }).setProtectedHeader({ alg:
|
|
151
|
+
return new SignJWT({ email: claims.email, name: claims.name, locale: claims.locale }).setProtectedHeader({ alg: k.alg, kid: k.kid }).setSubject(claims.sub).setIssuedAt().setExpirationTime(`${opts.ttlSeconds ?? 3600}s`).setIssuer(opts.issuer ?? "planetlogin").setAudience(opts.audience ?? "planetlogin").sign(k.sign);
|
|
115
152
|
}
|
|
116
153
|
async function verifySession(token) {
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
|
|
154
|
+
const { payload } = await jwtVerify(token, await verifier(), {
|
|
155
|
+
issuer: "planetlogin",
|
|
156
|
+
audience: "planetlogin"
|
|
157
|
+
});
|
|
120
158
|
return payload;
|
|
121
159
|
}
|
|
122
160
|
async function signMagicToken(identifier, ttlSeconds = 900) {
|
|
123
161
|
const k = await getKeys();
|
|
124
|
-
return new SignJWT({ purpose: "magic" }).setProtectedHeader({ alg:
|
|
162
|
+
return new SignJWT({ purpose: "magic" }).setProtectedHeader({ alg: k.alg, kid: k.kid }).setSubject(identifier).setJti(crypto.randomUUID()).setIssuedAt().setExpirationTime(`${ttlSeconds}s`).setIssuer("planetlogin").setAudience("planetlogin:magic").sign(k.sign);
|
|
125
163
|
}
|
|
126
164
|
async function verifyMagicToken(token) {
|
|
127
165
|
try {
|
|
128
|
-
const
|
|
129
|
-
const { payload } = await jwtVerify(token, createLocalJWKSet(set), {
|
|
166
|
+
const { payload } = await jwtVerify(token, await verifier(), {
|
|
130
167
|
issuer: "planetlogin",
|
|
131
168
|
audience: "planetlogin:magic"
|
|
132
169
|
});
|
|
@@ -201,6 +238,9 @@ var NoneStore = class {
|
|
|
201
238
|
async claimOnce() {
|
|
202
239
|
return true;
|
|
203
240
|
}
|
|
241
|
+
async incr() {
|
|
242
|
+
return 1;
|
|
243
|
+
}
|
|
204
244
|
};
|
|
205
245
|
var MemoryStore = class {
|
|
206
246
|
m = /* @__PURE__ */ new Map();
|
|
@@ -227,6 +267,16 @@ var MemoryStore = class {
|
|
|
227
267
|
await this.set(key, "1", ttlSeconds);
|
|
228
268
|
return true;
|
|
229
269
|
}
|
|
270
|
+
async incr(key, ttlSeconds) {
|
|
271
|
+
const e = this.alive(key);
|
|
272
|
+
if (!e) {
|
|
273
|
+
await this.set(key, "1", ttlSeconds);
|
|
274
|
+
return 1;
|
|
275
|
+
}
|
|
276
|
+
const n = Number(e.v) + 1;
|
|
277
|
+
e.v = String(n);
|
|
278
|
+
return n;
|
|
279
|
+
}
|
|
230
280
|
};
|
|
231
281
|
var cached = null;
|
|
232
282
|
function getStore(kind = process.env.PLANETLOGIN_SESSION_STORE || "none") {
|
|
@@ -245,13 +295,75 @@ function getStore(kind = process.env.PLANETLOGIN_SESSION_STORE || "none") {
|
|
|
245
295
|
}
|
|
246
296
|
var _stores = { NoneStore, MemoryStore };
|
|
247
297
|
|
|
298
|
+
// src/ratelimit.ts
|
|
299
|
+
var DEFAULT_RULES = {
|
|
300
|
+
login: { limit: 10, windowSeconds: 300 },
|
|
301
|
+
// 10 / 5 min
|
|
302
|
+
magic: { limit: 5, windowSeconds: 900 },
|
|
303
|
+
// 5 / 15 min
|
|
304
|
+
totp: { limit: 10, windowSeconds: 300 }
|
|
305
|
+
};
|
|
306
|
+
async function rateLimit(store, key, rule) {
|
|
307
|
+
try {
|
|
308
|
+
const count = await store.incr(`rl:${key}`, rule.windowSeconds);
|
|
309
|
+
const remaining = Math.max(0, rule.limit - count);
|
|
310
|
+
if (count > rule.limit) {
|
|
311
|
+
return { ok: false, remaining: 0, retryAfter: rule.windowSeconds, limit: rule.limit };
|
|
312
|
+
}
|
|
313
|
+
return { ok: true, remaining, retryAfter: 0, limit: rule.limit };
|
|
314
|
+
} catch {
|
|
315
|
+
return { ok: true, remaining: rule.limit, retryAfter: 0, limit: rule.limit };
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
function ruleFor(action, overrides) {
|
|
319
|
+
const envLimit = process.env[`PLANETLOGIN_RATELIMIT_${action.toUpperCase()}_LIMIT`];
|
|
320
|
+
const envWindow = process.env[`PLANETLOGIN_RATELIMIT_${action.toUpperCase()}_WINDOW`];
|
|
321
|
+
const base = overrides?.[action] ?? DEFAULT_RULES[action];
|
|
322
|
+
return {
|
|
323
|
+
limit: envLimit ? Number(envLimit) : base.limit,
|
|
324
|
+
windowSeconds: envWindow ? Number(envWindow) : base.windowSeconds
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
function rlKey(action, parts) {
|
|
328
|
+
const disc = [parts.ip, parts.identifier].filter(Boolean).join("|") || "anon";
|
|
329
|
+
return `${action}:${disc}`;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// src/cors.ts
|
|
333
|
+
var METHODS = "GET, POST, OPTIONS";
|
|
334
|
+
var ALLOW_HEADERS = "content-type, authorization";
|
|
335
|
+
function corsFromEnv() {
|
|
336
|
+
const raw = process.env.PLANETLOGIN_CORS_ORIGINS ?? "";
|
|
337
|
+
const origins = raw.split(",").map((s) => s.trim()).filter(Boolean);
|
|
338
|
+
const credentials = process.env.PLANETLOGIN_CORS_CREDENTIALS !== "false";
|
|
339
|
+
return { origins, credentials };
|
|
340
|
+
}
|
|
341
|
+
function originAllowed(origin, cfg2) {
|
|
342
|
+
if (!origin) return false;
|
|
343
|
+
if (cfg2.origins.includes(origin)) return true;
|
|
344
|
+
return cfg2.origins.includes("*") && cfg2.credentials === false;
|
|
345
|
+
}
|
|
346
|
+
function corsHeaders(origin, cfg2) {
|
|
347
|
+
const h = { vary: "Origin" };
|
|
348
|
+
if (!originAllowed(origin, cfg2)) return h;
|
|
349
|
+
h["access-control-allow-origin"] = cfg2.credentials === false && cfg2.origins.includes("*") ? "*" : origin;
|
|
350
|
+
if (cfg2.credentials !== false) h["access-control-allow-credentials"] = "true";
|
|
351
|
+
h["access-control-allow-methods"] = METHODS;
|
|
352
|
+
h["access-control-allow-headers"] = ALLOW_HEADERS;
|
|
353
|
+
h["access-control-max-age"] = "600";
|
|
354
|
+
return h;
|
|
355
|
+
}
|
|
356
|
+
function isPreflight(method, requestMethodHeader) {
|
|
357
|
+
return method === "OPTIONS" && !!requestMethodHeader;
|
|
358
|
+
}
|
|
359
|
+
|
|
248
360
|
// src/oauth.ts
|
|
249
361
|
import { createHash, randomBytes } from "crypto";
|
|
250
362
|
var b64url = (b) => b.toString("base64url");
|
|
251
363
|
function pkcePair() {
|
|
252
|
-
const
|
|
253
|
-
const challenge = b64url(createHash("sha256").update(
|
|
254
|
-
return { verifier, challenge };
|
|
364
|
+
const verifier2 = b64url(randomBytes(32));
|
|
365
|
+
const challenge = b64url(createHash("sha256").update(verifier2).digest());
|
|
366
|
+
return { verifier: verifier2, challenge };
|
|
255
367
|
}
|
|
256
368
|
var REGISTRY = {
|
|
257
369
|
google: {
|
|
@@ -297,9 +409,9 @@ function buildAuthUrl(p, a) {
|
|
|
297
409
|
return u.toString();
|
|
298
410
|
}
|
|
299
411
|
function oauthStart(p, a) {
|
|
300
|
-
const { verifier, challenge } = pkcePair();
|
|
412
|
+
const { verifier: verifier2, challenge } = pkcePair();
|
|
301
413
|
const state = b64url(randomBytes(16));
|
|
302
|
-
return { url: buildAuthUrl(p, { ...a, state, challenge }), state, codeVerifier:
|
|
414
|
+
return { url: buildAuthUrl(p, { ...a, state, challenge }), state, codeVerifier: verifier2 };
|
|
303
415
|
}
|
|
304
416
|
async function exchangeCode(p, a) {
|
|
305
417
|
const res = await fetch(p.tokenUrl, {
|
|
@@ -553,16 +665,20 @@ async function totpVerify(deps, input) {
|
|
|
553
665
|
return { ok: true };
|
|
554
666
|
}
|
|
555
667
|
export {
|
|
668
|
+
DEFAULT_RULES,
|
|
556
669
|
Downstream,
|
|
557
670
|
_stores,
|
|
558
671
|
authenticationOptions,
|
|
559
672
|
buildAuthUrl,
|
|
673
|
+
corsFromEnv,
|
|
674
|
+
corsHeaders,
|
|
560
675
|
downstreamFromEnv,
|
|
561
676
|
exchangeCode,
|
|
562
677
|
fetchProfile,
|
|
563
678
|
getProvider,
|
|
564
679
|
getStore,
|
|
565
680
|
hashPassword,
|
|
681
|
+
isPreflight,
|
|
566
682
|
jwks,
|
|
567
683
|
loadConfig,
|
|
568
684
|
newTotpSecret,
|
|
@@ -570,13 +686,17 @@ export {
|
|
|
570
686
|
oauthStart,
|
|
571
687
|
openEnc,
|
|
572
688
|
openOAuthState,
|
|
689
|
+
originAllowed,
|
|
573
690
|
passkeyAuthVerify,
|
|
574
691
|
passkeyRegisterVerify,
|
|
575
692
|
passwordLogin,
|
|
576
693
|
pkcePair,
|
|
577
694
|
publicConfig,
|
|
695
|
+
rateLimit,
|
|
578
696
|
registrationOptions,
|
|
579
697
|
requestMagicLink,
|
|
698
|
+
rlKey,
|
|
699
|
+
ruleFor,
|
|
580
700
|
sealEnc,
|
|
581
701
|
sealOAuthState,
|
|
582
702
|
signMagicToken,
|
package/dist/keygen.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
package/dist/keygen.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// bin/keygen.ts
|
|
4
|
+
import { generateKeyPair, exportPKCS8, exportJWK } from "jose";
|
|
5
|
+
import { writeFileSync } from "fs";
|
|
6
|
+
var { privateKey, publicKey } = await generateKeyPair("EdDSA", { extractable: true });
|
|
7
|
+
var pem = await exportPKCS8(privateKey);
|
|
8
|
+
var pubJwk = await exportJWK(publicKey);
|
|
9
|
+
var out = process.argv[2];
|
|
10
|
+
if (out) {
|
|
11
|
+
writeFileSync(out, pem, { mode: 384 });
|
|
12
|
+
console.error(`Private key written to ${out} (chmod 600).`);
|
|
13
|
+
console.error(`
|
|
14
|
+
Set: PLANETLOGIN_JWT_PRIVATE_KEY=${out}`);
|
|
15
|
+
} else {
|
|
16
|
+
process.stdout.write(pem);
|
|
17
|
+
console.error("\n# \u2191 EdDSA private key (PEM). Keep it secret. Then set one of:");
|
|
18
|
+
console.error("# PLANETLOGIN_JWT_PRIVATE_KEY=/path/to/this/key.pem (recommended: a secret file)");
|
|
19
|
+
console.error('# PLANETLOGIN_JWT_PRIVATE_KEY="$(cat key.pem)" (inline)');
|
|
20
|
+
}
|
|
21
|
+
console.error(`
|
|
22
|
+
# Public key (the kid in your JWKS will be PLANETLOGIN_JWT_KID, default "dev"):`);
|
|
23
|
+
console.error("# " + JSON.stringify({ ...pubJwk, use: "sig", alg: "EdDSA" }));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@planetlogin/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "PlanetLogin auth core — framework-agnostic flows, JWT, crypto, downstream contract. Consumed by every flavor; the HTTP binding stays per-framework.",
|
|
6
6
|
"exports": {
|
|
@@ -12,10 +12,17 @@
|
|
|
12
12
|
"main": "./dist/index.js",
|
|
13
13
|
"module": "./dist/index.js",
|
|
14
14
|
"types": "./dist/index.d.ts",
|
|
15
|
-
"
|
|
15
|
+
"bin": {
|
|
16
|
+
"planetlogin-keygen": "./dist/keygen.js"
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"dist"
|
|
20
|
+
],
|
|
16
21
|
"scripts": {
|
|
17
22
|
"build": "tsup",
|
|
23
|
+
"prepare": "tsup",
|
|
18
24
|
"prepublishOnly": "tsup",
|
|
25
|
+
"keygen": "tsx bin/keygen.ts",
|
|
19
26
|
"test": "vitest run",
|
|
20
27
|
"typecheck": "tsc --noEmit",
|
|
21
28
|
"mock": "tsx mock-downstream/server.ts"
|