@molecule/api-resource-message 1.0.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 (56) hide show
  1. package/LICENSE +115 -0
  2. package/dist/authorizers/index.d.ts +12 -0
  3. package/dist/authorizers/index.d.ts.map +1 -0
  4. package/dist/authorizers/index.js +11 -0
  5. package/dist/browser-guard.d.ts +2 -0
  6. package/dist/browser-guard.d.ts.map +1 -0
  7. package/dist/browser-guard.js +18 -0
  8. package/dist/handlers/createThread.d.ts +15 -0
  9. package/dist/handlers/createThread.d.ts.map +1 -0
  10. package/dist/handlers/createThread.js +54 -0
  11. package/dist/handlers/deleteMessage.d.ts +14 -0
  12. package/dist/handlers/deleteMessage.d.ts.map +1 -0
  13. package/dist/handlers/deleteMessage.js +56 -0
  14. package/dist/handlers/editMessage.d.ts +14 -0
  15. package/dist/handlers/editMessage.d.ts.map +1 -0
  16. package/dist/handlers/editMessage.js +63 -0
  17. package/dist/handlers/index.d.ts +9 -0
  18. package/dist/handlers/index.d.ts.map +1 -0
  19. package/dist/handlers/index.js +8 -0
  20. package/dist/handlers/listThreads.d.ts +14 -0
  21. package/dist/handlers/listThreads.d.ts.map +1 -0
  22. package/dist/handlers/listThreads.js +39 -0
  23. package/dist/handlers/markRead.d.ts +16 -0
  24. package/dist/handlers/markRead.d.ts.map +1 -0
  25. package/dist/handlers/markRead.js +68 -0
  26. package/dist/handlers/messages.d.ts +23 -0
  27. package/dist/handlers/messages.d.ts.map +1 -0
  28. package/dist/handlers/messages.js +135 -0
  29. package/dist/handlers/readThread.d.ts +15 -0
  30. package/dist/handlers/readThread.d.ts.map +1 -0
  31. package/dist/handlers/readThread.js +66 -0
  32. package/dist/handlers/unreadCount.d.ts +14 -0
  33. package/dist/handlers/unreadCount.d.ts.map +1 -0
  34. package/dist/handlers/unreadCount.js +37 -0
  35. package/dist/i18n.d.ts +10 -0
  36. package/dist/i18n.d.ts.map +1 -0
  37. package/dist/i18n.js +17 -0
  38. package/dist/index.d.ts +82 -0
  39. package/dist/index.d.ts.map +1 -0
  40. package/dist/index.js +81 -0
  41. package/dist/requestHandlerMap.d.ts +28 -0
  42. package/dist/requestHandlerMap.d.ts.map +1 -0
  43. package/dist/requestHandlerMap.js +27 -0
  44. package/dist/routes.d.ts +58 -0
  45. package/dist/routes.d.ts.map +1 -0
  46. package/dist/routes.js +67 -0
  47. package/dist/service.d.ts +117 -0
  48. package/dist/service.d.ts.map +1 -0
  49. package/dist/service.js +301 -0
  50. package/dist/types.d.ts +102 -0
  51. package/dist/types.d.ts.map +1 -0
  52. package/dist/types.js +32 -0
  53. package/dist/validation.d.ts +36 -0
  54. package/dist/validation.d.ts.map +1 -0
  55. package/dist/validation.js +38 -0
  56. package/package.json +74 -0
@@ -0,0 +1,135 @@
1
+ /**
2
+ * Send + list messages within a thread.
3
+ *
4
+ * @module
5
+ */
6
+ import { t } from '@molecule/api-i18n';
7
+ import { logger } from '@molecule/api-logger';
8
+ import { getThreadById, listMessages, sendMessage } from '../service.js';
9
+ import { sendMessageSchema } from '../validation.js';
10
+ /**
11
+ * Lists messages in a thread, newest first.
12
+ *
13
+ * @param req - The request with `threadId` URL param and optional
14
+ * `before` / `limit` query params.
15
+ * @param res - The response object.
16
+ */
17
+ export async function listMessagesHandler(req, res) {
18
+ const userId = res.locals.session?.userId;
19
+ if (!userId) {
20
+ res.status(401).json({
21
+ error: t('resource.error.unauthorized', undefined, { defaultValue: 'Unauthorized' }),
22
+ errorKey: 'resource.error.unauthorized',
23
+ });
24
+ return;
25
+ }
26
+ const { threadId } = req.params;
27
+ if (!threadId) {
28
+ res.status(400).json({
29
+ error: t('message.error.missingThreadId', undefined, {
30
+ defaultValue: 'Thread ID is required',
31
+ }),
32
+ errorKey: 'message.error.missingThreadId',
33
+ });
34
+ return;
35
+ }
36
+ const thread = await getThreadById(threadId);
37
+ if (!thread) {
38
+ res.status(404).json({
39
+ error: t('message.error.threadNotFound', undefined, {
40
+ defaultValue: 'Thread not found',
41
+ }),
42
+ errorKey: 'message.error.threadNotFound',
43
+ });
44
+ return;
45
+ }
46
+ if (thread.participantAId !== userId && thread.participantBId !== userId) {
47
+ res.status(403).json({
48
+ error: t('message.error.notParticipant', undefined, {
49
+ defaultValue: 'You are not a participant in this thread',
50
+ }),
51
+ errorKey: 'message.error.notParticipant',
52
+ });
53
+ return;
54
+ }
55
+ const before = typeof req.query.before === 'string' ? req.query.before : undefined;
56
+ const limit = parseInt(req.query.limit, 10) || 50;
57
+ try {
58
+ const messages = await listMessages(threadId, { before, limit });
59
+ res.json({ data: messages, limit });
60
+ }
61
+ catch (error) {
62
+ logger.error('Failed to list messages', { threadId, error });
63
+ res.status(500).json({
64
+ error: t('message.error.listMessagesFailed', undefined, {
65
+ defaultValue: 'Failed to list messages',
66
+ }),
67
+ errorKey: 'message.error.listMessagesFailed',
68
+ });
69
+ }
70
+ }
71
+ /**
72
+ * Sends a new message in a thread. Authenticated user must be a
73
+ * participant. Broadcasts a `message:sent` event when realtime is bonded.
74
+ *
75
+ * @param req - The request with `threadId` URL param and message body.
76
+ * @param res - The response object.
77
+ */
78
+ export async function sendMessageHandler(req, res) {
79
+ const userId = res.locals.session?.userId;
80
+ if (!userId) {
81
+ res.status(401).json({
82
+ error: t('resource.error.unauthorized', undefined, { defaultValue: 'Unauthorized' }),
83
+ errorKey: 'resource.error.unauthorized',
84
+ });
85
+ return;
86
+ }
87
+ const { threadId } = req.params;
88
+ if (!threadId) {
89
+ res.status(400).json({
90
+ error: t('message.error.missingThreadId', undefined, {
91
+ defaultValue: 'Thread ID is required',
92
+ }),
93
+ errorKey: 'message.error.missingThreadId',
94
+ });
95
+ return;
96
+ }
97
+ const parsed = sendMessageSchema.safeParse(req.body);
98
+ if (!parsed.success) {
99
+ const errors = parsed.error.issues.map((e) => `${e.path.join('.')}: ${e.message}`).join(', ');
100
+ res.status(400).json({ error: errors, errorKey: 'message.error.validationFailed' });
101
+ return;
102
+ }
103
+ try {
104
+ const message = await sendMessage(threadId, userId, parsed.data.body, parsed.data.attachments);
105
+ res.status(201).json(message);
106
+ }
107
+ catch (error) {
108
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
109
+ if (errorMessage.includes('not found')) {
110
+ res.status(404).json({
111
+ error: t('message.error.threadNotFound', undefined, {
112
+ defaultValue: 'Thread not found',
113
+ }),
114
+ errorKey: 'message.error.threadNotFound',
115
+ });
116
+ return;
117
+ }
118
+ if (errorMessage.includes('not a participant')) {
119
+ res.status(403).json({
120
+ error: t('message.error.notParticipant', undefined, {
121
+ defaultValue: 'You are not a participant in this thread',
122
+ }),
123
+ errorKey: 'message.error.notParticipant',
124
+ });
125
+ return;
126
+ }
127
+ logger.error('Failed to send message', { threadId, userId, error });
128
+ res.status(500).json({
129
+ error: t('message.error.sendFailed', undefined, {
130
+ defaultValue: 'Failed to send message',
131
+ }),
132
+ errorKey: 'message.error.sendFailed',
133
+ });
134
+ }
135
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Read a single thread by ID.
3
+ *
4
+ * @module
5
+ */
6
+ import type { MoleculeRequest, MoleculeResponse } from '@molecule/api-resource';
7
+ /**
8
+ * Returns the thread identified by `:threadId`. The authenticated user
9
+ * must be a participant.
10
+ *
11
+ * @param req - The request with `threadId` URL param.
12
+ * @param res - The response object.
13
+ */
14
+ export declare function readThread(req: MoleculeRequest, res: MoleculeResponse): Promise<void>;
15
+ //# sourceMappingURL=readThread.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"readThread.d.ts","sourceRoot":"","sources":["../../src/handlers/readThread.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAIH,OAAO,KAAK,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAA;AAI/E;;;;;;GAMG;AACH,wBAAsB,UAAU,CAAC,GAAG,EAAE,eAAe,EAAE,GAAG,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CAmD3F"}
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Read a single thread by ID.
3
+ *
4
+ * @module
5
+ */
6
+ import { t } from '@molecule/api-i18n';
7
+ import { logger } from '@molecule/api-logger';
8
+ import { getThreadById } from '../service.js';
9
+ /**
10
+ * Returns the thread identified by `:threadId`. The authenticated user
11
+ * must be a participant.
12
+ *
13
+ * @param req - The request with `threadId` URL param.
14
+ * @param res - The response object.
15
+ */
16
+ export async function readThread(req, res) {
17
+ const userId = res.locals.session?.userId;
18
+ if (!userId) {
19
+ res.status(401).json({
20
+ error: t('resource.error.unauthorized', undefined, { defaultValue: 'Unauthorized' }),
21
+ errorKey: 'resource.error.unauthorized',
22
+ });
23
+ return;
24
+ }
25
+ const { threadId } = req.params;
26
+ if (!threadId) {
27
+ res.status(400).json({
28
+ error: t('message.error.missingThreadId', undefined, {
29
+ defaultValue: 'Thread ID is required',
30
+ }),
31
+ errorKey: 'message.error.missingThreadId',
32
+ });
33
+ return;
34
+ }
35
+ try {
36
+ const thread = await getThreadById(threadId);
37
+ if (!thread) {
38
+ res.status(404).json({
39
+ error: t('message.error.threadNotFound', undefined, {
40
+ defaultValue: 'Thread not found',
41
+ }),
42
+ errorKey: 'message.error.threadNotFound',
43
+ });
44
+ return;
45
+ }
46
+ if (thread.participantAId !== userId && thread.participantBId !== userId) {
47
+ res.status(403).json({
48
+ error: t('message.error.notParticipant', undefined, {
49
+ defaultValue: 'You are not a participant in this thread',
50
+ }),
51
+ errorKey: 'message.error.notParticipant',
52
+ });
53
+ return;
54
+ }
55
+ res.json(thread);
56
+ }
57
+ catch (error) {
58
+ logger.error('Failed to read thread', { threadId, error });
59
+ res.status(500).json({
60
+ error: t('message.error.readThreadFailed', undefined, {
61
+ defaultValue: 'Failed to read thread',
62
+ }),
63
+ errorKey: 'message.error.readThreadFailed',
64
+ });
65
+ }
66
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Total unread count across all the authenticated user's threads.
3
+ *
4
+ * @module
5
+ */
6
+ import type { MoleculeRequest, MoleculeResponse } from '@molecule/api-resource';
7
+ /**
8
+ * Returns the authenticated user's total unread message count.
9
+ *
10
+ * @param req - The request.
11
+ * @param res - The response object.
12
+ */
13
+ export declare function unreadCount(req: MoleculeRequest, res: MoleculeResponse): Promise<void>;
14
+ //# sourceMappingURL=unreadCount.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"unreadCount.d.ts","sourceRoot":"","sources":["../../src/handlers/unreadCount.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAIH,OAAO,KAAK,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAA;AAI/E;;;;;GAKG;AACH,wBAAsB,WAAW,CAAC,GAAG,EAAE,eAAe,EAAE,GAAG,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CAsB5F"}
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Total unread count across all the authenticated user's threads.
3
+ *
4
+ * @module
5
+ */
6
+ import { t } from '@molecule/api-i18n';
7
+ import { logger } from '@molecule/api-logger';
8
+ import { getTotalUnreadCount } from '../service.js';
9
+ /**
10
+ * Returns the authenticated user's total unread message count.
11
+ *
12
+ * @param req - The request.
13
+ * @param res - The response object.
14
+ */
15
+ export async function unreadCount(req, res) {
16
+ const userId = res.locals.session?.userId;
17
+ if (!userId) {
18
+ res.status(401).json({
19
+ error: t('resource.error.unauthorized', undefined, { defaultValue: 'Unauthorized' }),
20
+ errorKey: 'resource.error.unauthorized',
21
+ });
22
+ return;
23
+ }
24
+ try {
25
+ const total = await getTotalUnreadCount(userId);
26
+ res.json({ unreadCount: total });
27
+ }
28
+ catch (error) {
29
+ logger.error('Failed to get unread count', { userId, error });
30
+ res.status(500).json({
31
+ error: t('message.error.unreadCountFailed', undefined, {
32
+ defaultValue: 'Failed to get unread count',
33
+ }),
34
+ errorKey: 'message.error.unreadCountFailed',
35
+ });
36
+ }
37
+ }
package/dist/i18n.d.ts ADDED
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Locale registration for the message resource.
3
+ *
4
+ * @module
5
+ */
6
+ /**
7
+ * The i18n registered.
8
+ */
9
+ export declare const i18nRegistered = true;
10
+ //# sourceMappingURL=i18n.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"i18n.d.ts","sourceRoot":"","sources":["../src/i18n.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAWH;;GAEG;AACH,eAAO,MAAM,cAAc,OAAO,CAAA"}
package/dist/i18n.js ADDED
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Locale registration for the message resource.
3
+ *
4
+ * @module
5
+ */
6
+ import { registerLocaleModule } from '@molecule/api-i18n';
7
+ try {
8
+ const locales = await import('@molecule/api-locales-resource-message');
9
+ registerLocaleModule(locales);
10
+ }
11
+ catch (_error) {
12
+ // Locale package not available — translations fall back to inline defaults.
13
+ }
14
+ /**
15
+ * The i18n registered.
16
+ */
17
+ export const i18nRegistered = true;
@@ -0,0 +1,82 @@
1
+ /**
2
+ * 1:1 messaging resource for molecule.dev.
3
+ *
4
+ * Direct-message threads between two participants with read-tracking,
5
+ * unread counters, optional attachments, and realtime broadcast over
6
+ * `@molecule/api-realtime` when bonded.
7
+ *
8
+ * @module
9
+ * @example
10
+ * ```typescript
11
+ * import { routes, requestHandlerMap } from '@molecule/api-resource-message'
12
+ *
13
+ * // Wire routes into your Express app via mlcl inject
14
+ * // POST /message-threads
15
+ * // GET /message-threads
16
+ * // GET /message-threads/unread-count
17
+ * // GET /message-threads/:threadId
18
+ * // GET /message-threads/:threadId/messages
19
+ * // POST /message-threads/:threadId/messages
20
+ * // POST /message-threads/:threadId/read
21
+ * // PATCH /message-threads/messages/:messageId
22
+ * // DELETE /message-threads/messages/:messageId
23
+ * ```
24
+ *
25
+ * @remarks
26
+ * Tables: `src/__setup__/messages.sql` creates `message_threads` + `messages`.
27
+ * An mlcl-scaffolded API replays `__setup__/*.sql` automatically on migrate;
28
+ * anywhere else run it once — nothing at runtime creates them.
29
+ *
30
+ * Threads are strictly 1:1 — a canonicalised participant pair (creating a
31
+ * thread with yourself throws) — and PARTICIPANT-SCOPED: every handler reads
32
+ * the authenticated user from `res.locals.session` (401 without a session;
33
+ * mount behind your global auth middleware) and rejects non-participants with
34
+ * 403/404. The sender is always the session user — never accept a sender id
35
+ * from the request body.
36
+ *
37
+ * Realtime delivery is best-effort — messages are persisted before any
38
+ * broadcast attempt and a missing realtime bond is silently a no-op.
39
+ * Subscribers should listen on `threadRoomId(threadId)` for
40
+ * {@link MESSAGE_REALTIME_EVENTS} payloads.
41
+ *
42
+ * User-facing strings go through `t(key, …, { defaultValue })`; translations
43
+ * ship in the companion `@molecule/api-locales-resource-message` bond.
44
+ *
45
+ * @e2e
46
+ * Integration checklist — drive the real messaging UI (live preview, no
47
+ * mocks), adapt each item to this app's actual threads/screens, and check
48
+ * every box off one by one. A box you can't check is an integration bug to
49
+ * fix — not a skip. Messaging PRIVACY is the point here, so the last item is
50
+ * not optional:
51
+ * - [ ] Sending a message in a thread persists it and it appears in that
52
+ * thread for BOTH participants in chronological (`createdAt`) order, stamped
53
+ * with the sender and a timestamp. Send from each side and confirm both see
54
+ * the same ordered transcript.
55
+ * - [ ] Read / unread works: a message you send is unread for the recipient —
56
+ * their thread badge and `GET /message-threads/unread-count` increment;
57
+ * opening the thread and marking it read (`POST /message-threads/:threadId/read`)
58
+ * clears that side's unread count to zero and the total badge drops to match.
59
+ * - [ ] Editing a message shows an edited state (an "edited" marker /
60
+ * `editedAt`) and deleting it removes it or renders a "message was deleted"
61
+ * tombstone (`deletedAt`) — and ONLY the author can edit or delete their OWN
62
+ * message: the other participant gets no edit/delete affordance and a forged
63
+ * PATCH/DELETE on someone else's message is rejected, never applied.
64
+ * - [ ] Delivery to the other participant: with `@molecule/api-realtime`
65
+ * bonded, a new message appears in their already-open thread WITHOUT a
66
+ * reload; with no realtime bond it appears on their next load/refresh.
67
+ * - [ ] PRIVACY / AUTHORIZATION — a thread and its messages are visible ONLY
68
+ * to its two participants. Sign in as a THIRD user and confirm they cannot
69
+ * read the thread or any message by guessing its id (403/404), cannot post
70
+ * into a thread they are not part of (403), and cannot spoof the sender —
71
+ * the sender is always the session user, never a request-body field.
72
+ */
73
+ export * from './authorizers/index.js';
74
+ export * from './browser-guard.js';
75
+ export * from './handlers/index.js';
76
+ export * from './i18n.js';
77
+ export * from './requestHandlerMap.js';
78
+ export * from './routes.js';
79
+ export * from './service.js';
80
+ export * from './types.js';
81
+ export * from './validation.js';
82
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuEG;AAEH,cAAc,wBAAwB,CAAA;AACtC,cAAc,oBAAoB,CAAA;AAClC,cAAc,qBAAqB,CAAA;AACnC,cAAc,WAAW,CAAA;AACzB,cAAc,wBAAwB,CAAA;AACtC,cAAc,aAAa,CAAA;AAC3B,cAAc,cAAc,CAAA;AAC5B,cAAc,YAAY,CAAA;AAC1B,cAAc,iBAAiB,CAAA"}
package/dist/index.js ADDED
@@ -0,0 +1,81 @@
1
+ /**
2
+ * 1:1 messaging resource for molecule.dev.
3
+ *
4
+ * Direct-message threads between two participants with read-tracking,
5
+ * unread counters, optional attachments, and realtime broadcast over
6
+ * `@molecule/api-realtime` when bonded.
7
+ *
8
+ * @module
9
+ * @example
10
+ * ```typescript
11
+ * import { routes, requestHandlerMap } from '@molecule/api-resource-message'
12
+ *
13
+ * // Wire routes into your Express app via mlcl inject
14
+ * // POST /message-threads
15
+ * // GET /message-threads
16
+ * // GET /message-threads/unread-count
17
+ * // GET /message-threads/:threadId
18
+ * // GET /message-threads/:threadId/messages
19
+ * // POST /message-threads/:threadId/messages
20
+ * // POST /message-threads/:threadId/read
21
+ * // PATCH /message-threads/messages/:messageId
22
+ * // DELETE /message-threads/messages/:messageId
23
+ * ```
24
+ *
25
+ * @remarks
26
+ * Tables: `src/__setup__/messages.sql` creates `message_threads` + `messages`.
27
+ * An mlcl-scaffolded API replays `__setup__/*.sql` automatically on migrate;
28
+ * anywhere else run it once — nothing at runtime creates them.
29
+ *
30
+ * Threads are strictly 1:1 — a canonicalised participant pair (creating a
31
+ * thread with yourself throws) — and PARTICIPANT-SCOPED: every handler reads
32
+ * the authenticated user from `res.locals.session` (401 without a session;
33
+ * mount behind your global auth middleware) and rejects non-participants with
34
+ * 403/404. The sender is always the session user — never accept a sender id
35
+ * from the request body.
36
+ *
37
+ * Realtime delivery is best-effort — messages are persisted before any
38
+ * broadcast attempt and a missing realtime bond is silently a no-op.
39
+ * Subscribers should listen on `threadRoomId(threadId)` for
40
+ * {@link MESSAGE_REALTIME_EVENTS} payloads.
41
+ *
42
+ * User-facing strings go through `t(key, …, { defaultValue })`; translations
43
+ * ship in the companion `@molecule/api-locales-resource-message` bond.
44
+ *
45
+ * @e2e
46
+ * Integration checklist — drive the real messaging UI (live preview, no
47
+ * mocks), adapt each item to this app's actual threads/screens, and check
48
+ * every box off one by one. A box you can't check is an integration bug to
49
+ * fix — not a skip. Messaging PRIVACY is the point here, so the last item is
50
+ * not optional:
51
+ * - [ ] Sending a message in a thread persists it and it appears in that
52
+ * thread for BOTH participants in chronological (`createdAt`) order, stamped
53
+ * with the sender and a timestamp. Send from each side and confirm both see
54
+ * the same ordered transcript.
55
+ * - [ ] Read / unread works: a message you send is unread for the recipient —
56
+ * their thread badge and `GET /message-threads/unread-count` increment;
57
+ * opening the thread and marking it read (`POST /message-threads/:threadId/read`)
58
+ * clears that side's unread count to zero and the total badge drops to match.
59
+ * - [ ] Editing a message shows an edited state (an "edited" marker /
60
+ * `editedAt`) and deleting it removes it or renders a "message was deleted"
61
+ * tombstone (`deletedAt`) — and ONLY the author can edit or delete their OWN
62
+ * message: the other participant gets no edit/delete affordance and a forged
63
+ * PATCH/DELETE on someone else's message is rejected, never applied.
64
+ * - [ ] Delivery to the other participant: with `@molecule/api-realtime`
65
+ * bonded, a new message appears in their already-open thread WITHOUT a
66
+ * reload; with no realtime bond it appears on their next load/refresh.
67
+ * - [ ] PRIVACY / AUTHORIZATION — a thread and its messages are visible ONLY
68
+ * to its two participants. Sign in as a THIRD user and confirm they cannot
69
+ * read the thread or any message by guessing its id (403/404), cannot post
70
+ * into a thread they are not part of (403), and cannot spoof the sender —
71
+ * the sender is always the session user, never a request-body field.
72
+ */
73
+ export * from './authorizers/index.js';
74
+ export * from './browser-guard.js';
75
+ export * from './handlers/index.js';
76
+ export * from './i18n.js';
77
+ export * from './requestHandlerMap.js';
78
+ export * from './routes.js';
79
+ export * from './service.js';
80
+ export * from './types.js';
81
+ export * from './validation.js';
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Maps route handler names to their implementations.
3
+ *
4
+ * @module
5
+ */
6
+ import { createThread } from './handlers/createThread.js';
7
+ import { deleteMessageHandler } from './handlers/deleteMessage.js';
8
+ import { editMessageHandler } from './handlers/editMessage.js';
9
+ import { listThreads } from './handlers/listThreads.js';
10
+ import { markReadHandler } from './handlers/markRead.js';
11
+ import { listMessagesHandler, sendMessageHandler } from './handlers/messages.js';
12
+ import { readThread } from './handlers/readThread.js';
13
+ import { unreadCount } from './handlers/unreadCount.js';
14
+ /**
15
+ * Handler map for message resource routes.
16
+ */
17
+ export declare const requestHandlerMap: {
18
+ readonly createThread: typeof createThread;
19
+ readonly listThreads: typeof listThreads;
20
+ readonly readThread: typeof readThread;
21
+ readonly listMessages: typeof listMessagesHandler;
22
+ readonly sendMessage: typeof sendMessageHandler;
23
+ readonly markRead: typeof markReadHandler;
24
+ readonly editMessage: typeof editMessageHandler;
25
+ readonly deleteMessage: typeof deleteMessageHandler;
26
+ readonly unreadCount: typeof unreadCount;
27
+ };
28
+ //# sourceMappingURL=requestHandlerMap.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"requestHandlerMap.d.ts","sourceRoot":"","sources":["../src/requestHandlerMap.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAA;AACzD,OAAO,EAAE,oBAAoB,EAAE,MAAM,6BAA6B,CAAA;AAClE,OAAO,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAA;AAC9D,OAAO,EAAE,WAAW,EAAE,MAAM,2BAA2B,CAAA;AACvD,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAA;AACxD,OAAO,EAAE,mBAAmB,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAA;AAChF,OAAO,EAAE,UAAU,EAAE,MAAM,0BAA0B,CAAA;AACrD,OAAO,EAAE,WAAW,EAAE,MAAM,2BAA2B,CAAA;AAEvD;;GAEG;AACH,eAAO,MAAM,iBAAiB;;;;;;;;;;CAUpB,CAAA"}
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Maps route handler names to their implementations.
3
+ *
4
+ * @module
5
+ */
6
+ import { createThread } from './handlers/createThread.js';
7
+ import { deleteMessageHandler } from './handlers/deleteMessage.js';
8
+ import { editMessageHandler } from './handlers/editMessage.js';
9
+ import { listThreads } from './handlers/listThreads.js';
10
+ import { markReadHandler } from './handlers/markRead.js';
11
+ import { listMessagesHandler, sendMessageHandler } from './handlers/messages.js';
12
+ import { readThread } from './handlers/readThread.js';
13
+ import { unreadCount } from './handlers/unreadCount.js';
14
+ /**
15
+ * Handler map for message resource routes.
16
+ */
17
+ export const requestHandlerMap = {
18
+ createThread,
19
+ listThreads,
20
+ readThread,
21
+ listMessages: listMessagesHandler,
22
+ sendMessage: sendMessageHandler,
23
+ markRead: markReadHandler,
24
+ editMessage: editMessageHandler,
25
+ deleteMessage: deleteMessageHandler,
26
+ unreadCount,
27
+ };
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Message resource route definitions.
3
+ *
4
+ * Declarative routing — `mlcl inject` reads these to generate the
5
+ * Express router.
6
+ *
7
+ * @module
8
+ */
9
+ /**
10
+ * Routes for 1:1 message threads, messages, and read-tracking.
11
+ */
12
+ export declare const routes: readonly [{
13
+ readonly method: "post";
14
+ readonly path: "/message-threads";
15
+ readonly handler: "createThread";
16
+ readonly middlewares: readonly ["authenticate"];
17
+ }, {
18
+ readonly method: "get";
19
+ readonly path: "/message-threads";
20
+ readonly handler: "listThreads";
21
+ readonly middlewares: readonly ["authenticate"];
22
+ }, {
23
+ readonly method: "get";
24
+ readonly path: "/message-threads/unread-count";
25
+ readonly handler: "unreadCount";
26
+ readonly middlewares: readonly ["authenticate"];
27
+ }, {
28
+ readonly method: "get";
29
+ readonly path: "/message-threads/:threadId";
30
+ readonly handler: "readThread";
31
+ readonly middlewares: readonly ["authenticate"];
32
+ }, {
33
+ readonly method: "get";
34
+ readonly path: "/message-threads/:threadId/messages";
35
+ readonly handler: "listMessages";
36
+ readonly middlewares: readonly ["authenticate"];
37
+ }, {
38
+ readonly method: "post";
39
+ readonly path: "/message-threads/:threadId/messages";
40
+ readonly handler: "sendMessage";
41
+ readonly middlewares: readonly ["authenticate"];
42
+ }, {
43
+ readonly method: "post";
44
+ readonly path: "/message-threads/:threadId/read";
45
+ readonly handler: "markRead";
46
+ readonly middlewares: readonly ["authenticate"];
47
+ }, {
48
+ readonly method: "patch";
49
+ readonly path: "/message-threads/messages/:messageId";
50
+ readonly handler: "editMessage";
51
+ readonly middlewares: readonly ["authenticate"];
52
+ }, {
53
+ readonly method: "delete";
54
+ readonly path: "/message-threads/messages/:messageId";
55
+ readonly handler: "deleteMessage";
56
+ readonly middlewares: readonly ["authenticate"];
57
+ }];
58
+ //# sourceMappingURL=routes.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../src/routes.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH;;GAEG;AACH,eAAO,MAAM,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAuDT,CAAA"}
package/dist/routes.js ADDED
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Message resource route definitions.
3
+ *
4
+ * Declarative routing — `mlcl inject` reads these to generate the
5
+ * Express router.
6
+ *
7
+ * @module
8
+ */
9
+ /**
10
+ * Routes for 1:1 message threads, messages, and read-tracking.
11
+ */
12
+ export const routes = [
13
+ {
14
+ method: 'post',
15
+ path: '/message-threads',
16
+ handler: 'createThread',
17
+ middlewares: ['authenticate'],
18
+ },
19
+ {
20
+ method: 'get',
21
+ path: '/message-threads',
22
+ handler: 'listThreads',
23
+ middlewares: ['authenticate'],
24
+ },
25
+ {
26
+ method: 'get',
27
+ path: '/message-threads/unread-count',
28
+ handler: 'unreadCount',
29
+ middlewares: ['authenticate'],
30
+ },
31
+ {
32
+ method: 'get',
33
+ path: '/message-threads/:threadId',
34
+ handler: 'readThread',
35
+ middlewares: ['authenticate'],
36
+ },
37
+ {
38
+ method: 'get',
39
+ path: '/message-threads/:threadId/messages',
40
+ handler: 'listMessages',
41
+ middlewares: ['authenticate'],
42
+ },
43
+ {
44
+ method: 'post',
45
+ path: '/message-threads/:threadId/messages',
46
+ handler: 'sendMessage',
47
+ middlewares: ['authenticate'],
48
+ },
49
+ {
50
+ method: 'post',
51
+ path: '/message-threads/:threadId/read',
52
+ handler: 'markRead',
53
+ middlewares: ['authenticate'],
54
+ },
55
+ {
56
+ method: 'patch',
57
+ path: '/message-threads/messages/:messageId',
58
+ handler: 'editMessage',
59
+ middlewares: ['authenticate'],
60
+ },
61
+ {
62
+ method: 'delete',
63
+ path: '/message-threads/messages/:messageId',
64
+ handler: 'deleteMessage',
65
+ middlewares: ['authenticate'],
66
+ },
67
+ ];