@planetlogin/core 0.3.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
@@ -26,7 +26,8 @@ declare function signSession(claims: SessionClaims, opts?: {
26
26
  audience?: string;
27
27
  ttlSeconds?: number;
28
28
  }): Promise<string>;
29
- /** 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. */
30
31
  declare function verifySession(token: string): Promise<jose.JWTPayload>;
31
32
  declare function signMagicToken(identifier: string, ttlSeconds?: number): Promise<string>;
32
33
  /** Returns {identifier, jti} if the token is a valid, unexpired magic token, else null. */
@@ -152,6 +153,7 @@ interface PlanetLoginConfig {
152
153
  audience?: string;
153
154
  ttlSeconds?: number;
154
155
  algorithm?: 'EdDSA' | 'RS256' | 'ES256' | 'HS256';
156
+ encrypt?: boolean;
155
157
  };
156
158
  session?: {
157
159
  store?: 'none' | 'memory' | 'redis' | 'sqlite' | 'downstream';
package/dist/index.js CHANGED
@@ -103,7 +103,10 @@ import {
103
103
  generateKeyPair,
104
104
  generateSecret,
105
105
  importPKCS8,
106
- createLocalJWKSet
106
+ createLocalJWKSet,
107
+ CompactEncrypt,
108
+ compactDecrypt,
109
+ base64url
107
110
  } from "jose";
108
111
  import { readFileSync as readFileSync2 } from "fs";
109
112
  var ASYMMETRIC = ["EdDSA", "RS256", "ES256"];
@@ -161,12 +164,38 @@ async function verifier() {
161
164
  if (k.alg === "HS256") return k.verify;
162
165
  return createLocalJWKSet({ keys: [k.pubJwk] });
163
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
+ }
164
192
  async function signSession(claims, opts = {}) {
165
193
  const k = await getKeys();
166
- 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);
167
196
  }
168
197
  async function verifySession(token) {
169
- const { payload } = await jwtVerify(token, await verifier(), {
198
+ const { payload } = await jwtVerify(await maybeDecrypt(token), await verifier(), {
170
199
  issuer: "planetlogin",
171
200
  audience: "planetlogin"
172
201
  });
@@ -455,12 +484,12 @@ async function fetchProfile(p, accessToken) {
455
484
  }
456
485
 
457
486
  // src/oauthState.ts
458
- import { EncryptJWT, jwtDecrypt, base64url } from "jose";
487
+ import { EncryptJWT, jwtDecrypt, base64url as base64url2 } from "jose";
459
488
  var stateKey = null;
460
489
  function getStateKey() {
461
490
  if (stateKey) return stateKey;
462
491
  const env = process.env.PLANETLOGIN_STATE_KEY;
463
- stateKey = env ? base64url.decode(env) : crypto.getRandomValues(new Uint8Array(32));
492
+ stateKey = env ? base64url2.decode(env) : crypto.getRandomValues(new Uint8Array(32));
464
493
  if (stateKey.length !== 32) throw new Error("PLANETLOGIN_STATE_KEY must decode to 32 bytes");
465
494
  return stateKey;
466
495
  }
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.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": {