@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 CHANGED
@@ -13,7 +13,9 @@ interface SessionClaims {
13
13
  name?: string;
14
14
  locale?: Locale;
15
15
  }
16
- /** Public JWK Set served at GET /auth/.well-known/jwks.json. */
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 JWKS (what a downstream service does). */
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 only). */
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 = process.env.PLANETLOGIN_JWT_PRIVATE_KEY_PEM;
128
+ const pem = loadPrivatePem();
97
129
  if (pem) {
98
- priv = await importPKCS8(pem, "EdDSA");
130
+ priv = await importPKCS8(pem, alg);
99
131
  } else {
100
- priv = (await generateKeyPair("EdDSA", { extractable: true })).privateKey;
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
- const kid = process.env.PLANETLOGIN_JWT_KID || "dev";
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: "EdDSA", kid: k.kid }).setSubject(claims.sub).setIssuedAt().setExpirationTime(`${opts.ttlSeconds ?? 3600}s`).setIssuer(opts.issuer ?? "planetlogin").setAudience(opts.audience ?? "planetlogin").sign(k.priv);
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 set = await jwks();
118
- const keySet = createLocalJWKSet(set);
119
- const { payload } = await jwtVerify(token, keySet, { issuer: "planetlogin", audience: "planetlogin" });
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: "EdDSA", kid: k.kid }).setSubject(identifier).setJti(crypto.randomUUID()).setIssuedAt().setExpirationTime(`${ttlSeconds}s`).setIssuer("planetlogin").setAudience("planetlogin:magic").sign(k.priv);
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 set = await jwks();
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 verifier = b64url(randomBytes(32));
253
- const challenge = b64url(createHash("sha256").update(verifier).digest());
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: verifier };
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,
@@ -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.1.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
- "files": ["dist"],
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"