@zintrust/core 0.7.8 → 0.9.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 (34) hide show
  1. package/package.json +1 -1
  2. package/src/auth/LoginFlow.d.ts.map +1 -1
  3. package/src/auth/LoginFlow.js +1 -3
  4. package/src/cli/OptionalCliExtensions.d.ts +1 -0
  5. package/src/cli/OptionalCliExtensions.d.ts.map +1 -1
  6. package/src/cli/OptionalCliExtensions.js +11 -1
  7. package/src/cli/commands/MySqlProxyCommand.d.ts.map +1 -1
  8. package/src/cli/commands/MySqlProxyCommand.js +1 -1
  9. package/src/cli/commands/ScheduleStartCommand.d.ts.map +1 -1
  10. package/src/cli/commands/ScheduleStartCommand.js +14 -9
  11. package/src/cli/scaffolding/GovernanceScaffolder.d.ts.map +1 -1
  12. package/src/cli/scaffolding/GovernanceScaffolder.js +7 -2
  13. package/src/cli/scaffolding/ProjectScaffolder.d.ts.map +1 -1
  14. package/src/cli/scaffolding/ProjectScaffolder.js +3 -10
  15. package/src/helper/index.d.ts +6 -0
  16. package/src/helper/index.d.ts.map +1 -1
  17. package/src/helper/index.js +19 -8
  18. package/src/index.d.ts +7 -4
  19. package/src/index.d.ts.map +1 -1
  20. package/src/index.js +7 -5
  21. package/src/middleware/BulletproofAuthMiddleware.d.ts +2 -1
  22. package/src/middleware/BulletproofAuthMiddleware.d.ts.map +1 -1
  23. package/src/middleware/BulletproofAuthMiddleware.js +103 -39
  24. package/src/security/BulletproofDeviceStore.d.ts.map +1 -1
  25. package/src/security/BulletproofDeviceStore.js +144 -33
  26. package/src/security/JwtVerifier.d.ts +75 -0
  27. package/src/security/JwtVerifier.d.ts.map +1 -0
  28. package/src/security/JwtVerifier.js +336 -0
  29. package/src/templates/project/basic/app/Controllers/AuthController.ts.tpl +1 -1
  30. package/src/templates/project/basic/config/trace.ts.tpl +73 -0
  31. package/src/templates/project/basic/package.json.tpl +1 -1
  32. package/src/zintrust.plugins.d.ts +3 -6
  33. package/src/zintrust.plugins.d.ts.map +1 -1
  34. package/src/zintrust.plugins.js +3 -6
@@ -0,0 +1,336 @@
1
+ import { ErrorFactory } from '../exceptions/ZintrustError.js';
2
+ import { isArray, isNonEmptyString, isObject } from '../helper/index.js';
3
+ import { detectRuntimePlatform, RuntimeServices } from '../runtime/RuntimeServices.js';
4
+ const jwksCache = new Map();
5
+ const getRuntime = () => RuntimeServices.create(detectRuntimePlatform());
6
+ const isFailure = (value) => {
7
+ return isObject(value) && value['ok'] === false && typeof value['reason'] === 'string';
8
+ };
9
+ const toFailure = (reason, message, details) => ({
10
+ ok: false,
11
+ reason,
12
+ message,
13
+ ...(details === undefined ? {} : { details }),
14
+ });
15
+ const toThrownError = (failure) => {
16
+ return ErrorFactory.createSecurityError(failure.message, {
17
+ reason: failure.reason,
18
+ ...(failure.details === undefined ? {} : { details: failure.details }),
19
+ });
20
+ };
21
+ const normalizeBase64Url = (value) => {
22
+ const normalized = value.replaceAll('-', '+').replaceAll('_', '/');
23
+ const padding = normalized.length % 4;
24
+ return padding === 0 ? normalized : normalized.padEnd(normalized.length + (4 - padding), '=');
25
+ };
26
+ const base64UrlToBytes = (value) => {
27
+ const binary = globalThis.atob(normalizeBase64Url(value));
28
+ const bytes = new Uint8Array(binary.length);
29
+ for (let index = 0; index < binary.length; index++) {
30
+ bytes[index] = binary.codePointAt(index) ?? 0;
31
+ }
32
+ return bytes;
33
+ };
34
+ const decodeJwtSegment = (value) => {
35
+ const json = new TextDecoder().decode(base64UrlToBytes(value));
36
+ return JSON.parse(json);
37
+ };
38
+ const parseTokenParts = (token) => {
39
+ const parts = token.split('.');
40
+ if (parts.length !== 3) {
41
+ return toFailure('invalid_token_format', 'JWT must contain header, payload, and signature');
42
+ }
43
+ const [encodedHeader, encodedPayload, encodedSignature] = parts;
44
+ if (!isNonEmptyString(encodedHeader) ||
45
+ !isNonEmptyString(encodedPayload) ||
46
+ !isNonEmptyString(encodedSignature)) {
47
+ return toFailure('invalid_token_format', 'JWT parts must not be empty');
48
+ }
49
+ return { encodedHeader, encodedPayload, encodedSignature };
50
+ };
51
+ const parseHeader = (encodedHeader) => {
52
+ try {
53
+ const header = decodeJwtSegment(encodedHeader);
54
+ if (!isObject(header)) {
55
+ return toFailure('invalid_header', 'JWT header must be an object');
56
+ }
57
+ return header;
58
+ }
59
+ catch (error) {
60
+ return toFailure('invalid_header', 'JWT header is not valid JSON', {
61
+ cause: error instanceof Error ? error.message : String(error),
62
+ });
63
+ }
64
+ };
65
+ const parsePayload = (encodedPayload) => {
66
+ try {
67
+ const payload = decodeJwtSegment(encodedPayload);
68
+ if (!isObject(payload)) {
69
+ return toFailure('invalid_payload', 'JWT payload must be an object');
70
+ }
71
+ return payload;
72
+ }
73
+ catch (error) {
74
+ return toFailure('invalid_payload', 'JWT payload is not valid JSON', {
75
+ cause: error instanceof Error ? error.message : String(error),
76
+ });
77
+ }
78
+ };
79
+ const getAlgorithm = (inputAlgorithm) => {
80
+ return inputAlgorithm ?? 'RS256';
81
+ };
82
+ const validateHeaderAlgorithm = (header, algorithm) => {
83
+ if (header['alg'] !== algorithm) {
84
+ return toFailure('unsupported_algorithm', `JWT algorithm must be ${algorithm}`);
85
+ }
86
+ return undefined;
87
+ };
88
+ const normalizeExpectedValues = (value) => {
89
+ if (value === undefined)
90
+ return [];
91
+ if (typeof value === 'string') {
92
+ const trimmed = value.trim();
93
+ return trimmed === '' ? [] : [trimmed];
94
+ }
95
+ return value
96
+ .map((item) => (typeof item === 'string' ? item.trim() : ''))
97
+ .filter((item) => item !== '');
98
+ };
99
+ const normalizeAudienceClaim = (audience) => {
100
+ if (typeof audience === 'string') {
101
+ const trimmed = audience.trim();
102
+ return trimmed === '' ? [] : [trimmed];
103
+ }
104
+ if (isArray(audience)) {
105
+ return audience
106
+ .map((item) => (typeof item === 'string' ? item.trim() : ''))
107
+ .filter((item) => item !== '');
108
+ }
109
+ return [];
110
+ };
111
+ const validateClaims = (payload, input) => {
112
+ const nowMs = input.nowMs ?? Date.now();
113
+ if (typeof payload.exp === 'number' &&
114
+ Number.isFinite(payload.exp) &&
115
+ nowMs >= payload.exp * 1000) {
116
+ return toFailure('token_expired', 'JWT has expired');
117
+ }
118
+ if (typeof payload.nbf === 'number' &&
119
+ Number.isFinite(payload.nbf) &&
120
+ nowMs < payload.nbf * 1000) {
121
+ return toFailure('token_not_yet_valid', 'JWT is not valid yet');
122
+ }
123
+ const expectedIssuers = normalizeExpectedValues(input.issuer);
124
+ if (expectedIssuers.length > 0) {
125
+ const issuer = typeof payload.iss === 'string' ? payload.iss.trim() : '';
126
+ if (!expectedIssuers.includes(issuer)) {
127
+ return toFailure('issuer_mismatch', 'JWT issuer did not match the expected issuer', {
128
+ expected: expectedIssuers,
129
+ received: issuer,
130
+ });
131
+ }
132
+ }
133
+ const expectedAudiences = normalizeExpectedValues(input.audience);
134
+ if (expectedAudiences.length > 0) {
135
+ const audiences = normalizeAudienceClaim(payload.aud);
136
+ const matched = audiences.some((audience) => expectedAudiences.includes(audience));
137
+ if (!matched) {
138
+ return toFailure('audience_mismatch', 'JWT audience did not match the expected audience', {
139
+ expected: expectedAudiences,
140
+ received: audiences,
141
+ });
142
+ }
143
+ }
144
+ return undefined;
145
+ };
146
+ const validateJwkForAlgorithm = (jwk, algorithm) => {
147
+ if (jwk.kty !== 'RSA') {
148
+ return toFailure('invalid_jwk', 'JWK must use RSA for RS256 verification', {
149
+ kty: jwk.kty,
150
+ });
151
+ }
152
+ if (!isNonEmptyString(jwk.n) || !isNonEmptyString(jwk.e)) {
153
+ return toFailure('invalid_jwk', 'JWK must include RSA modulus and exponent');
154
+ }
155
+ if (isNonEmptyString(jwk.alg) && jwk.alg !== algorithm) {
156
+ return toFailure('invalid_jwk', 'JWK algorithm does not match the requested JWT algorithm', {
157
+ expected: algorithm,
158
+ received: jwk.alg,
159
+ });
160
+ }
161
+ if (isNonEmptyString(jwk.use) && jwk.use !== 'sig') {
162
+ return toFailure('invalid_jwk', 'JWK use must allow signature verification', {
163
+ use: jwk.use,
164
+ });
165
+ }
166
+ return undefined;
167
+ };
168
+ const verifyRs256Signature = async (params) => {
169
+ const subtle = getRuntime().crypto.subtle;
170
+ try {
171
+ const key = await subtle.importKey('jwk', params.jwk, { name: 'RSASSA-PKCS1-v1_5', hash: 'SHA-256' }, false, ['verify']);
172
+ const signature = base64UrlToBytes(params.encodedSignature);
173
+ const signingInput = new TextEncoder().encode(params.signingInput);
174
+ return await subtle.verify('RSASSA-PKCS1-v1_5', key, signature, signingInput);
175
+ }
176
+ catch (error) {
177
+ return toFailure('invalid_jwk', 'JWK could not be imported for signature verification', {
178
+ cause: error instanceof Error ? error.message : String(error),
179
+ });
180
+ }
181
+ };
182
+ const fetchJwks = async (input) => {
183
+ const cacheKey = (input.cacheKey ?? input.jwksUrl).trim();
184
+ const nowMs = input.nowMs ?? Date.now();
185
+ const cached = jwksCache.get(cacheKey);
186
+ if (cached !== undefined && cached.expiresAtMs > nowMs) {
187
+ return { jwks: cached.jwks, cacheHit: true };
188
+ }
189
+ const fetcher = input.fetcher ?? getRuntime().fetch;
190
+ let response;
191
+ try {
192
+ response = await fetcher(input.jwksUrl, {
193
+ headers: { accept: 'application/json' },
194
+ });
195
+ }
196
+ catch (error) {
197
+ return toFailure('jwks_fetch_failed', 'Failed to fetch JWKS document', {
198
+ jwksUrl: input.jwksUrl,
199
+ cause: error instanceof Error ? error.message : String(error),
200
+ });
201
+ }
202
+ if (!response.ok) {
203
+ return toFailure('jwks_fetch_failed', 'JWKS endpoint returned a non-success response', {
204
+ jwksUrl: input.jwksUrl,
205
+ status: response.status,
206
+ });
207
+ }
208
+ let body;
209
+ try {
210
+ body = (await response.json());
211
+ }
212
+ catch (error) {
213
+ return toFailure('invalid_jwks', 'JWKS response was not valid JSON', {
214
+ jwksUrl: input.jwksUrl,
215
+ cause: error instanceof Error ? error.message : String(error),
216
+ });
217
+ }
218
+ if (!isObject(body) || !isArray(body['keys'])) {
219
+ return toFailure('invalid_jwks', 'JWKS response must contain a keys array', {
220
+ jwksUrl: input.jwksUrl,
221
+ });
222
+ }
223
+ const jwks = {
224
+ keys: body['keys'].filter((item) => isObject(item)),
225
+ };
226
+ const cacheTtlSeconds = typeof input.cacheTtlSeconds === 'number' && Number.isFinite(input.cacheTtlSeconds)
227
+ ? input.cacheTtlSeconds
228
+ : 3600;
229
+ const ttlMs = Math.max(1, cacheTtlSeconds) * 1000;
230
+ jwksCache.set(cacheKey, { jwks, expiresAtMs: nowMs + ttlMs });
231
+ return { jwks, cacheHit: false };
232
+ };
233
+ const resolveJwkFromJwks = (header, jwks, algorithm) => {
234
+ const kid = typeof header['kid'] === 'string' ? header['kid'].trim() : '';
235
+ if (kid === '') {
236
+ return toFailure('missing_kid', 'JWT header must include a kid when verifying with JWKS');
237
+ }
238
+ const jwk = jwks.keys.find((item) => {
239
+ if (item.kid !== kid)
240
+ return false;
241
+ if (isNonEmptyString(item.alg) && item.alg !== algorithm)
242
+ return false;
243
+ if (isNonEmptyString(item.use) && item.use !== 'sig')
244
+ return false;
245
+ return true;
246
+ });
247
+ if (jwk === undefined) {
248
+ return toFailure('key_not_found', 'No matching JWK was found for the JWT kid', { kid });
249
+ }
250
+ return jwk;
251
+ };
252
+ const verifyTokenWithJwk = async (input, jwk, cacheHit) => {
253
+ const tokenParts = parseTokenParts(input.token);
254
+ if (isFailure(tokenParts))
255
+ return tokenParts;
256
+ const header = parseHeader(tokenParts.encodedHeader);
257
+ if (isFailure(header))
258
+ return header;
259
+ const payload = parsePayload(tokenParts.encodedPayload);
260
+ if (isFailure(payload))
261
+ return payload;
262
+ const algorithm = getAlgorithm(input.algorithm);
263
+ const headerFailure = validateHeaderAlgorithm(header, algorithm);
264
+ if (headerFailure !== undefined)
265
+ return headerFailure;
266
+ const jwkFailure = validateJwkForAlgorithm(jwk, algorithm);
267
+ if (jwkFailure !== undefined)
268
+ return jwkFailure;
269
+ const signatureCheck = await verifyRs256Signature({
270
+ jwk,
271
+ signingInput: `${tokenParts.encodedHeader}.${tokenParts.encodedPayload}`,
272
+ encodedSignature: tokenParts.encodedSignature,
273
+ });
274
+ if (signatureCheck !== true) {
275
+ return signatureCheck === false
276
+ ? toFailure('invalid_signature', 'JWT signature could not be verified')
277
+ : signatureCheck;
278
+ }
279
+ const claimsFailure = validateClaims(payload, input);
280
+ if (claimsFailure !== undefined)
281
+ return claimsFailure;
282
+ return { ok: true, payload, header, jwk, ...(cacheHit === undefined ? {} : { cacheHit }) };
283
+ };
284
+ const verifyWithJwkResult = async (input) => {
285
+ return verifyTokenWithJwk(input, input.jwk);
286
+ };
287
+ const verifyWithJwksResult = async (input) => {
288
+ const tokenParts = parseTokenParts(input.token);
289
+ if (isFailure(tokenParts))
290
+ return tokenParts;
291
+ const header = parseHeader(tokenParts.encodedHeader);
292
+ if (isFailure(header))
293
+ return header;
294
+ const algorithm = getAlgorithm(input.algorithm);
295
+ const headerFailure = validateHeaderAlgorithm(header, algorithm);
296
+ if (headerFailure !== undefined)
297
+ return headerFailure;
298
+ const kid = typeof header['kid'] === 'string' ? header['kid'].trim() : '';
299
+ if (kid === '') {
300
+ return toFailure('missing_kid', 'JWT header must include a kid when verifying with JWKS');
301
+ }
302
+ const fetched = await fetchJwks(input);
303
+ if (isFailure(fetched))
304
+ return fetched;
305
+ const jwk = resolveJwkFromJwks(header, fetched.jwks, algorithm);
306
+ if (isFailure(jwk))
307
+ return jwk;
308
+ return verifyTokenWithJwk(input, jwk, fetched.cacheHit);
309
+ };
310
+ const verifyWithJwk = async (input) => {
311
+ const result = await verifyWithJwkResult(input);
312
+ if (!result.ok)
313
+ throw toThrownError(result);
314
+ return result.payload;
315
+ };
316
+ const verifyWithJwks = async (input) => {
317
+ const result = await verifyWithJwksResult(input);
318
+ if (!result.ok)
319
+ throw toThrownError(result);
320
+ return result.payload;
321
+ };
322
+ const clearCache = (cacheKey) => {
323
+ if (!isNonEmptyString(cacheKey)) {
324
+ jwksCache.clear();
325
+ return;
326
+ }
327
+ jwksCache.delete(cacheKey.trim());
328
+ };
329
+ export const JwtVerifier = Object.freeze({
330
+ clearCache,
331
+ verifyWithJwk,
332
+ verifyWithJwkResult,
333
+ verifyWithJwks,
334
+ verifyWithJwksResult,
335
+ });
336
+ export default JwtVerifier;
@@ -59,7 +59,7 @@ const getIssuedToken = (issued: unknown): string => {
59
59
  }
60
60
  }
61
61
 
62
- throw ErrorFactory.createSecurityError('LoginFlow jwt issuer returned an invalid access token');
62
+ throw ErrorFactory.createSecurityError('LoginFlow issuer returned an invalid access token');
63
63
  };
64
64
 
65
65
  const getIssuedString = (issued: unknown, key: string): string | undefined => {
@@ -0,0 +1,73 @@
1
+ import { Env } from '@zintrust/core';
2
+ import type { TraceConfigOverrides } from '@zintrust/trace';
3
+
4
+ /**
5
+ * SystemTrace Configuration
6
+ *
7
+ * Keep this file declarative:
8
+ * - Package owns defaults and type validation.
9
+ * - Edit values below to override for this project.
10
+ *
11
+ * Usage: import '@zintrust/trace/register' in your bootstrap.
12
+ * Protect /trace with your own middleware (auth, admin role, etc.).
13
+ */
14
+
15
+ export default {
16
+ enabled: Env.getBool('TRACE_ENABLED', false),
17
+
18
+ // Optional: use a separate DB connection for trace tables.
19
+ // Leave undefined to fall back to the app's default connection.
20
+ connection: Env.get('TRACE_DB_CONNECTION', '') || undefined,
21
+
22
+ pruneAfterHours: Env.getInt('TRACE_PRUNE_HOURS', 72),
23
+
24
+ ignoreRoutes: ['/trace', '/health', '/ping', '/metrics', '/api-docs', '/api-docs-json'],
25
+
26
+ ignorePaths: [
27
+ '/telemetry',
28
+ '/favicon.ico',
29
+ '/robots.txt',
30
+ '/sitemap.xml',
31
+ '/workers',
32
+ '/queue-monitor',
33
+ '.js',
34
+ '.css',
35
+ ],
36
+
37
+ slowQueryThreshold: Env.getInt('TRACE_SLOW_QUERY_MS', 100),
38
+
39
+ logMinLevel: Env.get('TRACE_LOG_LEVEL', 'warn') as 'debug' | 'info' | 'warn' | 'error' | 'fatal',
40
+
41
+ watchers: {
42
+ // Set a watcher to false to disable it entirely.
43
+ // All watchers are enabled by default when trace is enabled.
44
+ // Include/exclude filters are contains-based and can be applied per watcher.
45
+ // request: {
46
+ // get: { exclude: ['report','workers/events'] },
47
+ // post: { include: ['auth'] },
48
+ // patch: { include: ['profile'] },
49
+ // delete: { exclude: ['internal'] },
50
+ // },
51
+ // log: { exclude: ['healthcheck'] },
52
+ // exception: { include: ['trace'] },
53
+ // clientRequest: {
54
+ // exclude: ['internal-http'],
55
+ // sources: {
56
+ // termii: { enabled: false },
57
+ // sendgrid: { responseBody: false },
58
+ // s3: { requestHeaders: false, responseHeaders: false },
59
+ // },
60
+ // },
61
+ // cache: { include: ['session:'] },
62
+ // dump: false, // DumpWatcher is opt-in — enable explicitly if needed
63
+ },
64
+
65
+ redaction: {
66
+ // Extra keys to mask recursively before trace entries are persisted.
67
+ // You can also provide these via TRACE_REDACT_KEYS as JSON or CSV.
68
+ keys: ['password', 'token', 'secret', 'authorization', 'card', 'cardNumber', 'cvv'],
69
+ headers: ['authorization', 'cookie', 'x-api-key', 'x-auth-token'],
70
+ body: ['password', 'token', 'secret', 'apiKey', 'api_key', 'jwt', 'bearer'],
71
+ query: [],
72
+ },
73
+ } satisfies TraceConfigOverrides;
@@ -12,7 +12,7 @@
12
12
  "type-check": "tsc --noEmit"
13
13
  },
14
14
  "dependencies": {
15
- "@zintrust/core": "^{{coreVersion}}"
15
+ "@zintrust/core": "{{coreVersion}}"
16
16
  },
17
17
  "devDependencies": {
18
18
  "@zintrust/governance": "{{governanceVersion}}",
@@ -1,10 +1,7 @@
1
1
  /**
2
- * ZinTrust plugin auto-imports
3
- *
4
- * In real projects, this file is managed by `zin plugin install` and contains
5
- * side-effect imports (e.g. `@zintrust/db-sqlite/register`) that register
6
- * optional adapters/drivers into core registries.
7
- *
2
+ * Auto-generated fallback module.
3
+ * This file is created by scripts/ensure-worker-plugins.mjs when missing.
4
+ * It allows optional runtime plugin imports to resolve in CI/scaffolded setups.
8
5
  */
9
6
  export type {};
10
7
  export declare const __zintrustGeneratedPluginStub = "zintrust.plugins.ts";
@@ -1 +1 @@
1
- {"version":3,"file":"zintrust.plugins.d.ts","sourceRoot":"","sources":["../../src/zintrust.plugins.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAIH,YAAY,EAAE,CAAC;AAgBf,eAAO,MAAM,6BAA6B,wBAAwB,CAAC;;AACnE,wBAAkB"}
1
+ {"version":3,"file":"zintrust.plugins.d.ts","sourceRoot":"","sources":["../../src/zintrust.plugins.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAIH,YAAY,EAAE,CAAC;AAgBf,eAAO,MAAM,6BAA6B,wBAAwB,CAAC;;AACnE,wBAAkB"}
@@ -1,10 +1,7 @@
1
1
  /**
2
- * ZinTrust plugin auto-imports
3
- *
4
- * In real projects, this file is managed by `zin plugin install` and contains
5
- * side-effect imports (e.g. `@zintrust/db-sqlite/register`) that register
6
- * optional adapters/drivers into core registries.
7
- *
2
+ * Auto-generated fallback module.
3
+ * This file is created by scripts/ensure-worker-plugins.mjs when missing.
4
+ * It allows optional runtime plugin imports to resolve in CI/scaffolded setups.
8
5
  */
9
6
  import * as TraceRuntime from './runtime/plugins/trace-runtime.js';
10
7
  globalThis.__zintrust_system_trace_plugin_requested__ = true;