@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 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 { spec: c.spec, brand: c.brand, providers: c.providers, copy: c.copy ?? {}, layout: c.layout ?? {} };
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
- 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);
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 ? base64url.decode(env) : crypto.getRandomValues(new Uint8Array(32));
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.2.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": {