@bopen-io/messagebox-server 1.1.5 → 1.2.1

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 +68 -52
  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,192 @@
1
+ import { Response } from 'express'
2
+ import { PublicKey } from '@bsv/sdk'
3
+ import { Logger, log } from '../../utils/logger.js'
4
+ import { AuthRequest } from '@bsv/auth-express-middleware'
5
+ import { getRecipientFee, getServerDeliveryFee } from '../../utils/messagePermissions.js'
6
+
7
+ export interface GetQuoteRequest extends AuthRequest {
8
+ query: {
9
+ recipient: string | string[] // identityKey of recipient or array of recipients
10
+ messageBox?: string // messageBox type
11
+ }
12
+ }
13
+
14
+ /**
15
+ * @swagger
16
+ * /permissions/quote:
17
+ * get:
18
+ * summary: Get message delivery quote(s)
19
+ * description: Get pricing information for sending messages to one or many recipients' message boxes
20
+ * tags:
21
+ * - Permissions
22
+ * parameters:
23
+ * - in: query
24
+ * name: recipient
25
+ * required: true
26
+ * schema:
27
+ * oneOf:
28
+ * - type: string
29
+ * - type: array
30
+ * items:
31
+ * type: string
32
+ * description: identityKey of the recipient, or multiple recipients by repeating the parameter (?recipient=A&recipient=B)
33
+ * - in: query
34
+ * name: messageBox
35
+ * required: true
36
+ * schema:
37
+ * type: string
38
+ * description: messageBox type
39
+ * responses:
40
+ * 200:
41
+ * description: Quote(s) retrieved successfully
42
+ * 400:
43
+ * description: Invalid request parameters
44
+ * 401:
45
+ * description: Authentication required
46
+ * 500:
47
+ * description: Internal server error
48
+ */
49
+ export default {
50
+ type: 'get',
51
+ path: '/permissions/quote',
52
+ func: async (req: GetQuoteRequest, res: Response): Promise<Response> => {
53
+ try {
54
+ Logger.log('[DEBUG] Processing message quote request')
55
+ log.debug({ operation: 'permissions.quote' }, 'Processing message quote request')
56
+
57
+ // Validate authentication (the caller is the SENDER)
58
+ const sender = req.auth?.identityKey
59
+ if (sender == null) {
60
+ Logger.log('[DEBUG] Authentication required for message quote')
61
+ return res.status(401).json({
62
+ status: 'error',
63
+ code: 'ERR_AUTHENTICATION_REQUIRED',
64
+ description: 'Authentication required.'
65
+ })
66
+ }
67
+
68
+ const { recipient, messageBox } = req.query
69
+
70
+ // Required params
71
+ if (recipient == null || messageBox == null) {
72
+ Logger.log('[DEBUG] Missing required parameters for message quote')
73
+ return res.status(400).json({
74
+ status: 'error',
75
+ code: 'ERR_MISSING_PARAMETERS',
76
+ description: 'recipient and messageBox parameters are required.'
77
+ })
78
+ }
79
+
80
+ // Normalize recipients to array (preserve order)
81
+ const recipients: string[] = Array.isArray(recipient) ? recipient : [recipient]
82
+
83
+ if (recipients.length === 0) {
84
+ return res.status(400).json({
85
+ status: 'error',
86
+ code: 'ERR_MISSING_PARAMETERS',
87
+ description: 'At least one recipient is required.'
88
+ })
89
+ }
90
+
91
+ // Validate each recipient public key
92
+ const invalidIdx: number[] = []
93
+ for (let i = 0; i < recipients.length; i++) {
94
+ try {
95
+ PublicKey.fromString(recipients[i])
96
+ } catch {
97
+ invalidIdx.push(i)
98
+ }
99
+ }
100
+ if (invalidIdx.length > 0) {
101
+ Logger.log('[DEBUG] Invalid recipient public key format in array')
102
+ return res.status(400).json({
103
+ status: 'error',
104
+ code: 'ERR_INVALID_PUBLIC_KEY',
105
+ description: `Invalid recipient public key at index(es): ${invalidIdx.join(', ')}.`
106
+ })
107
+ }
108
+
109
+ // Delivery fee for this messageBox (applies per message/recipient)
110
+ const perMessageDeliveryFee = await getServerDeliveryFee(messageBox)
111
+
112
+ // Single-recipient path → keep legacy response shape for compatibility
113
+ if (recipients.length === 1) {
114
+ const recipientKey = recipients[0]
115
+ const recipientFee = await getRecipientFee(recipientKey, sender, messageBox)
116
+
117
+ return res.status(200).json({
118
+ status: 'success',
119
+ description: 'Message delivery quote generated.',
120
+ quote: {
121
+ deliveryFee: perMessageDeliveryFee,
122
+ recipientFee
123
+ }
124
+ })
125
+ }
126
+
127
+ // Multi-recipient path → compute per-recipient, plus aggregates
128
+ const quotesByRecipient: Array<{
129
+ recipient: string
130
+ messageBox: string
131
+ deliveryFee: number
132
+ recipientFee: number
133
+ status: 'blocked' | 'always_allow' | 'payment_required'
134
+ }> = []
135
+
136
+ const blockedRecipients: string[] = []
137
+ let totalRecipientFees = 0
138
+ let totalDeliveryFees = 0
139
+
140
+ // Helper to map fee -> status
141
+ const feeToStatus = (fee: number): 'blocked' | 'always_allow' | 'payment_required' => {
142
+ if (fee === -1) return 'blocked'
143
+ if (fee === 0) return 'always_allow'
144
+ return 'payment_required'
145
+ }
146
+
147
+ for (const r of recipients) {
148
+ const recipientFee = await getRecipientFee(r, sender, messageBox)
149
+ const status = feeToStatus(recipientFee)
150
+
151
+ quotesByRecipient.push({
152
+ recipient: r,
153
+ messageBox,
154
+ deliveryFee: perMessageDeliveryFee,
155
+ recipientFee,
156
+ status
157
+ })
158
+
159
+ // Aggregate: count deliveryFee per intended message, and recipientFee if not blocked
160
+ totalDeliveryFees += perMessageDeliveryFee
161
+ if (recipientFee === -1) {
162
+ blockedRecipients.push(r)
163
+ } else {
164
+ totalRecipientFees += recipientFee
165
+ }
166
+ }
167
+
168
+ const totals = {
169
+ deliveryFees: totalDeliveryFees,
170
+ recipientFees: totalRecipientFees,
171
+ // If any are blocked, caller may want to handle those separately.
172
+ // We still provide a full monetary total for non-blocked recipients:
173
+ totalForPayableRecipients: totalDeliveryFees + totalRecipientFees
174
+ }
175
+
176
+ return res.status(200).json({
177
+ status: 'success',
178
+ description: `Message delivery quotes generated for ${recipients.length} recipients.`,
179
+ quotesByRecipient,
180
+ totals,
181
+ blockedRecipients
182
+ })
183
+ } catch (error) {
184
+ Logger.error('[ERROR] Internal Server Error in message quote:', error)
185
+ return res.status(500).json({
186
+ status: 'error',
187
+ code: 'ERR_INTERNAL',
188
+ description: 'An internal error has occurred.'
189
+ })
190
+ }
191
+ }
192
+ }
@@ -0,0 +1,12 @@
1
+ // Export all permission-related routes
2
+ import setPermission from './setPermission.js'
3
+ import getPermission from './getPermission.js'
4
+ import getQuote from './getQuote.js'
5
+ import listPermissions from './listPermissions.js'
6
+
7
+ export const permissionRoutes = [
8
+ setPermission,
9
+ getPermission,
10
+ getQuote,
11
+ listPermissions
12
+ ]
@@ -0,0 +1,202 @@
1
+ import { Response } from 'express'
2
+ import { AuthRequest } from '@bsv/auth-express-middleware'
3
+ import { Logger } from '../../utils/logger.js'
4
+ import { runtimeDeps } from '../../runtimeDeps.js'
5
+
6
+ export interface ListPermissionsRequest extends AuthRequest {
7
+ query: {
8
+ messageBox?: string // Optional messageBox filter
9
+ limit?: string // Optional pagination limit
10
+ offset?: string // Optional pagination offset
11
+ createdAtOrder?: string // Optional sort order for created_at ('asc' | 'desc')
12
+ }
13
+ }
14
+
15
+ /**
16
+ * @swagger
17
+ * /permissions/list:
18
+ * get:
19
+ * summary: List message permissions for the authenticated user
20
+ * description: Retrieve all permissions set by the authenticated user with optional filtering and pagination
21
+ * tags:
22
+ * - Permissions
23
+ * parameters:
24
+ * - in: query
25
+ * name: messageBox
26
+ * required: false
27
+ * schema:
28
+ * type: string
29
+ * description: Optional messageBox type filter (e.g., 'notifications', 'inbox')
30
+ * - in: query
31
+ * name: limit
32
+ * required: false
33
+ * schema:
34
+ * type: integer
35
+ * minimum: 1
36
+ * maximum: 1000
37
+ * default: 100
38
+ * description: Maximum number of permissions to return
39
+ * - in: query
40
+ * name: offset
41
+ * required: false
42
+ * schema:
43
+ * type: integer
44
+ * minimum: 0
45
+ * default: 0
46
+ * description: Number of permissions to skip for pagination
47
+ * - in: query
48
+ * name: createdAtOrder
49
+ * required: false
50
+ * schema:
51
+ * type: string
52
+ * enum: ['asc', 'desc']
53
+ * default: 'desc'
54
+ * description: Sort order for created_at date (asc=oldest first, desc=newest first)
55
+ * responses:
56
+ * 200:
57
+ * description: Permissions retrieved successfully
58
+ * content:
59
+ * application/json:
60
+ * schema:
61
+ * type: object
62
+ * properties:
63
+ * status:
64
+ * type: string
65
+ * example: 'success'
66
+ * permissions:
67
+ * type: array
68
+ * items:
69
+ * type: object
70
+ * properties:
71
+ * sender:
72
+ * type: string
73
+ * nullable: true
74
+ * description: Sender identity key (null for box-wide defaults)
75
+ * messageBox:
76
+ * type: string
77
+ * description: MessageBox type
78
+ * recipientFee:
79
+ * type: integer
80
+ * description: Fee setting (-1=block all, 0=always allow, >0=satoshi amount)
81
+ * created_at:
82
+ * type: string
83
+ * format: date-time
84
+ * updated_at:
85
+ * type: string
86
+ * format: date-time
87
+ * total_count:
88
+ * type: integer
89
+ * description: Total number of permissions (useful for pagination)
90
+ * 400:
91
+ * description: Invalid query parameters
92
+ * 401:
93
+ * description: Unauthorized - authentication required
94
+ * 500:
95
+ * description: Internal server error
96
+ */
97
+ export default {
98
+ type: 'get',
99
+ path: '/permissions/list',
100
+ /**
101
+ * List message permissions for the authenticated user
102
+ *
103
+ * @param {ListPermissionsRequest} req - Authenticated request with query parameters
104
+ * @param {Response} res - Express response object
105
+ * @returns {Promise<Response>} JSON response with permissions list
106
+ */
107
+ func: async (req: ListPermissionsRequest, res: Response): Promise<Response> => {
108
+ Logger.log('[DEBUG] Processing /permissions/list request...')
109
+
110
+ try {
111
+ // Validate authentication
112
+ if (req.auth?.identityKey == null) {
113
+ return res.status(401).json({
114
+ status: 'error',
115
+ code: 'ERR_UNAUTHORIZED',
116
+ description: 'Authentication required'
117
+ })
118
+ }
119
+
120
+ // Parse and validate query parameters
121
+ const { messageBox, limit: limitStr, offset: offsetStr, createdAtOrder } = req.query
122
+
123
+ const limit = limitStr != null ? Number.parseInt(limitStr, 10) : 100
124
+ const offset = offsetStr != null ? Number.parseInt(offsetStr, 10) : 0
125
+ const sortOrder = createdAtOrder === 'asc' ? 'asc' : 'desc' // Default to 'desc'
126
+
127
+ // Validate pagination parameters
128
+ if (Number.isNaN(limit) || limit < 1 || limit > 1000) {
129
+ return res.status(400).json({
130
+ status: 'error',
131
+ code: 'ERR_INVALID_LIMIT',
132
+ description: 'Limit must be a number between 1 and 1000'
133
+ })
134
+ }
135
+
136
+ if (Number.isNaN(offset) || offset < 0) {
137
+ return res.status(400).json({
138
+ status: 'error',
139
+ code: 'ERR_INVALID_OFFSET',
140
+ description: 'Offset must be a non-negative number'
141
+ })
142
+ }
143
+
144
+ // Validate identity key format
145
+ const recipientKey = req.auth.identityKey
146
+
147
+ Logger.log(`[DEBUG] Listing permissions for recipient: ${recipientKey}, messageBox: ${messageBox ?? 'all'}, limit: ${limit}, offset: ${offset}, createdAtOrder: ${sortOrder}`)
148
+
149
+ // Build base query
150
+ let query = runtimeDeps.knex('message_permissions')
151
+ .select([
152
+ 'sender',
153
+ 'message_box',
154
+ 'recipient_fee',
155
+ 'created_at',
156
+ 'updated_at'
157
+ ])
158
+ .where('recipient', recipientKey)
159
+ .orderBy([
160
+ { column: 'message_box', order: 'asc' },
161
+ { column: 'sender', order: 'asc', nulls: 'first' }, // Box-wide (null) first
162
+ { column: 'created_at', order: sortOrder }
163
+ ])
164
+
165
+ // Apply messageBox filter if provided
166
+ if (messageBox != null) {
167
+ query = query.where('message_box', messageBox)
168
+ }
169
+
170
+ // Get total count for pagination info (before applying limit/offset)
171
+ const countQuery = query.clone().clearSelect().clearOrder().count('* as count')
172
+ const [{ count: totalCount }] = await countQuery
173
+ const total = Number.parseInt(String(totalCount), 10)
174
+
175
+ // Apply pagination
176
+ const permissions = await query
177
+ .limit(limit)
178
+ .offset(offset)
179
+
180
+ Logger.log(`[DEBUG] Found ${permissions.length} permissions (${total} total)`)
181
+
182
+ return res.status(200).json({
183
+ status: 'success',
184
+ permissions: permissions.map(p => ({
185
+ sender: p.sender, // null for box-wide defaults
186
+ messageBox: p.message_box,
187
+ recipientFee: p.recipient_fee,
188
+ createdAt: p.created_at,
189
+ updatedAt: p.updated_at
190
+ })),
191
+ totalCount: total
192
+ })
193
+ } catch (error) {
194
+ Logger.error('[ERROR] Error listing permissions:', error)
195
+ return res.status(500).json({
196
+ status: 'error',
197
+ code: 'ERR_LIST_PERMISSIONS_FAILED',
198
+ description: 'Failed to list permissions'
199
+ })
200
+ }
201
+ }
202
+ }
@@ -0,0 +1,155 @@
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 { setMessagePermission } from '../../utils/messagePermissions.js'
6
+
7
+ export interface SetPermissionRequestType extends AuthRequest {
8
+ body: {
9
+ sender?: string // Optional - if not provided, sets box-wide default
10
+ messageBox: string
11
+ recipientFee: number
12
+ }
13
+ }
14
+
15
+ /**
16
+ * @swagger
17
+ * /permissions/set:
18
+ * post:
19
+ * summary: Set message permission for a sender/box combination or box-wide default
20
+ * description: Set permission level for receiving messages. If sender is provided, sets permission for that specific sender. If sender is omitted, sets box-wide default for all senders.
21
+ * tags:
22
+ * - Permissions
23
+ * requestBody:
24
+ * required: true
25
+ * content:
26
+ * application/json:
27
+ * schema:
28
+ * type: object
29
+ * required:
30
+ * - messageBox
31
+ * - recipientFee
32
+ * properties:
33
+ * sender:
34
+ * type: string
35
+ * description: identityKey of the sender (optional - if omitted, sets box-wide default for all senders)
36
+ * messageBox:
37
+ * type: string
38
+ * description: messageBox type (e.g., 'notifications', 'inbox')
39
+ * recipientFee:
40
+ * type: integer
41
+ * description: Fee level (-1=blocked, 0=always allow, >0=satoshi amount required)
42
+ * responses:
43
+ * 200:
44
+ * description: Permission successfully set/updated
45
+ * 400:
46
+ * description: Invalid request data
47
+ * 401:
48
+ * description: Authentication required
49
+ * 500:
50
+ * description: Internal server error
51
+ */
52
+ export default {
53
+ type: 'post',
54
+ path: '/permissions/set',
55
+ func: async (req: SetPermissionRequestType, res: Response): Promise<Response> => {
56
+ try {
57
+ Logger.log('[DEBUG] Processing set message permission request')
58
+
59
+ // Validate authentication
60
+ const recipient = req.auth?.identityKey
61
+ if (recipient == null) {
62
+ Logger.log('[DEBUG] Authentication required for set permission')
63
+ return res.status(401).json({
64
+ status: 'error',
65
+ code: 'ERR_AUTHENTICATION_REQUIRED',
66
+ description: 'Authentication required.'
67
+ })
68
+ }
69
+
70
+ const { sender, messageBox, recipientFee } = req.body
71
+
72
+ // Validate request body (sender is optional)
73
+ if (messageBox == null || typeof recipientFee !== 'number') {
74
+ Logger.log('[DEBUG] Invalid request body for set permission')
75
+ return res.status(400).json({
76
+ status: 'error',
77
+ code: 'ERR_INVALID_REQUEST',
78
+ description: 'messageBox (string) and recipientFee (number) are required. sender (string) is optional for box-wide settings.'
79
+ })
80
+ }
81
+
82
+ // Validate sender public key format only if provided
83
+ if (sender != null) {
84
+ try {
85
+ PublicKey.fromString(sender)
86
+ } catch (error) {
87
+ Logger.log('[DEBUG] Invalid sender public key format')
88
+ return res.status(400).json({
89
+ status: 'error',
90
+ code: 'ERR_INVALID_PUBLIC_KEY',
91
+ description: 'Invalid sender public key format.'
92
+ })
93
+ }
94
+ }
95
+
96
+ // Validate recipientFee value
97
+ if (!Number.isInteger(recipientFee)) {
98
+ Logger.log('[DEBUG] Invalid recipientFee value - must be integer')
99
+ return res.status(400).json({
100
+ status: 'error',
101
+ code: 'ERR_INVALID_FEE_VALUE',
102
+ description: 'recipientFee must be an integer (-1, 0, or positive number).'
103
+ })
104
+ }
105
+
106
+ // Validate messageBox value
107
+ if (typeof messageBox !== 'string' || messageBox.trim() === '') {
108
+ Logger.log('[DEBUG] Invalid messageBox value')
109
+ return res.status(400).json({
110
+ status: 'error',
111
+ code: 'ERR_INVALID_MESSAGE_BOX',
112
+ description: 'messageBox must be a non-empty string.'
113
+ })
114
+ }
115
+
116
+ // Set the message permission (convert undefined sender to null for box-wide)
117
+ const success = await setMessagePermission(recipient, sender ?? null, messageBox, recipientFee)
118
+
119
+ if (success == null) {
120
+ return res.status(500).json({
121
+ status: 'error',
122
+ code: 'ERR_DATABASE_ERROR',
123
+ description: 'Failed to update message permission.'
124
+ })
125
+ }
126
+
127
+ const isBoxWide = sender == null
128
+ Logger.log(`[DEBUG] Successfully updated message permission: ${sender ?? 'BOX-WIDE'} -> ${recipient} (${messageBox}), fee: ${recipientFee}`)
129
+
130
+ let description: string
131
+ const senderText = isBoxWide ? 'all senders' : sender
132
+ const actionText = isBoxWide ? 'Box-wide default for' : 'Messages from'
133
+
134
+ if (recipientFee === -1) {
135
+ description = `${actionText} ${senderText} to ${messageBox} ${isBoxWide ? 'is' : 'are'} now blocked.`
136
+ } else if (recipientFee === 0) {
137
+ description = `${actionText} ${senderText} to ${messageBox} ${isBoxWide ? 'is' : 'are'} now always allowed.`
138
+ } else {
139
+ description = `${actionText} ${senderText} to ${messageBox} now require${isBoxWide ? 's' : ''} ${recipientFee} satoshis.`
140
+ }
141
+
142
+ return res.status(200).json({
143
+ status: 'success',
144
+ description
145
+ })
146
+ } catch (error) {
147
+ Logger.error('[ERROR] Internal Server Error in set permission:', error)
148
+ return res.status(500).json({
149
+ status: 'error',
150
+ code: 'ERR_INTERNAL',
151
+ description: 'An internal error has occurred.'
152
+ })
153
+ }
154
+ }
155
+ }