@planetlogin/core 0.2.0 → 0.4.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 +45 -2
- package/dist/index.js +77 -6
- package/dist/keygen.js +9 -1
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -6,6 +6,8 @@ interface Locale {
|
|
|
6
6
|
language?: string;
|
|
7
7
|
timezone?: string;
|
|
8
8
|
country?: string;
|
|
9
|
+
lat?: number;
|
|
10
|
+
lon?: number;
|
|
9
11
|
}
|
|
10
12
|
interface SessionClaims {
|
|
11
13
|
sub: string;
|
|
@@ -24,7 +26,8 @@ declare function signSession(claims: SessionClaims, opts?: {
|
|
|
24
26
|
audience?: string;
|
|
25
27
|
ttlSeconds?: number;
|
|
26
28
|
}): Promise<string>;
|
|
27
|
-
/** Verify a session token against our own key material (what a downstream does).
|
|
29
|
+
/** Verify a session token against our own key material (what a downstream does).
|
|
30
|
+
* Transparently decrypts a JWE-wrapped token first when encryption is on. */
|
|
28
31
|
declare function verifySession(token: string): Promise<jose.JWTPayload>;
|
|
29
32
|
declare function signMagicToken(identifier: string, ttlSeconds?: number): Promise<string>;
|
|
30
33
|
/** Returns {identifier, jti} if the token is a valid, unexpired magic token, else null. */
|
|
@@ -41,6 +44,13 @@ interface DownstreamUser {
|
|
|
41
44
|
locale?: Locale;
|
|
42
45
|
totpEnabled?: boolean;
|
|
43
46
|
}
|
|
47
|
+
/** Per-user, dev-owned preferences (spec §4). `locale` is the typed, first-class
|
|
48
|
+
* piece PlanetLogin reads/writes (fly-to + i18n); `data` is an open bag the
|
|
49
|
+
* integrator can store any "that kind of info" in. */
|
|
50
|
+
interface UserPreferences {
|
|
51
|
+
locale?: Locale;
|
|
52
|
+
data?: Record<string, unknown>;
|
|
53
|
+
}
|
|
44
54
|
declare class Downstream {
|
|
45
55
|
private baseUrl;
|
|
46
56
|
private secret;
|
|
@@ -86,6 +96,13 @@ declare class Downstream {
|
|
|
86
96
|
secret: string;
|
|
87
97
|
enabled: boolean;
|
|
88
98
|
}): Promise<unknown>;
|
|
99
|
+
/** Preferences (spec §4): per-user locale + open data bag. Null when none. */
|
|
100
|
+
preferencesGet(query: {
|
|
101
|
+
userId: string;
|
|
102
|
+
}): Promise<UserPreferences | null>;
|
|
103
|
+
preferencesSave(data: {
|
|
104
|
+
userId: string;
|
|
105
|
+
} & UserPreferences): Promise<unknown>;
|
|
89
106
|
}
|
|
90
107
|
|
|
91
108
|
interface PlanetLoginConfig {
|
|
@@ -127,11 +144,16 @@ interface PlanetLoginConfig {
|
|
|
127
144
|
showSearch?: boolean;
|
|
128
145
|
autoSpin?: boolean;
|
|
129
146
|
};
|
|
147
|
+
locale?: {
|
|
148
|
+
persist?: boolean;
|
|
149
|
+
flyToOnLogin?: boolean;
|
|
150
|
+
};
|
|
130
151
|
token?: {
|
|
131
152
|
issuer?: string;
|
|
132
153
|
audience?: string;
|
|
133
154
|
ttlSeconds?: number;
|
|
134
155
|
algorithm?: 'EdDSA' | 'RS256' | 'ES256' | 'HS256';
|
|
156
|
+
encrypt?: boolean;
|
|
135
157
|
};
|
|
136
158
|
session?: {
|
|
137
159
|
store?: 'none' | 'memory' | 'redis' | 'sqlite' | 'downstream';
|
|
@@ -198,6 +220,9 @@ declare function publicConfig(c?: PlanetLoginConfig): {
|
|
|
198
220
|
showSearch?: boolean;
|
|
199
221
|
autoSpin?: boolean;
|
|
200
222
|
};
|
|
223
|
+
locale: {
|
|
224
|
+
flyToOnLogin: boolean;
|
|
225
|
+
};
|
|
201
226
|
};
|
|
202
227
|
declare function downstreamFromEnv(): Downstream;
|
|
203
228
|
|
|
@@ -427,6 +452,24 @@ type PasswordLoginResult = {
|
|
|
427
452
|
};
|
|
428
453
|
declare function passwordLogin(deps: PasswordLoginDeps, input: PasswordLoginInput): Promise<PasswordLoginResult>;
|
|
429
454
|
|
|
455
|
+
interface PreferencesDeps {
|
|
456
|
+
downstream: Pick<Downstream, 'preferencesGet' | 'preferencesSave'>;
|
|
457
|
+
}
|
|
458
|
+
/** Keep only the known, well-typed locale fields (drop anything else). */
|
|
459
|
+
declare function sanitizeLocale(input: unknown): Locale | undefined;
|
|
460
|
+
/** Read the user's saved preferences. Always resolves (empty object if none/error). */
|
|
461
|
+
declare function getPreferences(deps: PreferencesDeps, input: {
|
|
462
|
+
userId: string;
|
|
463
|
+
}): Promise<UserPreferences>;
|
|
464
|
+
/** Write the user's preferences. No-op when there's nothing valid to write. */
|
|
465
|
+
declare function savePreferences(deps: PreferencesDeps, input: {
|
|
466
|
+
userId: string;
|
|
467
|
+
locale?: unknown;
|
|
468
|
+
data?: unknown;
|
|
469
|
+
}): Promise<{
|
|
470
|
+
saved: boolean;
|
|
471
|
+
}>;
|
|
472
|
+
|
|
430
473
|
interface MagicRequestDeps {
|
|
431
474
|
downstream: Downstream;
|
|
432
475
|
signMagicToken: (identifier: string) => Promise<string>;
|
|
@@ -570,4 +613,4 @@ declare function totpVerify(deps: TotpDeps, input: {
|
|
|
570
613
|
ok: boolean;
|
|
571
614
|
}>;
|
|
572
615
|
|
|
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 };
|
|
616
|
+
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 PreferencesDeps, type ProviderConfig, type RateLimitDecision, type RateLimitRule, type RegisterResult, type RegisterVerifyDeps, type SessionClaims, type SessionStore, type StoreKind, type StoredCredential, type TotpDeps, type UserPreferences, _stores, authenticationOptions, buildAuthUrl, corsFromEnv, corsHeaders, downstreamFromEnv, exchangeCode, fetchProfile, getPreferences, getProvider, getStore, hashPassword, isPreflight, jwks, loadConfig, newTotpSecret, oauthCallback, oauthStart, openEnc, openOAuthState, originAllowed, passkeyAuthVerify, passkeyRegisterVerify, passwordLogin, pkcePair, publicConfig, rateLimit, registrationOptions, requestMagicLink, rlKey, ruleFor, sanitizeLocale, savePreferences, sealEnc, sealOAuthState, signMagicToken, signSession, totpEnroll, totpKeyUri, totpVerify, verifyAuthentication, verifyMagicLink, verifyMagicToken, verifyPassword, verifyRegistration, verifySession, verifyTotp };
|
package/dist/index.js
CHANGED
|
@@ -55,6 +55,13 @@ var Downstream = class {
|
|
|
55
55
|
totpSave(data) {
|
|
56
56
|
return this.call("/totp/save", data);
|
|
57
57
|
}
|
|
58
|
+
/** Preferences (spec §4): per-user locale + open data bag. Null when none. */
|
|
59
|
+
preferencesGet(query) {
|
|
60
|
+
return this.call("/preferences/find", query);
|
|
61
|
+
}
|
|
62
|
+
preferencesSave(data) {
|
|
63
|
+
return this.call("/preferences/save", data);
|
|
64
|
+
}
|
|
58
65
|
};
|
|
59
66
|
|
|
60
67
|
// src/config.ts
|
|
@@ -71,7 +78,15 @@ function loadConfig() {
|
|
|
71
78
|
return cfg;
|
|
72
79
|
}
|
|
73
80
|
function publicConfig(c = loadConfig()) {
|
|
74
|
-
return {
|
|
81
|
+
return {
|
|
82
|
+
spec: c.spec,
|
|
83
|
+
brand: c.brand,
|
|
84
|
+
providers: c.providers,
|
|
85
|
+
copy: c.copy ?? {},
|
|
86
|
+
layout: c.layout ?? {},
|
|
87
|
+
// Only the client-relevant gate (flyToOnLogin drives the post-login fly-to).
|
|
88
|
+
locale: { flyToOnLogin: c.locale?.flyToOnLogin ?? false }
|
|
89
|
+
};
|
|
75
90
|
}
|
|
76
91
|
function downstreamFromEnv() {
|
|
77
92
|
const url = process.env.PLANETLOGIN_DOWNSTREAM_URL;
|
|
@@ -88,7 +103,10 @@ import {
|
|
|
88
103
|
generateKeyPair,
|
|
89
104
|
generateSecret,
|
|
90
105
|
importPKCS8,
|
|
91
|
-
createLocalJWKSet
|
|
106
|
+
createLocalJWKSet,
|
|
107
|
+
CompactEncrypt,
|
|
108
|
+
compactDecrypt,
|
|
109
|
+
base64url
|
|
92
110
|
} from "jose";
|
|
93
111
|
import { readFileSync as readFileSync2 } from "fs";
|
|
94
112
|
var ASYMMETRIC = ["EdDSA", "RS256", "ES256"];
|
|
@@ -146,12 +164,38 @@ async function verifier() {
|
|
|
146
164
|
if (k.alg === "HS256") return k.verify;
|
|
147
165
|
return createLocalJWKSet({ keys: [k.pubJwk] });
|
|
148
166
|
}
|
|
167
|
+
var jweKey = null;
|
|
168
|
+
function encryptEnabled() {
|
|
169
|
+
return process.env.PLANETLOGIN_JWT_ENCRYPT === "true";
|
|
170
|
+
}
|
|
171
|
+
function getJweKey() {
|
|
172
|
+
if (jweKey) return jweKey;
|
|
173
|
+
const env = process.env.PLANETLOGIN_JWE_KEY;
|
|
174
|
+
if (env) {
|
|
175
|
+
jweKey = base64url.decode(env);
|
|
176
|
+
if (jweKey.length !== 32) throw new Error("PLANETLOGIN_JWE_KEY must decode to 32 bytes (base64url)");
|
|
177
|
+
} else {
|
|
178
|
+
console.warn("[planetlogin] WARNING: PLANETLOGIN_JWT_ENCRYPT is on but no PLANETLOGIN_JWE_KEY \u2014 using an EPHEMERAL key. Tokens cannot be decrypted across instances/restarts. Do not use in production.");
|
|
179
|
+
jweKey = crypto.getRandomValues(new Uint8Array(32));
|
|
180
|
+
}
|
|
181
|
+
return jweKey;
|
|
182
|
+
}
|
|
183
|
+
async function maybeEncrypt(jws) {
|
|
184
|
+
if (!encryptEnabled()) return jws;
|
|
185
|
+
return new CompactEncrypt(new TextEncoder().encode(jws)).setProtectedHeader({ alg: "dir", enc: "A256GCM", cty: "JWT" }).encrypt(getJweKey());
|
|
186
|
+
}
|
|
187
|
+
async function maybeDecrypt(token) {
|
|
188
|
+
if (token.split(".").length !== 5) return token;
|
|
189
|
+
const { plaintext } = await compactDecrypt(token, getJweKey());
|
|
190
|
+
return new TextDecoder().decode(plaintext);
|
|
191
|
+
}
|
|
149
192
|
async function signSession(claims, opts = {}) {
|
|
150
193
|
const k = await getKeys();
|
|
151
|
-
|
|
194
|
+
const jws = await 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);
|
|
195
|
+
return maybeEncrypt(jws);
|
|
152
196
|
}
|
|
153
197
|
async function verifySession(token) {
|
|
154
|
-
const { payload } = await jwtVerify(token, await verifier(), {
|
|
198
|
+
const { payload } = await jwtVerify(await maybeDecrypt(token), await verifier(), {
|
|
155
199
|
issuer: "planetlogin",
|
|
156
200
|
audience: "planetlogin"
|
|
157
201
|
});
|
|
@@ -440,12 +484,12 @@ async function fetchProfile(p, accessToken) {
|
|
|
440
484
|
}
|
|
441
485
|
|
|
442
486
|
// src/oauthState.ts
|
|
443
|
-
import { EncryptJWT, jwtDecrypt, base64url } from "jose";
|
|
487
|
+
import { EncryptJWT, jwtDecrypt, base64url as base64url2 } from "jose";
|
|
444
488
|
var stateKey = null;
|
|
445
489
|
function getStateKey() {
|
|
446
490
|
if (stateKey) return stateKey;
|
|
447
491
|
const env = process.env.PLANETLOGIN_STATE_KEY;
|
|
448
|
-
stateKey = env ?
|
|
492
|
+
stateKey = env ? base64url2.decode(env) : crypto.getRandomValues(new Uint8Array(32));
|
|
449
493
|
if (stateKey.length !== 32) throw new Error("PLANETLOGIN_STATE_KEY must decode to 32 bytes");
|
|
450
494
|
return stateKey;
|
|
451
495
|
}
|
|
@@ -557,6 +601,30 @@ async function passwordLogin(deps, input) {
|
|
|
557
601
|
return { ok: true, token, user: { id: user.id, email: user.email, name: user.name } };
|
|
558
602
|
}
|
|
559
603
|
|
|
604
|
+
// src/flows/preferences.ts
|
|
605
|
+
function sanitizeLocale(input) {
|
|
606
|
+
if (!input || typeof input !== "object") return void 0;
|
|
607
|
+
const i = input;
|
|
608
|
+
const out = {};
|
|
609
|
+
if (typeof i.language === "string") out.language = i.language.slice(0, 16);
|
|
610
|
+
if (typeof i.timezone === "string") out.timezone = i.timezone.slice(0, 64);
|
|
611
|
+
if (typeof i.country === "string") out.country = i.country.slice(0, 2).toUpperCase();
|
|
612
|
+
if (typeof i.lat === "number" && Number.isFinite(i.lat)) out.lat = i.lat;
|
|
613
|
+
if (typeof i.lon === "number" && Number.isFinite(i.lon)) out.lon = i.lon;
|
|
614
|
+
return Object.keys(out).length ? out : void 0;
|
|
615
|
+
}
|
|
616
|
+
async function getPreferences(deps, input) {
|
|
617
|
+
const p = await deps.downstream.preferencesGet({ userId: input.userId }).catch(() => null);
|
|
618
|
+
return p ?? {};
|
|
619
|
+
}
|
|
620
|
+
async function savePreferences(deps, input) {
|
|
621
|
+
const locale = sanitizeLocale(input.locale);
|
|
622
|
+
const data = input.data && typeof input.data === "object" && !Array.isArray(input.data) ? input.data : void 0;
|
|
623
|
+
if (!locale && !data) return { saved: false };
|
|
624
|
+
await deps.downstream.preferencesSave({ userId: input.userId, ...locale ? { locale } : {}, ...data ? { data } : {} });
|
|
625
|
+
return { saved: true };
|
|
626
|
+
}
|
|
627
|
+
|
|
560
628
|
// src/flows/magicLink.ts
|
|
561
629
|
async function requestMagicLink(deps, input) {
|
|
562
630
|
try {
|
|
@@ -675,6 +743,7 @@ export {
|
|
|
675
743
|
downstreamFromEnv,
|
|
676
744
|
exchangeCode,
|
|
677
745
|
fetchProfile,
|
|
746
|
+
getPreferences,
|
|
678
747
|
getProvider,
|
|
679
748
|
getStore,
|
|
680
749
|
hashPassword,
|
|
@@ -697,6 +766,8 @@ export {
|
|
|
697
766
|
requestMagicLink,
|
|
698
767
|
rlKey,
|
|
699
768
|
ruleFor,
|
|
769
|
+
sanitizeLocale,
|
|
770
|
+
savePreferences,
|
|
700
771
|
sealEnc,
|
|
701
772
|
sealOAuthState,
|
|
702
773
|
signMagicToken,
|
package/dist/keygen.js
CHANGED
|
@@ -1,8 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// bin/keygen.ts
|
|
4
|
-
import { generateKeyPair, exportPKCS8, exportJWK } from "jose";
|
|
4
|
+
import { generateKeyPair, exportPKCS8, exportJWK, base64url } from "jose";
|
|
5
5
|
import { writeFileSync } from "fs";
|
|
6
|
+
if (process.argv.includes("--jwe")) {
|
|
7
|
+
const key = base64url.encode(crypto.getRandomValues(new Uint8Array(32)));
|
|
8
|
+
process.stdout.write(key + "\n");
|
|
9
|
+
console.error("\n# \u2191 32-byte JWE key (base64url). Shared out of band with services that read claims. Then set:");
|
|
10
|
+
console.error("# PLANETLOGIN_JWT_ENCRYPT=true");
|
|
11
|
+
console.error(`# PLANETLOGIN_JWE_KEY=${key}`);
|
|
12
|
+
process.exit(0);
|
|
13
|
+
}
|
|
6
14
|
var { privateKey, publicKey } = await generateKeyPair("EdDSA", { extractable: true });
|
|
7
15
|
var pem = await exportPKCS8(privateKey);
|
|
8
16
|
var pubJwk = await exportJWK(publicKey);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@planetlogin/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.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": {
|