@marxbiotech/signet-integration 0.1.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.
Files changed (66) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +111 -0
  3. package/dist/bearer-challenge.d.ts +27 -0
  4. package/dist/bearer-challenge.filter.d.ts +13 -0
  5. package/dist/bearer-challenge.filter.js +76 -0
  6. package/dist/bearer-challenge.filter.js.map +1 -0
  7. package/dist/bearer-challenge.js +120 -0
  8. package/dist/bearer-challenge.js.map +1 -0
  9. package/dist/config.d.ts +23 -0
  10. package/dist/config.js +56 -0
  11. package/dist/config.js.map +1 -0
  12. package/dist/deployment-profile.d.ts +25 -0
  13. package/dist/deployment-profile.js +103 -0
  14. package/dist/deployment-profile.js.map +1 -0
  15. package/dist/guard.d.ts +18 -0
  16. package/dist/guard.js +210 -0
  17. package/dist/guard.js.map +1 -0
  18. package/dist/index.d.ts +15 -0
  19. package/dist/index.js +50 -0
  20. package/dist/index.js.map +1 -0
  21. package/dist/jwt-verifier.d.ts +36 -0
  22. package/dist/jwt-verifier.js +167 -0
  23. package/dist/jwt-verifier.js.map +1 -0
  24. package/dist/log-sanitise.d.ts +3 -0
  25. package/dist/log-sanitise.js +23 -0
  26. package/dist/log-sanitise.js.map +1 -0
  27. package/dist/module.d.ts +12 -0
  28. package/dist/module.js +61 -0
  29. package/dist/module.js.map +1 -0
  30. package/dist/options.d.ts +31 -0
  31. package/dist/options.js +9 -0
  32. package/dist/options.js.map +1 -0
  33. package/dist/principal-resolver.d.ts +19 -0
  34. package/dist/principal-resolver.js +25 -0
  35. package/dist/principal-resolver.js.map +1 -0
  36. package/dist/principal.decorator.d.ts +2 -0
  37. package/dist/principal.decorator.js +28 -0
  38. package/dist/principal.decorator.js.map +1 -0
  39. package/dist/protected-resource.controller.d.ts +11 -0
  40. package/dist/protected-resource.controller.js +130 -0
  41. package/dist/protected-resource.controller.js.map +1 -0
  42. package/dist/public.decorator.d.ts +2 -0
  43. package/dist/public.decorator.js +11 -0
  44. package/dist/public.decorator.js.map +1 -0
  45. package/dist/scope-vocabulary.d.ts +17 -0
  46. package/dist/scope-vocabulary.js +52 -0
  47. package/dist/scope-vocabulary.js.map +1 -0
  48. package/dist/strategy.d.ts +16 -0
  49. package/dist/strategy.js +89 -0
  50. package/dist/strategy.js.map +1 -0
  51. package/dist/testing/fixture.d.ts +8 -0
  52. package/dist/testing/fixture.js +38 -0
  53. package/dist/testing/fixture.js.map +1 -0
  54. package/dist/testing/index.d.ts +3 -0
  55. package/dist/testing/index.js +11 -0
  56. package/dist/testing/index.js.map +1 -0
  57. package/dist/testing/jwks-server.d.ts +10 -0
  58. package/dist/testing/jwks-server.js +33 -0
  59. package/dist/testing/jwks-server.js.map +1 -0
  60. package/dist/testing/signet-token.d.ts +18 -0
  61. package/dist/testing/signet-token.js +31 -0
  62. package/dist/testing/signet-token.js.map +1 -0
  63. package/dist/validate-options.d.ts +8 -0
  64. package/dist/validate-options.js +88 -0
  65. package/dist/validate-options.js.map +1 -0
  66. package/package.json +93 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Marx Biotech
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,111 @@
1
+ # @marxbiotech/signet-integration
2
+
3
+ Signet (OAuth 2.1 / OIDC, Ory Hydra) **resource-server** integration for
4
+ NestJS: Bearer access-token verification against the issuer's JWKS, RFC 6750
5
+ `WWW-Authenticate` challenges, RFC 9728 protected-resource metadata, the
6
+ deployment-profile pair check, and token-scope narrowing. Signet is treated as
7
+ an **auth provider**: this package proves `(iss, sub)` and hands the claims to
8
+ your `SignetPrincipalResolver`; who that is locally, and how the mapping is
9
+ stored, is yours.
10
+
11
+ ## Wiring
12
+
13
+ ```ts
14
+ // your-signet.options.ts -- your values, one object
15
+ export const MY_OPTIONS = {
16
+ admissionScope: 'myservice:access',
17
+ canonicalResourceFor: (host) => `https://${host}/api/mcp`, // or `https://${host}` for a bare origin
18
+ deployedProfiles: {
19
+ production: { host: 'my.example', namespace: 'my-production' },
20
+ staging: { host: 'my.staging.example', namespace: 'my-staging' },
21
+ },
22
+ developmentProfile: { canonicalResource: 'http://localhost/api/mcp', host: 'localhost', namespace: 'development' },
23
+ env: {
24
+ deploymentNamespace: 'MY_DEPLOYMENT_NAMESPACE',
25
+ jwtAudience: 'MY_JWT_AUDIENCE',
26
+ jwtIssuer: 'MY_JWT_ISSUER',
27
+ jwtJwksUri: 'MY_JWT_JWKS_URI',
28
+ jwtClockToleranceS: 'MY_JWT_CLOCK_TOLERANCE_S',
29
+ signetEnabled: 'SIGNET_AUTH_ENABLED', // omit = the Bearer channel is always on
30
+ legacyApiKeyEnabled: 'LEGACY_API_KEY_ENABLED', // omit = no legacy channel
31
+ },
32
+ realm: 'myservice',
33
+ requestPrincipalKey: 'user', // where the guard attaches your principal
34
+ scopesSupported: ['myservice:access'], // RFC 9728 scopes_supported; must include the admission scope
35
+ } as const satisfies SignetIntegrationOptions<'production' | 'staging'>;
36
+
37
+ // your resolver -- Passport's verify callback, as a Nest provider.
38
+ // Annotate the return type: a class method gets no contextual typing from
39
+ // `implements`, so without it `ok: true` widens to `boolean` and fails to compile.
40
+ @Injectable()
41
+ export class MyResolver implements SignetPrincipalResolver<MyPrincipal> {
42
+ constructor(private readonly users: UsersRepository) {}
43
+ async resolve(identity: VerifiedSignetIdentity): Promise<PrincipalResolution<MyPrincipal>> {
44
+ const user = await this.users.findBySignetSubject(identity.subject); // or identity.claims.erp_user_id
45
+ if (!user) return { ok: false, reason: 'unknown_subject' }; // → 403, reason only logged
46
+ return { ok: true, principal: user, logFields: { userId: user.id } }; // logFields render on the decision line
47
+ // throw new PrincipalStoreUnavailableError(cause) when the store is unreachable → 503
48
+ }
49
+ }
50
+
51
+ // your auth module
52
+ @Module({
53
+ imports: [
54
+ UsersModule,
55
+ SignetIntegrationModule.forRoot({
56
+ options: MY_OPTIONS,
57
+ resolver: MyResolver, // a class, or { useFactory, inject } / { useValue } / { useExisting }
58
+ imports: [UsersModule], // whatever the resolver's constructor needs
59
+ }),
60
+ ],
61
+ exports: [SignetIntegrationModule], // so the process module below can inject the package's providers
62
+ })
63
+ export class AuthModule {}
64
+
65
+ // the process module that serves the resource
66
+ @Module({
67
+ imports: [AuthModule],
68
+ controllers: [createProtectedResourceController(MY_OPTIONS), ...yourControllers],
69
+ providers: [
70
+ { provide: APP_GUARD, useClass: SignetBearerGuard }, // honours @Public(); or call it from your own credential dispatcher
71
+ { provide: APP_FILTER, useClass: BearerChallengeFilter }, // or call challengeFor() from your own filter
72
+ ],
73
+ })
74
+ export class ApiModule {}
75
+ ```
76
+
77
+ Requirements: `ConfigModule` must be global (the strategy, filter and profile
78
+ service inject `ConfigService`); validate the env variables named in `options.env`
79
+ in your own config validation (Joi or otherwise), the package only re-checks the
80
+ clock tolerance and throws on a missing issuer or JWKS URI.
81
+
82
+ Handlers get a typed principal through
83
+ `createSignetPrincipalDecorator(MY_OPTIONS, isMyPrincipal)`; Nest does not check
84
+ the parameter's declared type, so the predicate is the enforcement.
85
+
86
+ `forRoot` validates the options at construction (admission scope present in
87
+ `scopesSupported`, no quote-breaking characters in `realm`/`admissionScope`,
88
+ unique namespaces and hosts, one resource path across every profile, no
89
+ reserved `requestPrincipalKey`) and `createScopeVocabulary` validates its
90
+ encoder (one-to-one, disjoint from the admission scope, defaults inside the
91
+ vocabulary).
92
+
93
+ Testing: `@marxbiotech/signet-integration/testing` exports `startTestIssuer()`
94
+ (a local JWKS endpoint plus a signer that mints tokens in the shape Signet
95
+ issues), `startJwksServer()`, `FIXTURE_OPTIONS` and `FIXTURE_DEVELOPMENT_PROFILE`.
96
+
97
+ ## What is deliberately NOT here
98
+
99
+ - How principals are stored (grant tables, a `users` column, a claim): the resolver's business.
100
+ - Consumer credential dispatch between Bearer and a legacy channel.
101
+ - Config validation of the env variables (see above).
102
+ - The login (OIDC authorization-code) flow. This package is the resource-server side only.
103
+
104
+ ## Rules a consumer relies on
105
+
106
+ - A canonical resource WITH a path (an MCP endpoint URI): only that path's 401 carries `resource_metadata`. A BARE-ORIGIN resource: every 401 does.
107
+ - The admission scope is checked by the guard (403 with `insufficient_scope`); a 403 carrying `requiredScopes` gets `insufficient_scope`; a resolver refusal is a bare 403.
108
+ - Verifier failures (bad signature, wrong audience, unreachable JWKS) are one uniform 401; the reason is only logged.
109
+ - `iat` is not required and no `maxTokenAge` is applied; `client_id` is required (RFC 9068 §2.2, Signet issues it always).
110
+ - JWKS options are fixed: 3 s timeout, 30 s cooldown, 10 min cache.
111
+ - The guard's decision log line is `metric reason <your logFields> clientId environment`; the fixed keys are reserved.
@@ -0,0 +1,27 @@
1
+ import { ForbiddenException, UnauthorizedException } from '@nestjs/common';
2
+ import { type ChannelFlags } from './config';
3
+ import { type DeploymentProfile } from './deployment-profile';
4
+ import { type SignetIntegrationOptions } from './options';
5
+ export declare function protectedResourceMetadataUrl(canonicalResource: string): string;
6
+ export declare function protectedResourceMetadataPath(options: SignetIntegrationOptions): string;
7
+ export declare function isResourceRequest(profile: DeploymentProfile, requestPath: string): boolean;
8
+ export declare function bearerChallenge(options: SignetIntegrationOptions, profile: DeploymentProfile, request: {
9
+ resource: boolean;
10
+ }): string;
11
+ export declare function insufficientScopeChallenge(options: SignetIntegrationOptions, scopes: readonly string[]): string;
12
+ export interface ScopeChallengeCarrier {
13
+ readonly requiredScopes: readonly string[];
14
+ }
15
+ export declare function carriesScopeChallenge(exception: ForbiddenException): exception is ForbiddenException & ScopeChallengeCarrier;
16
+ export declare class InsufficientScopeException extends ForbiddenException implements ScopeChallengeCarrier {
17
+ readonly requiredScopes: readonly string[];
18
+ constructor(admissionScope: string);
19
+ }
20
+ export type ChallengeDecision = {
21
+ header: null;
22
+ malformedCarrier: boolean;
23
+ } | {
24
+ header: string;
25
+ malformedCarrier: false;
26
+ };
27
+ export declare function challengeFor(options: SignetIntegrationOptions, profile: DeploymentProfile, channels: ChannelFlags, exception: ForbiddenException | UnauthorizedException, requestPath: string): ChallengeDecision;
@@ -0,0 +1,13 @@
1
+ import { type ArgumentsHost, ForbiddenException, UnauthorizedException } from '@nestjs/common';
2
+ import { ConfigService } from '@nestjs/config';
3
+ import { BaseExceptionFilter, HttpAdapterHost } from '@nestjs/core';
4
+ import { SignetDeploymentProfileService } from './deployment-profile';
5
+ import { type SignetIntegrationOptions } from './options';
6
+ export declare class BearerChallengeFilter extends BaseExceptionFilter {
7
+ private readonly profiles;
8
+ private readonly options;
9
+ private readonly logger;
10
+ private readonly channels;
11
+ constructor(profiles: SignetDeploymentProfileService, adapterHost: HttpAdapterHost, configService: ConfigService, options: SignetIntegrationOptions);
12
+ catch(exception: ForbiddenException | UnauthorizedException, host: ArgumentsHost): void;
13
+ }
@@ -0,0 +1,76 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ var __metadata = (this && this.__metadata) || function (k, v) {
9
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
10
+ };
11
+ var __param = (this && this.__param) || function (paramIndex, decorator) {
12
+ return function (target, key) { decorator(target, key, paramIndex); }
13
+ };
14
+ var BearerChallengeFilter_1;
15
+ Object.defineProperty(exports, "__esModule", { value: true });
16
+ exports.BearerChallengeFilter = void 0;
17
+ const common_1 = require("@nestjs/common");
18
+ const config_1 = require("@nestjs/config");
19
+ const core_1 = require("@nestjs/core");
20
+ const bearer_challenge_1 = require("./bearer-challenge");
21
+ const config_2 = require("./config");
22
+ const deployment_profile_1 = require("./deployment-profile");
23
+ const log_sanitise_1 = require("./log-sanitise");
24
+ const options_1 = require("./options");
25
+ // Attaches `WWW-Authenticate` to the refusals that admit one (RFC 6750 §3,
26
+ // RFC 9728 §5.1). A 401 raised anywhere the Nest router reaches -- a guard,
27
+ // an interceptor, a handler -- carries the same header. Middleware mounted on
28
+ // the http adapter (Swagger's Basic auth, say) never reaches the router and
29
+ // keeps its own challenge.
30
+ //
31
+ // The rendering of the response itself is untouched -- `super.catch` is
32
+ // Nest's own -- so the body stays whatever the exception carries, and only
33
+ // the header is added. A consumer that renders its own bodies keeps its own
34
+ // filter and calls `challengeFor` from it instead of registering this one.
35
+ //
36
+ // With the Signet channel on, a 401 from a route that authenticates with a
37
+ // different credential gets the Bearer challenge too.
38
+ let BearerChallengeFilter = BearerChallengeFilter_1 = class BearerChallengeFilter extends core_1.BaseExceptionFilter {
39
+ profiles;
40
+ options;
41
+ logger = new common_1.Logger(BearerChallengeFilter_1.name);
42
+ channels;
43
+ constructor(profiles, adapterHost, configService, options) {
44
+ super(adapterHost.httpAdapter);
45
+ this.profiles = profiles;
46
+ this.options = options;
47
+ this.channels = (0, config_2.readChannelFlags)(options, configService);
48
+ }
49
+ catch(exception, host) {
50
+ const request = host.switchToHttp().getRequest();
51
+ const decision = (0, bearer_challenge_1.challengeFor)(this.options, this.profiles.profile, this.channels, exception, request.path);
52
+ if (decision.header !== null) {
53
+ host
54
+ .switchToHttp()
55
+ .getResponse()
56
+ .setHeader('WWW-Authenticate', decision.header);
57
+ }
58
+ else if (decision.malformedCarrier) {
59
+ // A 403 that CARRIES `requiredScopes` in a shape that cannot be
60
+ // rendered is a bug in a carrier: still answered bare (a broken header
61
+ // helps no one), but not SILENTLY bare -- the structural check would
62
+ // otherwise turn a bug into a correct-looking response.
63
+ this.logger.warn(`403 carries malformed requiredScopes, answered without challenge: metric=scope_challenge_malformed errorName=${(0, log_sanitise_1.sanitiseLogToken)(exception.name)}`);
64
+ }
65
+ super.catch(exception, host);
66
+ }
67
+ };
68
+ exports.BearerChallengeFilter = BearerChallengeFilter;
69
+ exports.BearerChallengeFilter = BearerChallengeFilter = BearerChallengeFilter_1 = __decorate([
70
+ (0, common_1.Catch)(common_1.ForbiddenException, common_1.UnauthorizedException),
71
+ __param(3, (0, common_1.Inject)(options_1.SIGNET_INTEGRATION_OPTIONS)),
72
+ __metadata("design:paramtypes", [deployment_profile_1.SignetDeploymentProfileService,
73
+ core_1.HttpAdapterHost,
74
+ config_1.ConfigService, Object])
75
+ ], BearerChallengeFilter);
76
+ //# sourceMappingURL=bearer-challenge.filter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bearer-challenge.filter.js","sourceRoot":"","sources":["../src/bearer-challenge.filter.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;AAAA,2CAOwB;AACxB,2CAA+C;AAC/C,uCAAoE;AAGpE,yDAAkD;AAClD,qCAA+D;AAC/D,6DAAsE;AACtE,iDAAkD;AAClD,uCAGmB;AAEnB,2EAA2E;AAC3E,4EAA4E;AAC5E,8EAA8E;AAC9E,4EAA4E;AAC5E,2BAA2B;AAC3B,EAAE;AACF,wEAAwE;AACxE,2EAA2E;AAC3E,4EAA4E;AAC5E,2EAA2E;AAC3E,EAAE;AACF,2EAA2E;AAC3E,sDAAsD;AAE/C,IAAM,qBAAqB,6BAA3B,MAAM,qBAAsB,SAAQ,0BAAmB;IAKzC;IAIA;IARF,MAAM,GAAG,IAAI,eAAM,CAAC,uBAAqB,CAAC,IAAI,CAAC,CAAC;IAChD,QAAQ,CAAe;IAExC,YACmB,QAAwC,EACzD,WAA4B,EAC5B,aAA4B,EAEX,OAAiC;QAElD,KAAK,CAAC,WAAW,CAAC,WAAW,CAAC,CAAC;QANd,aAAQ,GAAR,QAAQ,CAAgC;QAIxC,YAAO,GAAP,OAAO,CAA0B;QAGlD,IAAI,CAAC,QAAQ,GAAG,IAAA,yBAAgB,EAAC,OAAO,EAAE,aAAa,CAAC,CAAC;IAC3D,CAAC;IAEQ,KAAK,CACZ,SAAqD,EACrD,IAAmB;QAEnB,MAAM,OAAO,GAAG,IAAI,CAAC,YAAY,EAAE,CAAC,UAAU,EAAW,CAAC;QAC1D,MAAM,QAAQ,GAAG,IAAA,+BAAY,EAC3B,IAAI,CAAC,OAAO,EACZ,IAAI,CAAC,QAAQ,CAAC,OAAO,EACrB,IAAI,CAAC,QAAQ,EACb,SAAS,EACT,OAAO,CAAC,IAAI,CACb,CAAC;QACF,IAAI,QAAQ,CAAC,MAAM,KAAK,IAAI,EAAE,CAAC;YAC7B,IAAI;iBACD,YAAY,EAAE;iBACd,WAAW,EAAY;iBACvB,SAAS,CAAC,kBAAkB,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC;QACpD,CAAC;aAAM,IAAI,QAAQ,CAAC,gBAAgB,EAAE,CAAC;YACrC,gEAAgE;YAChE,uEAAuE;YACvE,qEAAqE;YACrE,wDAAwD;YACxD,IAAI,CAAC,MAAM,CAAC,IAAI,CACd,gHAAgH,IAAA,+BAAgB,EAAC,SAAS,CAAC,IAAI,CAAC,EAAE,CACnJ,CAAC;QACJ,CAAC;QACD,KAAK,CAAC,KAAK,CAAC,SAAS,EAAE,IAAI,CAAC,CAAC;IAC/B,CAAC;CACF,CAAA;AA3CY,sDAAqB;gCAArB,qBAAqB;IADjC,IAAA,cAAK,EAAC,2BAAkB,EAAE,8BAAqB,CAAC;IAS5C,WAAA,IAAA,eAAM,EAAC,oCAA0B,CAAC,CAAA;qCAHR,mDAA8B;QAC5C,sBAAe;QACb,sBAAa;GAPnB,qBAAqB,CA2CjC"}
@@ -0,0 +1,120 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.InsufficientScopeException = void 0;
4
+ exports.protectedResourceMetadataUrl = protectedResourceMetadataUrl;
5
+ exports.protectedResourceMetadataPath = protectedResourceMetadataPath;
6
+ exports.isResourceRequest = isResourceRequest;
7
+ exports.bearerChallenge = bearerChallenge;
8
+ exports.insufficientScopeChallenge = insufficientScopeChallenge;
9
+ exports.carriesScopeChallenge = carriesScopeChallenge;
10
+ exports.challengeFor = challengeFor;
11
+ const common_1 = require("@nestjs/common");
12
+ // RFC 9728 §3.1: for a resource identifier WITH a path, the well-known
13
+ // segment is inserted between host and path --
14
+ // https://h/api/mcp → https://h/.well-known/oauth-protected-resource/api/mcp
15
+ // -- and for a bare origin it is appended at the root.
16
+ function protectedResourceMetadataUrl(canonicalResource) {
17
+ const url = new URL(canonicalResource);
18
+ if (url.search !== '' || url.hash !== '') {
19
+ throw new Error(`canonical resource must not carry a query or fragment: ${canonicalResource}`);
20
+ }
21
+ const path = url.pathname === '/' ? '' : url.pathname;
22
+ return `${url.origin}/.well-known/oauth-protected-resource${path}`;
23
+ }
24
+ // The metadata path relative to the origin, taken from the same builder the
25
+ // challenge uses and read off the development profile. Assumes every
26
+ // profile's canonical resource carries the same path; the consumer pins that
27
+ // for its own table. The metadata controller mounts its route on it.
28
+ function protectedResourceMetadataPath(options) {
29
+ return new URL(protectedResourceMetadataUrl(options.developmentProfile.canonicalResource)).pathname;
30
+ }
31
+ // The request paths whose 401 may carry `resource_metadata`: the ones that
32
+ // ARE the canonical resource. With a resource that carries a path (an MCP
33
+ // endpoint URI), only that path qualifies -- any other path's 401 must not
34
+ // announce it. With a bare-origin resource the whole origin is the resource,
35
+ // so every request qualifies. Express serves the trailing-slash spelling too,
36
+ // so the two spellings reach the same decision.
37
+ function isResourceRequest(profile, requestPath) {
38
+ const resourcePath = new URL(profile.canonicalResource).pathname;
39
+ if (resourcePath === '/')
40
+ return true;
41
+ const trim = (value) => value.replace(/\/+$/, '');
42
+ return trim(requestPath) === trim(resourcePath);
43
+ }
44
+ // RFC 6750 §3 challenge for a 401. `scope` names the SERVICE scope the token
45
+ // must carry; a client that already holds it and is still refused has a
46
+ // local-authorization problem no re-authorisation solves, which is why the
47
+ // 403 for that case carries no challenge at all (see carriesScopeChallenge).
48
+ function bearerChallenge(options, profile, request) {
49
+ const parts = [`Bearer realm="${options.realm}"`];
50
+ if (request.resource) {
51
+ parts.push(`resource_metadata="${protectedResourceMetadataUrl(profile.canonicalResource)}"`);
52
+ }
53
+ parts.push(`scope="${options.admissionScope}"`);
54
+ return parts.join(', ');
55
+ }
56
+ // RFC 6750 §3.1 `insufficient_scope`: the token is valid but does not carry
57
+ // the scope the request needs. This IS solvable by re-authorising with the
58
+ // right scope, so it says so -- and the 403s that carry it are the only ones
59
+ // that do. `scopes` is what the caller must carry, not what it is missing.
60
+ function insufficientScopeChallenge(options, scopes) {
61
+ return `Bearer realm="${options.realm}", error="insufficient_scope", scope="${scopes.join(' ')}"`;
62
+ }
63
+ // Checked for shape, not presence: a `requiredScopes` that is not a
64
+ // non-empty string array would render an unusable `scope=`. A plain
65
+ // authorization refusal carries no field at all. The filter logs the
66
+ // malformed case; this predicate only decides.
67
+ function carriesScopeChallenge(exception) {
68
+ const { requiredScopes } = exception;
69
+ return (Array.isArray(requiredScopes) &&
70
+ requiredScopes.length > 0 &&
71
+ requiredScopes.every((scope) => typeof scope === 'string'));
72
+ }
73
+ // A valid token without the admission scope. Takes the scope, not the
74
+ // options: the message and `requiredScopes` are both built from it here, so
75
+ // the guard cannot spell one without the other.
76
+ class InsufficientScopeException extends common_1.ForbiddenException {
77
+ requiredScopes;
78
+ constructor(admissionScope) {
79
+ super(`token does not carry the ${admissionScope} scope`);
80
+ this.requiredScopes = [admissionScope];
81
+ }
82
+ }
83
+ exports.InsufficientScopeException = InsufficientScopeException;
84
+ // The whole of the `WWW-Authenticate` decision, as a pure function, so a
85
+ // consumer with an exception filter of its own (a catch-all that renders its
86
+ // own bodies, guards streaming responses, ...) can make the same decision
87
+ // BearerChallengeFilter does and set the header itself.
88
+ //
89
+ // Signet channel off → no header: advertising a closed channel
90
+ // sends clients to log in for nothing
91
+ // 403 carrying requiredScopes → insufficient_scope (re-authorising fixes it)
92
+ // 403 with a malformed field → no header, malformedCarrier: true (a bug
93
+ // in a carrier; the caller should log it)
94
+ // any other 403 → no header: a wider OAuth scope supplies
95
+ // no local authorization
96
+ // 401 → Bearer challenge; resource_metadata only
97
+ // when the request IS the resource
98
+ function challengeFor(options, profile, channels, exception, requestPath) {
99
+ if (!channels.signetEnabled)
100
+ return { header: null, malformedCarrier: false };
101
+ if (exception instanceof common_1.ForbiddenException) {
102
+ if (carriesScopeChallenge(exception)) {
103
+ return {
104
+ header: insufficientScopeChallenge(options, exception.requiredScopes),
105
+ malformedCarrier: false,
106
+ };
107
+ }
108
+ return {
109
+ header: null,
110
+ malformedCarrier: 'requiredScopes' in exception,
111
+ };
112
+ }
113
+ return {
114
+ header: bearerChallenge(options, profile, {
115
+ resource: isResourceRequest(profile, requestPath),
116
+ }),
117
+ malformedCarrier: false,
118
+ };
119
+ }
120
+ //# sourceMappingURL=bearer-challenge.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bearer-challenge.js","sourceRoot":"","sources":["../src/bearer-challenge.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,23 @@
1
+ import { type ConfigService } from '@nestjs/config';
2
+ import { type DeploymentProfile } from './deployment-profile';
3
+ import { type SignetIntegrationOptions } from './options';
4
+ export type SignetEnvOptions = Pick<SignetIntegrationOptions, 'env'>;
5
+ export interface JwtSettings {
6
+ readonly audience: string;
7
+ readonly clockToleranceS: number;
8
+ readonly issuer: string;
9
+ readonly jwksUri: string;
10
+ }
11
+ export interface ChannelFlags {
12
+ readonly legacyApiKeyEnabled: boolean;
13
+ readonly signetEnabled: boolean;
14
+ }
15
+ export type SignetAuthConfig = ChannelFlags & ({
16
+ readonly jwt: JwtSettings;
17
+ readonly signetEnabled: true;
18
+ } | {
19
+ readonly jwt: null;
20
+ readonly signetEnabled: false;
21
+ });
22
+ export declare function readChannelFlags(options: SignetEnvOptions, configService: ConfigService): ChannelFlags;
23
+ export declare function readSignetAuthConfig(options: SignetEnvOptions, configService: ConfigService, profile: DeploymentProfile): SignetAuthConfig;
package/dist/config.js ADDED
@@ -0,0 +1,56 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.readChannelFlags = readChannelFlags;
4
+ exports.readSignetAuthConfig = readSignetAuthConfig;
5
+ // The channel switches alone -- what a credential dispatcher needs. Never
6
+ // touches the verifier settings, so a dispatcher can be built (and tested)
7
+ // without an issuer. A channel with no variable name has no switch: Signet
8
+ // is then always on, and there is no legacy channel.
9
+ function readChannelFlags(options, configService) {
10
+ const { legacyApiKeyEnabled, signetEnabled } = options.env;
11
+ return {
12
+ legacyApiKeyEnabled: legacyApiKeyEnabled !== undefined &&
13
+ configService.get(legacyApiKeyEnabled, 'true') === 'true',
14
+ signetEnabled: signetEnabled === undefined ||
15
+ configService.get(signetEnabled, 'false') === 'true',
16
+ };
17
+ }
18
+ // The clock tolerance, re-checked here rather than trusted. `Number('')` and
19
+ // `Number('none')` differ: the
20
+ // first is 0, the second is NaN, and jose compares `exp` as
21
+ // `exp <= now - clockTolerance`, which is false for every token once the
22
+ // tolerance is NaN. A token would then never expire, silently. Nothing in the
23
+ // type system says a ConfigService was built through validation, and this
24
+ // function takes one from anywhere.
25
+ function readClockTolerance(options, configService) {
26
+ const raw = configService.get(options.env.jwtClockToleranceS, 60);
27
+ const seconds = typeof raw === 'string' && raw.trim() === '' ? Number.NaN : Number(raw);
28
+ if (!Number.isInteger(seconds) || seconds < 0 || seconds > 300) {
29
+ throw new Error(`${options.env.jwtClockToleranceS} must be an integer between 0 and 300`);
30
+ }
31
+ return seconds;
32
+ }
33
+ // Reads the channel flags and the verifier settings. The consumer's config
34
+ // validation is the gate for the issuer and the JWKS URI: this never fills a
35
+ // default for either, so an enabled channel with missing settings must have
36
+ // failed at boot -- and throws here if it did not.
37
+ function readSignetAuthConfig(options, configService, profile) {
38
+ const { legacyApiKeyEnabled, signetEnabled } = readChannelFlags(options, configService);
39
+ if (!signetEnabled) {
40
+ return { jwt: null, legacyApiKeyEnabled, signetEnabled };
41
+ }
42
+ const issuer = configService.getOrThrow(options.env.jwtIssuer);
43
+ const jwksUri = configService.getOrThrow(options.env.jwtJwksUri);
44
+ const clockToleranceS = readClockTolerance(options, configService);
45
+ return {
46
+ jwt: {
47
+ audience: profile.canonicalResource,
48
+ clockToleranceS,
49
+ issuer,
50
+ jwksUri,
51
+ },
52
+ legacyApiKeyEnabled,
53
+ signetEnabled,
54
+ };
55
+ }
56
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":";;AAmCA,4CAaC;AA+BD,oDAyBC;AAzED,0EAA0E;AAC1E,2EAA2E;AAC3E,2EAA2E;AAC3E,qDAAqD;AACrD,SAAgB,gBAAgB,CAC9B,OAAyB,EACzB,aAA4B;IAE5B,MAAM,EAAE,mBAAmB,EAAE,aAAa,EAAE,GAAG,OAAO,CAAC,GAAG,CAAC;IAC3D,OAAO;QACL,mBAAmB,EACjB,mBAAmB,KAAK,SAAS;YACjC,aAAa,CAAC,GAAG,CAAS,mBAAmB,EAAE,MAAM,CAAC,KAAK,MAAM;QACnE,aAAa,EACX,aAAa,KAAK,SAAS;YAC3B,aAAa,CAAC,GAAG,CAAS,aAAa,EAAE,OAAO,CAAC,KAAK,MAAM;KAC/D,CAAC;AACJ,CAAC;AAED,6EAA6E;AAC7E,+BAA+B;AAC/B,4DAA4D;AAC5D,yEAAyE;AACzE,8EAA8E;AAC9E,0EAA0E;AAC1E,oCAAoC;AACpC,SAAS,kBAAkB,CACzB,OAAyB,EACzB,aAA4B;IAE5B,MAAM,GAAG,GAAG,aAAa,CAAC,GAAG,CAC3B,OAAO,CAAC,GAAG,CAAC,kBAAkB,EAC9B,EAAE,CACH,CAAC;IACF,MAAM,OAAO,GACX,OAAO,GAAG,KAAK,QAAQ,IAAI,GAAG,CAAC,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;IAC1E,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,OAAO,CAAC,IAAI,OAAO,GAAG,CAAC,IAAI,OAAO,GAAG,GAAG,EAAE,CAAC;QAC/D,MAAM,IAAI,KAAK,CACb,GAAG,OAAO,CAAC,GAAG,CAAC,kBAAkB,uCAAuC,CACzE,CAAC;IACJ,CAAC;IACD,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,2EAA2E;AAC3E,6EAA6E;AAC7E,4EAA4E;AAC5E,mDAAmD;AACnD,SAAgB,oBAAoB,CAClC,OAAyB,EACzB,aAA4B,EAC5B,OAA0B;IAE1B,MAAM,EAAE,mBAAmB,EAAE,aAAa,EAAE,GAAG,gBAAgB,CAC7D,OAAO,EACP,aAAa,CACd,CAAC;IACF,IAAI,CAAC,aAAa,EAAE,CAAC;QACnB,OAAO,EAAE,GAAG,EAAE,IAAI,EAAE,mBAAmB,EAAE,aAAa,EAAE,CAAC;IAC3D,CAAC;IACD,MAAM,MAAM,GAAG,aAAa,CAAC,UAAU,CAAS,OAAO,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;IACvE,MAAM,OAAO,GAAG,aAAa,CAAC,UAAU,CAAS,OAAO,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC;IACzE,MAAM,eAAe,GAAG,kBAAkB,CAAC,OAAO,EAAE,aAAa,CAAC,CAAC;IACnE,OAAO;QACL,GAAG,EAAE;YACH,QAAQ,EAAE,OAAO,CAAC,iBAAiB;YACnC,eAAe;YACf,MAAM;YACN,OAAO;SACR;QACD,mBAAmB;QACnB,aAAa;KACd,CAAC;AACJ,CAAC"}
@@ -0,0 +1,25 @@
1
+ import { ConfigService } from '@nestjs/config';
2
+ import { DEVELOPMENT_ENVIRONMENT, type SignetIntegrationOptions } from './options';
3
+ export interface DeploymentProfile<E extends string = string> {
4
+ readonly canonicalResource: string;
5
+ readonly environment: E | typeof DEVELOPMENT_ENVIRONMENT;
6
+ readonly host: string;
7
+ readonly namespace: string;
8
+ }
9
+ export type DeploymentProfileResolution<E extends string> = {
10
+ ok: false;
11
+ reason: string;
12
+ } | {
13
+ ok: true;
14
+ profile: DeploymentProfile<E>;
15
+ };
16
+ export interface DeploymentProfileInput {
17
+ audience: string | undefined;
18
+ namespace: string | undefined;
19
+ nodeEnv: string | undefined;
20
+ }
21
+ export declare function resolveDeploymentProfile<E extends string>(options: SignetIntegrationOptions<E>, input: DeploymentProfileInput): DeploymentProfileResolution<E>;
22
+ export declare class SignetDeploymentProfileService {
23
+ readonly profile: Readonly<DeploymentProfile>;
24
+ constructor(configService: ConfigService, options: SignetIntegrationOptions);
25
+ }
@@ -0,0 +1,103 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ var __metadata = (this && this.__metadata) || function (k, v) {
9
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
10
+ };
11
+ var __param = (this && this.__param) || function (paramIndex, decorator) {
12
+ return function (target, key) { decorator(target, key, paramIndex); }
13
+ };
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.SignetDeploymentProfileService = void 0;
16
+ exports.resolveDeploymentProfile = resolveDeploymentProfile;
17
+ const common_1 = require("@nestjs/common");
18
+ const config_1 = require("@nestjs/config");
19
+ const options_1 = require("./options");
20
+ // Pure, so the consumer's boot-time validation and the runtime service
21
+ // evaluate the identical rule. Exact string matches throughout: no
22
+ // trailing-slash tolerance or case folding.
23
+ function resolveDeploymentProfile(options, input) {
24
+ const { audience, namespace, nodeEnv } = input;
25
+ const { deploymentNamespace: namespaceEnv, jwtAudience: audienceEnv } = options.env;
26
+ if (namespace === undefined && audience === undefined) {
27
+ if (nodeEnv === 'production') {
28
+ return {
29
+ ok: false,
30
+ reason: `${namespaceEnv} and ${audienceEnv} are required in production`,
31
+ };
32
+ }
33
+ return {
34
+ ok: true,
35
+ profile: {
36
+ ...options.developmentProfile,
37
+ environment: options_1.DEVELOPMENT_ENVIRONMENT,
38
+ },
39
+ };
40
+ }
41
+ if (namespace === undefined || audience === undefined) {
42
+ return {
43
+ ok: false,
44
+ reason: `${namespaceEnv} and ${audienceEnv} must be configured together`,
45
+ };
46
+ }
47
+ const match = Object.entries(options.deployedProfiles).find(([, profile]) => profile.namespace === namespace);
48
+ if (match === undefined) {
49
+ return {
50
+ ok: false,
51
+ reason: `${namespaceEnv} names no known deployment (got a value not in the deployed profiles table)`,
52
+ };
53
+ }
54
+ const [environment, { host }] = match;
55
+ const expectedResource = options.canonicalResourceFor(host);
56
+ if (audience !== expectedResource) {
57
+ // The expected value is safe to print: it is a public identifier, and
58
+ // naming it is what lets an operator see which half of the pair was
59
+ // copied from the wrong overlay.
60
+ return {
61
+ ok: false,
62
+ reason: `${audienceEnv} does not match ${namespaceEnv}=${namespace} (expected ${expectedResource})`,
63
+ };
64
+ }
65
+ return {
66
+ ok: true,
67
+ profile: {
68
+ canonicalResource: expectedResource,
69
+ environment,
70
+ host,
71
+ namespace,
72
+ },
73
+ };
74
+ }
75
+ // Runtime access to the profile the consumer's config validation already
76
+ // checked at boot. Resolved once, at construction, from the same pure
77
+ // function -- so a process whose config bypassed that validation still cannot
78
+ // obtain a profile it should not have: it throws here instead.
79
+ //
80
+ // A consumer that wants `profile.environment` typed as its own union
81
+ // subclasses this and re-declares `profile` with the narrower type; the DI
82
+ // token cannot carry the type parameter.
83
+ let SignetDeploymentProfileService = class SignetDeploymentProfileService {
84
+ profile;
85
+ constructor(configService, options) {
86
+ const resolution = resolveDeploymentProfile(options, {
87
+ audience: configService.get(options.env.jwtAudience),
88
+ namespace: configService.get(options.env.deploymentNamespace),
89
+ nodeEnv: configService.get('NODE_ENV'),
90
+ });
91
+ if (!resolution.ok) {
92
+ throw new Error(`deployment profile rejected: ${resolution.reason}`);
93
+ }
94
+ this.profile = resolution.profile;
95
+ }
96
+ };
97
+ exports.SignetDeploymentProfileService = SignetDeploymentProfileService;
98
+ exports.SignetDeploymentProfileService = SignetDeploymentProfileService = __decorate([
99
+ (0, common_1.Injectable)(),
100
+ __param(1, (0, common_1.Inject)(options_1.SIGNET_INTEGRATION_OPTIONS)),
101
+ __metadata("design:paramtypes", [config_1.ConfigService, Object])
102
+ ], SignetDeploymentProfileService);
103
+ //# sourceMappingURL=deployment-profile.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"deployment-profile.js","sourceRoot":"","sources":["../src/deployment-profile.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,18 @@
1
+ import { type ExecutionContext } from '@nestjs/common';
2
+ import { Reflector } from '@nestjs/core';
3
+ import { SignetDeploymentProfileService } from './deployment-profile';
4
+ import { type SignetIntegrationOptions } from './options';
5
+ import { type SignetPrincipalResolver } from './principal-resolver';
6
+ declare const SignetBearerGuard_base: import("@nestjs/passport").Type<import("@nestjs/passport").IAuthGuard>;
7
+ export declare class SignetBearerGuard extends SignetBearerGuard_base {
8
+ private readonly resolver;
9
+ private readonly profiles;
10
+ private readonly signetOptions;
11
+ private readonly reflector;
12
+ private readonly logger;
13
+ constructor(resolver: SignetPrincipalResolver, profiles: SignetDeploymentProfileService, signetOptions: SignetIntegrationOptions, reflector: Reflector);
14
+ canActivate(context: ExecutionContext): Promise<boolean>;
15
+ private logDecision;
16
+ private logFailure;
17
+ }
18
+ export {};