@naylence/runtime 0.3.5-test.911 → 0.3.5-test.914

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.
Files changed (31) hide show
  1. package/dist/browser/index.cjs +78 -166
  2. package/dist/browser/index.mjs +78 -166
  3. package/dist/cjs/naylence/fame/config/extended-fame-config.js +58 -2
  4. package/dist/cjs/naylence/fame/http/jwks-api-router.js +16 -18
  5. package/dist/cjs/naylence/fame/http/oauth2-server.js +28 -31
  6. package/dist/cjs/naylence/fame/http/oauth2-token-router.js +153 -8
  7. package/dist/cjs/naylence/fame/http/openid-configuration-router.js +30 -32
  8. package/dist/cjs/naylence/fame/node/admission/admission-profile-factory.js +18 -0
  9. package/dist/cjs/naylence/fame/security/crypto/providers/default-crypto-provider.js +0 -162
  10. package/dist/cjs/version.js +2 -2
  11. package/dist/esm/naylence/fame/config/extended-fame-config.js +58 -2
  12. package/dist/esm/naylence/fame/http/jwks-api-router.js +16 -17
  13. package/dist/esm/naylence/fame/http/oauth2-server.js +28 -31
  14. package/dist/esm/naylence/fame/http/oauth2-token-router.js +153 -8
  15. package/dist/esm/naylence/fame/http/openid-configuration-router.js +30 -31
  16. package/dist/esm/naylence/fame/node/admission/admission-profile-factory.js +18 -0
  17. package/dist/esm/naylence/fame/security/crypto/providers/default-crypto-provider.js +0 -162
  18. package/dist/esm/version.js +2 -2
  19. package/dist/node/index.cjs +78 -166
  20. package/dist/node/index.mjs +78 -166
  21. package/dist/node/node.cjs +305 -251
  22. package/dist/node/node.mjs +305 -251
  23. package/dist/types/naylence/fame/http/jwks-api-router.d.ts +8 -8
  24. package/dist/types/naylence/fame/http/oauth2-server.d.ts +3 -3
  25. package/dist/types/naylence/fame/http/oauth2-token-router.d.ts +5 -5
  26. package/dist/types/naylence/fame/http/openid-configuration-router.d.ts +8 -8
  27. package/dist/types/naylence/fame/security/crypto/providers/default-crypto-provider.d.ts +0 -1
  28. package/dist/types/version.d.ts +1 -1
  29. package/package.json +4 -6
  30. package/dist/esm/naylence/fame/fastapi/oauth2-server.js +0 -205
  31. package/dist/types/naylence/fame/fastapi/oauth2-server.d.ts +0 -22
@@ -34,9 +34,6 @@ var __importStar = (this && this.__importStar) || (function () {
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.DefaultCryptoProvider = void 0;
37
- const asn1_schema_1 = require("@peculiar/asn1-schema");
38
- const asn1_x509_1 = require("@peculiar/asn1-x509");
39
- const asn1_csr_1 = require("@peculiar/asn1-csr");
40
37
  const core_1 = require("@naylence/core");
41
38
  const logging_js_1 = require("../../../util/logging.js");
42
39
  const util_js_1 = require("../../../util/util.js");
@@ -52,11 +49,6 @@ const DEFAULT_AUDIENCE = 'router-dev';
52
49
  const DEFAULT_TTL_SEC = 3600;
53
50
  const DEFAULT_HMAC_SECRET_BYTES = 32;
54
51
  const ENCRYPTION_ALG = 'ECDH-ES';
55
- const EXTENSION_REQUEST_OID = '1.2.840.113549.1.9.14';
56
- const COMMON_NAME_OID = '2.5.4.3';
57
- const ED25519_OID = '1.3.101.112';
58
- const CSR_PEM_TAG = 'CERTIFICATE REQUEST';
59
- const LOGICAL_URI_PREFIX = 'naylence://';
60
52
  function normalizeDefaultCryptoProviderOptions(options) {
61
53
  if (!options) {
62
54
  return {};
@@ -322,76 +314,6 @@ class DefaultCryptoProvider {
322
314
  has_chain: Boolean(certificateChainPem),
323
315
  });
324
316
  }
325
- async createCsr(nodeId, physicalPath, logicals, subjectName) {
326
- const trimmedNodeId = assertNonEmptyString(nodeId, 'nodeId');
327
- const trimmedPhysicalPath = assertNonEmptyString(physicalPath, 'physicalPath');
328
- try {
329
- if (this.artifacts.signing.algorithm !== 'EdDSA') {
330
- throw new Error('CSR creation only supported for Ed25519 signing keys in the default crypto provider');
331
- }
332
- const cryptoImpl = await ensureWebCrypto();
333
- const privateKey = await cryptoImpl.subtle.importKey('pkcs8', pemToArrayBuffer(this.signingPrivatePem), {
334
- name: 'Ed25519',
335
- }, false, ['sign']);
336
- const publicKeyDer = pemToArrayBuffer(this.signingPublicPem);
337
- const subjectPkInfo = asn1_schema_1.AsnConvert.parse(publicKeyDer, asn1_x509_1.SubjectPublicKeyInfo);
338
- const sanitizedLogicals = Array.isArray(logicals)
339
- ? logicals.filter((value) => typeof value === 'string' && value.trim().length > 0)
340
- : [];
341
- const commonName = typeof subjectName === 'string' && subjectName.trim().length > 0
342
- ? subjectName.trim()
343
- : trimmedNodeId;
344
- const subject = buildSubjectName(commonName);
345
- const attributes = new asn1_csr_1.Attributes();
346
- if (sanitizedLogicals.length > 0) {
347
- const san = new asn1_x509_1.SubjectAlternativeName(sanitizedLogicals.map((logical) => new asn1_x509_1.GeneralName({
348
- uniformResourceIdentifier: `${LOGICAL_URI_PREFIX}${logical}`,
349
- })));
350
- const extensions = new asn1_x509_1.Extensions([
351
- new asn1_x509_1.Extension({
352
- extnID: asn1_x509_1.id_ce_subjectAltName,
353
- critical: false,
354
- extnValue: new asn1_schema_1.OctetString(asn1_schema_1.AsnConvert.serialize(san)),
355
- }),
356
- ]);
357
- attributes.push(new asn1_x509_1.Attribute({
358
- type: EXTENSION_REQUEST_OID,
359
- values: [asn1_schema_1.AsnConvert.serialize(extensions)],
360
- }));
361
- }
362
- const requestInfo = new asn1_csr_1.CertificationRequestInfo({
363
- subject,
364
- subjectPKInfo: subjectPkInfo,
365
- attributes,
366
- });
367
- const requestInfoDer = asn1_schema_1.AsnConvert.serialize(requestInfo);
368
- const signature = await cryptoImpl.subtle.sign('Ed25519', privateKey, requestInfoDer);
369
- const certificationRequest = new asn1_csr_1.CertificationRequest({
370
- certificationRequestInfo: requestInfo,
371
- signatureAlgorithm: new asn1_x509_1.AlgorithmIdentifier({
372
- algorithm: ED25519_OID,
373
- }),
374
- signature: encodeBitString(signature),
375
- });
376
- certificationRequest.certificationRequestInfoRaw = requestInfoDer;
377
- const csrDer = asn1_schema_1.AsnConvert.serialize(certificationRequest);
378
- const csrPem = arrayBufferToPem(csrDer, CSR_PEM_TAG);
379
- logger.debug('csr_created', {
380
- node_id: trimmedNodeId,
381
- physical_path: trimmedPhysicalPath,
382
- logical_count: sanitizedLogicals.length,
383
- });
384
- return csrPem;
385
- }
386
- catch (error) {
387
- logger.error('csr_creation_failed', {
388
- node_id: trimmedNodeId,
389
- physical_path: trimmedPhysicalPath,
390
- error: error instanceof Error ? error.message : String(error),
391
- });
392
- throw error;
393
- }
394
- }
395
317
  }
396
318
  exports.DefaultCryptoProvider = DefaultCryptoProvider;
397
319
  async function buildProviderArtifacts(options) {
@@ -628,90 +550,6 @@ function pemToDerBase64(pem) {
628
550
  // Ensure the output is valid base64 without whitespace
629
551
  return base64.replace(/\s+/g, '');
630
552
  }
631
- let cryptoPromise = null;
632
- async function ensureWebCrypto() {
633
- if (typeof globalThis.crypto !== 'undefined' && globalThis.crypto?.subtle) {
634
- return globalThis.crypto;
635
- }
636
- if (!cryptoPromise) {
637
- if (typeof process !== 'undefined' &&
638
- typeof process.versions?.node === 'string') {
639
- cryptoPromise = Promise.resolve().then(() => __importStar(require('node:crypto'))).then((module) => {
640
- const webcrypto = module.webcrypto;
641
- if (!webcrypto || !webcrypto.subtle) {
642
- throw new Error('WebCrypto API is not available in this Node.js runtime');
643
- }
644
- globalThis.crypto = webcrypto;
645
- return webcrypto;
646
- });
647
- }
648
- else {
649
- cryptoPromise = Promise.reject(new Error('WebCrypto API is not available in this environment'));
650
- }
651
- }
652
- return cryptoPromise;
653
- }
654
- function pemToArrayBuffer(pem) {
655
- const normalized = pem
656
- .replace(/-----BEGIN[^-]+-----/g, '')
657
- .replace(/-----END[^-]+-----/g, '')
658
- .replace(/\s+/g, '');
659
- const bytes = base64ToBytes(normalized);
660
- return bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
661
- }
662
- function base64ToBytes(base64) {
663
- if (typeof Buffer !== 'undefined') {
664
- const buffer = Buffer.from(base64, 'base64');
665
- const bytes = new Uint8Array(buffer.length);
666
- for (let i = 0; i < buffer.length; i += 1) {
667
- bytes[i] = buffer[i];
668
- }
669
- return bytes;
670
- }
671
- if (typeof atob === 'function') {
672
- const binary = atob(base64);
673
- const bytes = new Uint8Array(binary.length);
674
- for (let i = 0; i < binary.length; i += 1) {
675
- bytes[i] = binary.charCodeAt(i);
676
- }
677
- return bytes;
678
- }
679
- throw new Error('No base64 decoder available in this environment');
680
- }
681
- function arrayBufferToPem(buffer, tag) {
682
- const base64 = bytesToBase64(new Uint8Array(buffer));
683
- return `-----BEGIN ${tag}-----\n${formatPem(base64)}\n-----END ${tag}-----\n`;
684
- }
685
- function formatPem(base64) {
686
- const lines = [];
687
- for (let i = 0; i < base64.length; i += 64) {
688
- lines.push(base64.slice(i, i + 64));
689
- }
690
- return lines.join('\n');
691
- }
692
- function encodeBitString(signature) {
693
- const bytes = new Uint8Array(signature);
694
- const bitString = new Uint8Array(bytes.length + 1);
695
- bitString.set(bytes, 1);
696
- return bitString.buffer;
697
- }
698
- function buildSubjectName(commonName) {
699
- const attribute = new asn1_x509_1.AttributeTypeAndValue({
700
- type: COMMON_NAME_OID,
701
- value: new asn1_x509_1.AttributeValue({ utf8String: commonName }),
702
- });
703
- return new asn1_x509_1.Name([new asn1_x509_1.RelativeDistinguishedName([attribute])]);
704
- }
705
- function assertNonEmptyString(value, name) {
706
- if (typeof value !== 'string') {
707
- throw new TypeError(`${name} must be a string`);
708
- }
709
- const trimmed = value.trim();
710
- if (trimmed.length === 0) {
711
- throw new TypeError(`${name} must be a non-empty string`);
712
- }
713
- return trimmed;
714
- }
715
553
  function cloneJson(value) {
716
554
  return JSON.parse(JSON.stringify(value));
717
555
  }
@@ -1,10 +1,10 @@
1
1
  "use strict";
2
2
  // This file is auto-generated during build - do not edit manually
3
- // Generated from package.json version: 0.3.5-test.911
3
+ // Generated from package.json version: 0.3.5-test.914
4
4
  Object.defineProperty(exports, "__esModule", { value: true });
5
5
  exports.VERSION = void 0;
6
6
  /**
7
7
  * The package version, injected at build time.
8
8
  * @internal
9
9
  */
10
- exports.VERSION = '0.3.5-test.911';
10
+ exports.VERSION = '0.3.5-test.914';
@@ -14,6 +14,61 @@ const CONFIG_SEARCH_PATHS = [
14
14
  ];
15
15
  const fsModuleSpecifier = String.fromCharCode(102) + String.fromCharCode(115);
16
16
  let cachedFsModule = null;
17
+ // Capture this module's URL without triggering TypeScript's import.meta restriction on CJS builds
18
+ const currentModuleUrl = (() => {
19
+ try {
20
+ return (0, eval)('import.meta.url');
21
+ }
22
+ catch {
23
+ return undefined;
24
+ }
25
+ })();
26
+ let cachedNodeRequire = typeof require === 'function' ? require : null;
27
+ function fileUrlToPath(url) {
28
+ try {
29
+ const parsed = new URL(url);
30
+ if (parsed.protocol !== 'file:') {
31
+ return null;
32
+ }
33
+ let pathname = parsed.pathname;
34
+ if (typeof process !== 'undefined' &&
35
+ process.platform === 'win32' &&
36
+ pathname.startsWith('/')) {
37
+ pathname = pathname.slice(1);
38
+ }
39
+ return decodeURIComponent(pathname);
40
+ }
41
+ catch {
42
+ return null;
43
+ }
44
+ }
45
+ function getNodeRequire() {
46
+ if (cachedNodeRequire) {
47
+ return cachedNodeRequire;
48
+ }
49
+ if (!isNode) {
50
+ return null;
51
+ }
52
+ const processBinding = process.binding;
53
+ if (typeof processBinding !== 'function') {
54
+ return null;
55
+ }
56
+ try {
57
+ const moduleWrap = processBinding('module_wrap');
58
+ if (typeof moduleWrap?.createRequire !== 'function') {
59
+ return null;
60
+ }
61
+ const modulePathFromUrl = currentModuleUrl
62
+ ? fileUrlToPath(currentModuleUrl)
63
+ : null;
64
+ const requireSource = modulePathFromUrl ?? `${process.cwd()}/.naylence-require-shim.js`;
65
+ cachedNodeRequire = moduleWrap.createRequire(requireSource);
66
+ return cachedNodeRequire;
67
+ }
68
+ catch {
69
+ return null;
70
+ }
71
+ }
17
72
  function getFsModule() {
18
73
  if (cachedFsModule) {
19
74
  return cachedFsModule;
@@ -21,9 +76,10 @@ function getFsModule() {
21
76
  if (!isNode) {
22
77
  throw new Error('File system access is not available in this environment');
23
78
  }
24
- if (typeof require === 'function') {
79
+ const nodeRequire = typeof require === 'function' ? require : getNodeRequire();
80
+ if (nodeRequire) {
25
81
  try {
26
- cachedFsModule = require(fsModuleSpecifier);
82
+ cachedFsModule = nodeRequire(fsModuleSpecifier);
27
83
  return cachedFsModule;
28
84
  }
29
85
  catch (error) {
@@ -1,10 +1,9 @@
1
1
  /**
2
- * JWKS (JSON Web Key Set) API router for Express
2
+ * JWKS (JSON Web Key Set) API plugin for Fastify
3
3
  *
4
4
  * Provides /.well-known/jwks.json endpoint for public key discovery
5
5
  * Used by OAuth2/JWT token verification
6
6
  */
7
- import express from 'express';
8
7
  import { getLogger } from '../util/logging.js';
9
8
  const logger = getLogger('naylence.fame.http.jwks_api_router');
10
9
  const DEFAULT_PREFIX = '';
@@ -84,23 +83,22 @@ function filterKeysByType(jwksData, allowedTypes) {
84
83
  return { ...jwksData, keys: filteredKeys };
85
84
  }
86
85
  /**
87
- * Create an Express router that exposes JWKS at /.well-known/jwks.json
86
+ * Create a Fastify plugin that exposes JWKS at /.well-known/jwks.json
88
87
  *
89
88
  * @param options - Router configuration options
90
- * @returns Express router with JWKS endpoint
89
+ * @returns Fastify plugin with JWKS endpoint
91
90
  *
92
91
  * @example
93
92
  * ```typescript
94
- * import express from 'express';
93
+ * import Fastify from 'fastify';
95
94
  * import { createJwksRouter } from '@naylence/runtime';
96
95
  *
97
- * const app = express();
96
+ * const app = Fastify();
98
97
  * const cryptoProvider = new MyCryptoProvider();
99
- * app.use(createJwksRouter({ cryptoProvider }));
98
+ * app.register(createJwksRouter({ cryptoProvider }));
100
99
  * ```
101
100
  */
102
101
  export function createJwksRouter(options = {}) {
103
- const router = express.Router();
104
102
  const { getJwksJson, cryptoProvider, prefix = DEFAULT_PREFIX, keyTypes, } = normalizeCreateJwksRouterOptions(options);
105
103
  // Get JWKS data
106
104
  let jwks;
@@ -123,14 +121,15 @@ export function createJwksRouter(options = {}) {
123
121
  key_types: allowedKeyTypes,
124
122
  total_keys: jwks.keys.length,
125
123
  });
126
- // JWKS endpoint
127
- router.get(`${prefix}/.well-known/jwks.json`, (_req, res) => {
128
- const filteredJwks = filterKeysByType(jwks, allowedKeyTypes);
129
- logger.debug('jwks_served', {
130
- total_keys: jwks.keys.length,
131
- filtered_keys: filteredJwks.keys.length,
124
+ const plugin = async (instance) => {
125
+ instance.get(`${prefix}/.well-known/jwks.json`, async (_request, reply) => {
126
+ const filteredJwks = filterKeysByType(jwks, allowedKeyTypes);
127
+ logger.debug('jwks_served', {
128
+ total_keys: jwks.keys.length,
129
+ filtered_keys: filteredJwks.keys.length,
130
+ });
131
+ reply.send(filteredJwks);
132
132
  });
133
- res.json(filteredJwks);
134
- });
135
- return router;
133
+ };
134
+ return plugin;
136
135
  }
@@ -21,7 +21,7 @@
21
21
  * FAME_JWT_ISSUER: JWT issuer (default: https://auth.fame.fabric)
22
22
  * FAME_JWT_ALGORITHM: JWT algorithm (default: EdDSA)
23
23
  */
24
- import express from 'express';
24
+ import Fastify from 'fastify';
25
25
  import { createOAuth2TokenRouter } from './oauth2-token-router.js';
26
26
  import { createJwksRouter } from './jwks-api-router.js';
27
27
  import { createOpenIDConfigurationRouter } from './openid-configuration-router.js';
@@ -53,23 +53,18 @@ async function getCryptoProvider() {
53
53
  return DefaultCryptoProvider.create();
54
54
  }
55
55
  /**
56
- * Create and configure the OAuth2 Express application
56
+ * Create and configure the OAuth2 Fastify application
57
57
  */
58
58
  export async function createApp() {
59
- const app = express();
60
- // Middleware
61
- app.use(express.json());
62
- app.use(express.urlencoded({ extended: true }));
59
+ const app = Fastify({ logger: false });
63
60
  // Get crypto provider
64
61
  const cryptoProvider = await getCryptoProvider();
65
62
  // Add routers
66
- app.use(createOAuth2TokenRouter({ cryptoProvider }));
67
- app.use(createJwksRouter({ cryptoProvider }));
68
- app.use(createOpenIDConfigurationRouter());
63
+ app.register(createOAuth2TokenRouter({ cryptoProvider }));
64
+ app.register(createJwksRouter({ cryptoProvider }));
65
+ app.register(createOpenIDConfigurationRouter());
69
66
  // Health check endpoint
70
- app.get('/health', (_req, res) => {
71
- res.json({ status: 'ok' });
72
- });
67
+ app.get('/health', async () => ({ status: 'ok' }));
73
68
  return app;
74
69
  }
75
70
  /**
@@ -97,27 +92,29 @@ async function main() {
97
92
  });
98
93
  const app = await createApp();
99
94
  // Start server
100
- app.listen(port, host, () => {
101
- logger.info('oauth2_server_started', {
102
- host,
103
- port,
104
- endpoints: {
105
- token: '/oauth/token',
106
- jwks: '/.well-known/jwks.json',
107
- openid_config: '/.well-known/openid-configuration',
108
- health: '/health',
109
- },
110
- });
95
+ await app.listen({ port, host });
96
+ logger.info('oauth2_server_started', {
97
+ host,
98
+ port,
99
+ endpoints: {
100
+ token: '/oauth/token',
101
+ jwks: '/.well-known/jwks.json',
102
+ openid_config: '/.well-known/openid-configuration',
103
+ health: '/health',
104
+ },
111
105
  });
106
+ const shutdown = (signal) => {
107
+ logger.info('oauth2_server_shutting_down', { signal });
108
+ app
109
+ .close()
110
+ .catch((error) => logger.error('oauth2_server_shutdown_error', {
111
+ error: error instanceof Error ? error.message : String(error),
112
+ }))
113
+ .finally(() => process.exit(0));
114
+ };
112
115
  // Graceful shutdown
113
- process.on('SIGINT', () => {
114
- logger.info('oauth2_server_shutting_down', { signal: 'SIGINT' });
115
- process.exit(0);
116
- });
117
- process.on('SIGTERM', () => {
118
- logger.info('oauth2_server_shutting_down', { signal: 'SIGTERM' });
119
- process.exit(0);
120
- });
116
+ process.on('SIGINT', () => shutdown('SIGINT'));
117
+ process.on('SIGTERM', () => shutdown('SIGTERM'));
121
118
  }
122
119
  // Export main for CLI usage
123
120
  export { main };
@@ -1,15 +1,160 @@
1
1
  /**
2
- * OAuth2 client credentials and authorization code (PKCE) grant router for Express
2
+ * OAuth2 client credentials and authorization code (PKCE) grant router for Fastify
3
3
  *
4
4
  * Provides /oauth/token and /oauth/authorize endpoints for local development and testing.
5
5
  * Implements OAuth2 client credentials grant with JWT token issuance and
6
6
  * OAuth2 authorization code grant with PKCE verification.
7
7
  */
8
- import express from 'express';
8
+ import formbody from '@fastify/formbody';
9
9
  import { createHash, randomBytes, timingSafeEqual } from 'node:crypto';
10
10
  import { JWTTokenIssuer } from '../security/auth/jwt-token-issuer.js';
11
11
  import { getLogger } from '../util/logging.js';
12
12
  const logger = getLogger('naylence.fame.http.oauth2_token_router');
13
+ class RouterCompat {
14
+ constructor() {
15
+ this.routes = [];
16
+ }
17
+ get(path, handler) {
18
+ this.routes.push({ method: 'GET', path, handler });
19
+ }
20
+ post(path, handler) {
21
+ this.routes.push({ method: 'POST', path, handler });
22
+ }
23
+ toPlugin() {
24
+ return async (fastify) => {
25
+ await fastify.register(formbody);
26
+ for (const route of this.routes) {
27
+ fastify.route({
28
+ method: route.method,
29
+ url: route.path,
30
+ handler: async (request, reply) => {
31
+ const compatRequest = toCompatRequest(request);
32
+ const compatResponse = new FastifyResponseAdapter(reply);
33
+ await route.handler(compatRequest, compatResponse);
34
+ },
35
+ });
36
+ }
37
+ };
38
+ }
39
+ }
40
+ class FastifyResponseAdapter {
41
+ constructor(reply) {
42
+ this.reply = reply;
43
+ }
44
+ status(code) {
45
+ this.reply.status(code);
46
+ return this;
47
+ }
48
+ set(field, value) {
49
+ if (field.toLowerCase() === 'set-cookie') {
50
+ this.appendHeader(field, value);
51
+ }
52
+ else {
53
+ this.reply.header(field, value);
54
+ }
55
+ return this;
56
+ }
57
+ type(contentType) {
58
+ const normalized = contentType === 'html'
59
+ ? 'text/html'
60
+ : contentType === 'json'
61
+ ? 'application/json'
62
+ : contentType;
63
+ this.reply.type(normalized);
64
+ return this;
65
+ }
66
+ json(payload) {
67
+ this.reply.send(payload);
68
+ }
69
+ send(payload) {
70
+ this.reply.send(payload);
71
+ }
72
+ redirect(statusOrUrl, maybeUrl) {
73
+ if (typeof statusOrUrl === 'number') {
74
+ if (maybeUrl === undefined) {
75
+ throw new Error('redirect url is required when status code is provided');
76
+ }
77
+ this.reply.status(statusOrUrl);
78
+ this.reply.header('Location', maybeUrl);
79
+ this.reply.send();
80
+ }
81
+ else {
82
+ this.reply.redirect(statusOrUrl);
83
+ }
84
+ }
85
+ cookie(name, value, options) {
86
+ const serialized = serializeCookie(name, value, options);
87
+ this.appendHeader('Set-Cookie', serialized);
88
+ }
89
+ appendHeader(name, value) {
90
+ const existing = this.reply.getHeader(name);
91
+ if (Array.isArray(existing)) {
92
+ this.reply.header(name, [...existing, value]);
93
+ }
94
+ else if (typeof existing === 'string') {
95
+ this.reply.header(name, [existing, value]);
96
+ }
97
+ else if (existing === undefined) {
98
+ this.reply.header(name, value);
99
+ }
100
+ else {
101
+ this.reply.header(name, [String(existing), value]);
102
+ }
103
+ }
104
+ }
105
+ function toCompatRequest(request) {
106
+ const headers = {};
107
+ for (const [key, value] of Object.entries(request.headers)) {
108
+ if (typeof value === 'string') {
109
+ headers[key.toLowerCase()] = value;
110
+ }
111
+ else if (Array.isArray(value)) {
112
+ headers[key.toLowerCase()] = value.join(', ');
113
+ }
114
+ else if (value !== undefined && value !== null) {
115
+ headers[key.toLowerCase()] = String(value);
116
+ }
117
+ else {
118
+ headers[key.toLowerCase()] = undefined;
119
+ }
120
+ }
121
+ return {
122
+ body: request.body,
123
+ headers,
124
+ method: request.method,
125
+ originalUrl: request.raw.url ?? request.url,
126
+ query: request.query ?? {},
127
+ };
128
+ }
129
+ function serializeCookie(name, value, options) {
130
+ const segments = [
131
+ `${encodeURIComponent(name)}=${encodeURIComponent(value)}`,
132
+ ];
133
+ if (options.maxAge !== undefined) {
134
+ const maxAgeMs = options.maxAge;
135
+ const maxAgeSeconds = Math.floor(maxAgeMs / 1000);
136
+ segments.push(`Max-Age=${maxAgeSeconds}`);
137
+ const expires = new Date(Date.now() + maxAgeMs).toUTCString();
138
+ segments.push(`Expires=${expires}`);
139
+ }
140
+ segments.push(`Path=${options.path ?? '/'}`);
141
+ if (options.httpOnly) {
142
+ segments.push('HttpOnly');
143
+ }
144
+ if (options.secure) {
145
+ segments.push('Secure');
146
+ }
147
+ if (options.sameSite) {
148
+ const normalized = options.sameSite.toLowerCase();
149
+ const formatted = normalized === 'strict'
150
+ ? 'Strict'
151
+ : normalized === 'none'
152
+ ? 'None'
153
+ : 'Lax';
154
+ segments.push(`SameSite=${formatted}`);
155
+ }
156
+ return segments.join('; ');
157
+ }
13
158
  const DEFAULT_PREFIX = '/oauth';
14
159
  const ENV_VAR_CLIENT_ID = 'FAME_JWT_CLIENT_ID';
15
160
  const ENV_VAR_CLIENT_SECRET = 'FAME_JWT_CLIENT_SECRET';
@@ -431,11 +576,11 @@ function respondInvalidClient(res) {
431
576
  });
432
577
  }
433
578
  /**
434
- * Create an Express router that implements OAuth2 token and authorization endpoints
579
+ * Create a Fastify plugin that implements OAuth2 token and authorization endpoints
435
580
  * with support for client credentials and authorization code (PKCE) grants.
436
581
  *
437
582
  * @param options - Router configuration options
438
- * @returns Express router with OAuth2 token and authorization endpoints
583
+ * @returns Fastify plugin with OAuth2 token and authorization endpoints
439
584
  *
440
585
  * Environment Variables:
441
586
  * FAME_JWT_CLIENT_ID: OAuth2 client identifier
@@ -449,7 +594,7 @@ function respondInvalidClient(res) {
449
594
  * FAME_OAUTH_CODE_TTL_SEC: Authorization code TTL in seconds (optional, default: 300)
450
595
  */
451
596
  export function createOAuth2TokenRouter(options) {
452
- const router = express.Router();
597
+ const router = new RouterCompat();
453
598
  const { cryptoProvider, prefix = DEFAULT_PREFIX, issuer, audience, tokenTtlSec, allowedScopes: configAllowedScopes, algorithm: configAlgorithm, enablePkce: configEnablePkce, allowPublicClients: configAllowPublicClients, authorizationCodeTtlSec: configAuthorizationCodeTtlSec, enableDevLogin: configEnableDevLogin, devLoginUsername: configDevLoginUsername, devLoginPassword: configDevLoginPassword, devLoginSessionTtlSec: configDevLoginSessionTtlSec, devLoginCookieName: configDevLoginCookieName, devLoginSecureCookie: configDevLoginSecureCookie, devLoginTitle: configDevLoginTitle, } = normalizeCreateOAuth2TokenRouterOptions(options);
454
599
  if (!cryptoProvider) {
455
600
  throw new Error('cryptoProvider is required to create OAuth2 token router');
@@ -748,7 +893,7 @@ export function createOAuth2TokenRouter(options) {
748
893
  };
749
894
  router.post(`${prefix}/logout`, logoutHandler);
750
895
  router.get(`${prefix}/logout`, logoutHandler);
751
- router.post(`${prefix}/token`, async (req, res, next) => {
896
+ router.post(`${prefix}/token`, async (req, res) => {
752
897
  try {
753
898
  cleanupAuthorizationCodes(authorizationCodes, Date.now());
754
899
  const { grant_type, client_id, client_secret, scope, audience: reqAudience, code, redirect_uri, code_verifier, } = req.body ?? {};
@@ -933,7 +1078,7 @@ export function createOAuth2TokenRouter(options) {
933
1078
  }
934
1079
  catch (error) {
935
1080
  logger.error('oauth2_token_error', { error: error.message });
936
- next(error);
1081
+ throw error;
937
1082
  }
938
1083
  });
939
1084
  async function issueTokenResponse(params) {
@@ -966,5 +1111,5 @@ export function createOAuth2TokenRouter(options) {
966
1111
  }
967
1112
  return response;
968
1113
  }
969
- return router;
1114
+ return router.toPlugin();
970
1115
  }