@nya-account/node-sdk 2.0.0 → 2.0.2

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.js CHANGED
@@ -5,6 +5,7 @@ import { z } from "zod";
5
5
  import { createRemoteJWKSet, errors, jwtVerify } from "jose";
6
6
 
7
7
  //#region src/core/schemas.ts
8
+ const TokenTypeHintSchema = z.enum(["access_token", "refresh_token"]);
8
9
  const TokenResponseSchema = z.object({
9
10
  access_token: z.string(),
10
11
  token_type: z.string(),
@@ -28,12 +29,14 @@ const IntrospectionResponseSchema = z.object({
28
29
  sub: z.string().optional(),
29
30
  aud: z.string().optional(),
30
31
  iss: z.string().optional(),
31
- jti: z.string().optional()
32
+ jti: z.string().optional(),
33
+ sid: z.string().optional(),
34
+ sv: z.number().optional()
32
35
  });
33
36
  const UserInfoSchema = z.object({
34
37
  sub: z.string(),
35
38
  name: z.string().optional(),
36
- preferred_username: z.string().optional(),
39
+ picture: z.string().optional(),
37
40
  email: z.string().optional(),
38
41
  email_verified: z.boolean().optional(),
39
42
  updated_at: z.number().optional()
@@ -60,12 +63,18 @@ const DiscoveryDocumentSchema = z.object({
60
63
  request_parameter_supported: z.boolean().optional(),
61
64
  request_uri_parameter_supported: z.boolean().optional()
62
65
  });
66
+ const PushedAuthorizationResponseSchema = z.object({
67
+ request_uri: z.string().min(1),
68
+ expires_in: z.number().int().positive()
69
+ });
63
70
  const AccessTokenPayloadSchema = z.object({
64
71
  iss: z.string(),
65
72
  sub: z.string(),
66
73
  aud: z.string(),
67
74
  scope: z.string(),
68
75
  ver: z.string(),
76
+ sid: z.string(),
77
+ sv: z.number().int().nonnegative(),
69
78
  iat: z.number(),
70
79
  exp: z.number(),
71
80
  jti: z.string(),
@@ -75,11 +84,11 @@ const IdTokenPayloadSchema = z.object({
75
84
  iss: z.string(),
76
85
  sub: z.string(),
77
86
  aud: z.string(),
87
+ sid: z.string().optional(),
78
88
  iat: z.number(),
79
89
  exp: z.number(),
80
90
  nonce: z.string().optional(),
81
91
  name: z.string().optional(),
82
- preferred_username: z.string().optional(),
83
92
  email: z.string().optional(),
84
93
  email_verified: z.boolean().optional(),
85
94
  updated_at: z.number().optional()
@@ -235,6 +244,7 @@ var JwtVerifier = class {
235
244
  //#region src/client.ts
236
245
  const DISCOVERY_ENDPOINT_MAP = {
237
246
  authorization: "authorizationEndpoint",
247
+ pushedAuthorizationRequest: "pushedAuthorizationRequestEndpoint",
238
248
  token: "tokenEndpoint",
239
249
  userinfo: "userinfoEndpoint",
240
250
  revocation: "revocationEndpoint",
@@ -242,79 +252,10 @@ const DISCOVERY_ENDPOINT_MAP = {
242
252
  jwks: "jwksUri",
243
253
  endSession: "endSessionEndpoint"
244
254
  };
255
+ /** Default issuer URL */
256
+ const DEFAULT_ISSUER = "https://account-api.edge.lolinya.net";
245
257
  /** Default discovery cache TTL: 1 hour */
246
258
  const DEFAULT_DISCOVERY_CACHE_TTL = 36e5;
247
- /**
248
-
249
- * Nya Account Node.js SDK client.
250
-
251
- *
252
-
253
- * Provides full OAuth 2.1 / OIDC flow support:
254
-
255
- * - Authorization Code + PKCE
256
-
257
- * - Token exchange / refresh / revocation / introspection
258
-
259
- * - Local JWT verification (via JWKS)
260
-
261
- * - OIDC UserInfo
262
-
263
- * - OIDC Discovery auto-discovery
264
-
265
- * - Express middleware (Bearer Token auth + scope validation)
266
-
267
- *
268
-
269
- * @example
270
-
271
- * ```typescript
272
-
273
- * const client = new NyaAccountClient({
274
-
275
- * issuer: 'https://account.example.com',
276
-
277
- * clientId: 'my-app',
278
-
279
- * clientSecret: 'my-secret',
280
-
281
- * })
282
-
283
- *
284
-
285
- * // Create authorization URL (with PKCE)
286
-
287
- * const { url, codeVerifier, state } = await client.createAuthorizationUrl({
288
-
289
- * redirectUri: 'https://myapp.com/callback',
290
-
291
- * scope: 'openid profile email',
292
-
293
- * })
294
-
295
- *
296
-
297
- * // Exchange code for tokens
298
-
299
- * const tokens = await client.exchangeCode({
300
-
301
- * code: callbackCode,
302
-
303
- * redirectUri: 'https://myapp.com/callback',
304
-
305
- * codeVerifier,
306
-
307
- * })
308
-
309
- *
310
-
311
- * // Get user info
312
-
313
- * const userInfo = await client.getUserInfo(tokens.accessToken)
314
-
315
- * ```
316
-
317
- */
318
259
  var NyaAccountClient = class {
319
260
  constructor(config) {
320
261
  this.httpClient = void 0;
@@ -323,14 +264,15 @@ var NyaAccountClient = class {
323
264
  this.discoveryCacheTimestamp = 0;
324
265
  this.discoveryCacheTtl = void 0;
325
266
  this.jwtVerifier = null;
326
- this.config = config;
267
+ this.config = {
268
+ ...config,
269
+ issuer: config.issuer ?? DEFAULT_ISSUER
270
+ };
327
271
  this.discoveryCacheTtl = config.discoveryCacheTtl ?? DEFAULT_DISCOVERY_CACHE_TTL;
328
272
  this.httpClient = axios.create({ timeout: config.timeout ?? 1e4 });
329
273
  }
330
274
  /**
331
-
332
275
  * Fetch the OIDC Discovery document. Results are cached with a configurable TTL.
333
-
334
276
  */
335
277
  async discover() {
336
278
  if (this.discoveryCache && Date.now() - this.discoveryCacheTimestamp < this.discoveryCacheTtl) return this.discoveryCache;
@@ -366,9 +308,7 @@ var NyaAccountClient = class {
366
308
  }
367
309
  }
368
310
  /**
369
-
370
311
  * Clear the cached Discovery document and JWT verifier, forcing a re-fetch on next use.
371
-
372
312
  */
373
313
  clearCache() {
374
314
  this.discoveryCache = null;
@@ -376,15 +316,10 @@ var NyaAccountClient = class {
376
316
  this.jwtVerifier = null;
377
317
  }
378
318
  /**
379
-
380
319
  * Create an OAuth authorization URL (automatically includes PKCE).
381
-
382
320
  *
383
-
384
321
  * The returned `codeVerifier` and `state` must be saved to the session
385
-
386
322
  * for later use in token exchange and CSRF validation.
387
-
388
323
  */
389
324
  async createAuthorizationUrl(options) {
390
325
  const authorizationEndpoint = await this.resolveEndpoint("authorization");
@@ -408,9 +343,72 @@ var NyaAccountClient = class {
408
343
  };
409
344
  }
410
345
  /**
411
-
346
+ * Push authorization parameters to PAR endpoint (RFC 9126).
347
+ *
348
+ * Returns a `request_uri` that can be used in the authorization endpoint.
349
+ */
350
+ async pushAuthorizationRequest(options) {
351
+ const parEndpoint = await this.resolveEndpoint("pushedAuthorizationRequest");
352
+ const codeVerifier = generateCodeVerifier();
353
+ const codeChallenge = generateCodeChallenge(codeVerifier);
354
+ const state = options.state ?? randomBytes(16).toString("base64url");
355
+ const payload = {
356
+ client_id: this.config.clientId,
357
+ client_secret: this.config.clientSecret,
358
+ redirect_uri: options.redirectUri,
359
+ response_type: "code",
360
+ scope: options.scope ?? "openid",
361
+ state,
362
+ code_challenge: codeChallenge,
363
+ code_challenge_method: "S256"
364
+ };
365
+ if (options.nonce) payload.nonce = options.nonce;
366
+ if (options.request) payload.request = options.request;
367
+ try {
368
+ const response = await this.httpClient.post(parEndpoint, payload);
369
+ const raw = PushedAuthorizationResponseSchema.parse(response.data);
370
+ return {
371
+ requestUri: raw.request_uri,
372
+ expiresIn: raw.expires_in,
373
+ codeVerifier,
374
+ state
375
+ };
376
+ } catch (error) {
377
+ throw this.handleTokenError(error);
378
+ }
379
+ }
380
+ /**
381
+ * Create an authorization URL using PAR `request_uri`.
382
+ */
383
+ async createAuthorizationUrlWithPar(options) {
384
+ const authorizationEndpoint = await this.resolveEndpoint("authorization");
385
+ const pushed = await this.pushAuthorizationRequest(options);
386
+ const params = new URLSearchParams({
387
+ client_id: this.config.clientId,
388
+ request_uri: pushed.requestUri
389
+ });
390
+ return {
391
+ url: `${authorizationEndpoint}?${params.toString()}`,
392
+ codeVerifier: pushed.codeVerifier,
393
+ state: pushed.state,
394
+ requestUri: pushed.requestUri,
395
+ expiresIn: pushed.expiresIn
396
+ };
397
+ }
398
+ /**
399
+ * Create OIDC RP-Initiated Logout URL (`end_session_endpoint`).
400
+ */
401
+ async createEndSessionUrl(options) {
402
+ const endSessionEndpoint = await this.resolveEndpoint("endSession");
403
+ const params = new URLSearchParams();
404
+ if (options?.idTokenHint) params.set("id_token_hint", options.idTokenHint);
405
+ if (options?.postLogoutRedirectUri) params.set("post_logout_redirect_uri", options.postLogoutRedirectUri);
406
+ if (options?.state) params.set("state", options.state);
407
+ params.set("client_id", options?.clientId ?? this.config.clientId);
408
+ return `${endSessionEndpoint}?${params.toString()}`;
409
+ }
410
+ /**
412
411
  * Exchange an authorization code for tokens (Authorization Code Grant).
413
-
414
412
  */
415
413
  async exchangeCode(options) {
416
414
  const tokenEndpoint = await this.resolveEndpoint("token");
@@ -429,9 +427,7 @@ var NyaAccountClient = class {
429
427
  }
430
428
  }
431
429
  /**
432
-
433
430
  * Refresh an Access Token using a Refresh Token.
434
-
435
431
  */
436
432
  async refreshToken(refreshToken) {
437
433
  const tokenEndpoint = await this.resolveEndpoint("token");
@@ -448,40 +444,35 @@ var NyaAccountClient = class {
448
444
  }
449
445
  }
450
446
  /**
451
-
452
447
  * Revoke a token (RFC 7009).
453
-
454
448
  *
455
-
456
449
  * Supports revoking Access Tokens or Refresh Tokens.
457
-
458
450
  * Revoking a Refresh Token also revokes its entire token family.
459
-
460
451
  */
461
- async revokeToken(token) {
452
+ async revokeToken(token, options) {
462
453
  const revocationEndpoint = await this.resolveEndpoint("revocation");
463
454
  try {
455
+ const tokenTypeHint = options?.tokenTypeHint ? TokenTypeHintSchema.parse(options.tokenTypeHint) : void 0;
464
456
  await this.httpClient.post(revocationEndpoint, {
465
457
  token,
458
+ token_type_hint: tokenTypeHint,
466
459
  client_id: this.config.clientId,
467
460
  client_secret: this.config.clientSecret
468
461
  });
469
462
  } catch {}
470
463
  }
471
464
  /**
472
-
473
465
  * Token introspection (RFC 7662).
474
-
475
466
  *
476
-
477
467
  * Query the server for the current state of a token (active status, associated user info, etc.).
478
-
479
468
  */
480
- async introspectToken(token) {
469
+ async introspectToken(token, options) {
481
470
  const introspectionEndpoint = await this.resolveEndpoint("introspection");
482
471
  try {
472
+ const tokenTypeHint = options?.tokenTypeHint ? TokenTypeHintSchema.parse(options.tokenTypeHint) : void 0;
483
473
  const response = await this.httpClient.post(introspectionEndpoint, {
484
474
  token,
475
+ token_type_hint: tokenTypeHint,
485
476
  client_id: this.config.clientId,
486
477
  client_secret: this.config.clientSecret
487
478
  });
@@ -497,24 +488,20 @@ var NyaAccountClient = class {
497
488
  sub: raw.sub,
498
489
  aud: raw.aud,
499
490
  iss: raw.iss,
500
- jti: raw.jti
491
+ jti: raw.jti,
492
+ sid: raw.sid,
493
+ sv: raw.sv
501
494
  };
502
495
  } catch (error) {
503
496
  throw this.handleTokenError(error);
504
497
  }
505
498
  }
506
499
  /**
507
-
508
500
  * Get user info using an Access Token (OIDC UserInfo Endpoint).
509
-
510
501
  *
511
-
512
502
  * The returned fields depend on the scopes included in the token:
513
-
514
- * - `profile`: name, preferredUsername, updatedAt
515
-
503
+ * - `profile`: name, picture, updatedAt
516
504
  * - `email`: email, emailVerified
517
-
518
505
  */
519
506
  async getUserInfo(accessToken) {
520
507
  const userinfoEndpoint = await this.resolveEndpoint("userinfo");
@@ -524,7 +511,7 @@ var NyaAccountClient = class {
524
511
  return {
525
512
  sub: raw.sub,
526
513
  name: raw.name,
527
- preferredUsername: raw.preferred_username,
514
+ picture: raw.picture,
528
515
  email: raw.email,
529
516
  emailVerified: raw.email_verified,
530
517
  updatedAt: raw.updated_at
@@ -534,77 +521,46 @@ var NyaAccountClient = class {
534
521
  }
535
522
  }
536
523
  /**
537
-
538
524
  * Locally verify a JWT Access Token (RFC 9068).
539
-
540
525
  *
541
-
542
526
  * Uses remote JWKS for signature verification, and validates issuer, audience, expiry, etc.
543
-
544
527
  *
545
-
546
528
  * @param token JWT Access Token string
547
-
548
529
  * @param options.audience Custom audience validation value (defaults to clientId)
549
-
550
530
  */
551
531
  async verifyAccessToken(token, options) {
552
532
  const verifier = await this.getJwtVerifier();
553
533
  return verifier.verifyAccessToken(token, options?.audience);
554
534
  }
555
535
  /**
556
-
557
536
  * Locally verify an OIDC ID Token.
558
-
559
537
  *
560
-
561
538
  * @param token JWT ID Token string
562
-
563
539
  * @param options.audience Custom audience validation value (defaults to clientId)
564
-
565
540
  * @param options.nonce Validate the nonce claim (required if nonce was sent during authorization)
566
-
567
541
  */
568
542
  async verifyIdToken(token, options) {
569
543
  const verifier = await this.getJwtVerifier();
570
544
  return verifier.verifyIdToken(token, options);
571
545
  }
572
546
  /**
573
-
574
547
  * Express middleware: verify the Bearer Token in the request.
575
-
576
548
  *
577
-
578
549
  * After successful verification, use `getAuth(req)` to retrieve the token payload.
579
-
580
550
  *
581
-
582
551
  * @param options.strategy Verification strategy: 'local' (default, JWT local verification) or 'introspection' (remote introspection)
583
-
584
552
  *
585
-
586
553
  * @example
587
-
588
554
  * ```typescript
589
-
590
555
  * import { getAuth } from '@nya-account/node-sdk/express'
591
-
592
556
  *
593
-
594
557
  * app.use('/api', client.authenticate())
595
-
596
558
  *
597
-
598
559
  * app.get('/api/me', (req, res) => {
599
-
600
560
  * const auth = getAuth(req)
601
-
602
561
  * res.json({ userId: auth?.sub })
603
-
604
562
  * })
605
-
606
563
  * ```
607
-
608
564
  */
609
565
  authenticate(options) {
610
566
  const strategy = options?.strategy ?? "local";
@@ -622,6 +578,11 @@ var NyaAccountClient = class {
622
578
  sendOAuthError(res, 401, "invalid_token", "Token is not active");
623
579
  return;
624
580
  }
581
+ const tokenType = introspection.tokenType?.toLowerCase();
582
+ if (tokenType && tokenType !== "bearer" && tokenType !== "access_token") {
583
+ sendOAuthError(res, 401, "invalid_token", "Token is not an access token");
584
+ return;
585
+ }
625
586
  payload = {
626
587
  iss: introspection.iss ?? "",
627
588
  sub: introspection.sub ?? "",
@@ -630,7 +591,9 @@ var NyaAccountClient = class {
630
591
  ver: "1",
631
592
  iat: introspection.iat ?? 0,
632
593
  exp: introspection.exp ?? 0,
633
- jti: introspection.jti ?? ""
594
+ jti: introspection.jti ?? "",
595
+ sid: introspection.sid ?? "",
596
+ sv: introspection.sv ?? 0
634
597
  };
635
598
  } else payload = await this.verifyAccessToken(token);
636
599
  setAuth(req, payload);
@@ -642,31 +605,18 @@ var NyaAccountClient = class {
642
605
  };
643
606
  }
644
607
  /**
645
-
646
608
  * Express middleware: validate that the token in the request contains the specified scopes.
647
-
648
609
  *
649
-
650
610
  * Must be used after the `authenticate()` middleware.
651
-
652
611
  *
653
-
654
612
  * @example
655
-
656
613
  * ```typescript
657
-
658
614
  * app.get('/api/profile',
659
-
660
615
  * client.authenticate(),
661
-
662
616
  * client.requireScopes('profile'),
663
-
664
617
  * (req, res) => { ... }
665
-
666
618
  * )
667
-
668
619
  * ```
669
-
670
620
  */
671
621
  requireScopes(...scopes) {
672
622
  return (req, res, next) => {