@vritti/api-sdk 0.3.11 → 0.3.13

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
@@ -1,10 +1,40 @@
1
1
  import { T as TokenExpiry, C as CookieConfig, G as GuardConfig, A as AuthConfig, a as TokenType, D as DecodedAccessToken, b as DecodedRefreshToken, R as RequestService } from './index.js';
2
2
  export { d as AUTH_CONFIG, e as AUTH_CONFIG_DEFAULTS, c as AccessTokenPayload, f as CookieSerializeOptions, O as OnAuthenticatedCallback, g as RefreshTokenPayload, h as TokenExpiryString } from './index.js';
3
3
  import * as _nestjs_common from '@nestjs/common';
4
- import { InjectionToken, DynamicModule, CanActivate, ExecutionContext } from '@nestjs/common';
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
7
  import { FastifyRequest } from 'fastify';
8
+ export { W as WORKSPACE_HEADER_ORDER } from './request-CDllp7xb.js';
9
+
10
+ /**
11
+ * The shared contract between this guard and a server that authenticates apps.
12
+ *
13
+ * The guard recognises `@RequireApp()` and enforces the type filter; the server
14
+ * resolves the credential in `guard.onAuthenticated`, because only it has a
15
+ * database. These are the values both sides have to agree on.
16
+ */
17
+ /** The client id an app sends alongside the standard signature headers. */
18
+ declare const CLIENT_ID_HEADER = "x-vritti-client-id";
19
+ /** The party (person) a signed request is acting for, when there is one. */
20
+ declare const PARTY_ID_HEADER = "x-party-id";
21
+
22
+ /**
23
+ * How far a signed request's timestamp may drift before it is refused.
24
+ *
25
+ * Five minutes, matching the deployment-signing path. Wide enough for ordinary
26
+ * clock drift between two servers; a captured request is replayable inside the
27
+ * window, which is why app operations should stay idempotent.
28
+ */
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";
8
38
 
9
39
  interface AuthConfigInput {
10
40
  tokenExpiry: TokenExpiry;
@@ -14,6 +44,14 @@ interface AuthConfigInput {
14
44
  interface AuthConfigModuleOptions<T extends unknown[] = unknown[]> {
15
45
  useFactory: (...args: [...T]) => AuthConfigInput | Promise<AuthConfigInput>;
16
46
  inject?: InjectionToken[];
47
+ /**
48
+ * Extra modules the factory's injected providers come from.
49
+ *
50
+ * Needed because `guard.onAuthenticated` is where a server resolves an app
51
+ * credential, so the factory has to be able to inject the service that owns that
52
+ * lookup. Mirrors `NatsClientModule.forRoot`.
53
+ */
54
+ imports?: ModuleMetadata['imports'];
17
55
  }
18
56
  declare class AuthConfigModule {
19
57
  static forRootAsync<T extends unknown[] = unknown[]>(options: AuthConfigModuleOptions<T>): DynamicModule;
@@ -35,6 +73,34 @@ declare const RefreshCookieOptions: (...dataOrPipes: unknown[]) => ParameterDeco
35
73
 
36
74
  declare const RefreshTokenCookie: (...dataOrPipes: unknown[]) => ParameterDecorator;
37
75
 
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
+
38
104
  declare const REQUIRE_SESSION_KEY = "requiredSessionTypes";
39
105
  declare const RequireSession: (...types: string[]) => _nestjs_common.CustomDecorator<string>;
40
106
 
@@ -84,6 +150,22 @@ declare class VrittiAuthGuard implements CanActivate {
84
150
  private readonly logger;
85
151
  constructor(reflector: Reflector, requestService: RequestService, tokenService: TokenService, config: AuthConfig);
86
152
  canActivate(context: ExecutionContext): Promise<boolean>;
153
+ /**
154
+ * Authenticates a signed request from an external app.
155
+ *
156
+ * The credential lookup and the signature check are the consuming server's job —
157
+ * it is the only side with a database — so this delegates to
158
+ * `guard.onAuthenticated`, the same hook the session path already uses to resolve
159
+ * organization and workspace context. That server fills in `organizationId`,
160
+ * `appType` and whatever else it knows.
161
+ *
162
+ * What stays here is the part only this side can do: reading the decorator's
163
+ * metadata and enforcing the app-type filter against what the server resolved.
164
+ *
165
+ * An empty list means "any type" — the caller is still authenticated. That makes
166
+ * `@RequireApp()` with no arguments mean what it reads like.
167
+ */
168
+ private handleAppAuth;
87
169
  private handleHttpAuth;
88
170
  private handleSseAuth;
89
171
  private validateCsrf;
@@ -92,4 +174,4 @@ declare class VrittiAuthGuard implements CanActivate {
92
174
  declare function hashToken(token: string): string;
93
175
  declare function verifyTokenHash(token: string, expectedHash: string): boolean;
94
176
 
95
- export { AccessToken, AuthConfig, AuthConfigModule, ClientIp, CookieConfig, CookieDomain, CookieName, DecodedAccessToken, DecodedRefreshToken, GuardConfig, Hostname, Public, REQUIRE_SESSION_KEY, RefreshCookieOptions, RefreshTokenCookie, RequireSession, SKIP_CSRF_KEY, SessionData, type SessionInfo$1 as SessionInfo, SkipCsrf, Subdomain, TokenExpiry, TokenService, TokenType, UserAgent, UserId, VrittiAuthGuard, hashToken, verifyTokenHash };
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 };
package/dist/auth.js CHANGED
@@ -1,6 +1,21 @@
1
1
  var __defProp = Object.defineProperty;
2
2
  var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
3
3
 
4
+ // src/signing/request.ts
5
+ import { createHash, createPrivateKey, createPublicKey, sign, verify } from "crypto";
6
+ var WORKSPACE_HEADER_ORDER = [
7
+ "x-site-id",
8
+ "x-sg-id",
9
+ "x-le-id",
10
+ "x-org-id"
11
+ ];
12
+
13
+ // src/auth/app-request.ts
14
+ var CLIENT_ID_HEADER = "x-vritti-client-id";
15
+ var PARTY_ID_HEADER = "x-party-id";
16
+ var MAX_CLOCK_SKEW_SECONDS = 300;
17
+ var APP_SESSION_TYPE = "APP";
18
+
4
19
  // src/auth/auth.config.ts
5
20
  var AUTH_CONFIG = Symbol("AUTH_CONFIG");
6
21
  var AUTH_CONFIG_DEFAULTS = {
@@ -117,6 +132,46 @@ var RequestService = class {
117
132
  getAllHeaders() {
118
133
  return this.request.headers || {};
119
134
  }
135
+ // Returns the HTTP method
136
+ getMethod() {
137
+ return this.request.method ?? "";
138
+ }
139
+ /**
140
+ * Returns the path with any query string stripped.
141
+ *
142
+ * Signature canonicals cover the path only, so a query string must not be part
143
+ * of what gets signed or verified.
144
+ */
145
+ getPath() {
146
+ const url = this.request.url ?? "";
147
+ return url.split("?")[0] ?? url;
148
+ }
149
+ /**
150
+ * Returns the raw query string, without the leading `?`.
151
+ *
152
+ * Companion to `getPath()`, which strips it. Signed separately so a REST request's
153
+ * filters are covered — `getPath()` alone would sign `GET /people?search=salt` as
154
+ * though the filter were not there.
155
+ */
156
+ getQuery() {
157
+ const url = this.request.url ?? "";
158
+ const index = url.indexOf("?");
159
+ return index === -1 ? "" : url.slice(index + 1);
160
+ }
161
+ /**
162
+ * Returns the raw request body, as `fastify-raw-body` leaves it.
163
+ *
164
+ * Read structurally rather than through a module augmentation: that plugin is the
165
+ * consuming server's dependency, not this SDK's, and declaring `rawBody` here
166
+ * would collide with the plugin's own declaration the moment the two drift.
167
+ *
168
+ * A server that has not registered it yields `undefined`, which hashes as an empty
169
+ * body and therefore fails any signature made over real bytes — refused, never
170
+ * waved through.
171
+ */
172
+ getRawBody() {
173
+ return this.request.rawBody;
174
+ }
120
175
  };
121
176
  RequestService = _ts_decorate([
122
177
  Injectable({
@@ -201,15 +256,20 @@ function getResponseFromContext(host) {
201
256
  }
202
257
  __name(getResponseFromContext, "getResponseFromContext");
203
258
 
204
- // src/auth/decorators/require-session.decorator.ts
259
+ // src/auth/decorators/require-app.decorator.ts
205
260
  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";
206
266
  var REQUIRE_SESSION_KEY = "requiredSessionTypes";
207
- var RequireSession = /* @__PURE__ */ __name((...types) => SetMetadata(REQUIRE_SESSION_KEY, types), "RequireSession");
267
+ var RequireSession = /* @__PURE__ */ __name((...types) => SetMetadata2(REQUIRE_SESSION_KEY, types), "RequireSession");
208
268
 
209
269
  // src/auth/decorators/skip-csrf.decorator.ts
210
- import { SetMetadata as SetMetadata2 } from "@nestjs/common";
270
+ import { SetMetadata as SetMetadata3 } from "@nestjs/common";
211
271
  var SKIP_CSRF_KEY = "skipCsrf";
212
- var SkipCsrf = /* @__PURE__ */ __name(() => SetMetadata2(SKIP_CSRF_KEY, true), "SkipCsrf");
272
+ var SkipCsrf = /* @__PURE__ */ __name(() => SetMetadata3(SKIP_CSRF_KEY, true), "SkipCsrf");
213
273
 
214
274
  // src/auth/services/token.service.ts
215
275
  import { Inject as Inject2, Injectable as Injectable2, Logger, UnauthorizedException } from "@nestjs/common";
@@ -427,6 +487,13 @@ var VrittiAuthGuard = class _VrittiAuthGuard {
427
487
  context.getHandler(),
428
488
  context.getClass()
429
489
  ]) || 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
+ }
430
497
  const isPublic = this.reflector.getAllAndOverride("isPublic", [
431
498
  context.getHandler(),
432
499
  context.getClass()
@@ -454,6 +521,39 @@ var VrittiAuthGuard = class _VrittiAuthGuard {
454
521
  }
455
522
  return true;
456
523
  }
524
+ /**
525
+ * Authenticates a signed request from an external app.
526
+ *
527
+ * The credential lookup and the signature check are the consuming server's job —
528
+ * it is the only side with a database — so this delegates to
529
+ * `guard.onAuthenticated`, the same hook the session path already uses to resolve
530
+ * organization and workspace context. That server fills in `organizationId`,
531
+ * `appType` and whatever else it knows.
532
+ *
533
+ * What stays here is the part only this side can do: reading the decorator's
534
+ * metadata and enforcing the app-type filter against what the server resolved.
535
+ *
536
+ * An empty list means "any type" — the caller is still authenticated. That makes
537
+ * `@RequireApp()` with no arguments mean what it reads like.
538
+ */
539
+ async handleAppAuth(request, requiredAppTypes, route) {
540
+ const onAuthenticated = this.config.guard.onAuthenticated;
541
+ if (!onAuthenticated) {
542
+ this.logger.error(`${route} \u2014 @RequireApp() requires guard.onAuthenticated to be configured`);
543
+ throw new UnauthorizedException2("App authentication is not configured");
544
+ }
545
+ const sessionInfo = {
546
+ sessionType: APP_SESSION_TYPE
547
+ };
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(", ")}]`);
552
+ throw new UnauthorizedException2("This client is not recognised.");
553
+ }
554
+ this.logger.debug(`${route} \u2014 authenticated app (${sessionInfo.appType})`);
555
+ return true;
556
+ }
457
557
  // Authenticates standard HTTP requests using the access token from Authorization header
458
558
  async handleHttpAuth(request, requiredSessionTypes) {
459
559
  const route = `${request.method} ${request.url}`;
@@ -589,6 +689,7 @@ var AuthConfigModule = class _AuthConfigModule {
589
689
  imports: [
590
690
  ConfigModule,
591
691
  RequestModule,
692
+ ...options.imports ?? [],
592
693
  JwtModule.registerAsync({
593
694
  imports: [
594
695
  ConfigModule
@@ -680,8 +781,8 @@ var Hostname = createParamDecorator5((_data, ctx) => {
680
781
  });
681
782
 
682
783
  // src/auth/decorators/public.decorator.ts
683
- import { SetMetadata as SetMetadata3 } from "@nestjs/common";
684
- var Public = /* @__PURE__ */ __name(() => SetMetadata3("isPublic", true), "Public");
784
+ import { SetMetadata as SetMetadata4 } from "@nestjs/common";
785
+ var Public = /* @__PURE__ */ __name(() => SetMetadata4("isPublic", true), "Public");
685
786
 
686
787
  // src/auth/decorators/refresh-cookie-options.decorator.ts
687
788
  import { createParamDecorator as createParamDecorator6 } from "@nestjs/common";
@@ -850,18 +951,24 @@ var UserId = createParamDecorator11((_data, ctx) => {
850
951
  return sessionInfo.userId;
851
952
  });
852
953
  export {
954
+ APP_SESSION_TYPE,
853
955
  AUTH_CONFIG,
854
956
  AUTH_CONFIG_DEFAULTS,
855
957
  AccessToken,
856
958
  AuthConfigModule,
959
+ CLIENT_ID_HEADER,
857
960
  ClientIp,
858
961
  CookieDomain,
859
962
  CookieName,
860
963
  Hostname,
964
+ MAX_CLOCK_SKEW_SECONDS,
965
+ PARTY_ID_HEADER,
861
966
  Public,
967
+ REQUIRE_APP_KEY,
862
968
  REQUIRE_SESSION_KEY,
863
969
  RefreshCookieOptions,
864
970
  RefreshTokenCookie,
971
+ RequireApp,
865
972
  RequireSession,
866
973
  SKIP_CSRF_KEY,
867
974
  SessionData,
@@ -872,6 +979,7 @@ export {
872
979
  UserAgent,
873
980
  UserId,
874
981
  VrittiAuthGuard,
982
+ WORKSPACE_HEADER_ORDER,
875
983
  hashToken,
876
984
  verifyTokenHash
877
985
  };