@vritti/api-sdk 0.3.10 → 0.3.12
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.cjs +198 -89
- package/dist/auth.cjs.map +1 -1
- package/dist/auth.d.cts +84 -2
- package/dist/auth.d.ts +84 -2
- package/dist/auth.js +119 -17
- package/dist/auth.js.map +1 -1
- package/dist/index.d.cts +34 -0
- package/dist/index.d.ts +34 -0
- package/dist/request-CDllp7xb.d.cts +111 -0
- package/dist/request-CDllp7xb.d.ts +111 -0
- package/dist/signing.cjs +18 -2
- package/dist/signing.cjs.map +1 -1
- package/dist/signing.d.cts +2 -31
- package/dist/signing.d.ts +2 -31
- package/dist/signing.js +18 -2
- package/dist/signing.js.map +1 -1
- package/package.json +1 -1
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-
|
|
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) =>
|
|
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
|
|
270
|
+
import { SetMetadata as SetMetadata3 } from "@nestjs/common";
|
|
211
271
|
var SKIP_CSRF_KEY = "skipCsrf";
|
|
212
|
-
var SkipCsrf = /* @__PURE__ */ __name(() =>
|
|
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
|
|
684
|
-
var Public = /* @__PURE__ */ __name(() =>
|
|
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";
|
|
@@ -824,18 +925,12 @@ var SessionData = createParamDecorator8((_data, ctx) => {
|
|
|
824
925
|
import { createParamDecorator as createParamDecorator9 } from "@nestjs/common";
|
|
825
926
|
var Subdomain = createParamDecorator9((_data, ctx) => {
|
|
826
927
|
const request = getRequestFromContext(ctx);
|
|
827
|
-
const origin = request.headers.origin;
|
|
828
|
-
if (origin) {
|
|
829
|
-
try {
|
|
830
|
-
const url = new URL(origin);
|
|
831
|
-
return url.hostname.split(".")[0];
|
|
832
|
-
} catch {
|
|
833
|
-
}
|
|
834
|
-
}
|
|
835
928
|
const forwarded = request.headers["x-forwarded-host"];
|
|
836
|
-
const
|
|
837
|
-
|
|
838
|
-
return void 0;
|
|
929
|
+
const raw = Array.isArray(forwarded) ? forwarded[0] : forwarded;
|
|
930
|
+
const hostStr = raw ?? request.hostname;
|
|
931
|
+
if (!hostStr) return void 0;
|
|
932
|
+
const host = hostStr.split(":")[0] ?? hostStr;
|
|
933
|
+
return host.split(".")[0] || void 0;
|
|
839
934
|
});
|
|
840
935
|
|
|
841
936
|
// src/auth/decorators/user-agent.decorator.ts
|
|
@@ -856,18 +951,24 @@ var UserId = createParamDecorator11((_data, ctx) => {
|
|
|
856
951
|
return sessionInfo.userId;
|
|
857
952
|
});
|
|
858
953
|
export {
|
|
954
|
+
APP_SESSION_TYPE,
|
|
859
955
|
AUTH_CONFIG,
|
|
860
956
|
AUTH_CONFIG_DEFAULTS,
|
|
861
957
|
AccessToken,
|
|
862
958
|
AuthConfigModule,
|
|
959
|
+
CLIENT_ID_HEADER,
|
|
863
960
|
ClientIp,
|
|
864
961
|
CookieDomain,
|
|
865
962
|
CookieName,
|
|
866
963
|
Hostname,
|
|
964
|
+
MAX_CLOCK_SKEW_SECONDS,
|
|
965
|
+
PARTY_ID_HEADER,
|
|
867
966
|
Public,
|
|
967
|
+
REQUIRE_APP_KEY,
|
|
868
968
|
REQUIRE_SESSION_KEY,
|
|
869
969
|
RefreshCookieOptions,
|
|
870
970
|
RefreshTokenCookie,
|
|
971
|
+
RequireApp,
|
|
871
972
|
RequireSession,
|
|
872
973
|
SKIP_CSRF_KEY,
|
|
873
974
|
SessionData,
|
|
@@ -878,6 +979,7 @@ export {
|
|
|
878
979
|
UserAgent,
|
|
879
980
|
UserId,
|
|
880
981
|
VrittiAuthGuard,
|
|
982
|
+
WORKSPACE_HEADER_ORDER,
|
|
881
983
|
hashToken,
|
|
882
984
|
verifyTokenHash
|
|
883
985
|
};
|