@nexus_js/graphql 0.9.3
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/complexity.d.ts +75 -0
- package/dist/complexity.d.ts.map +1 -0
- package/dist/complexity.js +182 -0
- package/dist/complexity.js.map +1 -0
- package/dist/dataloader.d.ts +96 -0
- package/dist/dataloader.d.ts.map +1 -0
- package/dist/dataloader.js +177 -0
- package/dist/dataloader.js.map +1 -0
- package/dist/handler.d.ts +142 -0
- package/dist/handler.d.ts.map +1 -0
- package/dist/handler.js +420 -0
- package/dist/handler.js.map +1 -0
- package/dist/index.d.ts +56 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +62 -0
- package/dist/index.js.map +1 -0
- package/dist/jwt.d.ts +109 -0
- package/dist/jwt.d.ts.map +1 -0
- package/dist/jwt.js +197 -0
- package/dist/jwt.js.map +1 -0
- package/dist/mask.d.ts +70 -0
- package/dist/mask.d.ts.map +1 -0
- package/dist/mask.js +98 -0
- package/dist/mask.js.map +1 -0
- package/dist/remote-executor.d.ts +126 -0
- package/dist/remote-executor.d.ts.map +1 -0
- package/dist/remote-executor.js +270 -0
- package/dist/remote-executor.js.map +1 -0
- package/dist/stitching.d.ts +145 -0
- package/dist/stitching.d.ts.map +1 -0
- package/dist/stitching.js +111 -0
- package/dist/stitching.js.map +1 -0
- package/package.json +64 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @nexus_js/graphql — GraphQL integration for Nexus.js
|
|
3
|
+
*
|
|
4
|
+
* Provides a security-first GraphQL adapter that integrates with the Nexus
|
|
5
|
+
* server pipeline without Express, without CORS conflicts, and without
|
|
6
|
+
* external runtime dependencies beyond `graphql` itself.
|
|
7
|
+
*
|
|
8
|
+
* Quick-start
|
|
9
|
+
* ───────────
|
|
10
|
+
* ```ts
|
|
11
|
+
* // nexus.config.ts or server startup
|
|
12
|
+
* import { createGraphQLHandler, createBatchLoader, createJwtService } from '@nexus_js/graphql';
|
|
13
|
+
* import { nexusVault } from '@nexus_js/security';
|
|
14
|
+
* import { schema } from './graphql/schema.js';
|
|
15
|
+
*
|
|
16
|
+
* const jwtSvc = createJwtService(nexusVault, { vaultKey: 'JWT_SECRET', issuer: 'my-app' });
|
|
17
|
+
*
|
|
18
|
+
* const gqlHandler = createGraphQLHandler({
|
|
19
|
+
* schema,
|
|
20
|
+
* dev: process.env.NODE_ENV !== 'production',
|
|
21
|
+
* cors: { origins: ['https://app.example.com'], credentials: true },
|
|
22
|
+
* shield: { maxCost: 500, maxDepth: 8, allowIntrospection: false },
|
|
23
|
+
* mask: { 'User.passwordHash': null, 'PaymentCard.cvv': 'REDACTED' },
|
|
24
|
+
* context: (req, ctx) => ({
|
|
25
|
+
* ...ctx,
|
|
26
|
+
* user: jwtSvc.verify(req.headers.get('authorization')?.replace('Bearer ', '') ?? ''),
|
|
27
|
+
* loaders: { user: createBatchLoader(ids => db.users.findMany(ids)) },
|
|
28
|
+
* }),
|
|
29
|
+
* });
|
|
30
|
+
*
|
|
31
|
+
* // Add to createNexusServer:
|
|
32
|
+
* mounts: [{ path: '/graphql', handler: gqlHandler }]
|
|
33
|
+
* ```
|
|
34
|
+
*
|
|
35
|
+
* Architecture
|
|
36
|
+
* ────────────
|
|
37
|
+
* Handler pipeline per request:
|
|
38
|
+
* 1. CORS preflight (OPTIONS) → 204 with Access-Control headers
|
|
39
|
+
* 2. GraphiQL HTML (dev GET with Accept: text/html)
|
|
40
|
+
* 3. Rate limiting (sliding window, per IP)
|
|
41
|
+
* 4. Parse GET/POST JSON/application/graphql body
|
|
42
|
+
* 5. graphql.parse() + graphql.validate()
|
|
43
|
+
* 6. Shield: complexity + depth + introspection gate
|
|
44
|
+
* 7. Context factory call
|
|
45
|
+
* 8. graphql.execute()
|
|
46
|
+
* 9. Field masking
|
|
47
|
+
* 10. JSON response
|
|
48
|
+
*/
|
|
49
|
+
// ── Handler ──────────────────────────────────────────────────────────────────
|
|
50
|
+
export { createGraphQLHandler, } from './handler.js';
|
|
51
|
+
// ── Complexity Shield ─────────────────────────────────────────────────────────
|
|
52
|
+
export { analyseComplexity, } from './complexity.js';
|
|
53
|
+
// ── Field masking ─────────────────────────────────────────────────────────────
|
|
54
|
+
export { maskResult, allowWhen, redactUnless, } from './mask.js';
|
|
55
|
+
// ── JWT + Vault rotation ──────────────────────────────────────────────────────
|
|
56
|
+
export { createJwtService, signJwt, verifyJwt, } from './jwt.js';
|
|
57
|
+
// ── DataLoader (N+1 prevention) ───────────────────────────────────────────────
|
|
58
|
+
export { BatchLoader, createBatchLoader, createLoaderRegistry, } from './dataloader.js';
|
|
59
|
+
// ── Legacy Bridge: Remote executor & stitching ────────────────────────────────
|
|
60
|
+
export { createRemoteExecutor, createRemoteExecutorWithSchema, } from './remote-executor.js';
|
|
61
|
+
export { stitchSchemas, createGatewayResolver, } from './stitching.js';
|
|
62
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+CG;AAEH,gFAAgF;AAChF,OAAO,EACL,oBAAoB,GAMrB,MAAM,cAAc,CAAC;AAEtB,iFAAiF;AACjF,OAAO,EACL,iBAAiB,GAGlB,MAAM,iBAAiB,CAAC;AAEzB,iFAAiF;AACjF,OAAO,EACL,UAAU,EACV,SAAS,EACT,YAAY,GAIb,MAAM,WAAW,CAAC;AAEnB,iFAAiF;AACjF,OAAO,EACL,gBAAgB,EAChB,OAAO,EACP,SAAS,GAIV,MAAM,UAAU,CAAC;AAElB,iFAAiF;AACjF,OAAO,EACL,WAAW,EACX,iBAAiB,EACjB,oBAAoB,GAGrB,MAAM,iBAAiB,CAAC;AAEzB,iFAAiF;AACjF,OAAO,EACL,oBAAoB,EACpB,8BAA8B,GAI/B,MAAM,sBAAsB,CAAC;AAE9B,OAAO,EACL,aAAa,EACb,qBAAqB,GAGtB,MAAM,gBAAgB,CAAC"}
|
package/dist/jwt.d.ts
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Nexus JWT — HS256 signing and verification backed by the Vault.
|
|
3
|
+
*
|
|
4
|
+
* Key design goals
|
|
5
|
+
* ────────────────
|
|
6
|
+
* 1. Zero external JWT library — uses only Node.js `node:crypto` to avoid
|
|
7
|
+
* supply-chain risk and keep the package small.
|
|
8
|
+
*
|
|
9
|
+
* 2. Vault-backed key rotation with a grace period so existing valid tokens
|
|
10
|
+
* are not instantly invalidated when you rotate the secret.
|
|
11
|
+
* On vault patch: old key stays valid for `gracePeriodMs` (default 5 min),
|
|
12
|
+
* then all tokens signed with it are rejected.
|
|
13
|
+
*
|
|
14
|
+
* 3. timingSafeEqual comparison everywhere to prevent timing attacks.
|
|
15
|
+
*
|
|
16
|
+
* Usage
|
|
17
|
+
* ─────
|
|
18
|
+
* // In your Nexus server init:
|
|
19
|
+
* const jwtService = createJwtService({
|
|
20
|
+
* vaultKey: 'JWT_SECRET', // vault key that holds the signing secret
|
|
21
|
+
* issuer: 'my-app',
|
|
22
|
+
* expiresIn: 3600, // 1 hour
|
|
23
|
+
* });
|
|
24
|
+
*
|
|
25
|
+
* // Sign:
|
|
26
|
+
* const token = jwtService.sign({ sub: userId, role: 'user' });
|
|
27
|
+
*
|
|
28
|
+
* // Verify (in GraphQL resolver / action):
|
|
29
|
+
* const payload = jwtService.verify(token); // throws on invalid
|
|
30
|
+
*
|
|
31
|
+
* // Rotate (call from vault hot-reload handler, or schedule):
|
|
32
|
+
* jwtService.rotate('new-secret-at-least-32-chars');
|
|
33
|
+
*/
|
|
34
|
+
import type { NexusVault } from '@nexus_js/security';
|
|
35
|
+
export interface JwtPayload {
|
|
36
|
+
/** Subject — typically user ID */
|
|
37
|
+
sub: string;
|
|
38
|
+
/** Issued-at (epoch seconds) */
|
|
39
|
+
iat: number;
|
|
40
|
+
/** Expiry (epoch seconds) */
|
|
41
|
+
exp: number;
|
|
42
|
+
/** Issuer */
|
|
43
|
+
iss?: string;
|
|
44
|
+
/** JWT ID — random, useful for revocation lists */
|
|
45
|
+
jti?: string;
|
|
46
|
+
/** Arbitrary custom claims */
|
|
47
|
+
[key: string]: unknown;
|
|
48
|
+
}
|
|
49
|
+
export interface JwtServiceOptions {
|
|
50
|
+
/**
|
|
51
|
+
* Vault key whose value is used as the HMAC signing secret.
|
|
52
|
+
* Must be ≥ 32 bytes (the vault value is used as UTF-8).
|
|
53
|
+
*/
|
|
54
|
+
vaultKey: string;
|
|
55
|
+
/**
|
|
56
|
+
* Issuer claim (`iss`). Include in tokens and verified on decode.
|
|
57
|
+
*/
|
|
58
|
+
issuer?: string;
|
|
59
|
+
/**
|
|
60
|
+
* Token lifetime in **seconds**. Default: 3600 (1 hour).
|
|
61
|
+
*/
|
|
62
|
+
expiresIn?: number;
|
|
63
|
+
/**
|
|
64
|
+
* How long (ms) the **old** key remains valid after rotation.
|
|
65
|
+
* Default: 300 000 (5 minutes). During this window tokens signed with the
|
|
66
|
+
* old key are still accepted so users are not force-logged-out mid-session.
|
|
67
|
+
*/
|
|
68
|
+
gracePeriodMs?: number;
|
|
69
|
+
/**
|
|
70
|
+
* Whether to include a unique `jti` (JWT ID) in every token.
|
|
71
|
+
* Needed for single-use / revocation scenarios. Default: false.
|
|
72
|
+
*/
|
|
73
|
+
includeJti?: boolean;
|
|
74
|
+
}
|
|
75
|
+
export interface JwtService {
|
|
76
|
+
/** Create a signed JWT containing the given payload additions. */
|
|
77
|
+
sign(claims: Omit<JwtPayload, 'iat' | 'exp' | 'iss' | 'jti'>): string;
|
|
78
|
+
/**
|
|
79
|
+
* Verify and decode a JWT.
|
|
80
|
+
* Throws an informative error (without leaking secret) on any failure.
|
|
81
|
+
*/
|
|
82
|
+
verify(token: string): JwtPayload;
|
|
83
|
+
/**
|
|
84
|
+
* Immediately rotate the signing key.
|
|
85
|
+
* The old key enters the grace window and is discarded after `gracePeriodMs`.
|
|
86
|
+
* Normally called automatically when the vault patches `vaultKey`.
|
|
87
|
+
*/
|
|
88
|
+
rotate(newSecret: string): void;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Create a JWT service bound to a Vault instance.
|
|
92
|
+
* Vault updates to `vaultKey` automatically trigger key rotation.
|
|
93
|
+
*
|
|
94
|
+
* @param vault Nexus vault instance (from `nexusVault` or your own)
|
|
95
|
+
* @param opts Service configuration
|
|
96
|
+
*/
|
|
97
|
+
export declare function createJwtService(vault: NexusVault, opts: JwtServiceOptions): JwtService;
|
|
98
|
+
/**
|
|
99
|
+
* Sign a JWT with an explicit secret (not vault-backed).
|
|
100
|
+
* Useful in tests or edge cases where you don't need hot rotation.
|
|
101
|
+
*/
|
|
102
|
+
export declare function signJwt(payload: Omit<JwtPayload, 'iat' | 'exp'> & {
|
|
103
|
+
sub: string;
|
|
104
|
+
}, secret: string, expiresIn?: number): string;
|
|
105
|
+
/**
|
|
106
|
+
* Verify a JWT with an explicit secret (not vault-backed).
|
|
107
|
+
*/
|
|
108
|
+
export declare function verifyJwt(token: string, secret: string, issuer?: string): JwtPayload;
|
|
109
|
+
//# sourceMappingURL=jwt.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"jwt.d.ts","sourceRoot":"","sources":["../src/jwt.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AAGH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAIrD,MAAM,WAAW,UAAU;IACzB,kCAAkC;IAClC,GAAG,EAAI,MAAM,CAAC;IACd,gCAAgC;IAChC,GAAG,EAAI,MAAM,CAAC;IACd,6BAA6B;IAC7B,GAAG,EAAI,MAAM,CAAC;IACd,aAAa;IACb,GAAG,CAAC,EAAG,MAAM,CAAC;IACd,mDAAmD;IACnD,GAAG,CAAC,EAAG,MAAM,CAAC;IACd,8BAA8B;IAC9B,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAED,MAAM,WAAW,iBAAiB;IAChC;;;OAGG;IACH,QAAQ,EAAI,MAAM,CAAC;IACnB;;OAEG;IACH,MAAM,CAAC,EAAK,MAAM,CAAC;IACnB;;OAEG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;;OAIG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;;;OAGG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB;AAED,MAAM,WAAW,UAAU;IACzB,kEAAkE;IAClE,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,UAAU,EAAE,KAAK,GAAG,KAAK,GAAG,KAAK,GAAG,KAAK,CAAC,GAAG,MAAM,CAAC;IACtE;;;OAGG;IACH,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,UAAU,CAAC;IAClC;;;;OAIG;IACH,MAAM,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;CACjC;AAWD;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAC9B,KAAK,EAAE,UAAU,EACjB,IAAI,EAAG,iBAAiB,GACvB,UAAU,CAyIZ;AAID;;;GAGG;AACH,wBAAgB,OAAO,CACrB,OAAO,EAAE,IAAI,CAAC,UAAU,EAAE,KAAK,GAAG,KAAK,CAAC,GAAG;IAAE,GAAG,EAAE,MAAM,CAAA;CAAE,EAC1D,MAAM,EAAG,MAAM,EACf,SAAS,SAAO,GACf,MAAM,CAOR;AAED;;GAEG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,UAAU,CA+BpF"}
|
package/dist/jwt.js
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Nexus JWT — HS256 signing and verification backed by the Vault.
|
|
3
|
+
*
|
|
4
|
+
* Key design goals
|
|
5
|
+
* ────────────────
|
|
6
|
+
* 1. Zero external JWT library — uses only Node.js `node:crypto` to avoid
|
|
7
|
+
* supply-chain risk and keep the package small.
|
|
8
|
+
*
|
|
9
|
+
* 2. Vault-backed key rotation with a grace period so existing valid tokens
|
|
10
|
+
* are not instantly invalidated when you rotate the secret.
|
|
11
|
+
* On vault patch: old key stays valid for `gracePeriodMs` (default 5 min),
|
|
12
|
+
* then all tokens signed with it are rejected.
|
|
13
|
+
*
|
|
14
|
+
* 3. timingSafeEqual comparison everywhere to prevent timing attacks.
|
|
15
|
+
*
|
|
16
|
+
* Usage
|
|
17
|
+
* ─────
|
|
18
|
+
* // In your Nexus server init:
|
|
19
|
+
* const jwtService = createJwtService({
|
|
20
|
+
* vaultKey: 'JWT_SECRET', // vault key that holds the signing secret
|
|
21
|
+
* issuer: 'my-app',
|
|
22
|
+
* expiresIn: 3600, // 1 hour
|
|
23
|
+
* });
|
|
24
|
+
*
|
|
25
|
+
* // Sign:
|
|
26
|
+
* const token = jwtService.sign({ sub: userId, role: 'user' });
|
|
27
|
+
*
|
|
28
|
+
* // Verify (in GraphQL resolver / action):
|
|
29
|
+
* const payload = jwtService.verify(token); // throws on invalid
|
|
30
|
+
*
|
|
31
|
+
* // Rotate (call from vault hot-reload handler, or schedule):
|
|
32
|
+
* jwtService.rotate('new-secret-at-least-32-chars');
|
|
33
|
+
*/
|
|
34
|
+
import { createHmac, timingSafeEqual, randomBytes } from 'node:crypto';
|
|
35
|
+
// ── Factory ──────────────────────────────────────────────────────────────────
|
|
36
|
+
/**
|
|
37
|
+
* Create a JWT service bound to a Vault instance.
|
|
38
|
+
* Vault updates to `vaultKey` automatically trigger key rotation.
|
|
39
|
+
*
|
|
40
|
+
* @param vault Nexus vault instance (from `nexusVault` or your own)
|
|
41
|
+
* @param opts Service configuration
|
|
42
|
+
*/
|
|
43
|
+
export function createJwtService(vault, opts) {
|
|
44
|
+
const { vaultKey, issuer, expiresIn = 3600, gracePeriodMs = 300_000, includeJti = false, } = opts;
|
|
45
|
+
let currentSecret = vault.get(vaultKey) ?? '';
|
|
46
|
+
const graceKeys = [];
|
|
47
|
+
// Subscribe to vault hot-reload
|
|
48
|
+
vault.subscribe(() => {
|
|
49
|
+
const next = vault.get(vaultKey);
|
|
50
|
+
if (next && next !== currentSecret) {
|
|
51
|
+
service.rotate(next);
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
// ── Internal helpers ───────────────────────────────────────────────────────
|
|
55
|
+
function b64url(str) {
|
|
56
|
+
return Buffer.from(str).toString('base64url');
|
|
57
|
+
}
|
|
58
|
+
function makeHeader() {
|
|
59
|
+
return b64url(JSON.stringify({ alg: 'HS256', typ: 'JWT' }));
|
|
60
|
+
}
|
|
61
|
+
function signParts(header, body, secret) {
|
|
62
|
+
return createHmac('sha256', secret)
|
|
63
|
+
.update(`${header}.${body}`)
|
|
64
|
+
.digest('base64url');
|
|
65
|
+
}
|
|
66
|
+
function pruneGraceKeys() {
|
|
67
|
+
const now = Date.now();
|
|
68
|
+
// Remove expired grace keys from the back
|
|
69
|
+
while (graceKeys.length > 0 && graceKeys[graceKeys.length - 1].expiresAt < now) {
|
|
70
|
+
graceKeys.pop();
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
// ── Service implementation ─────────────────────────────────────────────────
|
|
74
|
+
const service = {
|
|
75
|
+
sign(claims) {
|
|
76
|
+
if (!currentSecret || currentSecret.length < 32) {
|
|
77
|
+
throw new Error(`[Nexus JWT] Vault key "${vaultKey}" is missing or shorter than 32 characters. ` +
|
|
78
|
+
'Set it via NEXUS_SECRET / vault.patch().');
|
|
79
|
+
}
|
|
80
|
+
const now = Math.floor(Date.now() / 1000);
|
|
81
|
+
const payload = {
|
|
82
|
+
sub: String(claims.sub ?? ''),
|
|
83
|
+
...claims,
|
|
84
|
+
iat: now,
|
|
85
|
+
exp: now + expiresIn,
|
|
86
|
+
...(issuer ? { iss: issuer } : {}),
|
|
87
|
+
...(includeJti ? { jti: randomBytes(16).toString('hex') } : {}),
|
|
88
|
+
};
|
|
89
|
+
const header = makeHeader();
|
|
90
|
+
const body = b64url(JSON.stringify(payload));
|
|
91
|
+
const sig = signParts(header, body, currentSecret);
|
|
92
|
+
return `${header}.${body}.${sig}`;
|
|
93
|
+
},
|
|
94
|
+
verify(token) {
|
|
95
|
+
const parts = token.split('.');
|
|
96
|
+
if (parts.length !== 3) {
|
|
97
|
+
throw new Error('[Nexus JWT] Malformed token: expected 3 segments.');
|
|
98
|
+
}
|
|
99
|
+
const [header, body, sig] = parts;
|
|
100
|
+
pruneGraceKeys();
|
|
101
|
+
// Try current key first, then grace keys
|
|
102
|
+
const candidates = [
|
|
103
|
+
currentSecret,
|
|
104
|
+
...graceKeys.map(k => k.secret),
|
|
105
|
+
].filter(Boolean);
|
|
106
|
+
let payload = null;
|
|
107
|
+
for (const secret of candidates) {
|
|
108
|
+
const expected = Buffer.from(signParts(header, body, secret));
|
|
109
|
+
const actual = Buffer.from(sig);
|
|
110
|
+
// Lengths must match for timingSafeEqual
|
|
111
|
+
if (expected.length === actual.length && timingSafeEqual(expected, actual)) {
|
|
112
|
+
try {
|
|
113
|
+
payload = JSON.parse(Buffer.from(body, 'base64url').toString());
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
throw new Error('[Nexus JWT] Token body is not valid JSON.');
|
|
117
|
+
}
|
|
118
|
+
break;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
if (!payload) {
|
|
122
|
+
throw new Error('[Nexus JWT] Invalid signature.');
|
|
123
|
+
}
|
|
124
|
+
const now = Math.floor(Date.now() / 1000);
|
|
125
|
+
if (payload.exp !== undefined && now > payload.exp) {
|
|
126
|
+
throw new Error('[Nexus JWT] Token has expired.');
|
|
127
|
+
}
|
|
128
|
+
// Clock-skew guard: issued more than 5 seconds in the future → reject
|
|
129
|
+
if (payload.iat !== undefined && payload.iat > now + 5) {
|
|
130
|
+
throw new Error('[Nexus JWT] Token issued-at is in the future (clock skew).');
|
|
131
|
+
}
|
|
132
|
+
if (issuer && payload.iss !== issuer) {
|
|
133
|
+
throw new Error(`[Nexus JWT] Token issuer "${payload.iss}" does not match expected "${issuer}".`);
|
|
134
|
+
}
|
|
135
|
+
return payload;
|
|
136
|
+
},
|
|
137
|
+
rotate(newSecret) {
|
|
138
|
+
if (!newSecret || newSecret.length < 32) {
|
|
139
|
+
throw new Error('[Nexus JWT] New secret must be at least 32 characters.');
|
|
140
|
+
}
|
|
141
|
+
if (currentSecret) {
|
|
142
|
+
graceKeys.unshift({ secret: currentSecret, expiresAt: Date.now() + gracePeriodMs });
|
|
143
|
+
// Cap grace list to prevent unbounded growth (keep at most 5 old keys)
|
|
144
|
+
while (graceKeys.length > 5)
|
|
145
|
+
graceKeys.pop();
|
|
146
|
+
}
|
|
147
|
+
currentSecret = newSecret;
|
|
148
|
+
},
|
|
149
|
+
};
|
|
150
|
+
return service;
|
|
151
|
+
}
|
|
152
|
+
// ── Standalone helpers (for apps that manage their own key) ──────────────────
|
|
153
|
+
/**
|
|
154
|
+
* Sign a JWT with an explicit secret (not vault-backed).
|
|
155
|
+
* Useful in tests or edge cases where you don't need hot rotation.
|
|
156
|
+
*/
|
|
157
|
+
export function signJwt(payload, secret, expiresIn = 3600) {
|
|
158
|
+
const now = Math.floor(Date.now() / 1000);
|
|
159
|
+
const full = { ...payload, iat: now, exp: now + expiresIn };
|
|
160
|
+
const header = Buffer.from(JSON.stringify({ alg: 'HS256', typ: 'JWT' })).toString('base64url');
|
|
161
|
+
const body = Buffer.from(JSON.stringify(full)).toString('base64url');
|
|
162
|
+
const sig = createHmac('sha256', secret).update(`${header}.${body}`).digest('base64url');
|
|
163
|
+
return `${header}.${body}.${sig}`;
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Verify a JWT with an explicit secret (not vault-backed).
|
|
167
|
+
*/
|
|
168
|
+
export function verifyJwt(token, secret, issuer) {
|
|
169
|
+
const parts = token.split('.');
|
|
170
|
+
if (parts.length !== 3)
|
|
171
|
+
throw new Error('[Nexus JWT] Malformed token.');
|
|
172
|
+
const [header, body, sig] = parts;
|
|
173
|
+
const expected = Buffer.from(createHmac('sha256', secret).update(`${header}.${body}`).digest('base64url'));
|
|
174
|
+
const actual = Buffer.from(sig);
|
|
175
|
+
if (expected.length !== actual.length || !timingSafeEqual(expected, actual)) {
|
|
176
|
+
throw new Error('[Nexus JWT] Invalid signature.');
|
|
177
|
+
}
|
|
178
|
+
let payload;
|
|
179
|
+
try {
|
|
180
|
+
payload = JSON.parse(Buffer.from(body, 'base64url').toString());
|
|
181
|
+
}
|
|
182
|
+
catch {
|
|
183
|
+
throw new Error('[Nexus JWT] Token body is not valid JSON.');
|
|
184
|
+
}
|
|
185
|
+
const now = Math.floor(Date.now() / 1000);
|
|
186
|
+
if (payload.exp !== undefined && now > payload.exp) {
|
|
187
|
+
throw new Error('[Nexus JWT] Token has expired.');
|
|
188
|
+
}
|
|
189
|
+
if (payload.iat !== undefined && payload.iat > now + 5) {
|
|
190
|
+
throw new Error('[Nexus JWT] Token issued in the future.');
|
|
191
|
+
}
|
|
192
|
+
if (issuer && payload.iss !== issuer) {
|
|
193
|
+
throw new Error(`[Nexus JWT] Issuer mismatch.`);
|
|
194
|
+
}
|
|
195
|
+
return payload;
|
|
196
|
+
}
|
|
197
|
+
//# sourceMappingURL=jwt.js.map
|
package/dist/jwt.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"jwt.js","sourceRoot":"","sources":["../src/jwt.ts"],"names":[],"mappings":"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"}
|
package/dist/mask.d.ts
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Nexus GraphQL Field Masking
|
|
3
|
+
*
|
|
4
|
+
* Intercepts GraphQL execution results *after* resolution to null-out or
|
|
5
|
+
* redact fields the caller is not authorised to see.
|
|
6
|
+
*
|
|
7
|
+
* This is a last-resort defence layer — it does NOT replace resolver-level
|
|
8
|
+
* auth guards. Its purpose:
|
|
9
|
+
* - Prevent accidental secret leakage when a resolver returns a full DB row
|
|
10
|
+
* - Mask PII in logs / analytics pipelines without modifying every resolver
|
|
11
|
+
* - Provide a single policy source-of-truth instead of scattered checks
|
|
12
|
+
*
|
|
13
|
+
* Usage
|
|
14
|
+
* ─────
|
|
15
|
+
* const policy: MaskPolicy = {
|
|
16
|
+
* 'User.passwordHash': 'REDACTED',
|
|
17
|
+
* 'User.apiKey': (value, ctx) => ctx.user?.role === 'admin' ? value : '••••••••',
|
|
18
|
+
* 'PaymentMethod.cvv': null,
|
|
19
|
+
* };
|
|
20
|
+
*
|
|
21
|
+
* const masked = maskResult(result, policy, gqlCtx);
|
|
22
|
+
*/
|
|
23
|
+
/**
|
|
24
|
+
* Called when a masked field is accessed.
|
|
25
|
+
* Return the replacement value (or null/undefined to strip the field).
|
|
26
|
+
* `rawValue` is the resolved value before masking.
|
|
27
|
+
* `ctx` is whatever context you passed to `maskResult`.
|
|
28
|
+
*/
|
|
29
|
+
export type MaskFn<Ctx = unknown> = (rawValue: unknown, ctx: Ctx) => unknown;
|
|
30
|
+
/**
|
|
31
|
+
* Policy map: key is "TypeName.fieldName" or just "fieldName" (matches any type).
|
|
32
|
+
* Value is either:
|
|
33
|
+
* - A static replacement (string, number, null, undefined …)
|
|
34
|
+
* - A `MaskFn` for dynamic decisions (e.g. role-based access)
|
|
35
|
+
*/
|
|
36
|
+
export type MaskPolicy<Ctx = unknown> = Record<string, unknown | MaskFn<Ctx>>;
|
|
37
|
+
export interface GraphQLExecutionResult {
|
|
38
|
+
data?: Record<string, unknown> | null;
|
|
39
|
+
errors?: Array<{
|
|
40
|
+
message: string;
|
|
41
|
+
[key: string]: unknown;
|
|
42
|
+
}>;
|
|
43
|
+
extensions?: Record<string, unknown>;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Walk the GraphQL execution result and apply the mask policy.
|
|
47
|
+
* Returns a new result object — the original is never mutated.
|
|
48
|
+
*
|
|
49
|
+
* @param result Raw result from `graphql.execute()`
|
|
50
|
+
* @param policy Field masking policy
|
|
51
|
+
* @param ctx Arbitrary context forwarded to MaskFn handlers
|
|
52
|
+
* @param typePath Current object type path for nested resolution (internal use)
|
|
53
|
+
*/
|
|
54
|
+
export declare function maskResult<Ctx = unknown>(result: GraphQLExecutionResult, policy: MaskPolicy<Ctx>, ctx: Ctx): GraphQLExecutionResult;
|
|
55
|
+
/**
|
|
56
|
+
* Convenience factory: returns null when `guardFn` returns false, otherwise
|
|
57
|
+
* passes the raw value through unchanged.
|
|
58
|
+
*
|
|
59
|
+
* @example
|
|
60
|
+
* 'User.apiKey': allowWhen((ctx: MyCtx) => ctx.user?.role === 'admin')
|
|
61
|
+
*/
|
|
62
|
+
export declare function allowWhen<Ctx>(guardFn: (ctx: Ctx, rawValue: unknown) => boolean): MaskFn<Ctx>;
|
|
63
|
+
/**
|
|
64
|
+
* Convenience factory: return `replacement` when the guard fails.
|
|
65
|
+
*
|
|
66
|
+
* @example
|
|
67
|
+
* 'User.email': redactUnless((ctx: MyCtx) => ctx.user?.id === ctx.vars?.userId, '***@***.***')
|
|
68
|
+
*/
|
|
69
|
+
export declare function redactUnless<Ctx>(guardFn: (ctx: Ctx, rawValue: unknown) => boolean, replacement?: unknown): MaskFn<Ctx>;
|
|
70
|
+
//# sourceMappingURL=mask.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mask.d.ts","sourceRoot":"","sources":["../src/mask.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAIH;;;;;GAKG;AACH,MAAM,MAAM,MAAM,CAAC,GAAG,GAAG,OAAO,IAAI,CAAC,QAAQ,EAAE,OAAO,EAAE,GAAG,EAAE,GAAG,KAAK,OAAO,CAAC;AAE7E;;;;;GAKG;AACH,MAAM,MAAM,UAAU,CAAC,GAAG,GAAG,OAAO,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC;AAE9E,MAAM,WAAW,sBAAsB;IACrC,IAAI,CAAC,EAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IACxC,MAAM,CAAC,EAAE,KAAK,CAAC;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;KAAE,CAAC,CAAC;IAC5D,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACtC;AAID;;;;;;;;GAQG;AACH,wBAAgB,UAAU,CAAC,GAAG,GAAG,OAAO,EACtC,MAAM,EAAE,sBAAsB,EAC9B,MAAM,EAAE,UAAU,CAAC,GAAG,CAAC,EACvB,GAAG,EAAE,GAAG,GACP,sBAAsB,CAMxB;AAkDD;;;;;;GAMG;AACH,wBAAgB,SAAS,CAAC,GAAG,EAC3B,OAAO,EAAE,CAAC,GAAG,EAAE,GAAG,EAAE,QAAQ,EAAE,OAAO,KAAK,OAAO,GAChD,MAAM,CAAC,GAAG,CAAC,CAEb;AAED;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAC9B,OAAO,EAAM,CAAC,GAAG,EAAE,GAAG,EAAE,QAAQ,EAAE,OAAO,KAAK,OAAO,EACrD,WAAW,GAAE,OAAoB,GAChC,MAAM,CAAC,GAAG,CAAC,CAEb"}
|
package/dist/mask.js
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Nexus GraphQL Field Masking
|
|
3
|
+
*
|
|
4
|
+
* Intercepts GraphQL execution results *after* resolution to null-out or
|
|
5
|
+
* redact fields the caller is not authorised to see.
|
|
6
|
+
*
|
|
7
|
+
* This is a last-resort defence layer — it does NOT replace resolver-level
|
|
8
|
+
* auth guards. Its purpose:
|
|
9
|
+
* - Prevent accidental secret leakage when a resolver returns a full DB row
|
|
10
|
+
* - Mask PII in logs / analytics pipelines without modifying every resolver
|
|
11
|
+
* - Provide a single policy source-of-truth instead of scattered checks
|
|
12
|
+
*
|
|
13
|
+
* Usage
|
|
14
|
+
* ─────
|
|
15
|
+
* const policy: MaskPolicy = {
|
|
16
|
+
* 'User.passwordHash': 'REDACTED',
|
|
17
|
+
* 'User.apiKey': (value, ctx) => ctx.user?.role === 'admin' ? value : '••••••••',
|
|
18
|
+
* 'PaymentMethod.cvv': null,
|
|
19
|
+
* };
|
|
20
|
+
*
|
|
21
|
+
* const masked = maskResult(result, policy, gqlCtx);
|
|
22
|
+
*/
|
|
23
|
+
// ── Core masking ─────────────────────────────────────────────────────────────
|
|
24
|
+
/**
|
|
25
|
+
* Walk the GraphQL execution result and apply the mask policy.
|
|
26
|
+
* Returns a new result object — the original is never mutated.
|
|
27
|
+
*
|
|
28
|
+
* @param result Raw result from `graphql.execute()`
|
|
29
|
+
* @param policy Field masking policy
|
|
30
|
+
* @param ctx Arbitrary context forwarded to MaskFn handlers
|
|
31
|
+
* @param typePath Current object type path for nested resolution (internal use)
|
|
32
|
+
*/
|
|
33
|
+
export function maskResult(result, policy, ctx) {
|
|
34
|
+
if (!result.data)
|
|
35
|
+
return result;
|
|
36
|
+
return {
|
|
37
|
+
...result,
|
|
38
|
+
data: maskValue(result.data, '__ROOT__', policy, ctx),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
// ── Internal walker ─────────────────────────────────────────────────────────
|
|
42
|
+
function maskValue(value, typePath, policy, ctx) {
|
|
43
|
+
if (value === null || value === undefined)
|
|
44
|
+
return value;
|
|
45
|
+
if (Array.isArray(value)) {
|
|
46
|
+
return value.map(item => maskValue(item, typePath, policy, ctx));
|
|
47
|
+
}
|
|
48
|
+
if (typeof value === 'object') {
|
|
49
|
+
const obj = value;
|
|
50
|
+
// Detect GraphQL __typename for type-aware masking
|
|
51
|
+
const typeName = typeof obj['__typename'] === 'string'
|
|
52
|
+
? obj['__typename']
|
|
53
|
+
: typePath;
|
|
54
|
+
const masked = {};
|
|
55
|
+
for (const [key, val] of Object.entries(obj)) {
|
|
56
|
+
// Build lookup keys: "TypeName.fieldName" first, then bare "fieldName"
|
|
57
|
+
const qualifiedKey = `${typeName}.${key}`;
|
|
58
|
+
const hasMasked = qualifiedKey in policy || key in policy;
|
|
59
|
+
if (hasMasked) {
|
|
60
|
+
const policyEntry = qualifiedKey in policy ? policy[qualifiedKey] : policy[key];
|
|
61
|
+
if (typeof policyEntry === 'function') {
|
|
62
|
+
masked[key] = policyEntry(val, ctx);
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
masked[key] = policyEntry;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
// Recurse into nested objects — pass the field name as a type hint
|
|
70
|
+
// (if the nested object has __typename we'll pick it up there)
|
|
71
|
+
masked[key] = maskValue(val, key, policy, ctx);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return masked;
|
|
75
|
+
}
|
|
76
|
+
return value;
|
|
77
|
+
}
|
|
78
|
+
// ── Helper: build a role-based mask function ─────────────────────────────────
|
|
79
|
+
/**
|
|
80
|
+
* Convenience factory: returns null when `guardFn` returns false, otherwise
|
|
81
|
+
* passes the raw value through unchanged.
|
|
82
|
+
*
|
|
83
|
+
* @example
|
|
84
|
+
* 'User.apiKey': allowWhen((ctx: MyCtx) => ctx.user?.role === 'admin')
|
|
85
|
+
*/
|
|
86
|
+
export function allowWhen(guardFn) {
|
|
87
|
+
return (rawValue, ctx) => (guardFn(ctx, rawValue) ? rawValue : null);
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Convenience factory: return `replacement` when the guard fails.
|
|
91
|
+
*
|
|
92
|
+
* @example
|
|
93
|
+
* 'User.email': redactUnless((ctx: MyCtx) => ctx.user?.id === ctx.vars?.userId, '***@***.***')
|
|
94
|
+
*/
|
|
95
|
+
export function redactUnless(guardFn, replacement = 'REDACTED') {
|
|
96
|
+
return (rawValue, ctx) => (guardFn(ctx, rawValue) ? rawValue : replacement);
|
|
97
|
+
}
|
|
98
|
+
//# sourceMappingURL=mask.js.map
|
package/dist/mask.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mask.js","sourceRoot":"","sources":["../src/mask.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AA0BH,gFAAgF;AAEhF;;;;;;;;GAQG;AACH,MAAM,UAAU,UAAU,CACxB,MAA8B,EAC9B,MAAuB,EACvB,GAAQ;IAER,IAAI,CAAC,MAAM,CAAC,IAAI;QAAE,OAAO,MAAM,CAAC;IAChC,OAAO;QACL,GAAG,MAAM;QACT,IAAI,EAAE,SAAS,CAAC,MAAM,CAAC,IAAI,EAAE,UAAU,EAAE,MAAM,EAAE,GAAG,CAA4B;KACjF,CAAC;AACJ,CAAC;AAED,+EAA+E;AAE/E,SAAS,SAAS,CAChB,KAAiB,EACjB,QAAgB,EAChB,MAAyB,EACzB,GAAa;IAEb,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,KAAK,SAAS;QAAE,OAAO,KAAK,CAAC;IAExD,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QACzB,OAAO,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,SAAS,CAAC,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,GAAG,CAAC,CAAC,CAAC;IACnE,CAAC;IAED,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QAC9B,MAAM,GAAG,GAAG,KAAgC,CAAC;QAC7C,mDAAmD;QACnD,MAAM,QAAQ,GAAW,OAAO,GAAG,CAAC,YAAY,CAAC,KAAK,QAAQ;YAC5D,CAAC,CAAC,GAAG,CAAC,YAAY,CAAC;YACnB,CAAC,CAAC,QAAQ,CAAC;QAEb,MAAM,MAAM,GAA4B,EAAE,CAAC;QAC3C,KAAK,MAAM,CAAC,GAAG,EAAE,GAAG,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC;YAC7C,uEAAuE;YACvE,MAAM,YAAY,GAAG,GAAG,QAAQ,IAAI,GAAG,EAAE,CAAC;YAC1C,MAAM,SAAS,GAAM,YAAY,IAAI,MAAM,IAAI,GAAG,IAAI,MAAM,CAAC;YAE7D,IAAI,SAAS,EAAE,CAAC;gBACd,MAAM,WAAW,GAAG,YAAY,IAAI,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;gBAChF,IAAI,OAAO,WAAW,KAAK,UAAU,EAAE,CAAC;oBACtC,MAAM,CAAC,GAAG,CAAC,GAAI,WAA2B,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC;gBACvD,CAAC;qBAAM,CAAC;oBACN,MAAM,CAAC,GAAG,CAAC,GAAG,WAAW,CAAC;gBAC5B,CAAC;YACH,CAAC;iBAAM,CAAC;gBACN,mEAAmE;gBACnE,+DAA+D;gBAC/D,MAAM,CAAC,GAAG,CAAC,GAAG,SAAS,CAAC,GAAG,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,CAAC,CAAC;YACjD,CAAC;QACH,CAAC;QACD,OAAO,MAAM,CAAC;IAChB,CAAC;IAED,OAAO,KAAK,CAAC;AACf,CAAC;AAED,gFAAgF;AAEhF;;;;;;GAMG;AACH,MAAM,UAAU,SAAS,CACvB,OAAiD;IAEjD,OAAO,CAAC,QAAQ,EAAE,GAAG,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,GAAG,EAAE,QAAQ,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;AACvE,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,YAAY,CAC1B,OAAqD,EACrD,cAAuB,UAAU;IAEjC,OAAO,CAAC,QAAQ,EAAE,GAAG,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,GAAG,EAAE,QAAQ,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC;AAC9E,CAAC"}
|