@zintrust/core 0.5.9 → 0.7.2

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 (65) hide show
  1. package/package.json +7 -5
  2. package/src/auth/LoginFlow.d.ts +65 -0
  3. package/src/auth/LoginFlow.d.ts.map +1 -0
  4. package/src/auth/LoginFlow.js +352 -0
  5. package/src/cache/drivers/KVRemoteDriver.d.ts.map +1 -1
  6. package/src/cache/drivers/KVRemoteDriver.js +9 -3
  7. package/src/cli/commands/MigrateCommand.js +1 -1
  8. package/src/cli/commands/ScheduleListCommand.d.ts.map +1 -1
  9. package/src/cli/commands/ScheduleListCommand.js +3 -0
  10. package/src/cli/commands/ScheduleRunCommand.d.ts.map +1 -1
  11. package/src/cli/commands/ScheduleRunCommand.js +3 -0
  12. package/src/cli/commands/ScheduleStartCommand.d.ts.map +1 -1
  13. package/src/cli/commands/ScheduleStartCommand.js +3 -0
  14. package/src/cli/commands/schedule/ScheduleCliSupport.d.ts +1 -0
  15. package/src/cli/commands/schedule/ScheduleCliSupport.d.ts.map +1 -1
  16. package/src/cli/commands/schedule/ScheduleCliSupport.js +116 -6
  17. package/src/cli/scaffolding/GovernanceScaffolder.d.ts.map +1 -1
  18. package/src/cli/scaffolding/GovernanceScaffolder.js +22 -3
  19. package/src/cli/scaffolding/MigrationGenerator.d.ts.map +1 -1
  20. package/src/cli/scaffolding/MigrationGenerator.js +2 -1
  21. package/src/cli/scaffolding/ProjectScaffolder.d.ts.map +1 -1
  22. package/src/cli/scaffolding/ProjectScaffolder.js +79 -8
  23. package/src/cli/scaffolding/ScaffoldingVersionUtils.js +1 -1
  24. package/src/common/ContextLoader.d.ts +27 -0
  25. package/src/common/ContextLoader.d.ts.map +1 -0
  26. package/src/common/ContextLoader.js +187 -0
  27. package/src/config/logger.d.ts +2 -0
  28. package/src/config/logger.d.ts.map +1 -1
  29. package/src/config/logger.js +12 -0
  30. package/src/index.d.ts +7 -0
  31. package/src/index.d.ts.map +1 -1
  32. package/src/index.js +7 -3
  33. package/src/orm/Model.d.ts.map +1 -1
  34. package/src/orm/Model.js +33 -1
  35. package/src/orm/adapters/D1Adapter.d.ts.map +1 -1
  36. package/src/orm/adapters/D1Adapter.js +1 -1
  37. package/src/orm/adapters/MySQLAdapter.d.ts.map +1 -1
  38. package/src/orm/adapters/MySQLAdapter.js +1 -1
  39. package/src/orm/adapters/MySQLProxyAdapter.d.ts.map +1 -1
  40. package/src/orm/adapters/MySQLProxyAdapter.js +11 -9
  41. package/src/orm/adapters/PostgreSQLAdapter.js +1 -1
  42. package/src/orm/adapters/SQLServerAdapter.d.ts.map +1 -1
  43. package/src/orm/adapters/SQLServerAdapter.js +1 -1
  44. package/src/orm/adapters/SQLiteAdapter.js +1 -1
  45. package/src/proxy/ProxyServerUtils.d.ts.map +1 -1
  46. package/src/proxy/ProxyServerUtils.js +15 -11
  47. package/src/security/SecurePayload.d.ts +38 -0
  48. package/src/security/SecurePayload.d.ts.map +1 -0
  49. package/src/security/SecurePayload.js +214 -0
  50. package/src/templates/project/basic/app/Controllers/AuthController.ts.tpl +132 -46
  51. package/src/templates/project/basic/package.json.tpl +6 -2
  52. package/src/tools/notification/Composer.d.ts +40 -0
  53. package/src/tools/notification/Composer.d.ts.map +1 -0
  54. package/src/tools/notification/Composer.js +140 -0
  55. package/src/tools/notification/Notification.d.ts +6 -0
  56. package/src/tools/notification/Notification.d.ts.map +1 -1
  57. package/src/tools/notification/Notification.js +7 -0
  58. package/src/tools/queue/AdvancedQueue.js +15 -0
  59. package/src/tools/queue/Queue.d.ts +1 -0
  60. package/src/tools/queue/Queue.d.ts.map +1 -1
  61. package/src/types/Queue.d.ts +1 -0
  62. package/src/types/Queue.d.ts.map +1 -1
  63. package/src/zintrust.comon.d.ts +0 -11
  64. package/src/zintrust.comon.d.ts.map +0 -1
  65. package/src/zintrust.comon.js +0 -17
@@ -0,0 +1,214 @@
1
+ import { ErrorFactory } from '../exceptions/ZintrustError.js';
2
+ import { isObject, isString } from '../helper/index.js';
3
+ import { EncryptedEnvelope } from './EncryptedEnvelope.js';
4
+ import { Validator } from '../validation/Validator.js';
5
+ const decryptors = new Map();
6
+ const createStageError = (stage, message, details) => {
7
+ return ErrorFactory.createValidationError(`SecurePayload ${stage} failed: ${message}`, {
8
+ stage,
9
+ details,
10
+ });
11
+ };
12
+ const resolveDecryptor = (decryptor) => {
13
+ if (typeof decryptor === 'function') {
14
+ return decryptor;
15
+ }
16
+ if (typeof decryptor === 'string' && decryptor.trim().length > 0) {
17
+ const registered = decryptors.get(decryptor);
18
+ if (registered) {
19
+ return registered;
20
+ }
21
+ throw createStageError('decrypt', `Unknown decryptor: ${decryptor}`);
22
+ }
23
+ throw createStageError('decrypt', 'No decryptor was provided');
24
+ };
25
+ const coerceBoolean = (value) => {
26
+ if (typeof value === 'boolean')
27
+ return value;
28
+ if (typeof value === 'number') {
29
+ if (value === 1)
30
+ return true;
31
+ if (value === 0)
32
+ return false;
33
+ }
34
+ if (typeof value === 'string') {
35
+ const normalized = value.trim().toLowerCase();
36
+ if (['true', '1', 'yes', 'on'].includes(normalized))
37
+ return true;
38
+ if (['false', '0', 'no', 'off'].includes(normalized))
39
+ return false;
40
+ }
41
+ throw createStageError('coerce', 'Boolean coercion failed', { value });
42
+ };
43
+ const throwNumberCoercionError = (value, integer) => {
44
+ throw createStageError('coerce', integer ? 'Integer coercion failed' : 'Numeric coercion failed', {
45
+ value,
46
+ });
47
+ };
48
+ const coerceExistingNumber = (value, integer) => {
49
+ if (!Number.isFinite(value)) {
50
+ throwNumberCoercionError(value, integer);
51
+ }
52
+ if (integer && !Number.isInteger(value)) {
53
+ throwNumberCoercionError(value, true);
54
+ }
55
+ return value;
56
+ };
57
+ const coerceStringNumber = (value, integer) => {
58
+ const trimmed = value.trim();
59
+ if (trimmed.length === 0) {
60
+ throwNumberCoercionError(value, integer);
61
+ }
62
+ if (integer && !/^[-+]?\d+$/.test(trimmed)) {
63
+ throwNumberCoercionError(value, true);
64
+ }
65
+ const parsed = integer ? Number.parseInt(trimmed, 10) : Number.parseFloat(trimmed);
66
+ if (!Number.isFinite(parsed)) {
67
+ throwNumberCoercionError(value, integer);
68
+ }
69
+ return parsed;
70
+ };
71
+ const coerceNumber = (value, integer = false) => {
72
+ if (typeof value === 'number') {
73
+ return coerceExistingNumber(value, integer);
74
+ }
75
+ if (typeof value === 'string') {
76
+ return coerceStringNumber(value, integer);
77
+ }
78
+ return throwNumberCoercionError(value, integer);
79
+ };
80
+ const coerceValue = (value, target) => {
81
+ if (value === null || value === undefined)
82
+ return value;
83
+ if (target === 'string') {
84
+ return isString(value) ? value : String(value);
85
+ }
86
+ if (target === 'number') {
87
+ return coerceNumber(value, false);
88
+ }
89
+ if (target === 'integer') {
90
+ return coerceNumber(value, true);
91
+ }
92
+ return coerceBoolean(value);
93
+ };
94
+ const applyCoercion = (input, shape) => {
95
+ if (!isObject(input)) {
96
+ throw createStageError('coerce', 'Coercion requires a JSON object payload', {
97
+ receivedType: Array.isArray(input) ? 'array' : typeof input,
98
+ });
99
+ }
100
+ const output = { ...input };
101
+ for (const [key, target] of Object.entries(shape)) {
102
+ if (!(key in output))
103
+ continue;
104
+ output[key] = coerceValue(output[key], target);
105
+ }
106
+ return output;
107
+ };
108
+ const validatePayload = (input, schema) => {
109
+ if (!isObject(input)) {
110
+ throw createStageError('validate', 'Validation requires an object payload', {
111
+ receivedType: Array.isArray(input) ? 'array' : typeof input,
112
+ });
113
+ }
114
+ try {
115
+ return Validator.validate(input, schema);
116
+ }
117
+ catch (error) {
118
+ if (error instanceof Error) {
119
+ throw createStageError('validate', error.message, { cause: error.message });
120
+ }
121
+ throw createStageError('validate', 'Schema validation failed', { cause: error });
122
+ }
123
+ };
124
+ const parseJson = (input) => {
125
+ if (!isString(input)) {
126
+ throw createStageError('json', 'JSON parsing requires a string payload', {
127
+ receivedType: Array.isArray(input) ? 'array' : typeof input,
128
+ });
129
+ }
130
+ try {
131
+ return JSON.parse(input);
132
+ }
133
+ catch (error) {
134
+ throw createStageError('json', 'Invalid JSON payload', {
135
+ cause: error instanceof Error ? error.message : String(error),
136
+ });
137
+ }
138
+ };
139
+ const decryptPayload = async (input, options) => {
140
+ if (!isString(input)) {
141
+ throw createStageError('decrypt', 'Encrypted payload must be a string', {
142
+ receivedType: Array.isArray(input) ? 'array' : typeof input,
143
+ });
144
+ }
145
+ const decryptor = resolveDecryptor(options.decryptor);
146
+ try {
147
+ return await decryptor(input, options.context);
148
+ }
149
+ catch (error) {
150
+ if (error instanceof Error) {
151
+ throw createStageError('decrypt', error.message, { cause: error.message });
152
+ }
153
+ throw createStageError('decrypt', 'Decryptor failed', { cause: error });
154
+ }
155
+ };
156
+ const createPipeline = (raw, options, operations = []) => {
157
+ const withOperation = (operation) => {
158
+ return createPipeline(raw, options, [...operations, operation]);
159
+ };
160
+ const execute = async () => {
161
+ return operations.reduce(async (currentPromise, operation) => {
162
+ const current = await currentPromise;
163
+ if (operation.type === 'decrypt') {
164
+ return decryptPayload(current, options);
165
+ }
166
+ if (operation.type === 'json') {
167
+ return parseJson(current);
168
+ }
169
+ if (operation.type === 'coerce') {
170
+ return applyCoercion(current, operation.shape);
171
+ }
172
+ return validatePayload(current, operation.schema);
173
+ }, Promise.resolve(raw));
174
+ };
175
+ return Object.freeze({
176
+ decrypt: () => withOperation({ type: 'decrypt' }),
177
+ json: () => withOperation({ type: 'json' }),
178
+ coerce: (shape) => withOperation({ type: 'coerce', shape }),
179
+ validate: (schema) => withOperation({ type: 'validate', schema }),
180
+ value: async () => execute(),
181
+ typed: async () => execute(),
182
+ });
183
+ };
184
+ export const SecurePayload = Object.freeze({
185
+ registerDecryptor(name, decryptor) {
186
+ if (!isString(name) || name.trim().length === 0) {
187
+ throw ErrorFactory.createValidationError('SecurePayload decryptor name must be provided');
188
+ }
189
+ decryptors.set(name.trim(), decryptor);
190
+ },
191
+ unregisterDecryptor(name) {
192
+ return decryptors.delete(name);
193
+ },
194
+ hasDecryptor(name) {
195
+ return decryptors.has(name);
196
+ },
197
+ listDecryptors() {
198
+ return Array.from(decryptors.keys()).sort((left, right) => left.localeCompare(right));
199
+ },
200
+ clearDecryptors() {
201
+ decryptors.clear();
202
+ },
203
+ createEnvelopeDecryptor(options) {
204
+ return (raw) => EncryptedEnvelope.decryptString(raw, {
205
+ cipher: options.cipher,
206
+ key: options.key,
207
+ previousKeys: options.previousKeys,
208
+ });
209
+ },
210
+ decode(raw, options = {}) {
211
+ return createPipeline(raw, options);
212
+ },
213
+ });
214
+ export default SecurePayload;
@@ -8,15 +8,15 @@ import type { AuthControllerApi, JsonRecord, UserRow } from '@app/Types/controll
8
8
  import type { IRequest, IResponse } from '@zintrust/core';
9
9
  import {
10
10
  Auth,
11
+ ErrorFactory,
11
12
  JwtManager,
13
+ LoginFlow,
12
14
  Logger,
13
15
  getString,
14
16
  getValidatedBody,
15
17
  isUndefinedOrNull,
16
18
  } from '@zintrust/core';
17
19
 
18
-
19
-
20
20
  const pickPublicUser = (row: UserRow): { id: unknown; name: string; email: string } => {
21
21
  return {
22
22
  id: row.id,
@@ -25,75 +25,155 @@ const pickPublicUser = (row: UserRow): { id: unknown; name: string; email: strin
25
25
  };
26
26
  };
27
27
 
28
- /**
29
- * Authenticates a user by email and password.
30
- * Validates credentials against the database and returns a JWT access token on success.
31
- * Logs all authentication attempts for security auditing.
32
- * @param req - HTTP request containing email and password
33
- * @param res - HTTP response to send authentication result
34
- * @returns Promise that resolves after sending the response
35
- */
36
- async function login(req: IRequest, res: IResponse): Promise<void> {
37
- const body = getValidatedBody<JsonRecord>(req);
38
- if (!body) {
39
- Logger.error('AuthController.login: validation middleware did not populate req.validated.body');
40
- return res.setStatus(500).json({ error: 'Internal server error' });
28
+ type PasswordLoginContext = {
29
+ email: string;
30
+ ipAddress: string;
31
+ requestId?: string;
32
+ };
33
+
34
+ const toSubject = (id: unknown): string | undefined => {
35
+ if (typeof id === 'string' && id.length > 0) return id;
36
+ if (typeof id === 'number' && Number.isFinite(id)) return String(id);
37
+ return undefined;
38
+ };
39
+
40
+ const toDeviceId = (subject: string | undefined): string | undefined => {
41
+ return isUndefinedOrNull(subject) ? undefined : `dev-${subject}`;
42
+ };
43
+
44
+ const getClaimString = (claims: unknown, key: string): string | undefined => {
45
+ if (typeof claims !== 'object' || claims === null) {
46
+ return undefined;
41
47
  }
42
- const email = getString(body['email']);
43
- const password = getString(body['password']);
44
- const ipAddress = req.getRaw().socket.remoteAddress ?? 'unknown';
45
48
 
46
- try {
47
- const existing = await User.where('email', '=', email).limit(1).first<UserRow>();
49
+ const value = (claims as Record<string, unknown>)[key];
50
+ return typeof value === 'string' && value.trim() !== '' ? value : undefined;
51
+ };
52
+
53
+ const getIssuedToken = (issued: unknown): string => {
54
+ if (typeof issued === 'string' && issued.trim() !== '') {
55
+ return issued;
56
+ }
57
+
58
+ throw ErrorFactory.createSecurityError('LoginFlow jwt issuer returned an invalid access token');
59
+ };
60
+
61
+ const isLoginFlowUnauthorizedFailure = (error: unknown): boolean => {
62
+ if (typeof error !== 'object' || error === null) {
63
+ return false;
64
+ }
65
+
66
+ const details = (error as { details?: unknown }).details;
67
+ if (typeof details !== 'object' || details === null) {
68
+ return false;
69
+ }
70
+
71
+ const nested = (details as Record<string, unknown>)['error'];
72
+ return (
73
+ typeof nested === 'object' &&
74
+ nested !== null &&
75
+ (nested as { statusCode?: unknown }).statusCode === 401
76
+ );
77
+ };
78
+
79
+ const passwordLoginProvider = Object.freeze({
80
+ async identify(
81
+ input: { email: string },
82
+ context: PasswordLoginContext
83
+ ): Promise<UserRow | null> {
84
+ const existing = await User.where('email', '=', input.email).first<UserRow>();
48
85
 
49
86
  if (existing === null) {
50
87
  Logger.warn('AuthController.login: failed login attempt', {
51
- email,
52
- ip: ipAddress,
88
+ email: context.email,
89
+ ip: context.ipAddress,
53
90
  reason: 'user_not_found',
91
+ ...(context.requestId ? { requestId: context.requestId } : {}),
54
92
  timestamp: new Date().toISOString(),
55
93
  });
56
- res.setStatus(401).json({ error: 'Invalid credentials' });
57
- return;
58
94
  }
59
95
 
60
- const passwordHash = getString(existing.password);
61
- const ok = await Auth.compare(password, passwordHash);
96
+ return existing;
97
+ },
98
+
99
+ async verify(
100
+ identity: UserRow | null,
101
+ input: { password: string },
102
+ context: PasswordLoginContext
103
+ ) {
104
+ if (identity === null) {
105
+ throw ErrorFactory.createUnauthorizedError('Invalid credentials');
106
+ }
107
+
108
+ const passwordHash = getString(identity.password);
109
+ const ok = await Auth.compare(input.password, passwordHash);
62
110
  if (!ok) {
63
111
  Logger.warn('AuthController.login: failed login attempt', {
64
- email,
65
- ip: ipAddress,
112
+ email: context.email,
113
+ ip: context.ipAddress,
66
114
  reason: 'invalid_password',
115
+ ...(context.requestId ? { requestId: context.requestId } : {}),
67
116
  timestamp: new Date().toISOString(),
68
117
  });
69
- res.setStatus(401).json({ error: 'Invalid credentials' });
70
- return;
118
+ throw ErrorFactory.createUnauthorizedError('Invalid credentials');
71
119
  }
72
120
 
73
- const user = pickPublicUser(existing);
121
+ const user = pickPublicUser(identity);
122
+ const subject = toSubject(user.id);
123
+ const deviceId = toDeviceId(subject);
124
+
125
+ return {
126
+ user,
127
+ subject,
128
+ claims: {
129
+ sub: subject,
130
+ email: user.email,
131
+ ...(isUndefinedOrNull(deviceId) ? {} : { deviceId }),
132
+ },
133
+ };
134
+ },
135
+ });
74
136
 
75
- const subject = ((): string | undefined => {
76
- const id = user.id;
77
- if (typeof id === 'string' && id.length > 0) return id;
78
- if (typeof id === 'number' && Number.isFinite(id)) return String(id);
79
- return undefined;
80
- })();
137
+ /**
138
+ * Authenticates a user by email and password.
139
+ * Validates credentials against the database and returns a JWT access token on success.
140
+ * Logs all authentication attempts for security auditing.
141
+ * @param req - HTTP request containing email and password
142
+ * @param res - HTTP response to send authentication result
143
+ * @returns Promise that resolves after sending the response
144
+ */
145
+ async function login(req: IRequest, res: IResponse): Promise<void> {
146
+ const body = getValidatedBody<JsonRecord>(req);
147
+ if (!body) {
148
+ Logger.error('AuthController.login: validation middleware did not populate req.validated.body');
149
+ return res.setStatus(500).json({ error: 'Internal server error' });
150
+ }
151
+ const email = getString(body['email']);
152
+ const password = getString(body['password']);
153
+ const ipAddress = req.getRaw().socket.remoteAddress ?? 'unknown';
154
+ const requestId = typeof req.getHeader === 'function' ? req.getHeader('x-request-id') : undefined;
81
155
 
82
- // Bulletproof Auth (device binding) expects a device id header to match a JWT claim.
83
- // For the example app, we mint a stable device id derived from the subject.
84
- // Production apps should issue a per-device id and manage a per-device signing secret.
85
- const deviceId = isUndefinedOrNull(subject) ? undefined : `dev-${subject}`;
156
+ try {
157
+ const result = await LoginFlow.create({
158
+ provider: passwordLoginProvider,
159
+ context: Object.freeze({ email, ipAddress, requestId }),
160
+ })
161
+ .identify({ email })
162
+ .verify({ password })
163
+ .issue('jwt')
164
+ .audit()
165
+ .run();
86
166
 
87
- const token = await JwtManager.signAccessToken({
88
- sub: subject,
89
- email,
90
- ...(isUndefinedOrNull(deviceId) ? {} : { deviceId }),
91
- });
167
+ const user = result.verified.user as { id: unknown; name: string; email: string };
168
+ const subject = getClaimString(result.verified.claims, 'sub');
169
+ const deviceId = getClaimString(result.verified.claims, 'deviceId');
170
+ const token = getIssuedToken(result.issued);
92
171
 
93
172
  Logger.info('AuthController.login: successful login', {
94
173
  userId: subject,
95
174
  email,
96
175
  ip: ipAddress,
176
+ ...(requestId ? { requestId } : {}),
97
177
  timestamp: new Date().toISOString(),
98
178
  });
99
179
 
@@ -104,9 +184,15 @@ async function login(req: IRequest, res: IResponse): Promise<void> {
104
184
  user,
105
185
  });
106
186
  } catch (error) {
187
+ if (isLoginFlowUnauthorizedFailure(error)) {
188
+ res.setStatus(401).json({ error: 'Invalid credentials' });
189
+ return;
190
+ }
191
+
107
192
  Logger.error('AuthController.login: unexpected error', {
108
193
  email,
109
194
  ip: ipAddress,
195
+ ...(requestId ? { requestId } : {}),
110
196
  error: error instanceof Error ? error.message : String(error),
111
197
  timestamp: new Date().toISOString(),
112
198
  });
@@ -12,8 +12,7 @@
12
12
  "type-check": "tsc --noEmit"
13
13
  },
14
14
  "dependencies": {
15
- "@zintrust/core": "^{{coreVersion}}",
16
- "@zintrust/d1-migrator": "^0.4.6"
15
+ "@zintrust/core": "^{{coreVersion}}"
17
16
  },
18
17
  "devDependencies": {
19
18
  "@zintrust/governance": "{{governanceVersion}}",
@@ -23,5 +22,10 @@
23
22
  "tsc-alias": "^1.8.16",
24
23
  "typescript": "^5.9.3",
25
24
  "vitest": "^4.0.16"
25
+ },
26
+ "overrides": {
27
+ "@zintrust/governance": {
28
+ "@zintrust/core": ">=0.6.0"
29
+ }
26
30
  }
27
31
  }
@@ -0,0 +1,40 @@
1
+ export type NotificationComposePolicy = 'required' | 'best_effort';
2
+ export type NotificationChannelHandler<TPayload = unknown, TContext = unknown> = (payload: TPayload, context: TContext) => Promise<unknown>;
3
+ export type NotificationComposeChannelResult = {
4
+ channel: string;
5
+ policy: NotificationComposePolicy;
6
+ ok: boolean;
7
+ payload: unknown;
8
+ result?: unknown;
9
+ error?: unknown;
10
+ };
11
+ export type NotificationComposeResult = {
12
+ ok: boolean;
13
+ results: NotificationComposeChannelResult[];
14
+ };
15
+ export type NotificationComposeError = Error & {
16
+ results: NotificationComposeChannelResult[];
17
+ };
18
+ export type NotificationComposeOptions<TContext = unknown> = {
19
+ context?: TContext;
20
+ };
21
+ export type NotificationComposeBuilder<TContext = unknown> = {
22
+ channel: (name: string, payload: unknown) => NotificationComposeBuilder<TContext>;
23
+ email: (payload: unknown) => NotificationComposeBuilder<TContext>;
24
+ push: (payload: unknown) => NotificationComposeBuilder<TContext>;
25
+ sms: (payload: unknown) => NotificationComposeBuilder<TContext>;
26
+ webhook: (payload: unknown) => NotificationComposeBuilder<TContext>;
27
+ required: (channels: string[]) => NotificationComposeBuilder<TContext>;
28
+ bestEffort: (channels: string[]) => NotificationComposeBuilder<TContext>;
29
+ send: () => Promise<NotificationComposeResult>;
30
+ };
31
+ export type NotificationComposerNamespace = {
32
+ compose: <TContext = unknown>(options?: NotificationComposeOptions<TContext>) => NotificationComposeBuilder<TContext>;
33
+ registerChannel: <TPayload = unknown, TContext = unknown>(name: string, handler: NotificationChannelHandler<TPayload, TContext>) => void;
34
+ unregisterChannel: (name: string) => void;
35
+ hasChannel: (name: string) => boolean;
36
+ listChannels: () => string[];
37
+ clearChannels: () => void;
38
+ };
39
+ export declare const NotificationComposer: NotificationComposerNamespace;
40
+ //# sourceMappingURL=Composer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Composer.d.ts","sourceRoot":"","sources":["../../../../src/tools/notification/Composer.ts"],"names":[],"mappings":"AAIA,MAAM,MAAM,yBAAyB,GAAG,UAAU,GAAG,aAAa,CAAC;AAEnE,MAAM,MAAM,0BAA0B,CAAC,QAAQ,GAAG,OAAO,EAAE,QAAQ,GAAG,OAAO,IAAI,CAC/E,OAAO,EAAE,QAAQ,EACjB,OAAO,EAAE,QAAQ,KACd,OAAO,CAAC,OAAO,CAAC,CAAC;AAEtB,MAAM,MAAM,gCAAgC,GAAG;IAC7C,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,yBAAyB,CAAC;IAClC,EAAE,EAAE,OAAO,CAAC;IACZ,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB,CAAC;AAEF,MAAM,MAAM,yBAAyB,GAAG;IACtC,EAAE,EAAE,OAAO,CAAC;IACZ,OAAO,EAAE,gCAAgC,EAAE,CAAC;CAC7C,CAAC;AAEF,MAAM,MAAM,wBAAwB,GAAG,KAAK,GAAG;IAC7C,OAAO,EAAE,gCAAgC,EAAE,CAAC;CAC7C,CAAC;AAEF,MAAM,MAAM,0BAA0B,CAAC,QAAQ,GAAG,OAAO,IAAI;IAC3D,OAAO,CAAC,EAAE,QAAQ,CAAC;CACpB,CAAC;AAEF,MAAM,MAAM,0BAA0B,CAAC,QAAQ,GAAG,OAAO,IAAI;IAC3D,OAAO,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,KAAK,0BAA0B,CAAC,QAAQ,CAAC,CAAC;IAClF,KAAK,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,0BAA0B,CAAC,QAAQ,CAAC,CAAC;IAClE,IAAI,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,0BAA0B,CAAC,QAAQ,CAAC,CAAC;IACjE,GAAG,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,0BAA0B,CAAC,QAAQ,CAAC,CAAC;IAChE,OAAO,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,0BAA0B,CAAC,QAAQ,CAAC,CAAC;IACpE,QAAQ,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,0BAA0B,CAAC,QAAQ,CAAC,CAAC;IACvE,UAAU,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,0BAA0B,CAAC,QAAQ,CAAC,CAAC;IACzE,IAAI,EAAE,MAAM,OAAO,CAAC,yBAAyB,CAAC,CAAC;CAChD,CAAC;AAEF,MAAM,MAAM,6BAA6B,GAAG;IAC1C,OAAO,EAAE,CAAC,QAAQ,GAAG,OAAO,EAC1B,OAAO,CAAC,EAAE,0BAA0B,CAAC,QAAQ,CAAC,KAC3C,0BAA0B,CAAC,QAAQ,CAAC,CAAC;IAC1C,eAAe,EAAE,CAAC,QAAQ,GAAG,OAAO,EAAE,QAAQ,GAAG,OAAO,EACtD,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,0BAA0B,CAAC,QAAQ,EAAE,QAAQ,CAAC,KACpD,IAAI,CAAC;IACV,iBAAiB,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAC1C,UAAU,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC;IACtC,YAAY,EAAE,MAAM,MAAM,EAAE,CAAC;IAC7B,aAAa,EAAE,MAAM,IAAI,CAAC;CAC3B,CAAC;AAsMF,eAAO,MAAM,oBAAoB,EAAE,6BAOjC,CAAC"}
@@ -0,0 +1,140 @@
1
+ import { SystemTraceBridge } from '../../trace/SystemTraceBridge.js';
2
+ import { ErrorFactory } from '../../exceptions/ZintrustError.js';
3
+ import { isArray, isFunction, isNonEmptyString } from '../../helper/index.js';
4
+ const channelRegistry = new Map();
5
+ const normalizeChannelName = (name) => {
6
+ const normalized = String(name ?? '')
7
+ .trim()
8
+ .toLowerCase();
9
+ if (!isNonEmptyString(normalized)) {
10
+ throw ErrorFactory.createValidationError('Notification channel name must be a non-empty string');
11
+ }
12
+ return normalized;
13
+ };
14
+ const createComposeError = (message, results) => {
15
+ return Object.assign(ErrorFactory.createValidationError(message, {
16
+ results,
17
+ }), { results });
18
+ };
19
+ const ensureChannelsInput = (channels) => {
20
+ if (!isArray(channels)) {
21
+ throw ErrorFactory.createValidationError('Notification compose channel list must be an array');
22
+ }
23
+ return channels.map((channel) => normalizeChannelName(channel));
24
+ };
25
+ const ensureHandler = (handler) => {
26
+ if (!isFunction(handler)) {
27
+ throw ErrorFactory.createValidationError('Notification channel handler must be a function');
28
+ }
29
+ };
30
+ const getPolicy = (policies, channel) => {
31
+ return policies.get(channel) ?? 'required';
32
+ };
33
+ const ensureEntries = (entries) => {
34
+ if (entries.length === 0) {
35
+ throw ErrorFactory.createValidationError('Notification compose requires at least one channel before send()');
36
+ }
37
+ };
38
+ const deliverEntry = async (entry, policy, context) => {
39
+ const handler = channelRegistry.get(entry.channel);
40
+ if (!handler) {
41
+ return {
42
+ channel: entry.channel,
43
+ policy,
44
+ ok: false,
45
+ payload: entry.payload,
46
+ error: ErrorFactory.createConfigError(`Notification compose channel not registered: ${entry.channel}`),
47
+ };
48
+ }
49
+ try {
50
+ const result = await handler(entry.payload, context);
51
+ SystemTraceBridge.emitNotification(`compose:${entry.channel}`, [entry.channel], undefined, undefined, entry.payload);
52
+ return {
53
+ channel: entry.channel,
54
+ policy,
55
+ ok: true,
56
+ payload: entry.payload,
57
+ result,
58
+ };
59
+ }
60
+ catch (error) {
61
+ return {
62
+ channel: entry.channel,
63
+ policy,
64
+ ok: false,
65
+ payload: entry.payload,
66
+ error,
67
+ };
68
+ }
69
+ };
70
+ const hasRequiredFailure = (results) => {
71
+ return results.some((entry) => !entry.ok && entry.policy === 'required');
72
+ };
73
+ const compose = (options = {}) => {
74
+ const entries = [];
75
+ const policies = new Map();
76
+ const builder = Object.freeze({
77
+ channel(name, payload) {
78
+ entries.push({ channel: normalizeChannelName(name), payload });
79
+ return this;
80
+ },
81
+ email(payload) {
82
+ return this.channel('email', payload);
83
+ },
84
+ push(payload) {
85
+ return this.channel('push', payload);
86
+ },
87
+ sms(payload) {
88
+ return this.channel('sms', payload);
89
+ },
90
+ webhook(payload) {
91
+ return this.channel('webhook', payload);
92
+ },
93
+ required(channels) {
94
+ for (const channel of ensureChannelsInput(channels)) {
95
+ policies.set(channel, 'required');
96
+ }
97
+ return this;
98
+ },
99
+ bestEffort(channels) {
100
+ for (const channel of ensureChannelsInput(channels)) {
101
+ policies.set(channel, 'best_effort');
102
+ }
103
+ return this;
104
+ },
105
+ async send() {
106
+ ensureEntries(entries);
107
+ const results = await Promise.all(entries.map(async (entry) => deliverEntry(entry, getPolicy(policies, entry.channel), options.context)));
108
+ if (hasRequiredFailure(results)) {
109
+ throw createComposeError('Notification compose failed for required channels', results);
110
+ }
111
+ return {
112
+ ok: true,
113
+ results,
114
+ };
115
+ },
116
+ });
117
+ return builder;
118
+ };
119
+ const registerChannel = (name, handler) => {
120
+ ensureHandler(handler);
121
+ channelRegistry.set(normalizeChannelName(name), handler);
122
+ };
123
+ const unregisterChannel = (name) => {
124
+ channelRegistry.delete(normalizeChannelName(name));
125
+ };
126
+ const hasChannel = (name) => channelRegistry.has(normalizeChannelName(name));
127
+ const listChannels = () => {
128
+ return Array.from(channelRegistry.keys()).sort((left, right) => left.localeCompare(right));
129
+ };
130
+ const clearChannels = () => {
131
+ channelRegistry.clear();
132
+ };
133
+ export const NotificationComposer = Object.freeze({
134
+ compose,
135
+ registerChannel,
136
+ unregisterChannel,
137
+ hasChannel,
138
+ listChannels,
139
+ clearChannels,
140
+ });
@@ -18,6 +18,12 @@ export declare const Notification: Readonly<{
18
18
  channel: (name: string) => Readonly<{
19
19
  send: (recipient: string, message: string, options?: Record<string, unknown>) => Promise<unknown>;
20
20
  }>;
21
+ compose: <TContext = unknown>(options?: import("./Composer").NotificationComposeOptions<TContext>) => import("./Composer").NotificationComposeBuilder<TContext>;
22
+ registerChannel: <TPayload = unknown, TContext = unknown>(name: string, handler: import("./Composer").NotificationChannelHandler<TPayload, TContext>) => void;
23
+ unregisterChannel: (name: string) => void;
24
+ hasChannel: (name: string) => boolean;
25
+ listChannels: () => string[];
26
+ clearChannels: () => void;
21
27
  listDrivers: () => string[];
22
28
  }>;
23
29
  export default Notification;
@@ -1 +1 @@
1
- {"version":3,"file":"Notification.d.ts","sourceRoot":"","sources":["../../../../src/tools/notification/Notification.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAIH,eAAO,MAAM,YAAY;;;2BAQV,MAAM,WACR,MAAM,kBACA,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,iBACxB;QAAE,SAAS,CAAC,EAAE,MAAM,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE,GACvD,OAAO,CAAC,MAAM,CAAC;qBAcD,MAAM;iCAGN,MAAM,WACR,MAAM,kBACA,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,iBACxB;YAAE,SAAS,CAAC,EAAE,MAAM,CAAA;SAAE;;oBAM1B,MAAM;0BAEM,MAAM,WAAW,MAAM,YAAY,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;;;EAItF,CAAC;AAEH,eAAe,YAAY,CAAC"}
1
+ {"version":3,"file":"Notification.d.ts","sourceRoot":"","sources":["../../../../src/tools/notification/Notification.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAKH,eAAO,MAAM,YAAY;;;2BAQV,MAAM,WACR,MAAM,kBACA,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,iBACxB;QAAE,SAAS,CAAC,EAAE,MAAM,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE,GACvD,OAAO,CAAC,MAAM,CAAC;qBAcD,MAAM;iCAGN,MAAM,WACR,MAAM,kBACA,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,iBACxB;YAAE,SAAS,CAAC,EAAE,MAAM,CAAA;SAAE;;oBAM1B,MAAM;0BAEM,MAAM,WAAW,MAAM,YAAY,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;;;;;;;;;EAUtF,CAAC;AAEH,eAAe,YAAY,CAAC"}