@jacebenson/jsn 4.3.4-dev.4 → 4.3.4-dev.5

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 (2) hide show
  1. package/package.json +1 -1
  2. package/src/sdk.js +25 -43
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jacebenson/jsn",
3
- "version": "4.3.4-dev.4",
3
+ "version": "4.3.4-dev.5",
4
4
  "description": "A command-line interface for ServiceNow that follows the Unix philosophy: simple, composable, and scriptable.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/sdk.js CHANGED
@@ -266,6 +266,8 @@ export class SDKClient {
266
266
 
267
267
  /**
268
268
  * Upload a file as a new attachment on a record.
269
+ * Uses ServiceNow's binary attachment endpoint so file bytes are not expanded
270
+ * into base64 or embedded in a sys.scripts.do form body.
269
271
  * @param {string} table - parent table name (e.g. 'incident')
270
272
  * @param {string} sysID - parent record sys_id
271
273
  * @param {Buffer|string} content - file bytes or path
@@ -274,29 +276,30 @@ export class SDKClient {
274
276
  */
275
277
  async addAttachment(table, sysID, content, fileName) {
276
278
  const contentBuffer = Buffer.isBuffer(content) ? content : Buffer.from(String(content));
277
- const script = [
278
- '(function() {',
279
- ` var record = new GlideRecord(${scriptString(table)});`,
280
- ` if (!record.get(${scriptString(sysID)})) throw new Error('Record not found');`,
281
- ' var attachment = new GlideSysAttachment();',
282
- ` var fileName = ${scriptString(fileName)};`,
283
- ` var content = ${scriptString(contentBuffer.toString('base64'))};`,
284
- ' var sysId;',
285
- " if (typeof attachment.writeBase64 === 'function') {",
286
- " sysId = attachment.writeBase64(record, fileName, 'application/octet-stream', content);",
287
- ' } else {',
288
- " sysId = new Attachment().write(record.getTableName(), record.getUniqueValue(), fileName, 'application/octet-stream', GlideStringUtil.base64DecodeAsBytes(content));",
289
- ' }',
290
- " if (!sysId) throw new Error('Attachment was not created');",
291
- " gs.print('JSN_ATTACHMENT_RESULT:' + JSON.stringify({ sys_id: sysId }));",
292
- '}());',
293
- ].join('\n');
294
- const output = await this.executeScript(script, '');
295
- const result = parseAttachmentResult(output);
296
- if (!result?.sys_id) {
297
- throw new Error('Attachment upload returned no attachment sys_id');
279
+ const params = new URLSearchParams({
280
+ table_name: String(table),
281
+ table_sys_id: String(sysID),
282
+ file_name: String(fileName),
283
+ });
284
+ const endpoint = `${this.baseURL}/api/now/attachment/file?${params.toString()}`;
285
+
286
+ try {
287
+ const response = await this.request(endpoint, {
288
+ method: 'POST',
289
+ headers: { 'Content-Type': 'application/octet-stream' },
290
+ body: contentBuffer,
291
+ });
292
+ const result = response?.result;
293
+ if (!result?.sys_id) {
294
+ throw new Error('Attachment upload returned no attachment sys_id');
295
+ }
296
+ return result;
297
+ } catch (err) {
298
+ if (err?.status === 413) {
299
+ throw errAPI(413, 'Attachment is too large for the ServiceNow attachment endpoint. Reduce the file size or increase the instance attachment limit.');
300
+ }
301
+ throw err;
298
302
  }
299
- return result;
300
303
  }
301
304
 
302
305
  async getCurrentUser({ touchLastSeen = true } = {}) {
@@ -921,27 +924,6 @@ export class SDKClient {
921
924
  }
922
925
  }
923
926
 
924
- function scriptString(value) {
925
- return JSON.stringify(String(value)).replace(/\u2028/g, '\\u2028').replace(/\u2029/g, '\\u2029');
926
- }
927
-
928
- function parseAttachmentResult(output) {
929
- if (!output) return null;
930
- const lines = String(output).split(/\r?\n/).reverse();
931
- for (const line of lines) {
932
- const candidate = line.replace(/^\*\*\* Script:\s*/, '').trim();
933
- const markerCandidate = candidate.replace(/^JSN_ATTACHMENT_RESULT:\s*/, '');
934
- if (!markerCandidate.startsWith('{')) continue;
935
- try {
936
- const result = JSON.parse(markerCandidate);
937
- if (result && typeof result === 'object') return result;
938
- } catch {
939
- // Ignore unrelated script output and keep looking for the result marker.
940
- }
941
- }
942
- return null;
943
- }
944
-
945
927
  function detectScriptError(output) {
946
928
  const lines = String(output || '').split(/\r?\n/).map(line => line.trim()).filter(Boolean);
947
929
  const errorLine = lines.find(line => /(?:ReferenceError|TypeError|RhinoEcmaError|Script execution error|undefined is not a function|is not defined)/i.test(line));