@codemation/core-nodes 0.6.0 → 0.7.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,36 @@
1
1
  # @codemation/core-nodes
2
2
 
3
+ ## 0.7.1
4
+
5
+ ### Patch Changes
6
+
7
+ - [#130](https://github.com/MadeRelevant/codemation/pull/130) [`e8e3935`](https://github.com/MadeRelevant/codemation/commit/e8e39358a4282e0a780efb428ae0d71d105afd5f) Thanks [@cblokland90](https://github.com/cblokland90)! - `SubWorkflow` nodes now render with the Lucide `workflow` glyph by default, so they read at a glance on the canvas. Nodes that don't set an explicit `icon` (and have no semantic role like agent / model / tool) now fall back to a question-mark glyph instead of `Boxes` — a clearer "missing icon" signal for plugin authors. Unknown icon tokens (`builtin:`, `si:`, `lucide:` lookups that don't resolve) also fall back to the same question-mark glyph for consistency.
8
+
9
+ - Updated dependencies [[`d283b48`](https://github.com/MadeRelevant/codemation/commit/d283b481f01a1a259d38d25c1482006eff963384)]:
10
+ - @codemation/core@0.10.2
11
+
12
+ ## 0.7.0
13
+
14
+ ### Minor Changes
15
+
16
+ - [#123](https://github.com/MadeRelevant/codemation/pull/123) [`c191557`](https://github.com/MadeRelevant/codemation/commit/c19155783a012d293568f55427ae36b31171af11) Thanks [@cblokland90](https://github.com/cblokland90)! - feat(core-nodes): HttpRequest body and response support binary slots
17
+ - Add `responseFormat: "binary"` config field to store response bytes directly in `ctx.binary` rather than parsing as JSON/text. Output JSON carries `{ status, headers, binarySlot, contentType, size, filename }`.
18
+ - Add `responseBinarySlot?: string` (default `"response"`) and `responseSizeCapBytes?: number` (default 100 MiB, checked against `Content-Length` before allocating).
19
+ - Add `body: { kind: "binary", slot: string }` body spec to send raw bytes from a binary attachment slot as the request body. The attachment's `mimeType` is used as `Content-Type` unless an explicit header overrides it.
20
+ - Fix: explicit `headers["content-type"]` now correctly wins over the body-derived content type for all body kinds (was previously overwritten).
21
+ - Extract `HttpBodyBuilder.readStreamToBuffer` private helper to deduplicate stream-reading code shared between multipart and binary body kinds.
22
+
23
+ ### Patch Changes
24
+
25
+ - [#126](https://github.com/MadeRelevant/codemation/pull/126) [`d0f2bd9`](https://github.com/MadeRelevant/codemation/commit/d0f2bd9a670ff80c2e2e12f7c410c63d14c94b55) Thanks [@cblokland90](https://github.com/cblokland90)! - DriveDownload and OnNewMail now stream binary attachments directly into binary storage instead of buffering the entire payload in RAM (`Buffer.concat` / `Buffer.from(x, "base64")`). Functionally equivalent — only the memory profile improves (critical for multi-GB files).
26
+
27
+ Adds `codemation/no-buffer-everything` ESLint rule (error severity) to prevent future regressions: flags `Buffer.from(x,"base64")`, `.arrayBuffer()`, and `Buffer.concat()` with guidance on streaming alternatives. Genuine constraints (AES-GCM cipher, Graph upload requiring Content-Length, Excel workbook responses) are suppressed with justified `-- <reason>` comments.
28
+
29
+ Follow-up: support streaming multipart upload via the form-data package to remove the suppression in `HttpBodyBuilder`.
30
+
31
+ - Updated dependencies [[`1f10121`](https://github.com/MadeRelevant/codemation/commit/1f10121a093ef0612a33c873419b032709c9964d)]:
32
+ - @codemation/core@0.10.1
33
+
3
34
  ## 0.6.0
4
35
 
5
36
  ### Minor Changes
package/dist/index.cjs CHANGED
@@ -323,7 +323,8 @@ var HttpRequestExecutor = class {
323
323
  ...credentialDelta.query ?? {}
324
324
  };
325
325
  const encodedBody = await this.bodyBuilder.build(spec.body, item, spec.ctx);
326
- if (encodedBody && encodedBody.contentType) mergedHeaders["content-type"] = encodedBody.contentType;
326
+ const hasExplicitContentType = Object.keys(mergedHeaders).some((k) => k.toLowerCase() === "content-type");
327
+ if (encodedBody && encodedBody.contentType && !hasExplicitContentType) mergedHeaders["content-type"] = encodedBody.contentType;
327
328
  return {
328
329
  url: this.urlBuilder.build(spec.url, mergedQuery),
329
330
  init: {
@@ -422,21 +423,7 @@ var HttpBodyBuilder = class {
422
423
  if (attachment) {
423
424
  const readResult = await ctx.binary.openReadStream(attachment);
424
425
  if (readResult) {
425
- const reader = readResult.body.getReader();
426
- const chunks = [];
427
- let done = false;
428
- while (!done) {
429
- const result = await reader.read();
430
- done = result.done;
431
- if (result.value) chunks.push(result.value);
432
- }
433
- const totalLength = chunks.reduce((acc, chunk) => acc + chunk.length, 0);
434
- const merged = new Uint8Array(totalLength);
435
- let offset = 0;
436
- for (const chunk of chunks) {
437
- merged.set(chunk, offset);
438
- offset += chunk.length;
439
- }
426
+ const merged = await this.readStreamToBuffer(readResult.body);
440
427
  const blob = new Blob([merged], { type: attachment.mimeType });
441
428
  formData.append(fieldName, blob, attachment.filename ?? binaryRef);
442
429
  }
@@ -447,6 +434,34 @@ var HttpBodyBuilder = class {
447
434
  contentType: ""
448
435
  };
449
436
  }
437
+ if (spec.kind === "binary") {
438
+ const attachment = item.binary?.[spec.slot];
439
+ if (!attachment) throw new Error(`HttpRequest bodyFormat "binary": no binary attachment found at slot "${spec.slot}". Ensure a previous node attached binary data at that slot.`);
440
+ const readResult = await ctx.binary.openReadStream(attachment);
441
+ if (!readResult) throw new Error(`HttpRequest bodyFormat "binary": could not open read stream for slot "${spec.slot}".`);
442
+ return {
443
+ body: readResult.body,
444
+ contentType: attachment.mimeType
445
+ };
446
+ }
447
+ }
448
+ async readStreamToBuffer(stream) {
449
+ const reader = stream.getReader();
450
+ const chunks = [];
451
+ let done = false;
452
+ while (!done) {
453
+ const result = await reader.read();
454
+ done = result.done;
455
+ if (result.value) chunks.push(result.value);
456
+ }
457
+ const totalLength = chunks.reduce((acc, chunk) => acc + chunk.length, 0);
458
+ const merged = new Uint8Array(new ArrayBuffer(totalLength));
459
+ let offset = 0;
460
+ for (const chunk of chunks) {
461
+ merged.set(chunk, offset);
462
+ offset += chunk.length;
463
+ }
464
+ return merged;
450
465
  }
451
466
  };
452
467
 
@@ -6540,12 +6555,16 @@ let HttpRequestNode = class HttpRequestNode$1 {
6540
6555
  mode: ctx.config.downloadMode,
6541
6556
  binaryName: ctx.config.binaryName
6542
6557
  },
6558
+ responseFormat: ctx.config.responseFormat,
6559
+ responseBinarySlot: ctx.config.responseBinarySlot,
6560
+ responseSizeCapBytes: ctx.config.responseSizeCapBytes,
6543
6561
  ctx
6544
6562
  };
6545
6563
  const { url: resolvedUrl, init } = await new HttpRequestExecutor(globalThis.fetch, new HttpBodyBuilder(), new HttpUrlBuilder()).buildRequest(spec, item);
6546
6564
  const response = await globalThis.fetch(resolvedUrl, init);
6547
6565
  const headers = this.readHeaders(response.headers);
6548
6566
  const mimeType = this.resolveMimeType(headers);
6567
+ if (ctx.config.responseFormat === "binary") return await this.handleBinaryResponse(response, resolvedUrl, headers, mimeType, ctx);
6549
6568
  const binaryName = ctx.config.binaryName;
6550
6569
  if (this.shouldAttachBody(ctx.config.downloadMode, mimeType)) {
6551
6570
  const outputJson = {
@@ -6589,6 +6608,36 @@ let HttpRequestNode = class HttpRequestNode$1 {
6589
6608
  ...text !== void 0 ? { text } : {}
6590
6609
  } };
6591
6610
  }
6611
+ async handleBinaryResponse(response, resolvedUrl, headers, mimeType, ctx) {
6612
+ const slotName = ctx.config.responseBinarySlot;
6613
+ const sizeCap = ctx.config.responseSizeCapBytes;
6614
+ const contentLengthHeader = headers["content-length"];
6615
+ if (contentLengthHeader) {
6616
+ const declaredSize = parseInt(contentLengthHeader, 10);
6617
+ if (!isNaN(declaredSize) && declaredSize > sizeCap) throw new Error(`HttpRequest responseFormat "binary": response Content-Length (${declaredSize} bytes) exceeds responseSizeCapBytes (${sizeCap} bytes).`);
6618
+ }
6619
+ const filename = this.resolveFilename(resolvedUrl, headers);
6620
+ const attachment = await ctx.binary.attach({
6621
+ name: slotName,
6622
+ body: response.body ? response.body : new Uint8Array(await response.arrayBuffer()),
6623
+ mimeType,
6624
+ filename
6625
+ });
6626
+ let outputItem = { json: {
6627
+ url: resolvedUrl,
6628
+ method: ctx.config.method,
6629
+ ok: response.ok,
6630
+ status: response.status,
6631
+ statusText: response.statusText,
6632
+ headers,
6633
+ binarySlot: slotName,
6634
+ contentType: mimeType,
6635
+ size: attachment.size,
6636
+ ...filename !== void 0 ? { filename } : {}
6637
+ } };
6638
+ outputItem = ctx.binary.withAttachment(outputItem, slotName, attachment);
6639
+ return outputItem;
6640
+ }
6592
6641
  async resolveCredential(ctx) {
6593
6642
  const slotKey = ctx.config.args.credentialSlot;
6594
6643
  if (!slotKey) return;
@@ -6660,6 +6709,8 @@ const HTTP_REQUEST_ACCEPTED_CREDENTIAL_TYPES = [
6660
6709
  basicAuthCredentialType.definition.typeId,
6661
6710
  oauth2ClientCredentialsType.definition.typeId
6662
6711
  ];
6712
+ /** Default maximum response size for binary mode: 100 MiB. */
6713
+ const DEFAULT_RESPONSE_SIZE_CAP_BYTES = 100 * 1024 * 1024;
6663
6714
  var HttpRequest = class {
6664
6715
  kind = "node";
6665
6716
  type = HttpRequestNode;
@@ -6685,6 +6736,15 @@ var HttpRequest = class {
6685
6736
  get downloadMode() {
6686
6737
  return this.args.downloadMode ?? "auto";
6687
6738
  }
6739
+ get responseFormat() {
6740
+ return this.args.responseFormat;
6741
+ }
6742
+ get responseBinarySlot() {
6743
+ return this.args.responseBinarySlot ?? "response";
6744
+ }
6745
+ get responseSizeCapBytes() {
6746
+ return this.args.responseSizeCapBytes ?? DEFAULT_RESPONSE_SIZE_CAP_BYTES;
6747
+ }
6688
6748
  getCredentialRequirements() {
6689
6749
  if (!this.args.credentialSlot) return [];
6690
6750
  return [{
@@ -7178,6 +7238,10 @@ let SubWorkflowNode = class SubWorkflowNode$1 {
7178
7238
  engineMaxSubworkflowDepth: args.ctx.engineMaxSubworkflowDepth
7179
7239
  }
7180
7240
  });
7241
+ await args.ctx.nodeState?.setChildRunId?.({
7242
+ nodeId: args.ctx.nodeId,
7243
+ childRunId: result.runId
7244
+ });
7181
7245
  if (result.status !== "completed") throw new Error(`Subworkflow ${args.ctx.config.workflowId} did not complete (status=${result.status})`);
7182
7246
  const out = [];
7183
7247
  for (const produced of result.outputs) {
@@ -7209,6 +7273,7 @@ SubWorkflowNode = __decorate([
7209
7273
  var SubWorkflow = class {
7210
7274
  kind = "node";
7211
7275
  type = SubWorkflowNode;
7276
+ icon = "lucide:workflow";
7212
7277
  constructor(name, workflowId, upstreamRefs, startAt, id) {
7213
7278
  this.name = name;
7214
7279
  this.workflowId = workflowId;