@zintrust/core 0.7.0 → 0.7.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.
Files changed (61) 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 +24 -21
  7. package/src/cli/commands/ScheduleListCommand.d.ts.map +1 -1
  8. package/src/cli/commands/ScheduleListCommand.js +3 -0
  9. package/src/cli/commands/ScheduleRunCommand.d.ts.map +1 -1
  10. package/src/cli/commands/ScheduleRunCommand.js +3 -0
  11. package/src/cli/commands/ScheduleStartCommand.d.ts.map +1 -1
  12. package/src/cli/commands/ScheduleStartCommand.js +3 -0
  13. package/src/cli/commands/schedule/ScheduleCliSupport.d.ts +1 -0
  14. package/src/cli/commands/schedule/ScheduleCliSupport.d.ts.map +1 -1
  15. package/src/cli/commands/schedule/ScheduleCliSupport.js +116 -6
  16. package/src/cli/scaffolding/GovernanceScaffolder.d.ts.map +1 -1
  17. package/src/cli/scaffolding/GovernanceScaffolder.js +22 -3
  18. package/src/cli/scaffolding/MigrationGenerator.d.ts.map +1 -1
  19. package/src/cli/scaffolding/MigrationGenerator.js +2 -1
  20. package/src/cli/scaffolding/ProjectScaffolder.d.ts.map +1 -1
  21. package/src/cli/scaffolding/ProjectScaffolder.js +79 -8
  22. package/src/cli/scaffolding/ScaffoldingVersionUtils.js +1 -1
  23. package/src/common/ContextLoader.d.ts +27 -0
  24. package/src/common/ContextLoader.d.ts.map +1 -0
  25. package/src/common/ContextLoader.js +187 -0
  26. package/src/config/logger.d.ts +2 -0
  27. package/src/config/logger.d.ts.map +1 -1
  28. package/src/config/logger.js +12 -0
  29. package/src/index.d.ts +7 -0
  30. package/src/index.d.ts.map +1 -1
  31. package/src/index.js +7 -3
  32. package/src/orm/Model.d.ts.map +1 -1
  33. package/src/orm/Model.js +1 -1
  34. package/src/orm/adapters/D1Adapter.d.ts.map +1 -1
  35. package/src/orm/adapters/D1Adapter.js +1 -1
  36. package/src/orm/adapters/MySQLAdapter.d.ts.map +1 -1
  37. package/src/orm/adapters/MySQLAdapter.js +1 -1
  38. package/src/orm/adapters/MySQLProxyAdapter.d.ts.map +1 -1
  39. package/src/orm/adapters/MySQLProxyAdapter.js +11 -9
  40. package/src/orm/adapters/PostgreSQLAdapter.js +1 -1
  41. package/src/orm/adapters/SQLServerAdapter.d.ts.map +1 -1
  42. package/src/orm/adapters/SQLServerAdapter.js +1 -1
  43. package/src/orm/adapters/SQLiteAdapter.js +1 -1
  44. package/src/proxy/ProxyServerUtils.d.ts.map +1 -1
  45. package/src/proxy/ProxyServerUtils.js +50 -13
  46. package/src/security/SecurePayload.d.ts +38 -0
  47. package/src/security/SecurePayload.d.ts.map +1 -0
  48. package/src/security/SecurePayload.js +214 -0
  49. package/src/templates/project/basic/app/Controllers/AuthController.ts.tpl +132 -46
  50. package/src/templates/project/basic/package.json.tpl +5 -0
  51. package/src/tools/notification/Composer.d.ts +40 -0
  52. package/src/tools/notification/Composer.d.ts.map +1 -0
  53. package/src/tools/notification/Composer.js +140 -0
  54. package/src/tools/notification/Notification.d.ts +6 -0
  55. package/src/tools/notification/Notification.d.ts.map +1 -1
  56. package/src/tools/notification/Notification.js +7 -0
  57. package/src/tools/queue/AdvancedQueue.js +15 -0
  58. package/src/tools/queue/Queue.d.ts +1 -0
  59. package/src/tools/queue/Queue.d.ts.map +1 -1
  60. package/src/types/Queue.d.ts +1 -0
  61. package/src/types/Queue.d.ts.map +1 -1
@@ -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
  });
@@ -22,5 +22,10 @@
22
22
  "tsc-alias": "^1.8.16",
23
23
  "typescript": "^5.9.3",
24
24
  "vitest": "^4.0.16"
25
+ },
26
+ "overrides": {
27
+ "@zintrust/governance": {
28
+ "@zintrust/core": ">=0.6.0"
29
+ }
25
30
  }
26
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"}
@@ -4,6 +4,7 @@
4
4
  * A small wrapper over NotificationService to provide the expected module name.
5
5
  */
6
6
  import { NotificationService } from './Service.js';
7
+ import { NotificationComposer } from './Composer.js';
7
8
  export const Notification = Object.freeze({
8
9
  send: NotificationService.send,
9
10
  // Alias for send() - explicit intent for immediate notification
@@ -30,6 +31,12 @@ export const Notification = Object.freeze({
30
31
  channel: (name) => Object.freeze({
31
32
  send: async (recipient, message, options) => NotificationService.sendVia(name, recipient, message, options),
32
33
  }),
34
+ compose: NotificationComposer.compose,
35
+ registerChannel: NotificationComposer.registerChannel,
36
+ unregisterChannel: NotificationComposer.unregisterChannel,
37
+ hasChannel: NotificationComposer.hasChannel,
38
+ listChannels: NotificationComposer.listChannels,
39
+ clearChannels: NotificationComposer.clearChannels,
33
40
  listDrivers: NotificationService.listDrivers,
34
41
  });
35
42
  export default Notification;
@@ -358,6 +358,7 @@ function validateUniqueId(uniqueId) {
358
358
  async function handleDeduplication(deduplicationOptions, // DeduplicationOptions - using any to avoid circular import
359
359
  lockProvider, queueName) {
360
360
  const { id, ttl, replace } = deduplicationOptions;
361
+ const collisionBehavior = deduplicationOptions.collisionBehavior === 'enqueue' ? 'enqueue' : 'suppress';
361
362
  const scopedLockKey = resolveScopedDeduplicationLockKey(queueName, id);
362
363
  try {
363
364
  // Check if lock already exists
@@ -373,6 +374,13 @@ lockProvider, queueName) {
373
374
  status: ZintrustLang.QUEUED,
374
375
  };
375
376
  }
377
+ else if (collisionBehavior === 'enqueue') {
378
+ return {
379
+ id,
380
+ deduplicated: false,
381
+ status: ZintrustLang.QUEUED,
382
+ };
383
+ }
376
384
  else {
377
385
  // Job is deduplicated
378
386
  return {
@@ -384,6 +392,13 @@ lockProvider, queueName) {
384
392
  }
385
393
  // Acquire new lock
386
394
  const lock = await lockProvider.acquire(scopedLockKey, { ttl });
395
+ if (!lock.acquired && collisionBehavior === 'enqueue') {
396
+ return {
397
+ id,
398
+ deduplicated: false,
399
+ status: ZintrustLang.QUEUED,
400
+ };
401
+ }
387
402
  return {
388
403
  id,
389
404
  deduplicated: false,
@@ -43,6 +43,7 @@ export interface BullMQPayload {
43
43
  condition: string;
44
44
  delay: number;
45
45
  };
46
+ collisionBehavior?: 'suppress' | 'enqueue';
46
47
  };
47
48
  uniqueVia?: string;
48
49
  [key: string]: unknown;
@@ -1 +1 @@
1
- {"version":3,"file":"Queue.d.ts","sourceRoot":"","sources":["../../../../src/tools/queue/Queue.ts"],"names":[],"mappings":"AAWA,MAAM,MAAM,YAAY,CAAC,CAAC,GAAG,OAAO,IAAI;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,CAAC,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAC;AAErF,MAAM,WAAW,YAAY;IAC3B,OAAO,CAAC,CAAC,GAAG,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACjE,OAAO,CAAC,CAAC,GAAG,OAAO,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC,CAAC,GAAG,SAAS,CAAC,CAAC;IAC1E,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC9C,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACvC,KAAK,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACrC;AAED;;GAEG;AACH,MAAM,WAAW,aAAa;IAE5B,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACvC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAGlB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,gBAAgB,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;IACpC,YAAY,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;IAChC,OAAO,CAAC,EAAE;QACR,IAAI,EAAE,OAAO,GAAG,aAAa,CAAC;QAC9B,KAAK,EAAE,MAAM,CAAC;KACf,CAAC;IACF,MAAM,CAAC,EAAE;QACP,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,KAAK,CAAC,EAAE,MAAM,CAAC;KAChB,CAAC;IACF,IAAI,CAAC,EAAE,OAAO,CAAC;IAGf,aAAa,CAAC,EAAE;QACd,EAAE,EAAE,MAAM,CAAC;QACX,GAAG,CAAC,EAAE,MAAM,CAAC;QACb,YAAY,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG;YAAE,SAAS,EAAE,MAAM,CAAC;YAAC,KAAK,EAAE,MAAM,CAAA;SAAE,CAAC;KACvE,CAAC;IAGF,SAAS,CAAC,EAAE,MAAM,CAAC;IAGnB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAID;;;GAGG;AACH,eAAO,MAAM,iBAAiB,QAAO,MAOpC,CAAC;AAwFF,eAAO,MAAM,KAAK;mBACD,MAAM,UAAU,YAAY;aAIlC,IAAI;eAIF,MAAM,GAAG,YAAY;mBASX,MAAM,WAAW,aAAa,eAAe,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;YA0E5E,CAAC,mBACN,MAAM,eACA,MAAM,GAClB,OAAO,CAAC,YAAY,CAAC,CAAC,CAAC,GAAG,SAAS,CAAC;eActB,MAAM,MAAM,MAAM,eAAe,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;kBAcpD,MAAM,eAAe,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;iBAc9C,MAAM,eAAe,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;EAa9D,CAAC;AAEH,eAAe,KAAK,CAAC;AAErB,OAAO,EAAE,2BAA2B,EAAE,MAAM,yBAAyB,CAAC"}
1
+ {"version":3,"file":"Queue.d.ts","sourceRoot":"","sources":["../../../../src/tools/queue/Queue.ts"],"names":[],"mappings":"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"}
@@ -8,6 +8,7 @@ export interface DeduplicationOptions {
8
8
  dontRelease?: boolean;
9
9
  replace?: boolean;
10
10
  releaseAfter?: string | number | ReleaseCondition;
11
+ collisionBehavior?: 'suppress' | 'enqueue';
11
12
  }
12
13
  export interface ReleaseCondition {
13
14
  condition: string;
@@ -1 +1 @@
1
- {"version":3,"file":"Queue.d.ts","sourceRoot":"","sources":["../../../src/types/Queue.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,MAAM,WAAW,oBAAoB;IACnC,EAAE,EAAE,MAAM,CAAC;IACX,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,YAAY,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,gBAAgB,CAAC;CACnD;AAED,MAAM,WAAW,gBAAgB;IAC/B,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,WAAW;IAC1B,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,IAAI;IACnB,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,EAAE,OAAO,CAAC;IAClB,OAAO,EAAE,IAAI,CAAC;CACf;AAED,MAAM,WAAW,UAAU;IACzB,MAAM,EAAE,OAAO,CAAC;IAChB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,IAAI,CAAC;CAChB;AAED,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,OAAO,GAAG,UAAU,GAAG,QAAQ,CAAC;IACtC,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,kBAAkB;IACjC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,aAAa,CAAC,EAAE,oBAAoB,CAAC;CACtC;AAED,MAAM,WAAW,SAAS;IACxB,EAAE,EAAE,MAAM,CAAC;IACX,YAAY,EAAE,OAAO,CAAC;IACtB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,QAAQ,GAAG,cAAc,GAAG,QAAQ,CAAC;CAC9C;AAED,MAAM,WAAW,YAAY;IAC3B,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1D,OAAO,CAAC,IAAI,EAAE,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACnC,MAAM,CAAC,IAAI,EAAE,IAAI,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAClD,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC;IACzC,IAAI,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;CAC3C"}
1
+ {"version":3,"file":"Queue.d.ts","sourceRoot":"","sources":["../../../src/types/Queue.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,MAAM,WAAW,oBAAoB;IACnC,EAAE,EAAE,MAAM,CAAC;IACX,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,YAAY,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,gBAAgB,CAAC;IAClD,iBAAiB,CAAC,EAAE,UAAU,GAAG,SAAS,CAAC;CAC5C;AAED,MAAM,WAAW,gBAAgB;IAC/B,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,WAAW;IAC1B,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,IAAI;IACnB,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,EAAE,OAAO,CAAC;IAClB,OAAO,EAAE,IAAI,CAAC;CACf;AAED,MAAM,WAAW,UAAU;IACzB,MAAM,EAAE,OAAO,CAAC;IAChB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,IAAI,CAAC;CAChB;AAED,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,OAAO,GAAG,UAAU,GAAG,QAAQ,CAAC;IACtC,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,kBAAkB;IACjC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,aAAa,CAAC,EAAE,oBAAoB,CAAC;CACtC;AAED,MAAM,WAAW,SAAS;IACxB,EAAE,EAAE,MAAM,CAAC;IACX,YAAY,EAAE,OAAO,CAAC;IACtB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,QAAQ,GAAG,cAAc,GAAG,QAAQ,CAAC;CAC9C;AAED,MAAM,WAAW,YAAY;IAC3B,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1D,OAAO,CAAC,IAAI,EAAE,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACnC,MAAM,CAAC,IAAI,EAAE,IAAI,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAClD,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC;IACzC,IAAI,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;CAC3C"}