@web-ts-toolkit/message-service 0.52.0 → 0.54.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 (6) hide show
  1. package/README.md +833 -11
  2. package/index.d.mts +270 -943
  3. package/index.d.ts +270 -943
  4. package/index.js +174 -43
  5. package/index.mjs +172 -43
  6. package/package.json +3 -3
package/index.mjs CHANGED
@@ -205,25 +205,32 @@ function includesAction(templateCd, actionCd, registry) {
205
205
 
206
206
  // src/schemas/methods.ts
207
207
  import { Types } from "mongoose";
208
+ function isValidMessageUserRoles(roles) {
209
+ if (roles === void 0) return true;
210
+ if (!Array.isArray(roles)) return false;
211
+ for (const role of roles) {
212
+ if (typeof role !== "string") return false;
213
+ }
214
+ return true;
215
+ }
208
216
  function isSender(user) {
209
- const fromUser = normalizeStoredUserId(this.fromUser);
210
- const userId = normalizeMessageUserId(user);
211
- return fromUser !== null && userId !== null && fromUser === userId;
217
+ if (!isValidMessageUserRoles(user?.roles)) return false;
218
+ return matchesStoredUserId(this.fromUser, user);
212
219
  }
213
220
  function isReceiver(user) {
214
- const toUser = normalizeStoredUserId(this.toUser);
215
- const userId = normalizeMessageUserId(user);
216
- return toUser !== null && userId !== null && toUser === userId || this.toRoles.some((r) => user.roles?.includes(r));
221
+ const roles = user?.roles;
222
+ if (!isValidMessageUserRoles(roles)) return false;
223
+ return matchesStoredUserId(this.toUser, user) || this.toRoles.some((r) => roles?.includes(r));
217
224
  }
218
- function normalizeMessageUserId(user) {
219
- return normalizeId(user?._id);
220
- }
221
- function normalizeStoredUserId(value) {
222
- if (value instanceof Types.ObjectId) return value.toHexString();
223
- if (value !== null && typeof value === "object") {
224
- return normalizeId(value._id);
225
- }
226
- return normalizeId(value);
225
+ function matchesStoredUserId(value, user) {
226
+ const stored = value instanceof Types.ObjectId || value === null || typeof value !== "object" ? value : value._id;
227
+ const userId = normalizeId(user?._id);
228
+ if (userId === null) return false;
229
+ if (stored instanceof Types.ObjectId && /^[a-f\d]{24}$/i.test(userId)) {
230
+ return stored.toHexString() === userId.toLowerCase();
231
+ }
232
+ const storedId = normalizeId(stored);
233
+ return storedId !== null && storedId === userId;
227
234
  }
228
235
  function normalizeId(value) {
229
236
  if (typeof value === "string") {
@@ -233,6 +240,11 @@ function normalizeId(value) {
233
240
  return value instanceof Types.ObjectId ? value.toHexString() : null;
234
241
  }
235
242
 
243
+ // src/user-id.ts
244
+ function normalizeUserIdForStorage(id) {
245
+ return typeof id === "string" ? id.trim() : id;
246
+ }
247
+
236
248
  // src/errors.ts
237
249
  var TRANSACTION_REQUIRED_MESSAGE = "message-service requires MongoDB replica set or sharded-cluster transactions for idempotent batch creation";
238
250
  var MessageNotFoundError = class extends Error {
@@ -285,6 +297,16 @@ var ActionNotAllowedError = class extends Error {
285
297
  markRuntimeError(this, this.name);
286
298
  }
287
299
  };
300
+ var ActionInputValidationError = class extends Error {
301
+ static [Symbol.hasInstance](value) {
302
+ return isRuntimeError(value, "ActionInputValidationError");
303
+ }
304
+ constructor(message = "invalid action input") {
305
+ super(message);
306
+ this.name = "ActionInputValidationError";
307
+ markRuntimeError(this, this.name);
308
+ }
309
+ };
288
310
  var InvalidMessageUserError = class extends Error {
289
311
  static [Symbol.hasInstance](value) {
290
312
  return isRuntimeError(value, "InvalidMessageUserError");
@@ -440,6 +462,16 @@ var MessageModelResolutionError = class extends Error {
440
462
  }
441
463
  }
442
464
  };
465
+ var PaymentProviderRequiredError = class extends Error {
466
+ static [Symbol.hasInstance](value) {
467
+ return isRuntimeError(value, "PaymentProviderRequiredError");
468
+ }
469
+ constructor() {
470
+ super("paymentProvider is required for messages with a non-empty paymentCd");
471
+ this.name = "PaymentProviderRequiredError";
472
+ markRuntimeError(this, this.name);
473
+ }
474
+ };
443
475
  var PaymentSessionCompensationError = class extends Error {
444
476
  static [Symbol.hasInstance](value) {
445
477
  return isRuntimeError(value, "PaymentSessionCompensationError") || isRuntimeError(value, "PaymentSessionCompensationAggregateError");
@@ -650,6 +682,9 @@ function requireMessageUserId(user) {
650
682
  if (!isValidMessageUserId(id)) {
651
683
  throw new InvalidMessageUserError();
652
684
  }
685
+ if (!isValidMessageUserRoles(user.roles)) {
686
+ throw new InvalidMessageUserError("user.roles must be an array of strings when provided");
687
+ }
653
688
  return typeof id === "string" ? id.trim() : String(id);
654
689
  }
655
690
  var MessageService = class {
@@ -689,7 +724,11 @@ var MessageService = class {
689
724
  );
690
725
  this.clientRequestDelay = options.clientRequestDelay ?? this.delay;
691
726
  this.clientRequestNow = options.clientRequestNow ?? (() => Date.now());
692
- this.actionLeaseMs = ACTION_LEASE_MS;
727
+ this.actionLeaseMs = this.validateDurationOption(
728
+ "actionLeaseMs",
729
+ options.actionLeaseMs === void 0 ? ACTION_LEASE_MS : options.actionLeaseMs,
730
+ 1
731
+ );
693
732
  }
694
733
  // -------------------------------------------------------------------------
695
734
  // Message lookup
@@ -735,10 +774,25 @@ var MessageService = class {
735
774
  *
736
775
  * User-facing entry point: `user` (and `payerUser` when provided) must
737
776
  * carry a valid principal id (non-empty string or ObjectId; numbers,
738
- * arrays, and plain objects are rejected with `InvalidMessageUserError`)
777
+ * arrays, and plain objects are rejected with `InvalidMessageUserError`),
778
+ * and optional `roles` must be undefined or an array of strings,
739
779
  * before any template, provider, or model effect runs. Both the
740
780
  * idempotent (`clientRequestId`) and direct branches enforce this.
741
781
  *
782
+ * Each `PrepareResult.documents` selects that item's attachments; `[]` clears
783
+ * them. Omission/undefined retains the original caller `payload.documents`
784
+ * fallback, independently of the prepared business payload. Selected arrays
785
+ * are copied per item without mutating caller/preparation arrays.
786
+ *
787
+ * Each fresh item's `item.paymentCd || template.paymentCd || ''` requests a
788
+ * session when nonempty, independently of recipients. An empty item override
789
+ * inherits the template code. Absent/null providers throw
790
+ * `PaymentProviderRequiredError`; configured-provider rejections propagate and
791
+ * null/empty session returns throw `Error('payment session creation failed')`.
792
+ * The payer remains `(payerUser || user)._id`, not the prepared sender/recipient.
793
+ * Direct batches retain committed prefixes; idempotent batches commit atomically
794
+ * and compensate all known uncommitted sessions on failure.
795
+ *
742
796
  * Idempotent replay contract (MSGF-06): a completed same-scope replay
743
797
  * returns the current active/archive records merged from both collections
744
798
  * (`Array<IMessage | IMessageArchive>`), sorted by exact
@@ -906,15 +960,17 @@ var MessageService = class {
906
960
  * or matches one of the recipient's roles.
907
961
  *
908
962
  * User-facing operation: validates `user` via the shared principal
909
- * contract. Returns `IMessage[]` ordered by `{ createdAt: -1, _id: -1 }`.
963
+ * contract before model resolution. Returns `IMessage[]` ordered by
964
+ * `{ createdAt: -1, _id: -1 }`.
910
965
  */
911
966
  async listMessages(params) {
912
967
  const { user, limit: rawLimit, skip: rawSkip, populate } = params;
913
968
  const limit = this.normalizeListLimit(rawLimit);
914
969
  const skip = this.normalizeListSkip(rawSkip);
970
+ const filter = this.buildVisibilityFilter(user);
915
971
  const Message = this.resolveModel("active");
916
972
  const query = this.applyPopulate(
917
- Message.find(this.buildVisibilityFilter(user)).sort({ createdAt: -1, _id: -1 }).skip(skip).limit(limit),
973
+ Message.find(filter).sort({ createdAt: -1, _id: -1 }).skip(skip).limit(limit),
918
974
  populate
919
975
  );
920
976
  return query;
@@ -923,11 +979,13 @@ var MessageService = class {
923
979
  * Count active (non-archived) messages visible to a user.
924
980
  * Useful for badge indicators ("3 new messages").
925
981
  *
926
- * User-facing operation: validates `user` via the shared principal contract.
982
+ * User-facing operation: validates `user` via the shared principal contract
983
+ * before model resolution.
927
984
  */
928
985
  async countMessages(user) {
986
+ const filter = this.buildVisibilityFilter(user);
929
987
  const Message = this.resolveModel("active");
930
- return Message.countDocuments(this.buildVisibilityFilter(user));
988
+ return Message.countDocuments(filter);
931
989
  }
932
990
  // -------------------------------------------------------------------------
933
991
  // Get actions for a message
@@ -954,8 +1012,7 @@ var MessageService = class {
954
1012
  * render empty), distinct from creation-time `templateData`.
955
1013
  */
956
1014
  async getActions(messageId, usertype, options) {
957
- const user = requireMessageUserId(options?.user);
958
- const authorizedUser = { ...options.user, _id: user };
1015
+ const authorizedUser = this.buildAuthorizedUser(options?.user);
959
1016
  const message = await this.findMessage(messageId);
960
1017
  if (!message) return null;
961
1018
  if (!options.isAdmin) {
@@ -989,18 +1046,33 @@ var MessageService = class {
989
1046
  * Execute an action with a durable claim, stable handler attempt key,
990
1047
  * transactional archive, and post-commit sender notification status.
991
1048
  *
1049
+ * `actionLeaseMs` controls when a same-action retry may take over. Expiry alone
1050
+ * does not cancel the handler or reject a still-current owner; takeover fences
1051
+ * stale completion/failure writes with `ActionConflictError`. There is no
1052
+ * heartbeat or renewal; external effects must deduplicate by `actionAttemptId`.
1053
+ *
992
1054
  * User-facing operation: `data.user` is validated before any template or
993
- * model effect. Authorization and handler selection bind to the persisted
1055
+ * model effect. Handler users contain only the documented `MessageUser`
1056
+ * fields, read explicitly (including getters), with a copied roles array;
1057
+ * the caller's user and original `req` are not modified.
1058
+ * Authorization and handler selection bind to the persisted
994
1059
  * claim, not the caller-supplied `data.message` copy. Archived messages
995
1060
  * are re-read by id on their owning connection and apply a stored
996
1061
  * sender/receiver relationship gate before disclosing current attempt IDs
997
1062
  * or notification state. A missing archive throws `MessageNotFoundError`.
1063
+ * `data.input` is explicit submitted data, never inferred from `req`. The
1064
+ * current claimed action's optional synchronous pure `parseInput` supplies
1065
+ * `ctx.input` after authorization. Invalid input throws
1066
+ * `ActionInputValidationError`; unexpected parser failures preserve their
1067
+ * cause as server errors. Both use fenced pre-handler release: fresh claims
1068
+ * return to active, retries retain their stable attempt/action as retryable.
1069
+ * Hosts own input bounds, runtime validation, and durable command/result
1070
+ * binding at first host reservation (not at package claim).
998
1071
  * Returns the handler's value (`unknown`; templates
999
1072
  * document their own shape).
1000
1073
  */
1001
1074
  async handleAction(templateCd, actionCd, data) {
1002
- const userId = this.requireUserId(data.user);
1003
- const authorizedUser = { ...data.user, _id: userId };
1075
+ const authorizedUser = this.buildAuthorizedUser(data.user);
1004
1076
  if (this.isArchivedMessage(data.message)) {
1005
1077
  const archived = await this.resolveModel("archive", data.message).findById(
1006
1078
  data.message._id
@@ -1020,25 +1092,28 @@ var MessageService = class {
1020
1092
  }
1021
1093
  const claim = await this.claimAction(data.message, actionCd, authorizedUser);
1022
1094
  let authorizedAction;
1095
+ let input;
1023
1096
  try {
1024
1097
  authorizedAction = this.authorizeClaimedAction(templateCd, actionCd, claim.message, authorizedUser, {
1025
1098
  permissions: data.permissions
1026
1099
  });
1027
- } catch (authError) {
1100
+ input = this.parseActionInput(authorizedAction, data.input);
1101
+ } catch (preHandlerError) {
1028
1102
  try {
1029
- await this.releaseDeniedActionClaim(claim, authError);
1103
+ await this.releaseDeniedActionClaim(claim, preHandlerError);
1030
1104
  } catch (releaseError) {
1031
1105
  if (!(releaseError instanceof ActionConflictError)) {
1032
1106
  throw releaseError;
1033
1107
  }
1034
1108
  }
1035
- throw authError;
1109
+ throw preHandlerError;
1036
1110
  }
1037
1111
  const ctx = this.buildActionContext({
1038
1112
  ...data,
1039
1113
  user: authorizedUser,
1040
1114
  message: claim.message,
1041
- actionAttemptId: claim.actionAttemptId
1115
+ actionAttemptId: claim.actionAttemptId,
1116
+ input
1042
1117
  });
1043
1118
  let result;
1044
1119
  try {
@@ -1159,13 +1234,14 @@ var MessageService = class {
1159
1234
  }
1160
1235
  }
1161
1236
  async buildMessageDocument(template, m, ctx, clientRequestScope, clientRequestItemIndex) {
1162
- const toUser = m.toUser ?? null;
1163
- const toRoles = (m.toRoles && m.toRoles.length > 0 ? m.toRoles : this.adminRoles).slice();
1237
+ const toUser = m.toUser == null ? null : normalizeUserIdForStorage(m.toUser);
1238
+ const toRoles = (m.toRoles && m.toRoles.length > 0 ? m.toRoles : toUser === null ? this.adminRoles : []).slice();
1164
1239
  const type = m.type || template.type || "notification";
1165
1240
  const paymentCd = m.paymentCd || template.paymentCd || "";
1166
1241
  let paymentSession = null;
1167
1242
  try {
1168
- if (paymentCd && this.paymentProvider && (toUser || toRoles.length > 0)) {
1243
+ if (paymentCd) {
1244
+ if (!this.paymentProvider) throw new PaymentProviderRequiredError();
1169
1245
  paymentSession = await this.paymentProvider.createSession(
1170
1246
  (ctx.payerUser || ctx.user)._id,
1171
1247
  paymentCd,
@@ -1177,12 +1253,12 @@ var MessageService = class {
1177
1253
  return {
1178
1254
  type,
1179
1255
  templateCd: template.templateCd,
1180
- fromUser: m.fromUser || ctx.user._id,
1256
+ fromUser: normalizeUserIdForStorage(m.fromUser || ctx.user._id),
1181
1257
  toUser,
1182
1258
  toRoles,
1183
1259
  senderContent: interpolated.senderContent,
1184
1260
  receiverContent: interpolated.receiverContent,
1185
- documents: ctx.payload.documents || [],
1261
+ documents: (m.documents !== void 0 ? m.documents : ctx.payload.documents || []).slice(),
1186
1262
  paymentSession,
1187
1263
  paymentCd,
1188
1264
  payload: m.payload || ctx.payload,
@@ -1292,12 +1368,36 @@ var MessageService = class {
1292
1368
  message: data.message,
1293
1369
  user: data.user,
1294
1370
  actionAttemptId: data.actionAttemptId,
1371
+ input: data.input,
1295
1372
  getModel: this.getTemplateModelResolver(data.message),
1296
1373
  expireSession: this.expirePaymentSession,
1297
1374
  refundPayment: this.refundPaymentSession,
1298
1375
  req: data.req
1299
1376
  };
1300
1377
  }
1378
+ parseActionInput(action, input) {
1379
+ if (!action.parseInput) return input;
1380
+ try {
1381
+ const parsed = action.parseInput(input);
1382
+ if (parsed !== null && (typeof parsed === "object" || typeof parsed === "function")) {
1383
+ const then = parsed.then;
1384
+ if (typeof then === "function") {
1385
+ void Promise.resolve().then(() => {
1386
+ const discarded = Reflect.apply(then, parsed, [() => {
1387
+ }, () => {
1388
+ }]);
1389
+ return discarded === parsed ? void 0 : discarded;
1390
+ }).catch(() => {
1391
+ });
1392
+ throw new Error("action.parseInput must be synchronous");
1393
+ }
1394
+ }
1395
+ return parsed;
1396
+ } catch (error) {
1397
+ if (error instanceof ActionInputValidationError) throw error;
1398
+ throw new Error("action.parseInput failed", { cause: error });
1399
+ }
1400
+ }
1301
1401
  async claimAction(message, actionCd, user) {
1302
1402
  const Message = this.resolveModel("active", message);
1303
1403
  const messageId = message._id;
@@ -1624,6 +1724,11 @@ var MessageService = class {
1624
1724
  requireUserId(user) {
1625
1725
  return requireMessageUserId(user);
1626
1726
  }
1727
+ buildAuthorizedUser(user) {
1728
+ const _id = this.requireUserId(user);
1729
+ const { id, roles, displayName } = user;
1730
+ return { _id, id, roles: roles?.slice(), displayName };
1731
+ }
1627
1732
  async findByClientRequestScope(scope) {
1628
1733
  const Message = this.resolveModel("active");
1629
1734
  const docs = await Message.find(scope).sort({ clientRequestItemIndex: 1, _id: 1 }).limit(Number.MAX_SAFE_INTEGER);
@@ -1917,7 +2022,7 @@ function createArchiveMethod(ctx) {
1917
2022
  const archiveDoc = {
1918
2023
  ...source,
1919
2024
  actionCd,
1920
- archivedBy,
2025
+ archivedBy: normalizeUserIdForStorage(archivedBy),
1921
2026
  archivedAt: /* @__PURE__ */ new Date(),
1922
2027
  actionNotificationState: "none",
1923
2028
  actionNotificationError: null,
@@ -1986,22 +2091,31 @@ function createPostSaveEmailHook(ctx) {
1986
2091
  if (!title || ctx.emailNotificationExclusions.includes(normalizeEmailTitle(title))) {
1987
2092
  return;
1988
2093
  }
1989
- let user;
2094
+ let email;
1990
2095
  try {
1991
- const User = resolveDocumentModel(this, ctx.userModelName, "user");
1992
- user = await User.findById(this.toUser).select("email").lean();
2096
+ if (ctx.emailRecipientResolver) {
2097
+ email = await ctx.emailRecipientResolver({
2098
+ recipientId: this.toUser,
2099
+ connection: this.constructor.db,
2100
+ message: this
2101
+ });
2102
+ } else {
2103
+ const User = resolveDocumentModel(this, ctx.userModelName, "user");
2104
+ const user = await User.findById(this.toUser).select("email").lean();
2105
+ email = user?.email;
2106
+ }
1993
2107
  } catch (error) {
1994
2108
  await reportEmailFailure(ctx, "recipientLookup", error, this, title);
1995
2109
  return;
1996
2110
  }
1997
- if (!user?.email) {
2111
+ if (!email) {
1998
2112
  return;
1999
2113
  }
2000
2114
  const long = this.receiverContent.long || "";
2001
2115
  const short = this.receiverContent.short || "";
2002
2116
  const body = long.length > short.length ? long : short;
2003
2117
  try {
2004
- await ctx.emailNotifier(user.email, title, body);
2118
+ await ctx.emailNotifier(email, title, body);
2005
2119
  } catch (error) {
2006
2120
  await reportEmailFailure(ctx, "notifier", error, this, title);
2007
2121
  }
@@ -2031,6 +2145,7 @@ function resolveConfig(config) {
2031
2145
  return {
2032
2146
  onTransactionCleanupFailure: config?.onTransactionCleanupFailure,
2033
2147
  emailNotifier: config?.emailNotifier ?? null,
2148
+ emailRecipientResolver: config?.emailRecipientResolver,
2034
2149
  onEmailDeliveryFailure: config?.onEmailDeliveryFailure,
2035
2150
  emailNotificationExclusions: (config?.emailNotificationExclusions ?? []).map(normalizeEmailTitle),
2036
2151
  userModelName: config?.userModelName ?? "User",
@@ -2040,7 +2155,7 @@ function resolveConfig(config) {
2040
2155
  }
2041
2156
  function buildMessageSchema(config) {
2042
2157
  const resolved = resolveConfig(config);
2043
- if (resolved.emailNotifier) {
2158
+ if (resolved.emailNotifier && !resolved.emailRecipientResolver) {
2044
2159
  assertModelRegistered(resolved.connection, resolved.userModelName, "user");
2045
2160
  }
2046
2161
  const schema = new mongoose3.Schema(BaseMessageFields, {
@@ -2091,6 +2206,7 @@ function buildMessageSchema(config) {
2091
2206
  "save",
2092
2207
  createPostSaveEmailHook({
2093
2208
  emailNotifier: resolved.emailNotifier,
2209
+ emailRecipientResolver: resolved.emailRecipientResolver,
2094
2210
  onEmailDeliveryFailure: resolved.onEmailDeliveryFailure,
2095
2211
  emailNotificationExclusions: resolved.emailNotificationExclusions,
2096
2212
  userModelName: resolved.userModelName
@@ -2317,6 +2433,9 @@ function isMongooseCastError(error) {
2317
2433
  return !!error && typeof error === "object" && error.name === "CastError";
2318
2434
  }
2319
2435
  function mapServiceError(error) {
2436
+ if (error instanceof ActionInputValidationError) {
2437
+ throw new JsonRouter.clientErrors.BadRequestError("invalid action input");
2438
+ }
2320
2439
  if (error instanceof MessageNotFoundError) {
2321
2440
  throw new JsonRouter.clientErrors.NotFoundError("message not found");
2322
2441
  }
@@ -2368,6 +2487,7 @@ var SERVICE_CONSTRUCTION_KEYS = [
2368
2487
  "registry",
2369
2488
  "defaultListLimit",
2370
2489
  "maxListLimit",
2490
+ "actionLeaseMs",
2371
2491
  "clientRequestLeaseMs",
2372
2492
  "clientRequestWaitMs",
2373
2493
  "clientRequestPollMs",
@@ -2406,6 +2526,7 @@ function createMessageRoutes(options) {
2406
2526
  registry: construction.registry,
2407
2527
  defaultListLimit: construction.defaultListLimit,
2408
2528
  maxListLimit: construction.maxListLimit,
2529
+ actionLeaseMs: construction.actionLeaseMs,
2409
2530
  clientRequestLeaseMs: construction.clientRequestLeaseMs,
2410
2531
  clientRequestWaitMs: construction.clientRequestWaitMs,
2411
2532
  clientRequestPollMs: construction.clientRequestPollMs,
@@ -2476,7 +2597,13 @@ function createMessageRoutes(options) {
2476
2597
  }
2477
2598
  const permissions = getPermissions(req);
2478
2599
  try {
2479
- return await service.handleAction(message.templateCd, actionCd, { message, user, permissions, req });
2600
+ return await service.handleAction(message.templateCd, actionCd, {
2601
+ message,
2602
+ user,
2603
+ permissions,
2604
+ input: req.body,
2605
+ req
2606
+ });
2480
2607
  } catch (error) {
2481
2608
  if (error instanceof ActionNotificationPendingError) {
2482
2609
  return new JsonRouter.success.Accepted({ message: error.message, actionAttemptId: error.actionAttemptId });
@@ -2499,6 +2626,7 @@ function defaultGetIdentity(req) {
2499
2626
  }
2500
2627
  export {
2501
2628
  ActionConflictError,
2629
+ ActionInputValidationError,
2502
2630
  ActionNotAllowedError,
2503
2631
  ActionNotFoundError,
2504
2632
  ActionNotificationPendingError,
@@ -2528,6 +2656,7 @@ export {
2528
2656
  MessageTransactionRequiredError,
2529
2657
  NoopEmailProvider,
2530
2658
  NoopPaymentProvider,
2659
+ PaymentProviderRequiredError,
2531
2660
  PaymentSessionCompensationAggregateError,
2532
2661
  PaymentSessionCompensationError,
2533
2662
  TemplateNotFoundError,
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@web-ts-toolkit/message-service",
3
3
  "description": "Template-driven messaging core for Mongoose + Express applications",
4
4
  "homepage": "https://web-ts-toolkit.pages.dev/docs/packages/message-service",
5
- "version": "0.52.0",
5
+ "version": "0.54.0",
6
6
  "keywords": [
7
7
  "mongoose",
8
8
  "messaging",
@@ -30,8 +30,8 @@
30
30
  "node": ">=22"
31
31
  },
32
32
  "dependencies": {
33
- "@web-ts-toolkit/express-json-router": "0.52.0",
34
- "@web-ts-toolkit/utils": "0.52.0",
33
+ "@web-ts-toolkit/express-json-router": "0.54.0",
34
+ "@web-ts-toolkit/utils": "0.54.0",
35
35
  "handlebars": "^4.7.8"
36
36
  },
37
37
  "peerDependencies": {