ocpipe 0.3.0 → 0.3.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/src/parsing.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * DSTS SDK response parsing.
2
+ * ocpipe response parsing.
3
3
  *
4
4
  * Extracts and validates LLM responses using JSON or field marker formats.
5
5
  */
@@ -507,13 +507,13 @@ export function applyJqPatch(
507
507
  /\binput\b/,
508
508
  /\binputs\b/,
509
509
  /\bsystem\b/,
510
- /\@base64d/,
511
- /\@uri/,
512
- /\@csv/,
513
- /\@tsv/,
514
- /\@json/,
515
- /\@text/,
516
- /\@sh/,
510
+ /@base64d/,
511
+ /@uri/,
512
+ /@csv/,
513
+ /@tsv/,
514
+ /@json/,
515
+ /@text/,
516
+ /@sh/,
517
517
  /`[^`]*`/, // Backtick string interpolation
518
518
  /\bimport\b/,
519
519
  /\binclude\b/,
@@ -532,7 +532,7 @@ export function applyJqPatch(
532
532
 
533
533
  // Only allow patches that look like field operations
534
534
  // Valid: .foo = "bar", .items[0].name = .items[0].title, del(.foo) | .bar = 1
535
- const safePattern = /^[\s\w\[\]."'=|,:\-{}]*$/
535
+ const safePattern = /^[\s\w[\]."'=|,:\-{}]*$/
536
536
  if (!safePattern.test(patch)) {
537
537
  console.error(` Invalid characters in patch, skipping: ${patch}`)
538
538
  return obj
@@ -648,44 +648,63 @@ export function buildBatchJsonPatchPrompt(
648
648
  return lines.join('\n')
649
649
  }
650
650
 
651
- /** extractJsonPatch extracts a JSON Patch array from an LLM response. */
652
- export function extractJsonPatch(response: string): JsonPatchOperation[] {
653
- // Try to find JSON array in code blocks first
654
- const codeBlockMatch = response.match(
655
- /```(?:json)?\s*(\[[\s\S]*?\])[\s\S]*?```/,
656
- )
657
- if (codeBlockMatch?.[1]) {
658
- try {
659
- return JSON.parse(codeBlockMatch[1]) as JsonPatchOperation[]
660
- } catch {
661
- // Continue to try other methods
651
+ /**
652
+ * extractBalancedArray extracts a balanced JSON array from a string starting at startIdx.
653
+ * Returns the array substring or null if not found/unbalanced.
654
+ */
655
+ function extractBalancedArray(text: string, startIdx: number): string | null {
656
+ if (startIdx === -1 || startIdx >= text.length) return null
657
+
658
+ let bracketCount = 0
659
+ let endIdx = startIdx
660
+ for (let i = startIdx; i < text.length; i++) {
661
+ if (text[i] === '[') bracketCount++
662
+ else if (text[i] === ']') {
663
+ bracketCount--
664
+ if (bracketCount === 0) {
665
+ endIdx = i + 1
666
+ break
667
+ }
662
668
  }
663
669
  }
664
670
 
665
- // Try to find raw JSON array by counting brackets
666
- const startIdx = response.indexOf('[')
667
- if (startIdx !== -1) {
668
- let bracketCount = 0
669
- let endIdx = startIdx
670
- for (let i = startIdx; i < response.length; i++) {
671
- if (response[i] === '[') bracketCount++
672
- else if (response[i] === ']') {
673
- bracketCount--
674
- if (bracketCount === 0) {
675
- endIdx = i + 1
676
- break
671
+ if (endIdx > startIdx && bracketCount === 0) {
672
+ return text.slice(startIdx, endIdx)
673
+ }
674
+ return null
675
+ }
676
+
677
+ /** extractJsonPatch extracts a JSON Patch array from an LLM response. */
678
+ export function extractJsonPatch(response: string): JsonPatchOperation[] {
679
+ // Try to find JSON array in code blocks first
680
+ // Use indexOf to find code block boundaries to avoid ReDoS vulnerabilities
681
+ const codeBlockStart = response.indexOf('```')
682
+ if (codeBlockStart !== -1) {
683
+ const codeBlockEnd = response.indexOf('```', codeBlockStart + 3)
684
+ if (codeBlockEnd !== -1) {
685
+ const codeBlockContent = response.slice(codeBlockStart + 3, codeBlockEnd)
686
+ // Skip optional "json" language identifier and whitespace
687
+ const arrayStart = codeBlockContent.indexOf('[')
688
+ if (arrayStart !== -1) {
689
+ const arrayJson = extractBalancedArray(codeBlockContent, arrayStart)
690
+ if (arrayJson) {
691
+ try {
692
+ return JSON.parse(arrayJson) as JsonPatchOperation[]
693
+ } catch {
694
+ // Continue to try other methods
695
+ }
677
696
  }
678
697
  }
679
698
  }
699
+ }
680
700
 
681
- if (endIdx > startIdx) {
682
- try {
683
- return JSON.parse(
684
- response.slice(startIdx, endIdx),
685
- ) as JsonPatchOperation[]
686
- } catch {
687
- // Fall through to empty array
688
- }
701
+ // Try to find raw JSON array by counting brackets
702
+ const arrayJson = extractBalancedArray(response, response.indexOf('['))
703
+ if (arrayJson) {
704
+ try {
705
+ return JSON.parse(arrayJson) as JsonPatchOperation[]
706
+ } catch {
707
+ // Fall through to empty array
689
708
  }
690
709
  }
691
710
 
@@ -775,6 +794,37 @@ export function applyJsonPatch(
775
794
  return result
776
795
  }
777
796
 
797
+ /** Keys that could be used for prototype pollution attacks. */
798
+ const UNSAFE_KEYS = new Set(['__proto__', 'constructor', 'prototype'])
799
+
800
+ function isUnsafeKey(key: string): boolean {
801
+ return UNSAFE_KEYS.has(key)
802
+ }
803
+
804
+ /**
805
+ * Unescape a single JSON Pointer path segment according to RFC 6901.
806
+ * This ensures that checks for dangerous keys are applied to the
807
+ * effective property name, not the escaped form.
808
+ */
809
+ function unescapeJsonPointerSegment(segment: string): string {
810
+ return segment.replace(/~1/g, '/').replace(/~0/g, '~')
811
+ }
812
+
813
+ /**
814
+ * isSafePathSegment determines whether a JSON Pointer path segment is safe to use
815
+ * as a property key on an object. It rejects keys that are known to enable
816
+ * prototype pollution or that contain characters commonly used in special
817
+ * property notations.
818
+ */
819
+ function isSafePathSegment(segment: string): boolean {
820
+ // Normalize the segment as it will appear as a property key.
821
+ const normalized = unescapeJsonPointerSegment(String(segment))
822
+ if (isUnsafeKey(normalized)) return false
823
+ // Disallow bracket notation-style segments to avoid unexpected coercions.
824
+ if (normalized.includes('[') || normalized.includes(']')) return false
825
+ return true
826
+ }
827
+
778
828
  /** getValueAtPath retrieves a value at a JSON Pointer path. */
779
829
  function getValueAtPath(
780
830
  obj: Record<string, unknown>,
@@ -782,6 +832,15 @@ function getValueAtPath(
782
832
  ): unknown {
783
833
  let current: unknown = obj
784
834
  for (const part of parts) {
835
+ // Block prototype-pollution: reject __proto__, constructor, prototype
836
+ if (
837
+ part === '__proto__' ||
838
+ part === 'constructor' ||
839
+ part === 'prototype'
840
+ ) {
841
+ return undefined
842
+ }
843
+ if (!isSafePathSegment(part)) return undefined
785
844
  if (current === null || current === undefined) return undefined
786
845
  if (Array.isArray(current)) {
787
846
  const idx = parseInt(part, 10)
@@ -806,25 +865,49 @@ function setValueAtPath(
806
865
  let current: unknown = obj
807
866
  for (let i = 0; i < parts.length - 1; i++) {
808
867
  const part = parts[i]!
868
+ // Block prototype-pollution: reject __proto__, constructor, prototype
869
+ if (
870
+ part === '__proto__' ||
871
+ part === 'constructor' ||
872
+ part === 'prototype'
873
+ ) {
874
+ return
875
+ }
876
+ if (!isSafePathSegment(part)) {
877
+ // Avoid writing to dangerous or malformed prototype-related properties
878
+ return
879
+ }
809
880
  if (Array.isArray(current)) {
810
881
  const idx = parseInt(part, 10)
811
882
  if (current[idx] === undefined) {
812
883
  // Create intermediate object or array
813
884
  const nextPart = parts[i + 1]!
814
- current[idx] = /^\d+$/.test(nextPart) ? [] : {}
885
+ current[idx] = /^\d+$/.test(nextPart) ? [] : Object.create(null)
815
886
  }
816
887
  current = current[idx]
817
888
  } else if (typeof current === 'object' && current !== null) {
818
889
  const rec = current as Record<string, unknown>
819
890
  if (rec[part] === undefined) {
820
891
  const nextPart = parts[i + 1]!
821
- rec[part] = /^\d+$/.test(nextPart) ? [] : {}
892
+ rec[part] = /^\d+$/.test(nextPart) ? [] : Object.create(null)
822
893
  }
823
894
  current = rec[part]
824
895
  }
825
896
  }
826
897
 
827
898
  const lastPart = parts[parts.length - 1]!
899
+ // Block prototype-pollution: reject __proto__, constructor, prototype
900
+ if (
901
+ lastPart === '__proto__' ||
902
+ lastPart === 'constructor' ||
903
+ lastPart === 'prototype'
904
+ ) {
905
+ return
906
+ }
907
+ if (!isSafePathSegment(lastPart)) {
908
+ // Avoid writing to dangerous or malformed prototype-related properties
909
+ return
910
+ }
828
911
  if (Array.isArray(current)) {
829
912
  const idx = parseInt(lastPart, 10)
830
913
  current[idx] = value
@@ -843,6 +926,18 @@ function removeValueAtPath(
843
926
  let current: unknown = obj
844
927
  for (let i = 0; i < parts.length - 1; i++) {
845
928
  const part = parts[i]!
929
+ // Block prototype-pollution: reject __proto__, constructor, prototype
930
+ if (
931
+ part === '__proto__' ||
932
+ part === 'constructor' ||
933
+ part === 'prototype'
934
+ ) {
935
+ return
936
+ }
937
+ if (!isSafePathSegment(part)) {
938
+ // Avoid accessing dangerous prototype-related properties
939
+ return
940
+ }
846
941
  if (Array.isArray(current)) {
847
942
  current = current[parseInt(part, 10)]
848
943
  } else if (typeof current === 'object' && current !== null) {
@@ -853,6 +948,18 @@ function removeValueAtPath(
853
948
  }
854
949
 
855
950
  const lastPart = parts[parts.length - 1]!
951
+ // Block prototype-pollution: reject __proto__, constructor, prototype
952
+ if (
953
+ lastPart === '__proto__' ||
954
+ lastPart === 'constructor' ||
955
+ lastPart === 'prototype'
956
+ ) {
957
+ return
958
+ }
959
+ if (!isSafePathSegment(lastPart)) {
960
+ // Avoid deleting dangerous or malformed properties
961
+ return
962
+ }
856
963
  if (Array.isArray(current)) {
857
964
  const idx = parseInt(lastPart, 10)
858
965
  current.splice(idx, 1)
package/src/pipeline.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * DSTS SDK Pipeline orchestrator.
2
+ * ocpipe Pipeline orchestrator.
3
3
  *
4
4
  * Manages execution context, state, checkpointing, logging, retry logic, and sub-pipelines.
5
5
  */
package/src/predict.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * DSTS SDK Predict class.
2
+ * ocpipe Predict class.
3
3
  *
4
4
  * Executes a signature by generating a prompt, calling OpenCode, and parsing the response.
5
5
  */
@@ -47,8 +47,13 @@ export interface PredictConfig {
47
47
  correction?: CorrectionConfig | false
48
48
  }
49
49
 
50
+ type AnySignature = SignatureDef<
51
+ Record<string, FieldConfig>,
52
+ Record<string, FieldConfig>
53
+ >
54
+
50
55
  /** Predict executes a signature by calling an LLM and parsing the response. */
51
- export class Predict<S extends SignatureDef<any, any>> {
56
+ export class Predict<S extends AnySignature> {
52
57
  constructor(
53
58
  public readonly sig: S,
54
59
  public readonly config: PredictConfig = {},
@@ -239,7 +244,7 @@ export class Predict<S extends SignatureDef<any, any>> {
239
244
 
240
245
  // Input fields as JSON
241
246
  const inputsWithDescriptions: Record<string, unknown> = {}
242
- for (const [name, config] of Object.entries(this.sig.inputs) as [
247
+ for (const [name] of Object.entries(this.sig.inputs) as [
243
248
  string,
244
249
  FieldConfig,
245
250
  ][]) {
package/src/signature.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * DSTS SDK signature definition.
2
+ * ocpipe signature definition.
3
3
  *
4
4
  * Signatures declare input/output contracts for LLM interactions using Zod for validation.
5
5
  */
package/src/state.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * DSTS SDK state management.
2
+ * ocpipe state management.
3
3
  *
4
4
  * Provides base state types and helpers for checkpointable workflow state.
5
5
  */
package/src/testing.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
- * DSTS SDK testing utilities.
2
+ * ocpipe testing utilities.
3
3
  *
4
- * Provides mock backends and test helpers for unit testing DSTS components.
4
+ * Provides mock backends and test helpers for unit testing ocpipe components.
5
5
  */
6
6
 
7
7
  import type { RunAgentOptions, RunAgentResult, FieldConfig } from './types.js'
@@ -175,11 +175,12 @@ export function generateMockOutputs(
175
175
  case 'ZodObject':
176
176
  result[name] = {}
177
177
  break
178
- case 'ZodEnum':
178
+ case 'ZodEnum': {
179
179
  // Get first enum value via options property
180
180
  const enumType = config.type as { options?: readonly string[] }
181
181
  result[name] = enumType.options?.[0] ?? 'unknown'
182
182
  break
183
+ }
183
184
  default:
184
185
  result[name] = null
185
186
  }
package/src/types.ts CHANGED
@@ -1,7 +1,5 @@
1
1
  /**
2
- * DSTS SDK shared types.
3
- *
4
- * Core type definitions for the Declarative Self-Improving TypeScript SDK.
2
+ * ocpipe shared types.
5
3
  */
6
4
 
7
5
  import type { z } from 'zod/v4'
@@ -131,14 +129,24 @@ export interface SignatureDef<
131
129
  }
132
130
 
133
131
  /** Infer the input type from a signature definition. */
134
- export type InferInputs<S extends SignatureDef<any, any>> =
135
- S extends SignatureDef<infer I, any> ?
132
+ export type InferInputs<
133
+ S extends SignatureDef<
134
+ Record<string, FieldConfig>,
135
+ Record<string, FieldConfig>
136
+ >,
137
+ > =
138
+ S extends SignatureDef<infer I, Record<string, FieldConfig>> ?
136
139
  { [K in keyof I]: z.infer<I[K]['type']> }
137
140
  : never
138
141
 
139
142
  /** Infer the output type from a signature definition. */
140
- export type InferOutputs<S extends SignatureDef<any, any>> =
141
- S extends SignatureDef<any, infer O> ?
143
+ export type InferOutputs<
144
+ S extends SignatureDef<
145
+ Record<string, FieldConfig>,
146
+ Record<string, FieldConfig>
147
+ >,
148
+ > =
149
+ S extends SignatureDef<Record<string, FieldConfig>, infer O> ?
142
150
  { [K in keyof O]: z.infer<O[K]['type']> }
143
151
  : never
144
152