@vritti/api-sdk 0.3.16 → 0.4.1

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/auth.d.ts CHANGED
@@ -4,7 +4,7 @@ import * as _nestjs_common from '@nestjs/common';
4
4
  import { InjectionToken, ModuleMetadata, DynamicModule, CanActivate, ExecutionContext } from '@nestjs/common';
5
5
  import { Reflector } from '@nestjs/core';
6
6
  import { JwtService, JwtSignOptions } from '@nestjs/jwt';
7
- import { FastifyRequest } from 'fastify';
7
+ import { VrittiSessionAuth } from 'fastify';
8
8
  export { W as WORKSPACE_HEADER_ORDER } from './request-CDllp7xb.js';
9
9
 
10
10
  /**
@@ -27,14 +27,6 @@ declare const PARTY_ID_HEADER = "x-party-id";
27
27
  * window, which is why app operations should stay idempotent.
28
28
  */
29
29
  declare const MAX_CLOCK_SKEW_SECONDS = 300;
30
- /**
31
- * `sessionInfo.sessionType` for an app request.
32
- *
33
- * Not a value of any server's `session_type` enum — no session row exists. It is
34
- * here so anything reading `sessionType` can tell an app apart from a person
35
- * without a second field.
36
- */
37
- declare const APP_SESSION_TYPE = "APP";
38
30
 
39
31
  interface AuthConfigInput {
40
32
  tokenExpiry: TokenExpiry;
@@ -67,42 +59,22 @@ declare const CookieName: (...dataOrPipes: unknown[]) => ParameterDecorator;
67
59
 
68
60
  declare const Hostname: (...dataOrPipes: unknown[]) => ParameterDecorator;
69
61
 
70
- declare const Public: () => _nestjs_common.CustomDecorator<string>;
71
-
72
62
  declare const RefreshCookieOptions: (...dataOrPipes: unknown[]) => ParameterDecorator;
73
63
 
74
64
  declare const RefreshTokenCookie: (...dataOrPipes: unknown[]) => ParameterDecorator;
75
65
 
76
- declare const REQUIRE_APP_KEY = "requiredAppTypes";
77
- /**
78
- * Authenticates an external app by its request signature, and restricts the
79
- * endpoint to the given app types.
80
- *
81
- * ```ts
82
- * @RequireApp(AppTypeValues.GRAPHQL)
83
- * @Mutation(() => Person, { name: 'createPerson' })
84
- * async createPerson(@Args('input') input: CreatePersonInput) {}
85
- * ```
86
- *
87
- * Metadata only, exactly like `@RequireSession` — `VrittiAuthGuard` reads it and
88
- * branches. Nothing else is needed at the call site:
89
- *
90
- * - no `@Public()`, because the guard's app branch runs *before* the public check
91
- * - no `@UseGuards()`, because the global guard owns this
92
- * - no `@SkipCsrf()`, because that branch returns before CSRF is ever reached —
93
- * which also covers REST, where a transport-level exemption would not
94
- *
95
- * Types are compared as strings: the enum belongs to the consuming server's
96
- * schema, and this only ever compares. Passing none authenticates the caller
97
- * without restricting which kind it is.
98
- *
99
- * The consuming server resolves the credential in its `onAuthenticated` callback,
100
- * which is the only side with a database. See `GuardConfig.onAuthenticated`.
101
- */
102
- declare const RequireApp: (...types: string[]) => _nestjs_common.CustomDecorator<string>;
103
-
104
- declare const REQUIRE_SESSION_KEY = "requiredSessionTypes";
105
- declare const RequireSession: (...types: string[]) => _nestjs_common.CustomDecorator<string>;
66
+ declare const REQUIRE_AUTH_KEY = "requireAuth";
67
+ declare enum AuthType {
68
+ Session = "session",
69
+ App = "app",
70
+ Cloud = "cloud",
71
+ Public = "public"
72
+ }
73
+ interface AuthRequirement {
74
+ type: AuthType;
75
+ subtypes: string[];
76
+ }
77
+ declare const Require: (type: AuthType, ...subtypes: string[]) => _nestjs_common.CustomDecorator<string>;
106
78
 
107
79
  interface SessionInfo$1 {
108
80
  userId: string;
@@ -120,7 +92,7 @@ declare const UserAgent: (...dataOrPipes: unknown[]) => ParameterDecorator;
120
92
 
121
93
  declare const UserId: (...dataOrPipes: unknown[]) => ParameterDecorator;
122
94
 
123
- type SessionInfo = NonNullable<FastifyRequest['sessionInfo']>;
95
+ type SessionInfo = Omit<VrittiSessionAuth, 'kind'>;
124
96
  declare class TokenService {
125
97
  private readonly jwtService;
126
98
  private readonly config;
@@ -150,6 +122,16 @@ declare class VrittiAuthGuard implements CanActivate {
150
122
  private readonly logger;
151
123
  constructor(reflector: Reflector, requestService: RequestService, tokenService: TokenService, config: AuthConfig);
152
124
  canActivate(context: ExecutionContext): Promise<boolean>;
125
+ /**
126
+ * Authenticates a signed request from the control plane.
127
+ *
128
+ * Same shape as the app branch and for the same reason: verifying the signature needs a key
129
+ * the consuming server holds, so this delegates to `guard.onAuthenticated` and keeps only
130
+ * what this side can do — recognising the decorator and seeding the context.
131
+ *
132
+ * There is no subtype filter. Cloud is one caller, not a family of them.
133
+ */
134
+ private handleCloudAuth;
153
135
  /**
154
136
  * Authenticates a signed request from an external app.
155
137
  *
@@ -174,4 +156,4 @@ declare class VrittiAuthGuard implements CanActivate {
174
156
  declare function hashToken(token: string): string;
175
157
  declare function verifyTokenHash(token: string, expectedHash: string): boolean;
176
158
 
177
- export { APP_SESSION_TYPE, AccessToken, AuthConfig, AuthConfigModule, CLIENT_ID_HEADER, ClientIp, CookieConfig, CookieDomain, CookieName, DecodedAccessToken, DecodedRefreshToken, GuardConfig, Hostname, MAX_CLOCK_SKEW_SECONDS, PARTY_ID_HEADER, Public, REQUIRE_APP_KEY, REQUIRE_SESSION_KEY, RefreshCookieOptions, RefreshTokenCookie, RequireApp, RequireSession, SKIP_CSRF_KEY, SessionData, type SessionInfo$1 as SessionInfo, SkipCsrf, Subdomain, TokenExpiry, TokenService, TokenType, UserAgent, UserId, VrittiAuthGuard, hashToken, verifyTokenHash };
159
+ export { AccessToken, AuthConfig, AuthConfigModule, type AuthRequirement, AuthType, CLIENT_ID_HEADER, ClientIp, CookieConfig, CookieDomain, CookieName, DecodedAccessToken, DecodedRefreshToken, GuardConfig, Hostname, MAX_CLOCK_SKEW_SECONDS, PARTY_ID_HEADER, REQUIRE_AUTH_KEY, RefreshCookieOptions, RefreshTokenCookie, Require, SKIP_CSRF_KEY, SessionData, type SessionInfo$1 as SessionInfo, SkipCsrf, Subdomain, TokenExpiry, TokenService, TokenType, UserAgent, UserId, VrittiAuthGuard, hashToken, verifyTokenHash };
package/dist/auth.js CHANGED
@@ -14,7 +14,6 @@ var WORKSPACE_HEADER_ORDER = [
14
14
  var CLIENT_ID_HEADER = "x-vritti-client-id";
15
15
  var PARTY_ID_HEADER = "x-party-id";
16
16
  var MAX_CLOCK_SKEW_SECONDS = 300;
17
- var APP_SESSION_TYPE = "APP";
18
17
 
19
18
  // src/auth/auth.config.ts
20
19
  var AUTH_CONFIG = Symbol("AUTH_CONFIG");
@@ -159,6 +158,15 @@ var RequestService = class {
159
158
  return index === -1 ? "" : url.slice(index + 1);
160
159
  }
161
160
  /**
161
+ * Returns a single decoded query param, or undefined when absent.
162
+ *
163
+ * Reads the raw URL rather than Fastify's parsed `query` so it is usable from the
164
+ * guard, which runs before a route's own parsing is meaningful.
165
+ */
166
+ getQueryParam(key) {
167
+ return new URLSearchParams(this.getQuery()).get(key) ?? void 0;
168
+ }
169
+ /**
162
170
  * Returns the raw request body, as `fastify-raw-body` leaves it.
163
171
  *
164
172
  * Read structurally rather than through a module augmentation: that plugin is the
@@ -256,20 +264,25 @@ function getResponseFromContext(host) {
256
264
  }
257
265
  __name(getResponseFromContext, "getResponseFromContext");
258
266
 
259
- // src/auth/decorators/require-app.decorator.ts
267
+ // src/auth/decorators/require.decorator.ts
260
268
  import { SetMetadata } from "@nestjs/common";
261
- var REQUIRE_APP_KEY = "requiredAppTypes";
262
- var RequireApp = /* @__PURE__ */ __name((...types) => SetMetadata(REQUIRE_APP_KEY, types), "RequireApp");
263
-
264
- // src/auth/decorators/require-session.decorator.ts
265
- import { SetMetadata as SetMetadata2 } from "@nestjs/common";
266
- var REQUIRE_SESSION_KEY = "requiredSessionTypes";
267
- var RequireSession = /* @__PURE__ */ __name((...types) => SetMetadata2(REQUIRE_SESSION_KEY, types), "RequireSession");
269
+ var REQUIRE_AUTH_KEY = "requireAuth";
270
+ var AuthType = /* @__PURE__ */ (function(AuthType2) {
271
+ AuthType2["Session"] = "session";
272
+ AuthType2["App"] = "app";
273
+ AuthType2["Cloud"] = "cloud";
274
+ AuthType2["Public"] = "public";
275
+ return AuthType2;
276
+ })({});
277
+ var Require = /* @__PURE__ */ __name((type, ...subtypes) => SetMetadata(REQUIRE_AUTH_KEY, {
278
+ type,
279
+ subtypes
280
+ }), "Require");
268
281
 
269
282
  // src/auth/decorators/skip-csrf.decorator.ts
270
- import { SetMetadata as SetMetadata3 } from "@nestjs/common";
283
+ import { SetMetadata as SetMetadata2 } from "@nestjs/common";
271
284
  var SKIP_CSRF_KEY = "skipCsrf";
272
- var SkipCsrf = /* @__PURE__ */ __name(() => SetMetadata3(SKIP_CSRF_KEY, true), "SkipCsrf");
285
+ var SkipCsrf = /* @__PURE__ */ __name(() => SetMetadata2(SKIP_CSRF_KEY, true), "SkipCsrf");
273
286
 
274
287
  // src/auth/services/token.service.ts
275
288
  import { Inject as Inject2, Injectable as Injectable2, Logger, UnauthorizedException } from "@nestjs/common";
@@ -487,38 +500,64 @@ var VrittiAuthGuard = class _VrittiAuthGuard {
487
500
  context.getHandler(),
488
501
  context.getClass()
489
502
  ]) || csrfExemptTransports.includes(context.getType());
490
- const requiredAppTypes = this.reflector.getAllAndOverride(REQUIRE_APP_KEY, [
491
- context.getHandler(),
492
- context.getClass()
493
- ]);
494
- if (requiredAppTypes) {
495
- return this.handleAppAuth(request, requiredAppTypes, route);
496
- }
497
- const isPublic = this.reflector.getAllAndOverride("isPublic", [
503
+ const requirement = this.reflector.getAllAndOverride(REQUIRE_AUTH_KEY, [
498
504
  context.getHandler(),
499
505
  context.getClass()
500
- ]);
501
- if (isPublic) {
502
- if (!skipCsrf) {
503
- await this.validateCsrf(request, reply);
506
+ ]) ?? {
507
+ type: AuthType.Session,
508
+ subtypes: []
509
+ };
510
+ switch (requirement.type) {
511
+ // Signed server-to-server calls return before CSRF is ever reached. Neither carries a
512
+ // cookie for CSRF to protect, and that holds on REST too — which a transport-level
513
+ // exemption would not cover.
514
+ case AuthType.App:
515
+ return this.handleAppAuth(request, requirement.subtypes, route);
516
+ case AuthType.Cloud:
517
+ return this.handleCloudAuth(request, route);
518
+ case AuthType.Public: {
519
+ if (!skipCsrf) {
520
+ await this.validateCsrf(request, reply);
521
+ }
522
+ this.logger.debug(`${route} \u2014 public endpoint, skipping auth`);
523
+ return true;
524
+ }
525
+ case AuthType.Session: {
526
+ const isSseEndpoint = this.reflector.get(SSE_METADATA, context.getHandler());
527
+ if (isSseEndpoint) {
528
+ this.logger.debug(`${route} \u2014 SSE endpoint, authenticating via refresh cookie`);
529
+ return this.handleSseAuth(request, requirement.subtypes);
530
+ }
531
+ const sessionType = await this.handleHttpAuth(request, requirement.subtypes);
532
+ const csrfExemptSessionTypes = this.config.guard.csrfExemptSessionTypes ?? [];
533
+ if (!skipCsrf && !csrfExemptSessionTypes.includes(sessionType)) {
534
+ await this.validateCsrf(request, reply);
535
+ }
536
+ return true;
504
537
  }
505
- this.logger.debug(`${route} \u2014 public endpoint, skipping auth`);
506
- return true;
507
- }
508
- const requiredSessionTypes = this.reflector.getAllAndOverride(REQUIRE_SESSION_KEY, [
509
- context.getHandler(),
510
- context.getClass()
511
- ]);
512
- const isSseEndpoint = this.reflector.get(SSE_METADATA, context.getHandler());
513
- if (isSseEndpoint) {
514
- this.logger.debug(`${route} \u2014 SSE endpoint, authenticating via refresh cookie`);
515
- return this.handleSseAuth(request, requiredSessionTypes);
516
538
  }
517
- const sessionType = await this.handleHttpAuth(request, requiredSessionTypes);
518
- const csrfExemptSessionTypes = this.config.guard.csrfExemptSessionTypes ?? [];
519
- if (!skipCsrf && !csrfExemptSessionTypes.includes(sessionType)) {
520
- await this.validateCsrf(request, reply);
539
+ }
540
+ /**
541
+ * Authenticates a signed request from the control plane.
542
+ *
543
+ * Same shape as the app branch and for the same reason: verifying the signature needs a key
544
+ * the consuming server holds, so this delegates to `guard.onAuthenticated` and keeps only
545
+ * what this side can do — recognising the decorator and seeding the context.
546
+ *
547
+ * There is no subtype filter. Cloud is one caller, not a family of them.
548
+ */
549
+ async handleCloudAuth(request, route) {
550
+ const onAuthenticated = this.config.guard.onAuthenticated;
551
+ if (!onAuthenticated) {
552
+ this.logger.error(`${route} \u2014 @Require(AuthType.Cloud) requires guard.onAuthenticated to be configured`);
553
+ throw new UnauthorizedException2("Cloud authentication is not configured");
521
554
  }
555
+ const auth = {
556
+ kind: "cloud"
557
+ };
558
+ request.auth = auth;
559
+ await onAuthenticated(this.requestService, auth);
560
+ this.logger.debug(`${route} \u2014 authenticated cloud request`);
522
561
  return true;
523
562
  }
524
563
  /**
@@ -542,16 +581,17 @@ var VrittiAuthGuard = class _VrittiAuthGuard {
542
581
  this.logger.error(`${route} \u2014 @RequireApp() requires guard.onAuthenticated to be configured`);
543
582
  throw new UnauthorizedException2("App authentication is not configured");
544
583
  }
545
- const sessionInfo = {
546
- sessionType: APP_SESSION_TYPE
584
+ const auth = {
585
+ kind: "app"
547
586
  };
548
- request.sessionInfo = sessionInfo;
549
- await onAuthenticated(this.requestService, sessionInfo);
550
- if (requiredAppTypes.length && !requiredAppTypes.includes(sessionInfo.appType ?? "")) {
551
- this.logger.warn(`${route} \u2014 app type ${sessionInfo.appType ?? "unknown"} not in allowed: [${requiredAppTypes.join(", ")}]`);
587
+ request.auth = auth;
588
+ await onAuthenticated(this.requestService, auth);
589
+ const appType = auth.kind === "app" ? auth.appType : void 0;
590
+ if (requiredAppTypes.length && !requiredAppTypes.includes(appType ?? "")) {
591
+ this.logger.warn(`${route} \u2014 app type ${appType ?? "unknown"} not in allowed: [${requiredAppTypes.join(", ")}]`);
552
592
  throw new UnauthorizedException2("This client is not recognised.");
553
593
  }
554
- this.logger.debug(`${route} \u2014 authenticated app (${sessionInfo.appType})`);
594
+ this.logger.debug(`${route} \u2014 authenticated app (${appType})`);
555
595
  return true;
556
596
  }
557
597
  // Authenticates standard HTTP requests using the access token from Authorization header
@@ -575,17 +615,21 @@ var VrittiAuthGuard = class _VrittiAuthGuard {
575
615
  this.logger.warn(`${route} \u2014 session type ${decoded.sessionType} not in allowed: [${requiredSessionTypes.join(", ")}]`);
576
616
  throw new UnauthorizedException2(`${decoded.sessionType} sessions cannot access this endpoint`);
577
617
  }
578
- const { tokenType: _tokenType, refreshTokenHash: _hash, exp: _exp, iat: _iat, ...sessionInfo } = decoded;
579
- request.sessionInfo = sessionInfo;
618
+ const { tokenType: _tokenType, refreshTokenHash: _hash, exp: _exp, iat: _iat, ...claims } = decoded;
619
+ const auth = {
620
+ kind: "session",
621
+ ...claims
622
+ };
623
+ request.auth = auth;
580
624
  const onAuthenticated = this.config.guard.onAuthenticated;
581
625
  if (onAuthenticated) {
582
- await onAuthenticated(this.requestService, request.sessionInfo);
626
+ await onAuthenticated(this.requestService, auth);
583
627
  }
584
628
  this.logger.debug(`${route} \u2014 authenticated user: ${decoded.userId} (${decoded.sessionType})`);
585
629
  return decoded.sessionType;
586
630
  }
587
631
  // Authenticates SSE connections using the refresh token httpOnly cookie
588
- handleSseAuth(request, requiredSessionTypes) {
632
+ async handleSseAuth(request, requiredSessionTypes) {
589
633
  const refreshToken = this.requestService.getRefreshToken();
590
634
  if (!refreshToken) {
591
635
  this.logger.warn(`SSE ${request.url} \u2014 no refresh token cookie`);
@@ -596,8 +640,16 @@ var VrittiAuthGuard = class _VrittiAuthGuard {
596
640
  this.logger.warn(`SSE ${request.url} \u2014 session type ${decoded.sessionType} not allowed`);
597
641
  throw new UnauthorizedException2(`${decoded.sessionType} sessions cannot access this endpoint`);
598
642
  }
599
- const { tokenType: _tokenType, exp: _exp, iat: _iat, ...sessionInfo } = decoded;
600
- request.sessionInfo = sessionInfo;
643
+ const { tokenType: _tokenType, exp: _exp, iat: _iat, ...claims } = decoded;
644
+ const auth = {
645
+ kind: "session",
646
+ ...claims
647
+ };
648
+ request.auth = auth;
649
+ const onAuthenticated = this.config.guard.onAuthenticated;
650
+ if (onAuthenticated) {
651
+ await onAuthenticated(this.requestService, auth);
652
+ }
601
653
  this.logger.debug(`SSE ${request.url} \u2014 authenticated user: ${decoded.userId} (${decoded.sessionType})`);
602
654
  return true;
603
655
  }
@@ -780,10 +832,6 @@ var Hostname = createParamDecorator5((_data, ctx) => {
780
832
  return hostStr.split(":")[0] ?? hostStr;
781
833
  });
782
834
 
783
- // src/auth/decorators/public.decorator.ts
784
- import { SetMetadata as SetMetadata4 } from "@nestjs/common";
785
- var Public = /* @__PURE__ */ __name(() => SetMetadata4("isPublic", true), "Public");
786
-
787
835
  // src/auth/decorators/refresh-cookie-options.decorator.ts
788
836
  import { createParamDecorator as createParamDecorator6 } from "@nestjs/common";
789
837
 
@@ -909,15 +957,14 @@ var RefreshTokenCookie = createParamDecorator7((_data, ctx) => {
909
957
  // src/auth/decorators/session-data.decorator.ts
910
958
  import { createParamDecorator as createParamDecorator8 } from "@nestjs/common";
911
959
  var SessionData = createParamDecorator8((_data, ctx) => {
912
- const request = getRequestFromContext(ctx);
913
- const sessionInfo = request.sessionInfo;
914
- if (!sessionInfo?.sessionId) {
915
- throw new Error("Session info not found on request. Ensure route is protected by auth guard.");
960
+ const auth = getRequestFromContext(ctx).auth;
961
+ if (auth?.kind !== "session") {
962
+ throw new Error(`No session on this request (auth: ${auth?.kind ?? "none"}). @SessionData() is session-only.`);
916
963
  }
917
964
  return {
918
- userId: sessionInfo.userId,
919
- sessionId: sessionInfo.sessionId,
920
- sessionType: sessionInfo.sessionType
965
+ userId: auth.userId,
966
+ sessionId: auth.sessionId,
967
+ sessionType: auth.sessionType
921
968
  };
922
969
  });
923
970
 
@@ -943,19 +990,18 @@ var UserAgent = createParamDecorator10((_data, ctx) => {
943
990
  // src/auth/decorators/user-id.decorator.ts
944
991
  import { createParamDecorator as createParamDecorator11 } from "@nestjs/common";
945
992
  var UserId = createParamDecorator11((_data, ctx) => {
946
- const request = getRequestFromContext(ctx);
947
- const sessionInfo = request.sessionInfo;
948
- if (!sessionInfo?.userId) {
949
- throw new Error("User ID not found on request. Ensure route is protected by auth guard.");
993
+ const auth = getRequestFromContext(ctx).auth;
994
+ if (auth?.kind !== "session") {
995
+ throw new Error(`No user on this request (auth: ${auth?.kind ?? "none"}). @UserId() is session-only.`);
950
996
  }
951
- return sessionInfo.userId;
997
+ return auth.userId;
952
998
  });
953
999
  export {
954
- APP_SESSION_TYPE,
955
1000
  AUTH_CONFIG,
956
1001
  AUTH_CONFIG_DEFAULTS,
957
1002
  AccessToken,
958
1003
  AuthConfigModule,
1004
+ AuthType,
959
1005
  CLIENT_ID_HEADER,
960
1006
  ClientIp,
961
1007
  CookieDomain,
@@ -963,13 +1009,10 @@ export {
963
1009
  Hostname,
964
1010
  MAX_CLOCK_SKEW_SECONDS,
965
1011
  PARTY_ID_HEADER,
966
- Public,
967
- REQUIRE_APP_KEY,
968
- REQUIRE_SESSION_KEY,
1012
+ REQUIRE_AUTH_KEY,
969
1013
  RefreshCookieOptions,
970
1014
  RefreshTokenCookie,
971
- RequireApp,
972
- RequireSession,
1015
+ Require,
973
1016
  SKIP_CSRF_KEY,
974
1017
  SessionData,
975
1018
  SkipCsrf,