@bopen-io/messagebox-server 1.1.5 → 1.2.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 (113) hide show
  1. package/.dockerignore +11 -0
  2. package/.env.example +11 -0
  3. package/.eslintignore +2 -0
  4. package/.gcloudignore +17 -0
  5. package/.gitattributes +2 -0
  6. package/.github/workflows/build-and-push.yml +51 -0
  7. package/.github/workflows/deploy.yaml.example +64 -0
  8. package/.github/workflows/image.yaml +66 -0
  9. package/.vscode/launch.json +67 -0
  10. package/AGENTS.md +92 -0
  11. package/BASELINE.md +55 -0
  12. package/DEPLOYING.md +120 -0
  13. package/Dockerfile +63 -0
  14. package/docker-compose.yml +64 -0
  15. package/jest.config.integration.ts +26 -0
  16. package/jest.config.mjs +42 -0
  17. package/jest.config.ts +35 -0
  18. package/jest.setup.mjs +18 -0
  19. package/nginx.conf +36 -0
  20. package/package.json +65 -51
  21. package/scripts/build.js +19 -0
  22. package/scripts/mkenv.sh +31 -0
  23. package/scripts/start.sh +16 -0
  24. package/src/__tests/compose.test.ts +30 -0
  25. package/src/app.ts +210 -0
  26. package/src/compose.ts +286 -0
  27. package/src/config/firebase.ts +183 -0
  28. package/src/context.ts +49 -0
  29. package/src/index.ts +375 -0
  30. package/src/routes/__tests/acknowledgeMessage.test.ts +158 -0
  31. package/src/routes/__tests/listMessages.test.ts +240 -0
  32. package/src/routes/__tests/sendMessage.test.ts +362 -0
  33. package/src/routes/acknowledgeMessage.ts +153 -0
  34. package/src/routes/index.ts +17 -0
  35. package/src/routes/listDevices.ts +80 -0
  36. package/src/routes/listMessages.ts +195 -0
  37. package/src/routes/permissions/getPermission.ts +152 -0
  38. package/src/routes/permissions/getQuote.ts +192 -0
  39. package/src/routes/permissions/index.ts +12 -0
  40. package/src/routes/permissions/listPermissions.ts +202 -0
  41. package/src/routes/permissions/setPermission.ts +155 -0
  42. package/src/routes/registerDevice.ts +155 -0
  43. package/src/routes/sendMessage.ts +531 -0
  44. package/src/runtimeDeps.ts +29 -0
  45. package/src/swagger.ts +54 -0
  46. package/src/telemetry.ts +158 -0
  47. package/src/types/messagePermissions.ts +18 -0
  48. package/src/types/mock-knex.d.ts +18 -0
  49. package/src/types/notifications.ts +27 -0
  50. package/src/utils/logger.ts +92 -0
  51. package/src/utils/messagePermissions.ts +148 -0
  52. package/src/utils/sendFCMNotification.ts +143 -0
  53. package/tsconfig.json +36 -0
  54. package/tsconfig.types.json +11 -0
  55. package/out/knexfile.js +0 -25
  56. package/out/knexfile.js.map +0 -1
  57. package/out/src/app.js +0 -160
  58. package/out/src/app.js.map +0 -1
  59. package/out/src/config/firebase.js +0 -150
  60. package/out/src/config/firebase.js.map +0 -1
  61. package/out/src/index.js +0 -295
  62. package/out/src/index.js.map +0 -1
  63. package/out/src/migrations/2022-12-28-001-initial-migration.js +0 -28
  64. package/out/src/migrations/2022-12-28-001-initial-migration.js.map +0 -1
  65. package/out/src/migrations/2023-01-17-messages-update.js +0 -11
  66. package/out/src/migrations/2023-01-17-messages-update.js.map +0 -1
  67. package/out/src/migrations/2024-03-05-001-messageID-upgrade.js +0 -21
  68. package/out/src/migrations/2024-03-05-001-messageID-upgrade.js.map +0 -1
  69. package/out/src/migrations/2025-01-31-001-notification-permissions.js +0 -59
  70. package/out/src/migrations/2025-01-31-001-notification-permissions.js.map +0 -1
  71. package/out/src/migrations/2025-01-31-002-device-registrations.js +0 -24
  72. package/out/src/migrations/2025-01-31-002-device-registrations.js.map +0 -1
  73. package/out/src/routes/__tests/acknowledgeMessage.test.js +0 -129
  74. package/out/src/routes/__tests/acknowledgeMessage.test.js.map +0 -1
  75. package/out/src/routes/__tests/listMessages.test.js +0 -211
  76. package/out/src/routes/__tests/listMessages.test.js.map +0 -1
  77. package/out/src/routes/__tests/sendMessage.test.js +0 -307
  78. package/out/src/routes/__tests/sendMessage.test.js.map +0 -1
  79. package/out/src/routes/acknowledgeMessage.js +0 -144
  80. package/out/src/routes/acknowledgeMessage.js.map +0 -1
  81. package/out/src/routes/index.js +0 -17
  82. package/out/src/routes/index.js.map +0 -1
  83. package/out/src/routes/listDevices.js +0 -74
  84. package/out/src/routes/listDevices.js.map +0 -1
  85. package/out/src/routes/listMessages.js +0 -186
  86. package/out/src/routes/listMessages.js.map +0 -1
  87. package/out/src/routes/permissions/getPermission.js +0 -149
  88. package/out/src/routes/permissions/getPermission.js.map +0 -1
  89. package/out/src/routes/permissions/getQuote.js +0 -165
  90. package/out/src/routes/permissions/getQuote.js.map +0 -1
  91. package/out/src/routes/permissions/index.js +0 -12
  92. package/out/src/routes/permissions/index.js.map +0 -1
  93. package/out/src/routes/permissions/listPermissions.js +0 -190
  94. package/out/src/routes/permissions/listPermissions.js.map +0 -1
  95. package/out/src/routes/permissions/setPermission.js +0 -137
  96. package/out/src/routes/permissions/setPermission.js.map +0 -1
  97. package/out/src/routes/registerDevice.js +0 -150
  98. package/out/src/routes/registerDevice.js.map +0 -1
  99. package/out/src/routes/sendMessage.js +0 -451
  100. package/out/src/routes/sendMessage.js.map +0 -1
  101. package/out/src/swagger.js +0 -49
  102. package/out/src/swagger.js.map +0 -1
  103. package/out/src/types/messagePermissions.js +0 -2
  104. package/out/src/types/messagePermissions.js.map +0 -1
  105. package/out/src/types/notifications.js +0 -2
  106. package/out/src/types/notifications.js.map +0 -1
  107. package/out/src/utils/logger.js +0 -24
  108. package/out/src/utils/logger.js.map +0 -1
  109. package/out/src/utils/messagePermissions.js +0 -126
  110. package/out/src/utils/messagePermissions.js.map +0 -1
  111. package/out/src/utils/sendFCMNotification.js +0 -121
  112. package/out/src/utils/sendFCMNotification.js.map +0 -1
  113. package/out/tsconfig.tsbuildinfo +0 -1
@@ -0,0 +1,153 @@
1
+ /**
2
+ * @file acknowledgeMessage.ts
3
+ * @description
4
+ * Express route to allow a client to acknowledge receipt of one or more messages.
5
+ * Acknowledged messages are permanently removed from the database for the
6
+ * authenticated identity key (recipient).
7
+ *
8
+ * This is used in the MessageBox system to clear delivered messages once received
9
+ * and handled on the client side (e.g., after syncing or displaying them).
10
+ */
11
+
12
+ import { Request, Response } from 'express'
13
+ import { Logger } from '../utils/logger.js'
14
+ import { runtimeDeps } from '../runtimeDeps.js'
15
+
16
+ /**
17
+ * @interface AcknowledgeRequest
18
+ * @extends Request
19
+ * @description Represents an authenticated request body for acknowledging messages.
20
+ */
21
+ export interface AcknowledgeRequest extends Request {
22
+ auth: { identityKey: string }
23
+ body: { messageIds?: string[] }
24
+ }
25
+
26
+ /**
27
+ * @openapi
28
+ * /acknowledgeMessage:
29
+ * post:
30
+ * summary: Acknowledge receipt of one or more messages
31
+ * description: |
32
+ * Removes acknowledged messages from the database for the authenticated identity key.
33
+ * This is used after a client has received and handled messages.
34
+ * tags:
35
+ * - Message
36
+ * requestBody:
37
+ * required: true
38
+ * content:
39
+ * application/json:
40
+ * schema:
41
+ * type: object
42
+ * properties:
43
+ * messageIds:
44
+ * type: array
45
+ * items:
46
+ * type: string
47
+ * description: Array of message IDs to acknowledge
48
+ * responses:
49
+ * 200:
50
+ * description: Successfully acknowledged messages
51
+ * content:
52
+ * application/json:
53
+ * schema:
54
+ * type: object
55
+ * properties:
56
+ * status:
57
+ * type: string
58
+ * example: success
59
+ * 400:
60
+ * description: Invalid input or message not found
61
+ * 500:
62
+ * description: Internal server error
63
+ */
64
+
65
+ /**
66
+ * @exports
67
+ * Route definition for acknowledging MessageBox messages.
68
+ * This object is consumed by the Express route loader to register the endpoint.
69
+ */
70
+ export default {
71
+ type: 'post',
72
+ path: '/acknowledgeMessage',
73
+ get knex () { return runtimeDeps.knex },
74
+ summary: 'Use this route to acknowledge a message has been received',
75
+ parameters: {
76
+ messageIds: ['3301']
77
+ },
78
+ exampleResponse: {
79
+ status: 'success'
80
+ },
81
+ errors: [],
82
+
83
+ /**
84
+ * @function func
85
+ * @description
86
+ * Express route handler that processes a POST request to acknowledge messages.
87
+ * Deletes messages from the database where:
88
+ * - recipient matches the authenticated identity key
89
+ * - messageId matches one or more of the provided IDs
90
+ *
91
+ * Returns:
92
+ * - 200 success if deletion occurs
93
+ * - 400 if no messages were found or input is invalid
94
+ * - 500 on internal error
95
+ *
96
+ * @param {AcknowledgeRequest} req - Express request object containing auth and message IDs
97
+ * @param {Response} res - Express response object
98
+ * @returns {Promise<Response>} JSON response with status and optional error codes
99
+ */
100
+ func: async (req: AcknowledgeRequest, res: Response): Promise<Response> => {
101
+ try {
102
+ const { messageIds } = req.body
103
+
104
+ Logger.log('[SERVER] acknowledgeMessage called for messageIds:', messageIds, 'by', req.auth.identityKey)
105
+
106
+ // Validate request: must be a non-empty array of strings
107
+ if ((messageIds == null) || (Array.isArray(messageIds) && messageIds.length === 0)) {
108
+ return res.status(400).json({
109
+ status: 'error',
110
+ code: 'ERR_MESSAGE_ID_REQUIRED',
111
+ description: 'Please provide the ID of the message(s) to acknowledge!'
112
+ })
113
+ }
114
+
115
+ if (!Array.isArray(messageIds) || messageIds.some(id => typeof id !== 'string')) {
116
+ return res.status(400).json({
117
+ status: 'error',
118
+ code: 'ERR_INVALID_MESSAGE_ID',
119
+ description: 'Message IDs must be formatted as an array of strings!'
120
+ })
121
+ }
122
+
123
+ // Delete acknowledged messages for this recipient from the database
124
+ const deleted = await runtimeDeps.knex('messages')
125
+ .where({ recipient: req.auth.identityKey })
126
+ .whereIn('messageId', Array.isArray(messageIds) ? messageIds : [messageIds])
127
+ .del()
128
+
129
+ // No matching messages found
130
+ if (deleted === 0) {
131
+ return res.status(400).json({
132
+ status: 'error',
133
+ code: 'ERR_INVALID_ACKNOWLEDGMENT',
134
+ description: 'Message not found!'
135
+ })
136
+ }
137
+
138
+ // Deletion failed unexpectedly
139
+ if (deleted < 0) {
140
+ throw new Error('Deletion failed')
141
+ }
142
+
143
+ return res.status(200).json({ status: 'success' })
144
+ } catch (e) {
145
+ Logger.error(e)
146
+ return res.status(500).json({
147
+ status: 'error',
148
+ code: 'ERR_INTERNAL_ERROR',
149
+ description: 'An internal error has occurred while acknowledging the message'
150
+ })
151
+ }
152
+ }
153
+ }
@@ -0,0 +1,17 @@
1
+ import sendMessage from './sendMessage.js'
2
+ import listMessages from './listMessages.js'
3
+ import acknowledgeMessage from './acknowledgeMessage.js'
4
+ import registerDevice from './registerDevice.js'
5
+ import listDevices from './listDevices.js'
6
+ import { permissionRoutes } from './permissions/index.js'
7
+
8
+ // Explicitly type the exported arrays to avoid type inference issues
9
+ export const preAuth: Array<{ type: string, path: string, func: Function }> = []
10
+ export const postAuth: Array<{ type: string, path: string, func: Function }> = [
11
+ sendMessage,
12
+ listMessages,
13
+ acknowledgeMessage,
14
+ registerDevice,
15
+ listDevices,
16
+ ...permissionRoutes
17
+ ]
@@ -0,0 +1,80 @@
1
+ import { Response } from 'express'
2
+ import { Logger } from '../utils/logger.js'
3
+ import { AuthRequest } from '@bsv/auth-express-middleware'
4
+ import { runtimeDeps } from '../runtimeDeps.js'
5
+
6
+ export interface RegisteredDevice {
7
+ id: number
8
+ deviceId: string | null
9
+ platform: string | null
10
+ fcmToken: string
11
+ active: boolean
12
+ createdAt: string
13
+ updatedAt: string
14
+ lastUsed: string
15
+ }
16
+
17
+ export default {
18
+ type: 'get',
19
+ path: '/devices',
20
+ func: async (req: AuthRequest, res: Response): Promise<Response> => {
21
+ try {
22
+ Logger.log('[DEBUG] Processing list devices request')
23
+
24
+ // Validate authentication
25
+ const identityKey = req.auth?.identityKey
26
+ if (identityKey == null) {
27
+ Logger.log('[DEBUG] Authentication required for listing devices')
28
+ return res.status(401).json({
29
+ status: 'error',
30
+ code: 'ERR_AUTHENTICATION_REQUIRED',
31
+ description: 'Authentication required.'
32
+ })
33
+ }
34
+
35
+ try {
36
+ // Query devices for the authenticated user
37
+ const devices = await runtimeDeps.knex('device_registrations')
38
+ .select([
39
+ 'id',
40
+ 'device_id as deviceId',
41
+ 'platform',
42
+ 'fcm_token as fcmToken',
43
+ 'active',
44
+ 'created_at as createdAt',
45
+ 'updated_at as updatedAt',
46
+ 'last_used as lastUsed'
47
+ ])
48
+ .where('identity_key', identityKey)
49
+ .orderBy('updated_at', 'desc')
50
+
51
+ Logger.log(`[DEBUG] Found ${devices.length} registered devices for ${identityKey}`)
52
+
53
+ return res.status(200).json({
54
+ status: 'success',
55
+ devices: devices.map(device => ({
56
+ ...device,
57
+ // Truncate FCM token for security (show only last 10 characters)
58
+ fcmToken: `...${device.fcmToken.slice(-10)}`
59
+ }))
60
+ })
61
+
62
+ } catch (dbError: any) {
63
+ Logger.error('[ERROR] Database error during device listing:', dbError)
64
+ return res.status(500).json({
65
+ status: 'error',
66
+ code: 'ERR_DATABASE_ERROR',
67
+ description: 'Failed to retrieve devices.'
68
+ })
69
+ }
70
+
71
+ } catch (error) {
72
+ Logger.error('[ERROR] Internal Server Error in listDevices:', error)
73
+ return res.status(500).json({
74
+ status: 'error',
75
+ code: 'ERR_INTERNAL',
76
+ description: 'An internal error has occurred.'
77
+ })
78
+ }
79
+ }
80
+ }
@@ -0,0 +1,195 @@
1
+ /**
2
+ * @file listMessages.ts
3
+ * @description
4
+ * This route allows an authenticated user to retrieve messages from a specific named messageBox.
5
+ *
6
+ * Messages are only returned if the authenticated identity has access to the specified messageBox.
7
+ * If the messageBox does not exist, an empty message list is returned.
8
+ *
9
+ * Typical usage: Inbox or queue retrieval for real-time or deferred message delivery.
10
+ */
11
+
12
+ import { Response } from 'express'
13
+ import { AuthRequest } from '@bsv/auth-express-middleware'
14
+ import { log } from '../utils/logger.js'
15
+ import { runtimeDeps } from '../runtimeDeps.js'
16
+
17
+ /**
18
+ * @interface ListMessagesRequest
19
+ * @extends Request
20
+ * @description Extends Express Request to include `auth` identity and expected `messageBox` body property.
21
+ */
22
+ interface ListMessagesRequest extends AuthRequest {
23
+ body: { messageBox?: string }
24
+ }
25
+
26
+ /**
27
+ * @openapi
28
+ * /listMessages:
29
+ * post:
30
+ * summary: Retrieve messages from a specific messageBox
31
+ * description: |
32
+ * Returns all stored messages for the specified messageBox that belong to the authenticated identity.
33
+ * If the box does not exist or has no messages, an empty array is returned.
34
+ * tags:
35
+ * - Message
36
+ * requestBody:
37
+ * required: true
38
+ * content:
39
+ * application/json:
40
+ * schema:
41
+ * type: object
42
+ * properties:
43
+ * messageBox:
44
+ * type: string
45
+ * description: The name of the messageBox to retrieve messages from
46
+ * responses:
47
+ * 200:
48
+ * description: Successfully retrieved messages (can be empty)
49
+ * content:
50
+ * application/json:
51
+ * schema:
52
+ * type: object
53
+ * properties:
54
+ * status:
55
+ * type: string
56
+ * example: success
57
+ * messages:
58
+ * type: array
59
+ * items:
60
+ * type: object
61
+ * properties:
62
+ * messageId:
63
+ * type: string
64
+ * body:
65
+ * type: string
66
+ * sender:
67
+ * type: string
68
+ * createdAt:
69
+ * type: string
70
+ * format: date-time
71
+ * updatedAt:
72
+ * type: string
73
+ * format: date-time
74
+ * 400:
75
+ * description: Invalid or missing messageBox name
76
+ * 500:
77
+ * description: Internal server/database error
78
+ */
79
+
80
+ /**
81
+ * @exports
82
+ * Route definition used by the Express router to expose the `/listMessages` POST endpoint.
83
+ * Responsible for querying stored messages from a messageBox owned by the authenticated user.
84
+ */
85
+ export default {
86
+ type: 'post',
87
+ path: '/listMessages',
88
+ get knex () { return runtimeDeps.knex },
89
+ summary: 'Use this route to list messages from your messageBox.',
90
+ parameters: {
91
+ messageBox: 'The name of the messageBox you would like to list messages from.'
92
+ },
93
+ exampleResponse: {
94
+ status: 'success',
95
+ messages: [
96
+ {
97
+ messageId: '3301',
98
+ body: '{}',
99
+ sender: '028d37b941208cd6b8a4c28288eda5f2f16c2b3ab0fcb6d13c18b47fe37b971fc1'
100
+ }
101
+ ]
102
+ },
103
+ /**
104
+ * @function func
105
+ * @description
106
+ * Express handler for listing stored messages in a specified messageBox.
107
+ *
108
+ * Input:
109
+ * - `req.body.messageBox`: Name of the messageBox to retrieve messages from.
110
+ * - `req.auth.identityKey`: Authenticated user’s public identity key.
111
+ *
112
+ * Behavior:
113
+ * - Checks if the specified messageBox exists for the identity.
114
+ * - If found, returns all messages in that messageBox.
115
+ * - If not found, returns an empty array.
116
+ * - Normalizes all message bodies to strings for consistent output.
117
+ *
118
+ * Output:
119
+ * - 200 with `{ status: 'success', messages: [...] }`
120
+ * - 400 if input is missing or malformed.
121
+ * - 500 on internal server/database errors.
122
+ *
123
+ * @param {ListMessagesRequest} req - Authenticated request containing the messageBox name
124
+ * @param {Response} res - Express response object
125
+ * @returns {Promise<Response>} JSON response containing message records or an error
126
+ */
127
+ func: async (req: ListMessagesRequest, res: Response): Promise<Response> => {
128
+ try {
129
+ const { messageBox } = req.body
130
+
131
+ // Validate a messageBox is provided and is a string
132
+ if (messageBox == null || messageBox === '') {
133
+ return res.status(400).json({
134
+ status: 'error',
135
+ code: 'ERR_MESSAGEBOX_REQUIRED',
136
+ description: 'Please provide the name of a valid MessageBox!'
137
+ })
138
+ }
139
+
140
+ if (typeof messageBox !== 'string') {
141
+ return res.status(400).json({
142
+ status: 'error',
143
+ code: 'ERR_INVALID_MESSAGEBOX',
144
+ description: 'MessageBox name must be a string!'
145
+ })
146
+ }
147
+
148
+ // Find the messageBox ID for this user
149
+ const [messageBoxRecord] = await runtimeDeps.knex('messageBox')
150
+ .where({
151
+ identityKey: req.auth?.identityKey,
152
+ type: messageBox
153
+ })
154
+ .select('messageBoxId')
155
+
156
+ // Return empty array if no messageBox was found
157
+ if (messageBoxRecord === undefined) {
158
+ return res.status(200).json({
159
+ status: 'success',
160
+ messages: []
161
+ })
162
+ }
163
+
164
+ // Retrieve all messages associated with the messageBox
165
+ const messages = await runtimeDeps.knex('messages')
166
+ .where({
167
+ recipient: req.auth?.identityKey,
168
+ messageBoxId: messageBoxRecord.messageBoxId
169
+ })
170
+ .select('messageId', 'body', 'sender', 'created_at', 'updated_at')
171
+
172
+ // Normalize all message bodies to strings and convert to camelCase
173
+ const formattedMessages = messages.map(message => ({
174
+ messageId: message.messageId,
175
+ body: typeof message.body === 'string' ? message.body : JSON.stringify(message.body),
176
+ sender: message.sender,
177
+ createdAt: message.created_at,
178
+ updatedAt: message.updated_at
179
+ }))
180
+
181
+ // Return a list of matching messages
182
+ return res.status(200).json({
183
+ status: 'success',
184
+ messages: formattedMessages
185
+ })
186
+ } catch (e) {
187
+ log.error({ operation: 'messages.list', outcome: 'error', err: e }, 'Failed to list messages')
188
+ return res.status(500).json({
189
+ status: 'error',
190
+ code: 'ERR_INTERNAL_ERROR',
191
+ description: 'An internal error has occurred while listing messages.'
192
+ })
193
+ }
194
+ }
195
+ }
@@ -0,0 +1,152 @@
1
+ import { Response } from 'express'
2
+ import { PublicKey } from '@bsv/sdk'
3
+ import { Logger } from '../../utils/logger.js'
4
+ import { AuthRequest } from '@bsv/auth-express-middleware'
5
+ import { runtimeDeps } from '../../runtimeDeps.js'
6
+
7
+ export interface GetPermissionRequest extends AuthRequest {
8
+ query: {
9
+ sender?: string // identityKey of sender to check
10
+ messageBox?: string // messageBox type to check
11
+ }
12
+ }
13
+
14
+ /**
15
+ * @swagger
16
+ * /permissions/get:
17
+ * get:
18
+ * summary: Get message permission for a sender/box combination
19
+ * description: Retrieve the permission setting for a specific sender and message box combination
20
+ * tags:
21
+ * - Permissions
22
+ * parameters:
23
+ * - in: query
24
+ * name: sender
25
+ * required: false
26
+ * schema:
27
+ * type: string
28
+ * description: identityKey of the sender to check (omit for box-wide default)
29
+ * - in: query
30
+ * name: messageBox
31
+ * required: true
32
+ * schema:
33
+ * type: string
34
+ * description: messageBox type to check
35
+ * responses:
36
+ * 200:
37
+ * description: Permission setting retrieved successfully (or undefined if not set)
38
+ * 400:
39
+ * description: Invalid request parameters
40
+ * 401:
41
+ * description: Authentication required
42
+ * 500:
43
+ * description: Internal server error
44
+ */
45
+ export default {
46
+ type: 'get',
47
+ path: '/permissions/get',
48
+ func: async (req: GetPermissionRequest, res: Response): Promise<Response> => {
49
+ try {
50
+ Logger.log('[DEBUG] Processing get message permission request')
51
+
52
+ // Validate authentication
53
+ if (req.auth?.identityKey == null) {
54
+ Logger.log('[DEBUG] Authentication required for get permission')
55
+ return res.status(401).json({
56
+ status: 'error',
57
+ code: 'ERR_AUTHENTICATION_REQUIRED',
58
+ description: 'Authentication required.'
59
+ })
60
+ }
61
+
62
+ const { sender, messageBox } = req.query
63
+
64
+ // Validate required parameters
65
+ if (messageBox == null) {
66
+ Logger.log('[DEBUG] Missing required parameters for get permission')
67
+ return res.status(400).json({
68
+ status: 'error',
69
+ code: 'ERR_MISSING_PARAMETERS',
70
+ description: 'messageBox parameter is required.'
71
+ })
72
+ }
73
+
74
+ // Validate sender public key format if provided
75
+ if (sender != null) {
76
+ try {
77
+ PublicKey.fromString(sender)
78
+ } catch (error) {
79
+ Logger.log('[DEBUG] Invalid sender public key format')
80
+ return res.status(400).json({
81
+ status: 'error',
82
+ code: 'ERR_INVALID_PUBLIC_KEY',
83
+ description: 'Invalid sender public key format.'
84
+ })
85
+ }
86
+ }
87
+
88
+ const recipient = req.auth.identityKey
89
+
90
+ // Get message permission directly from database
91
+ const whereClause: any = {
92
+ recipient,
93
+ message_box: messageBox
94
+ }
95
+
96
+ // Add sender condition (null for box-wide, specific sender for sender-specific)
97
+ if (sender != null) {
98
+ whereClause.sender = sender
99
+ } else {
100
+ whereClause.sender = null
101
+ }
102
+
103
+ const permission = await runtimeDeps.knex('message_permissions')
104
+ .where(whereClause)
105
+ .select('recipient_fee', 'created_at', 'updated_at')
106
+ .first()
107
+
108
+ Logger.log(`[DEBUG] Permission record for ${sender ?? 'box-wide'} -> ${recipient} (${messageBox}): ${JSON.stringify(permission)}`)
109
+
110
+ if (permission != null) {
111
+ // Helper function to determine status from recipient fee
112
+ const getStatusFromFee = (fee: number): 'always_allow' | 'blocked' | 'payment_required' => {
113
+ if (fee === -1) return 'blocked'
114
+ if (fee === 0) return 'always_allow'
115
+ return 'payment_required'
116
+ }
117
+
118
+ // Permission is set, return it
119
+ return res.status(200).json({
120
+ status: 'success',
121
+ description: sender != null
122
+ ? `Permission setting found for sender ${sender} to ${messageBox}.`
123
+ : `Box-wide permission setting found for ${messageBox}.`,
124
+ permission: {
125
+ sender: sender ?? null,
126
+ messageBox,
127
+ recipientFee: permission.recipient_fee,
128
+ status: getStatusFromFee(permission.recipient_fee),
129
+ createdAt: permission.created_at.toISOString(),
130
+ updatedAt: permission.updated_at.toISOString()
131
+ }
132
+ })
133
+ } else {
134
+ // No permission set, return undefined
135
+ return res.status(200).json({
136
+ status: 'success',
137
+ description: sender != null
138
+ ? `No permission setting found for sender ${sender} to ${messageBox}.`
139
+ : `No box-wide permission setting found for ${messageBox}.`,
140
+ permission: undefined
141
+ })
142
+ }
143
+ } catch (error) {
144
+ Logger.error('[ERROR] Internal Server Error in get permission:', error)
145
+ return res.status(500).json({
146
+ status: 'error',
147
+ code: 'ERR_INTERNAL',
148
+ description: 'An internal error has occurred.'
149
+ })
150
+ }
151
+ }
152
+ }