kalai-attach 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 (98) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +451 -0
  3. package/_i18n/i18n.properties +6 -0
  4. package/_i18n/i18n_ar.properties +6 -0
  5. package/_i18n/i18n_bg.properties +6 -0
  6. package/_i18n/i18n_cs.properties +6 -0
  7. package/_i18n/i18n_da.properties +6 -0
  8. package/_i18n/i18n_de.properties +6 -0
  9. package/_i18n/i18n_el.properties +6 -0
  10. package/_i18n/i18n_en.properties +6 -0
  11. package/_i18n/i18n_en_US_saptrc.properties +6 -0
  12. package/_i18n/i18n_es.properties +6 -0
  13. package/_i18n/i18n_es_MX.properties +6 -0
  14. package/_i18n/i18n_fi.properties +6 -0
  15. package/_i18n/i18n_fr.properties +6 -0
  16. package/_i18n/i18n_he.properties +6 -0
  17. package/_i18n/i18n_hr.properties +6 -0
  18. package/_i18n/i18n_hu.properties +6 -0
  19. package/_i18n/i18n_it.properties +6 -0
  20. package/_i18n/i18n_ja.properties +6 -0
  21. package/_i18n/i18n_kk.properties +6 -0
  22. package/_i18n/i18n_ko.properties +6 -0
  23. package/_i18n/i18n_ms.properties +6 -0
  24. package/_i18n/i18n_nl.properties +6 -0
  25. package/_i18n/i18n_no.properties +6 -0
  26. package/_i18n/i18n_pl.properties +6 -0
  27. package/_i18n/i18n_pt.properties +6 -0
  28. package/_i18n/i18n_ro.properties +6 -0
  29. package/_i18n/i18n_ru.properties +6 -0
  30. package/_i18n/i18n_sh.properties +6 -0
  31. package/_i18n/i18n_sk.properties +6 -0
  32. package/_i18n/i18n_sl.properties +6 -0
  33. package/_i18n/i18n_sv.properties +6 -0
  34. package/_i18n/i18n_th.properties +6 -0
  35. package/_i18n/i18n_tr.properties +6 -0
  36. package/_i18n/i18n_uk.properties +6 -0
  37. package/_i18n/i18n_vi.properties +6 -0
  38. package/_i18n/i18n_zh_CN.properties +6 -0
  39. package/_i18n/i18n_zh_TW.properties +6 -0
  40. package/_i18n/messages.properties +6 -0
  41. package/_i18n/messages_en_US_saptrc.properties +4 -0
  42. package/cds-plugin.js +4 -0
  43. package/db/data/sap.attachments-ScanStates.csv +6 -0
  44. package/db/data/sap.attachments-ScanStates_texts.csv +6 -0
  45. package/db/data/sap.attachments-ScanStates_texts_ar.csv +6 -0
  46. package/db/data/sap.attachments-ScanStates_texts_bg.csv +6 -0
  47. package/db/data/sap.attachments-ScanStates_texts_cs.csv +6 -0
  48. package/db/data/sap.attachments-ScanStates_texts_da.csv +6 -0
  49. package/db/data/sap.attachments-ScanStates_texts_de.csv +6 -0
  50. package/db/data/sap.attachments-ScanStates_texts_el.csv +6 -0
  51. package/db/data/sap.attachments-ScanStates_texts_en.csv +6 -0
  52. package/db/data/sap.attachments-ScanStates_texts_en_US_saptrc.csv +6 -0
  53. package/db/data/sap.attachments-ScanStates_texts_es.csv +6 -0
  54. package/db/data/sap.attachments-ScanStates_texts_es_MX.csv +6 -0
  55. package/db/data/sap.attachments-ScanStates_texts_fi.csv +6 -0
  56. package/db/data/sap.attachments-ScanStates_texts_fr.csv +6 -0
  57. package/db/data/sap.attachments-ScanStates_texts_he.csv +6 -0
  58. package/db/data/sap.attachments-ScanStates_texts_hr.csv +6 -0
  59. package/db/data/sap.attachments-ScanStates_texts_hu.csv +6 -0
  60. package/db/data/sap.attachments-ScanStates_texts_it.csv +6 -0
  61. package/db/data/sap.attachments-ScanStates_texts_ja.csv +6 -0
  62. package/db/data/sap.attachments-ScanStates_texts_kk.csv +6 -0
  63. package/db/data/sap.attachments-ScanStates_texts_ko.csv +6 -0
  64. package/db/data/sap.attachments-ScanStates_texts_ms.csv +6 -0
  65. package/db/data/sap.attachments-ScanStates_texts_nl.csv +6 -0
  66. package/db/data/sap.attachments-ScanStates_texts_no.csv +6 -0
  67. package/db/data/sap.attachments-ScanStates_texts_pl.csv +6 -0
  68. package/db/data/sap.attachments-ScanStates_texts_pt.csv +6 -0
  69. package/db/data/sap.attachments-ScanStates_texts_ro.csv +6 -0
  70. package/db/data/sap.attachments-ScanStates_texts_ru.csv +6 -0
  71. package/db/data/sap.attachments-ScanStates_texts_sh.csv +6 -0
  72. package/db/data/sap.attachments-ScanStates_texts_sk.csv +6 -0
  73. package/db/data/sap.attachments-ScanStates_texts_sl.csv +6 -0
  74. package/db/data/sap.attachments-ScanStates_texts_sv.csv +6 -0
  75. package/db/data/sap.attachments-ScanStates_texts_th.csv +6 -0
  76. package/db/data/sap.attachments-ScanStates_texts_tr.csv +6 -0
  77. package/db/data/sap.attachments-ScanStates_texts_uk.csv +6 -0
  78. package/db/data/sap.attachments-ScanStates_texts_vi.csv +6 -0
  79. package/db/data/sap.attachments-ScanStates_texts_zh_CN.csv +6 -0
  80. package/db/data/sap.attachments-ScanStates_texts_zh_TW.csv +6 -0
  81. package/db/index.cds +85 -0
  82. package/index.cds +1 -0
  83. package/lib/csn-runtime-extension.js +31 -0
  84. package/lib/generic-handlers.js +199 -0
  85. package/lib/helper.js +407 -0
  86. package/lib/mtx/server.js +583 -0
  87. package/lib/plugin.js +112 -0
  88. package/package.json +67 -0
  89. package/srv/aws-s3.js +249 -0
  90. package/srv/azure-blob-storage.js +202 -0
  91. package/srv/basic.js +331 -0
  92. package/srv/gcp.js +226 -0
  93. package/srv/malwareScanner-mocked.cds +3 -0
  94. package/srv/malwareScanner-mocked.js +127 -0
  95. package/srv/malwareScanner.cds +23 -0
  96. package/srv/malwareScanner.js +151 -0
  97. package/srv/object-store.js +44 -0
  98. package/srv/standard.js +3 -0
package/lib/helper.js ADDED
@@ -0,0 +1,407 @@
1
+ const axios = require('axios')
2
+ const https = require("https")
3
+ const crypto = require("crypto")
4
+ const stream = require('stream/promises')
5
+ const cds = require('@sap/cds')
6
+ const LOG = cds.log("attachments")
7
+
8
+ /**
9
+ * Validates the presence of required Service Manager credentials
10
+ * @param {*} serviceManagerCreds - Service Manager credentials object
11
+ * @throws Will throw an error if validation fails
12
+ */
13
+ function validateServiceManagerCredentials(serviceManagerCreds) {
14
+ if (!serviceManagerCreds) {
15
+ LOG.error('serviceManager.credentials is missing',
16
+ 'Bind a Service Manager instance for separate object store mode')
17
+ throw new Error("Service Manager Instance is not bound")
18
+ }
19
+
20
+ const requiredSmFields = ['sm_url', 'url', 'clientid']
21
+ const missingSmFields = requiredSmFields.filter(field => !serviceManagerCreds[field])
22
+
23
+ if (missingSmFields.length > 0) {
24
+ LOG.error('serviceManager.credentials is missing a few fields. Passed object: ', serviceManagerCreds,
25
+ `Service Manager credentials missing: ${missingSmFields.join(', ')}`)
26
+ throw new Error(`Missing Service Manager credentials: ${missingSmFields.join(', ')}`)
27
+ }
28
+ }
29
+
30
+ /**
31
+ * Validates the inputs required for fetching object store credentials
32
+ * @param {string} tenantID - Tenant ID
33
+ * @param {string} sm_url - Service Manager URL
34
+ * @param {string} token - Access token
35
+ * @returns
36
+ */
37
+ function validateInputs(tenantID, sm_url, token) {
38
+ if (!tenantID) {
39
+ LOG.error('Tenant ID is required for object store credentials', null,
40
+ 'Ensure multitenancy is properly configured and tenant context is available', { tenantID })
41
+ return false
42
+ }
43
+
44
+ if (!sm_url) {
45
+ LOG.error('serviceManager.credentials.sm_url', sm_url, false,
46
+ 'Bind a Service Manager instance to your application')
47
+ return false
48
+ }
49
+
50
+ if (!token) {
51
+ LOG.error('Access token is required for Service Manager API', null,
52
+ 'Check if token fetching completed successfully', { hasToken: !!token })
53
+ return false
54
+ }
55
+
56
+ return true
57
+ }
58
+
59
+ /**
60
+ * Fetches object store service binding from Service Manager
61
+ * @param {string} tenantID - Tenant ID
62
+ * @param {string?} token - Access token, if nothing is provided access token is fetched
63
+ * @returns {Promise<Array>} - Promise resolving to array of service bindings
64
+ */
65
+ async function fetchObjectStoreBinding(tenantID, token) {
66
+ const serviceManagerCreds = cds.env.requires?.serviceManager?.credentials
67
+
68
+ validateServiceManagerCredentials(serviceManagerCreds)
69
+
70
+ const { sm_url, url, clientid, clientsecret, certificate, key, certurl } = serviceManagerCreds
71
+
72
+
73
+ if (!token) {
74
+ LOG.debug('Fetching access token for tenant', { tenantID, sm_url: sm_url })
75
+ token = await fetchToken(url, clientid, clientsecret, certificate, key, certurl)
76
+ }
77
+
78
+ LOG.debug('Fetching object store credentials', { tenantID, sm_url })
79
+
80
+ if (!validateInputs(tenantID, sm_url, token)) {
81
+ return null
82
+ }
83
+
84
+ LOG.debug('Making Service Manager API call', {
85
+ tenantID,
86
+ endpoint: `${sm_url}/v1/service_bindings`,
87
+ labelQuery: `service eq 'OBJECT_STORE' and tenant_id eq '${tenantID}'`
88
+ })
89
+ const response = await axios.get(`${sm_url}/v1/service_bindings`, {
90
+ params: { labelQuery: `service eq 'OBJECT_STORE' and tenant_id eq '${tenantID}'` },
91
+ headers: {
92
+ 'Accept': 'application/json',
93
+ 'Authorization': `Bearer ${token}`,
94
+ 'Content-Type': 'application/json'
95
+ }
96
+ })
97
+
98
+ return response.data?.items || []
99
+ }
100
+
101
+ /**
102
+ * Retrieves object store credentials for a given tenant
103
+ * @param {string} tenantID - Tenant ID
104
+ * @returns {Promise<Object|null>} - Promise resolving to object store credentials or null
105
+ */
106
+ async function getObjectStoreCredentials(tenantID) {
107
+ try {
108
+ const items = await fetchObjectStoreBinding(tenantID)
109
+
110
+ if (!items.length) {
111
+ LOG.error(`No object store service binding found for tenant`, null,
112
+ 'Ensure an Object Store instance is subscribed and bound for this tenant',
113
+ { tenantID, itemsFound: 0 })
114
+ return null
115
+ }
116
+
117
+ const credentials = items[0]
118
+ LOG.info('Object store credentials retrieved successfully', {
119
+ tenantID,
120
+ hasCredentials: !!credentials,
121
+ bucket: credentials?.credentials?.bucket
122
+ })
123
+
124
+ return credentials
125
+ } catch (error) {
126
+ const suggestion = error.response?.status === 401 ?
127
+ 'Check Service Manager credentials and token validity' :
128
+ error.response?.status === 404 ?
129
+ 'Verify Service Manager URL and API endpoint' :
130
+ 'Check network connectivity and Service Manager instance health'
131
+
132
+ LOG.error('Failed to fetch object store credentials', error, suggestion, {
133
+ tenantID,
134
+ httpStatus: error.response?.status,
135
+ responseData: error.response?.data
136
+ })
137
+ return null
138
+ }
139
+ }
140
+
141
+ /**
142
+ * Fetches an OAuth token using either client credentials or MTLS
143
+ * @param {string} url - Token endpoint URL
144
+ * @param {string} clientid - Client ID
145
+ * @param {string} clientsecret - Client Secret
146
+ * @param {string} certificate - MTLS Certificate
147
+ * @param {string} key - MTLS Key
148
+ * @param {string} certURL - MTLS Certificate URL
149
+ * @returns {Promise<string>} - Promise resolving to access token
150
+ */
151
+ async function fetchToken(url, clientid, clientsecret, certificate, key, certURL) {
152
+ LOG.info('Determining token fetch method', {
153
+ hasClientCredentials: !!(clientid && clientsecret),
154
+ hasMTLSCredentials: !!(certificate && key && certURL),
155
+ url,
156
+ clientid
157
+ })
158
+
159
+ // Validate credentials
160
+ if (!clientid) {
161
+ LOG.error('serviceManager.credentials.clientid is missing',
162
+ 'Check Service Manager service binding for client ID')
163
+ throw new Error("Client ID is required for token fetching")
164
+ }
165
+
166
+ if (certificate && key && certURL) {
167
+ LOG.debug('Using MTLS authentication for token fetch', { clientid, certURL })
168
+ return fetchTokenWithMTLS(certURL, clientid, certificate, key)
169
+ } else if (clientid && clientsecret) {
170
+ LOG.debug('Using client credentials authentication for token fetch', { clientid, url })
171
+ return fetchTokenWithClientSecret(url, clientid, clientsecret)
172
+ } else {
173
+ const suggestion = 'Ensure Service Manager binding includes either (clientid + clientsecret) or (certificate + key + certurl)'
174
+ LOG.error('Insufficient credentials for token fetching', null, suggestion, {
175
+ hasClientId: !!clientid,
176
+ hasClientSecret: !!clientsecret,
177
+ hasCertificate: !!certificate,
178
+ hasKey: !!key,
179
+ hasCertURL: !!certURL
180
+ })
181
+ throw new Error("Invalid credentials provided for token fetching.")
182
+ }
183
+ }
184
+
185
+ /**
186
+ * Fetches OAuth token using client credentials flow
187
+ * @param {string} url - Token endpoint URL
188
+ * @param {string} clientid - Client ID
189
+ * @param {string} clientsecret - Client Secret
190
+ * @returns
191
+ */
192
+ async function fetchTokenWithClientSecret(url, clientid, clientsecret) {
193
+ const startTime = Date.now()
194
+
195
+ try {
196
+ LOG.debug('Initiating OAuth client credentials flow', {
197
+ endpoint: `${url}/oauth/token`,
198
+ clientid
199
+ })
200
+
201
+ const headers = {
202
+ 'Accept': 'application/json',
203
+ 'Content-Type': 'application/x-www-form-urlencoded'
204
+ }
205
+
206
+ const response = await axios.post(`${url}/oauth/token`, null, {
207
+ headers,
208
+ params: {
209
+ grant_type: "client_credentials",
210
+ client_id: clientid,
211
+ client_secret: clientsecret,
212
+ },
213
+ })
214
+
215
+ const duration = Date.now() - startTime
216
+ LOG.debug('OAuth token fetched successfully', { clientid, duration, tokenType: response.data?.token_type })
217
+
218
+ return response.data.access_token
219
+ } catch (error) {
220
+ const duration = Date.now() - startTime
221
+ const suggestion = error.response?.status === 401 ?
222
+ 'Verify Service Manager client credentials (clientid and clientsecret)' :
223
+ error.response?.status === 404 ?
224
+ 'Check Service Manager URL is correct' :
225
+ 'Verify Service Manager instance is running and accessible'
226
+
227
+ LOG.error(
228
+ 'Failed to fetch OAuth token using client credentials', error,
229
+ suggestion,
230
+ { clientid, duration, httpStatus: error.response?.status, errorMessage: error.message })
231
+
232
+ throw error
233
+ }
234
+ }
235
+
236
+ /**
237
+ * Fetches OAuth token using MTLS authentication
238
+ * @param {string} certURL - Certificate URL
239
+ * @param {string} clientid - Client ID
240
+ * @param {string} certificate - MTLS Certificate
241
+ * @param {string} key - MTLS Key
242
+ * @returns {Promise<string>} - Promise resolving to access token
243
+ */
244
+ async function fetchTokenWithMTLS(certURL, clientid, certificate, key) {
245
+ const startTime = Date.now()
246
+
247
+ try {
248
+ LOG.debug('Initiating MTLS authentication flow', {
249
+ endpoint: `${certURL}/oauth/token`,
250
+ clientid
251
+ })
252
+
253
+ const requestBody = new URLSearchParams({
254
+ grant_type: 'client_credentials',
255
+ response_type: 'token',
256
+ client_id: clientid
257
+ }).toString()
258
+
259
+ const options = {
260
+ headers: {
261
+ 'Accept': 'application/json',
262
+ 'Content-Type': 'application/x-www-form-urlencoded'
263
+ },
264
+ url: `${certURL}/oauth/token`,
265
+ method: 'POST',
266
+ data: requestBody,
267
+ httpsAgent: new https.Agent({
268
+ cert: certificate,
269
+ key: key
270
+ })
271
+ }
272
+
273
+ const response = await axios(options)
274
+ const duration = Date.now() - startTime
275
+
276
+ if (!response.data?.access_token) {
277
+ LOG.error('MTLS token response missing access_token', null,
278
+ 'Check MTLS certificate/key validity and Service Manager configuration',
279
+ { clientid, duration, responseData: response.data })
280
+ throw new Error('Access token not found in MTLS token response')
281
+ }
282
+
283
+ LOG.debug('MTLS token fetched successfully', { clientid, duration, tokenType: response.data.token_type })
284
+
285
+ return response.data.access_token
286
+ } catch (error) {
287
+ const duration = Date.now() - startTime
288
+
289
+ LOG.error(
290
+ 'Failed to fetch OAuth token using MTLS', error,
291
+ 'Check MTLS certificate, key, and Service Manager connectivity',
292
+ { clientid, duration, httpStatus: error.response?.status, errorMessage: error.message })
293
+
294
+ throw error
295
+ }
296
+ }
297
+
298
+ /**
299
+ * Checks if the given mimeType matches any of the allowedTypes patterns
300
+ * @param {Array<string>} allowedTypes - Array of allowed mime types (can include wildcards)
301
+ * @param {string} mimeType - Mime type to check
302
+ * @returns {boolean} - True if mimeType matches any allowedTypes, false otherwise
303
+ */
304
+ function checkMimeTypeMatch(allowedTypes, mimeType) {
305
+ if (!allowedTypes || allowedTypes.length === 0) {
306
+ return true
307
+ }
308
+
309
+ if (typeof allowedTypes === 'string') {
310
+ allowedTypes = [allowedTypes]
311
+ }
312
+
313
+ if (allowedTypes.includes('*/*')) {
314
+ return true
315
+ }
316
+
317
+ // Remove any parameters (e.g., "; charset=utf-8", "; boundary=...")
318
+ const baseMimeType = mimeType.split(';')[0].trim()
319
+
320
+ return allowedTypes.some(allowedType => {
321
+ if (allowedType.endsWith('/*')) {
322
+ const prefix = allowedType.slice(0, -2)
323
+ return baseMimeType.startsWith(prefix + '/')
324
+ } else {
325
+ return baseMimeType === allowedType
326
+ }
327
+ })
328
+ }
329
+
330
+ async function computeHash(input) {
331
+ const hash = crypto.createHash('sha256')
332
+
333
+ // Connect the output of the `input` stream to the input of `hash`
334
+ // and let Node.js do the streaming
335
+ await stream.pipeline(input, hash)
336
+
337
+ return hash.digest('hex')
338
+ }
339
+
340
+
341
+ const multipliers = {}
342
+ multipliers.B = 1;
343
+ multipliers.KB = multipliers.B * 1024;
344
+ multipliers.MB = multipliers.KB * 1024;
345
+ multipliers.GB = multipliers.MB * 1024;
346
+ multipliers.TB = multipliers.GB * 1024;
347
+ multipliers.PB = multipliers.TB * 1024;
348
+ multipliers.EB = multipliers.PB * 1024;
349
+ multipliers.ZB = multipliers.EB * 1024;
350
+
351
+ const MAX_FILE_SIZE = 419430400 //400 MB in bytes
352
+
353
+ /**
354
+ * Converts a byte size string into the corresponding number.
355
+ *
356
+ * @param {string} size 20mb for example
357
+ * @returns Byte size or null if nothing was found
358
+ */
359
+ function sizeInBytes(size, target) {
360
+ if (!size || (typeof size !== 'string' && typeof size !== 'number')) {
361
+ LOG.warn(`Could not determine the maximum byte size for the content of ${target}`)
362
+ return
363
+ }
364
+
365
+ if (typeof size === 'number') {
366
+ return size
367
+ }
368
+
369
+ const value = parseFloat(size);
370
+ if (isNaN(value)) {
371
+ LOG.warn(`Could not determine the maximum byte size for the content of ${target}`)
372
+ return
373
+ }
374
+
375
+ const unitMatches = size.toUpperCase().match(/([KMGTPEZ]I?)?B$/)
376
+ // Remove any optional "i" from the unit of measurement (ex, MiB).
377
+ const unit = unitMatches[0]?.replace(/i/i, "")
378
+
379
+ if (!unit) {
380
+ LOG.warn(`Could not determine the maximum byte size for the content of ${target}`)
381
+ return
382
+ }
383
+
384
+ return value * multipliers[unit]
385
+ }
386
+
387
+ function traverseEntity(root, path) {
388
+ let current = root
389
+ for (const part of path) {
390
+ if (!current.elements || !current.elements[part]) return undefined
391
+ current = current.elements[part]._target
392
+ if (!current) return undefined
393
+ }
394
+ return current
395
+ }
396
+
397
+ module.exports = {
398
+ fetchToken,
399
+ getObjectStoreCredentials,
400
+ computeHash,
401
+ sizeInBytes,
402
+ fetchObjectStoreBinding,
403
+ validateServiceManagerCredentials,
404
+ checkMimeTypeMatch,
405
+ traverseEntity,
406
+ MAX_FILE_SIZE
407
+ }