@nexart/ai-execution 0.9.0 → 0.11.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.
package/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # @nexart/ai-execution v0.8.0
1
+ # @nexart/ai-execution v0.11.0
2
2
 
3
3
  Tamper-evident records and Certified Execution Records (CER) for AI operations.
4
4
 
@@ -7,7 +7,7 @@ Tamper-evident records and Certified Execution Records (CER) for AI operations.
7
7
  | Component | Version |
8
8
  |---|---|
9
9
  | Service | — |
10
- | SDK | 0.8.0 |
10
+ | SDK | 0.11.0 |
11
11
  | Protocol | 1.2.0 |
12
12
 
13
13
  ## Why Not Just Store Logs?
@@ -22,6 +22,7 @@ This package creates integrity records for AI executions. Every time you call an
22
22
  - What you got back (output)
23
23
  - The exact parameters used (temperature, model, etc.)
24
24
  - SHA-256 hashes of everything for tamper detection
25
+ - Optionally: upstream context signals from other systems, sealed into the same certificate hash (v0.10.0+)
25
26
 
26
27
  These records can be verified offline to detect any post-hoc modification and prove integrity of the recorded execution.
27
28
 
@@ -168,6 +169,13 @@ Auto-generated fields (set by `createSnapshot`, do not set manually): `type`, `p
168
169
  "createdAt": "2026-02-12T00:00:00.000Z",
169
170
  "version": "0.1",
170
171
  "snapshot": { "..." : "..." },
172
+ "context": {
173
+ "signals": [
174
+ { "type": "approval", "source": "github-actions", "step": 0,
175
+ "timestamp": "2026-02-12T00:00:00.000Z", "actor": "alice",
176
+ "status": "ok", "payload": { "pr": 42 } }
177
+ ]
178
+ },
171
179
  "meta": { "source": "my-app", "tags": ["production"] },
172
180
  "declaration": {
173
181
  "stabilitySchemeId": "nexart-cer-v1",
@@ -178,9 +186,13 @@ Auto-generated fields (set by `createSnapshot`, do not set manually): `type`, `p
178
186
  }
179
187
  ```
180
188
 
189
+ `context` is **optional** and only appears when signals are supplied. Bundles without `context` are structurally identical to all prior versions.
190
+
181
191
  ### Certificate Hash Computation
182
192
 
183
- The `certificateHash` is SHA-256 of the UTF-8 bytes of the canonical JSON of exactly: `{ bundleType, version, createdAt, snapshot }`. `meta` and `declaration` are excluded. Key-ordering is recursive. This computation is identical across all SDK versions.
193
+ The `certificateHash` is SHA-256 of the UTF-8 bytes of the canonical JSON of exactly: `{ bundleType, version, createdAt, snapshot }` — or, when context signals are present: `{ bundleType, version, createdAt, snapshot, context }`. `meta` and `declaration` are always excluded. Key-ordering is recursive. This computation is identical across all SDK versions.
194
+
195
+ **Context and backward compatibility:** when no signals are provided (or an empty array is passed), `context` is omitted from the hash payload entirely. The resulting `certificateHash` is byte-for-byte identical to a pre-v0.10.0 bundle with the same snapshot. Passing signals is strictly additive — it can only extend the hash, never break it for existing callers.
184
196
 
185
197
  ### Declaration Block (v0.7.0+)
186
198
 
@@ -582,7 +594,7 @@ Fixtures at `fixtures/vectors/` and `fixtures/golden/`. Cross-language implement
582
594
  | `verifySnapshot(snapshot)` | Verify snapshot hashes and structure |
583
595
  | `sealCer(snapshot, options?)` | Seal snapshot into CER bundle |
584
596
  | `verify(bundle)` / `verifyCer(bundle)` | Verify CER bundle |
585
- | `certifyDecision(params)` | One-call: createSnapshot + sealCer |
597
+ | `certifyDecision(params)` | One-call: createSnapshot + sealCer. Accepts optional `signals?: CerContextSignal[]` — included in `certificateHash` when present (v0.10.0+) |
586
598
 
587
599
  ### Workflow
588
600
 
@@ -672,6 +684,263 @@ Priority when multiple failures exist: `CANONICALIZATION_ERROR` > `SCHEMA_ERROR`
672
684
  | `runAnthropicExecution` | `@nexart/ai-execution/providers/anthropic` |
673
685
  | `wrapProvider` | `@nexart/ai-execution/providers/wrap` |
674
686
 
687
+ ## LangChain Integration
688
+
689
+ `@nexart/ai-execution` includes a minimal LangChain helper surface (introduced v0.10.0, current v0.11.0). Certify the final input and output of any LangChain chain, agent, or runnable as a tamper-evident CER — no LangChain package dependency required.
690
+
691
+ `certifyLangChainRun` operates in two modes depending on whether `nodeUrl` and `apiKey` are supplied:
692
+
693
+ | Mode | How to call | Returns |
694
+ |---|---|---|
695
+ | **Local** (default) | No `nodeUrl` / `apiKey` | `LangChainCerResult` (sync) |
696
+ | **Node-attested** | `nodeUrl` + `apiKey` on input | `Promise<LangChainAttestedResult>` |
697
+
698
+ ### Mode 1 — Local CER creation (synchronous, no network)
699
+
700
+ `createLangChainCer` is always local. `certifyLangChainRun` without `nodeUrl`/`apiKey` is identical.
701
+
702
+ ```ts
703
+ import { createLangChainCer, certifyLangChainRun } from '@nexart/ai-execution';
704
+ // or: import { ... } from '@nexart/ai-execution/langchain';
705
+
706
+ // Using createLangChainCer — always explicit about local-only behaviour
707
+ const { bundle, certificateHash, executionId } = createLangChainCer({
708
+ executionId: 'run-001', // optional — UUID generated if omitted
709
+ provider: 'openai',
710
+ model: 'gpt-4o-mini',
711
+ input: { messages: [{ role: 'user', content: 'What is the capital of France?' }] },
712
+ output: { text: 'Paris.' },
713
+ metadata: { appId: 'my-app', projectId: 'docs-bot' },
714
+ parameters: { temperature: 0, maxTokens: 200, topP: null, seed: null },
715
+ createdAt: new Date().toISOString(), // pin for deterministic hash
716
+ });
717
+
718
+ console.log(certificateHash); // sha256:...
719
+
720
+ // certifyLangChainRun without nodeUrl/apiKey: identical, sync
721
+ const { bundle: b2 } = certifyLangChainRun({
722
+ provider: 'openai', model: 'gpt-4o-mini',
723
+ input: { messages: [{ role: 'user', content: 'Summarise this.' }] },
724
+ output: { text: 'Summary...' },
725
+ });
726
+ ```
727
+
728
+ ### Mode 2 — Node-attested certification (async)
729
+
730
+ Add `nodeUrl` and `apiKey` to the same input to route through the existing `certifyAndAttestDecision()` path. The function returns a `Promise<LangChainAttestedResult>` with the `receipt` from the NexArt node.
731
+
732
+ ```ts
733
+ import { certifyLangChainRun } from '@nexart/ai-execution';
734
+
735
+ const result = await certifyLangChainRun({
736
+ executionId: 'run-001',
737
+ provider: 'openai',
738
+ model: 'gpt-4o-mini',
739
+ input: { messages: [{ role: 'user', content: 'What is the capital of France?' }] },
740
+ output: { text: 'Paris.' },
741
+ metadata: { appId: 'my-app', projectId: 'docs-bot' },
742
+ createdAt: new Date().toISOString(),
743
+ nodeUrl: 'https://node.nexart.io', // ← triggers attested mode
744
+ apiKey: process.env.NEXART_API_KEY!, // ← required with nodeUrl
745
+ });
746
+
747
+ console.log(result.certificateHash); // sha256:... (same semantics as local)
748
+ console.log(result.attested); // true
749
+ console.log(result.receipt);
750
+ // {
751
+ // attestationId: "nxa_attest_...",
752
+ // certificateHash: "sha256:...",
753
+ // nodeRuntimeHash: "sha256:...",
754
+ // protocolVersion: "1.2.0"
755
+ // }
756
+ ```
757
+
758
+ `result.bundle` passes `verifyCer()` identically to local mode — the `certificateHash` covers only the CER content, not the receipt fields.
759
+
760
+ ### Three helpers
761
+
762
+ All three helpers accept an optional `signals?: CerContextSignal[]` field on their input. When provided, signals are sealed into `bundle.context` and included in `certificateHash`. Omitting signals produces a hash identical to a bundle without signals.
763
+
764
+ | Helper | Returns | Network |
765
+ |---|---|---|
766
+ | `createLangChainCer(input)` | `LangChainCerResult` (sync) | Never |
767
+ | `certifyLangChainRun(input)` without `nodeUrl`/`apiKey` | `LangChainCerResult` (sync) | Never |
768
+ | `certifyLangChainRun({ ...input, nodeUrl, apiKey })` | `Promise<LangChainAttestedResult>` | Yes — NexArt node |
769
+
770
+ ### Result types
771
+
772
+ ```ts
773
+ interface LangChainCerResult {
774
+ bundle: CerAiExecutionBundle;
775
+ certificateHash: string;
776
+ executionId: string;
777
+ }
778
+
779
+ interface LangChainAttestedResult extends LangChainCerResult {
780
+ receipt: AttestationReceipt;
781
+ attested: true;
782
+ }
783
+ ```
784
+
785
+ ### Input normalization
786
+
787
+ | Input shape | Stored in CER as |
788
+ |---|---|
789
+ | `string` | `string` (pass-through) |
790
+ | Plain object `{}` | `Record<string, unknown>` (pass-through) |
791
+ | Array `[]` | `{ items: [...] }` |
792
+ | Anything else | `String(value)` |
793
+
794
+ ### Prompt extraction
795
+
796
+ Resolved in this order: `metadata.prompt` → `input.messages[0].content` → `input.prompt` → `input.text` → `"[LangChain run]"`.
797
+
798
+ ### Metadata mapping
799
+
800
+ | Metadata key | Mapped to |
801
+ |---|---|
802
+ | `appId` | `snapshot.appId` |
803
+ | `runId` | `snapshot.runId` |
804
+ | `workflowId` | `snapshot.workflowId` |
805
+ | `conversationId` | `snapshot.conversationId` |
806
+ | `prompt` | Used as the CER `prompt` field |
807
+ | All other keys | `bundle.meta.tags` (as `"key:value"` strings) |
808
+
809
+ ### Verification
810
+
811
+ Bundles from all three helpers are fully protocol-aligned:
812
+
813
+ ```ts
814
+ import { verifyCer, verifyAiCerBundleDetailed } from '@nexart/ai-execution';
815
+
816
+ const basic = verifyCer(bundle);
817
+ // { ok: true, code: 'OK' }
818
+
819
+ const detailed = verifyAiCerBundleDetailed(bundle);
820
+ // { status: 'VERIFIED', checks: { bundleIntegrity: 'PASS', nodeSignature: 'SKIPPED' }, ... }
821
+ ```
822
+
823
+ ### Testing the attested path without a network
824
+
825
+ Use the injectable `_attestFn` test option (same pattern as `certifyAndAttestRun`'s `attestStep`):
826
+
827
+ ```ts
828
+ import type { AttestDecisionFn } from '@nexart/ai-execution';
829
+
830
+ const mockAttest: AttestDecisionFn = async (params, _opts) => {
831
+ const { certifyDecision } = await import('@nexart/ai-execution');
832
+ const bundle = certifyDecision(params);
833
+ return { bundle, receipt: { attestationId: 'mock-123', certificateHash: bundle.certificateHash,
834
+ nodeRuntimeHash: 'sha256:' + 'beef'.repeat(16), protocolVersion: '1.2.0' } };
835
+ };
836
+
837
+ const result = await certifyLangChainRun(
838
+ { ...input, nodeUrl: 'https://node.nexart.io', apiKey: 'nxa_test' },
839
+ { _attestFn: mockAttest },
840
+ );
841
+ ```
842
+
843
+ ### V3 roadmap (not yet implemented)
844
+
845
+ - `BaseCallbackHandler` integration for automatic mid-chain CER capture at `onLLMEnd`
846
+ - LangSmith trace correlation via `metadata.runId` → `snapshot.conversationId`
847
+ - Separate `@nexart/langchain` package once the callback surface is proven in production
848
+
849
+ ---
850
+
851
+ ## Context Signals (v0.10.0+)
852
+
853
+ CERs can optionally include **context signals** — structured, protocol-agnostic records of upstream events that were present when the AI execution ran. Signals are evidence-only: they have no effect on how the AI call is made, what parameters are used, or how the output is processed. Their only role is to be bound into the `certificateHash` alongside the execution record so that any post-hoc modification to either the signals or the snapshot is detectable.
854
+
855
+ **What signals are:**
856
+ - Records of things that happened before or alongside the AI call (approvals, deploys, audits, tool invocations, review outcomes, etc.)
857
+ - Protocol-agnostic — `type`, `source`, `actor`, and `payload` are all free-form strings and objects
858
+ - Included in `certificateHash` only when present — omitting signals produces a hash identical to pre-v0.10.0
859
+
860
+ **What signals are not:**
861
+ - Governance enforcement — NexArt does not interpret signal content or enforce policy based on it
862
+ - Execution dependencies — signals do not gate or modify the AI call in any way
863
+ - A replacement for `toolCalls` — `toolCalls` records tool invocations *within* a run; signals record upstream context *around* a run
864
+
865
+ ### Quick example
866
+
867
+ ```ts
868
+ import { createSignalCollector } from '@nexart/signals';
869
+ import { certifyDecision, verifyCer } from '@nexart/ai-execution';
870
+
871
+ // 1. Collect upstream signals during your pipeline run
872
+ const collector = createSignalCollector({ defaultSource: 'github-actions' });
873
+ collector.add({ type: 'approval', actor: 'alice', status: 'ok', payload: { pr: 42 } });
874
+ collector.add({ type: 'deploy', actor: 'ci-bot', status: 'ok', payload: { env: 'prod' } });
875
+ const { signals } = collector.export();
876
+
877
+ // 2. Certify the AI execution with signals bound as context evidence
878
+ const bundle = certifyDecision({
879
+ provider: 'openai',
880
+ model: 'gpt-4o-mini',
881
+ prompt: 'Summarise.',
882
+ input: userQuery,
883
+ output: llmResponse,
884
+ parameters: { temperature: 0, maxTokens: 512, topP: null, seed: null },
885
+ signals, // ← included in certificateHash; omit to get identical hash as before
886
+ });
887
+
888
+ verifyCer(bundle).ok; // true
889
+ console.log(bundle.context?.signals.length); // 2 — signals are sealed in the bundle
890
+ ```
891
+
892
+ `NexArtSignal[]` from `@nexart/signals` is structurally identical to `CerContextSignal[]` — no casting or conversion needed. `@nexart/ai-execution` has no hard dependency on `@nexart/signals`; any object matching the `CerContextSignal` shape works.
893
+
894
+ ### Signals also work on the LangChain path
895
+
896
+ ```ts
897
+ import { certifyLangChainRun } from '@nexart/ai-execution';
898
+
899
+ const { bundle } = certifyLangChainRun({
900
+ provider: 'openai',
901
+ model: 'gpt-4o-mini',
902
+ input: { messages: [...] },
903
+ output: { text: '...' },
904
+ signals, // ← same field, same semantics
905
+ });
906
+ ```
907
+
908
+ ### CerContextSignal shape
909
+
910
+ Every `CerContextSignal` has the following fields. All are always present in the stored bundle — no undefined values:
911
+
912
+ | Field | Type | Default | Description |
913
+ |---|---|---|---|
914
+ | `type` | `string` | required | Signal category — free-form (e.g. `"approval"`, `"deploy"`, `"audit"`) |
915
+ | `source` | `string` | required | Upstream system — free-form (e.g. `"github-actions"`, `"linear"`) |
916
+ | `step` | `number` | `0` | Position in sequence — used for ordering within the `context.signals` array |
917
+ | `timestamp` | `string` | current time | ISO 8601 |
918
+ | `actor` | `string` | `"unknown"` | Who produced the signal — free-form |
919
+ | `status` | `string` | `"ok"` | Outcome — free-form (e.g. `"ok"`, `"error"`, `"pending"`) |
920
+ | `payload` | `Record<string, unknown>` | `{}` | Opaque upstream data — NexArt does not interpret this |
921
+
922
+ Signal ordering within the array is part of the sealed hash. Reordering signals produces a different `certificateHash`.
923
+
924
+ ### Tamper detection
925
+
926
+ `verifyCer()` always reconstructs `context` from the stored `bundle.context.signals` when recomputing the hash. Any mutation — changing a signal field, adding or removing signals, or injecting a `context` block into a no-signals bundle — produces a `CERTIFICATE_HASH_MISMATCH` result:
927
+
928
+ ```ts
929
+ const tampered = { ...bundle, context: { signals: [...altered] } };
930
+ verifyCer(tampered).ok; // false
931
+ verifyCer(tampered).code; // 'CERTIFICATE_HASH_MISMATCH'
932
+ ```
933
+
934
+ ### Exported types
935
+
936
+ ```ts
937
+ import type { CerContextSignal, CerContext } from '@nexart/ai-execution';
938
+ ```
939
+
940
+ `CerContext` is the shape of `bundle.context` — `{ signals: CerContextSignal[] }`. Both are exported from the package root.
941
+
942
+ ---
943
+
675
944
  ## Version History
676
945
 
677
946
  | Version | Description |
@@ -685,7 +954,10 @@ Priority when multiple failures exist: `CANONICALIZATION_ERROR` > `SCHEMA_ERROR`
685
954
  | v0.5.0 | Ed25519 signed receipt verification: `verifyNodeReceiptSignature`, `verifyBundleAttestation`, `fetchNodeKeys`, `selectNodeKey`; new attestation `CerVerifyCode` entries; `SPEC.md`; `NodeKeysDocument`, `SignedAttestationReceipt`, `NodeReceiptVerifyResult` types |
686
955
  | v0.6.0 | Frictionless integration: `certifyDecisionFromProviderCall` (OpenAI/Anthropic/Gemini/Mistral/Bedrock drop-in); `sanitizeForStorage` + `sanitizeForStamp` redaction helpers; `createClient(defaults)` factory; regression fixture suite; all backward-compatible, no hash changes |
687
956
  | v0.7.0 | AIEF alignment: `verifyAief()` (AIEF §9.1 adapter); `verifyRunSummary()` chain verifier; `makeToolEvent()` + `hashToolOutput()` + `snapshot.toolCalls` (AIEF-06); `BundleDeclaration` block (`stabilitySchemeId`, `protectedSetId`, `protectedFields`) excluded from `certificateHash`; `redactBeforeSeal()` pre-seal verifiable redaction; `validateProfile()` (flexible/AIEF_L2/L3/L4); 5 new `CerVerifyCode` entries; backward-compatible, no hash changes |
688
- | **v0.8.0** | Helper APIs: `exportVerifiableRedacted()` (post-seal re-seal with redacted snapshot + provenance metadata); `certifyAndAttestRun()` (one-call multi-step certify + optional per-step attestation with injectable mock); test determinism fix (no time-dependent first-run failures); all v0.1–v0.7 bundles verify identically, no hashing or canonicalization changes |
957
+ | v0.8.0 | Helper APIs: `exportVerifiableRedacted()` (post-seal re-seal with redacted snapshot + provenance metadata); `certifyAndAttestRun()` (one-call multi-step certify + optional per-step attestation with injectable mock); test determinism fix; all v0.1–v0.7 bundles verify identically |
958
+ | v0.9.0 | CER Protocol types: `CerVerificationResult`, `ReasonCode`, `CheckStatus`; `verifyAiCerBundleDetailed()`; `CertifyDecisionParams.createdAt` wired through; determinism bug fix |
959
+ | v0.10.0 | LangChain integration: `createLangChainCer()` (sync/local); `certifyLangChainRun()` dual-mode — local (sync) or node-attested (`Promise<LangChainAttestedResult>` when `nodeUrl`+`apiKey` supplied); `LangChainAttestedResult`, `AttestDecisionFn`; injectable `_attestFn` for test mocking. **Context signals:** optional `signals?: CerContextSignal[]` on `certifyDecision`, `certifyLangChainRun`, and `createLangChainCer` — sealed into `bundle.context` and included in `certificateHash` when non-empty; absent or empty = hash unchanged from pre-v0.10.0. New types: `CerContextSignal`, `CerContext`. New example: `examples/signals-cer.ts`. 413 total tests; all prior bundles verify identically |
960
+ | **v0.11.0** | Version release. Ships all v0.10.0 features (LangChain integration + context signals) as the published package version. `cerSignals.test.js` added to the `npm test` script. No API, hash, or canonicalization changes |
689
961
  | v1.0.0 | Planned: API stabilization, freeze public API surface |
690
962
 
691
963
  ## Releasing
package/dist/index.cjs CHANGED
@@ -41,9 +41,11 @@ __export(src_exports, {
41
41
  certifyAndAttestRun: () => certifyAndAttestRun,
42
42
  certifyDecision: () => certifyDecision,
43
43
  certifyDecisionFromProviderCall: () => certifyDecisionFromProviderCall,
44
+ certifyLangChainRun: () => certifyLangChainRun,
44
45
  computeInputHash: () => computeInputHash,
45
46
  computeOutputHash: () => computeOutputHash,
46
47
  createClient: () => createClient,
48
+ createLangChainCer: () => createLangChainCer,
47
49
  createSnapshot: () => createSnapshot,
48
50
  exportCer: () => exportCer,
49
51
  exportVerifiableRedacted: () => exportVerifiableRedacted,
@@ -337,14 +339,22 @@ function computeCertificateHash(payload) {
337
339
  const canonical = toCanonicalJson(payload);
338
340
  return `sha256:${sha256Hex(canonical)}`;
339
341
  }
342
+ function buildContext(signals) {
343
+ if (!signals || signals.length === 0) return void 0;
344
+ return { signals };
345
+ }
340
346
  function sealCer(snapshot, options) {
341
347
  const createdAt = options?.createdAt ?? (/* @__PURE__ */ new Date()).toISOString();
348
+ const context = buildContext(options?.signals);
342
349
  const payload = {
343
350
  bundleType: "cer.ai.execution.v1",
344
351
  createdAt,
345
352
  snapshot,
346
353
  version: "0.1"
347
354
  };
355
+ if (context) {
356
+ payload.context = context;
357
+ }
348
358
  const certificateHash = computeCertificateHash(payload);
349
359
  const bundle = {
350
360
  bundleType: "cer.ai.execution.v1",
@@ -353,6 +363,9 @@ function sealCer(snapshot, options) {
353
363
  version: "0.1",
354
364
  snapshot
355
365
  };
366
+ if (context) {
367
+ bundle.context = context;
368
+ }
356
369
  if (options?.meta) {
357
370
  bundle.meta = options.meta;
358
371
  }
@@ -401,6 +414,10 @@ function verifyCer(bundle) {
401
414
  snapshot: bundle.snapshot,
402
415
  version: "0.1"
403
416
  };
417
+ const verifyContext = buildContext(bundle.context?.signals);
418
+ if (verifyContext) {
419
+ payload.context = verifyContext;
420
+ }
404
421
  const expectedHash = computeCertificateHash(payload);
405
422
  if (bundle.certificateHash !== expectedHash) {
406
423
  certHashErrors.push(`certificateHash mismatch: expected ${expectedHash}, got ${bundle.certificateHash}`);
@@ -461,7 +478,11 @@ function certifyDecision(params) {
461
478
  conversationId: params.conversationId,
462
479
  prevStepHash: params.prevStepHash
463
480
  });
464
- return sealCer(snapshot, { createdAt: params.createdAt, meta: params.meta });
481
+ return sealCer(snapshot, {
482
+ createdAt: params.createdAt,
483
+ meta: params.meta,
484
+ signals: params.signals
485
+ });
465
486
  }
466
487
 
467
488
  // src/run.ts
@@ -2622,6 +2643,100 @@ function verifyAiCerBundleDetailed(bundle) {
2622
2643
  verifier
2623
2644
  };
2624
2645
  }
2646
+
2647
+ // src/langchain.ts
2648
+ var crypto6 = __toESM(require("crypto"), 1);
2649
+ function normalizeCerValue(value) {
2650
+ if (typeof value === "string") return value;
2651
+ if (value !== null && typeof value === "object" && !Array.isArray(value)) {
2652
+ return value;
2653
+ }
2654
+ if (Array.isArray(value)) return { items: value };
2655
+ return String(value);
2656
+ }
2657
+ function extractPrompt(input, metadata) {
2658
+ if (typeof metadata?.prompt === "string" && metadata.prompt.length > 0) {
2659
+ return metadata.prompt;
2660
+ }
2661
+ if (input !== null && typeof input === "object" && !Array.isArray(input)) {
2662
+ const obj = input;
2663
+ if (Array.isArray(obj.messages) && obj.messages.length > 0) {
2664
+ const first = obj.messages[0];
2665
+ if (typeof first?.content === "string" && first.content.length > 0) {
2666
+ return first.content;
2667
+ }
2668
+ }
2669
+ if (typeof obj.prompt === "string" && obj.prompt.length > 0) return obj.prompt;
2670
+ if (typeof obj.text === "string" && obj.text.length > 0) return obj.text;
2671
+ }
2672
+ if (typeof input === "string" && input.length > 0) return input;
2673
+ return "[LangChain run]";
2674
+ }
2675
+ function buildMeta(metadata) {
2676
+ if (!metadata || Object.keys(metadata).length === 0) return void 0;
2677
+ const reserved = /* @__PURE__ */ new Set(["prompt", "appId", "runId", "workflowId", "conversationId"]);
2678
+ const extraTags = [];
2679
+ for (const [k, v] of Object.entries(metadata)) {
2680
+ if (!reserved.has(k)) {
2681
+ extraTags.push(typeof v === "string" ? `${k}:${v}` : `${k}:${JSON.stringify(v)}`);
2682
+ }
2683
+ }
2684
+ return extraTags.length > 0 ? { tags: extraTags } : void 0;
2685
+ }
2686
+ function resolveParameters(params) {
2687
+ return {
2688
+ temperature: params?.temperature ?? 0,
2689
+ maxTokens: params?.maxTokens ?? 0,
2690
+ topP: params?.topP ?? null,
2691
+ seed: params?.seed ?? null
2692
+ };
2693
+ }
2694
+ function buildCertifyParams(input, executionId) {
2695
+ return {
2696
+ executionId,
2697
+ timestamp: input.timestamp,
2698
+ createdAt: input.createdAt,
2699
+ provider: input.provider,
2700
+ model: input.model,
2701
+ modelVersion: input.modelVersion ?? null,
2702
+ prompt: extractPrompt(input.input, input.metadata),
2703
+ input: normalizeCerValue(input.input),
2704
+ output: normalizeCerValue(input.output),
2705
+ parameters: resolveParameters(input.parameters),
2706
+ appId: typeof input.metadata?.appId === "string" ? input.metadata.appId : null,
2707
+ runId: typeof input.metadata?.runId === "string" ? input.metadata.runId : void 0,
2708
+ workflowId: typeof input.metadata?.workflowId === "string" ? input.metadata.workflowId : void 0,
2709
+ conversationId: typeof input.metadata?.conversationId === "string" ? input.metadata.conversationId : void 0,
2710
+ meta: buildMeta(input.metadata),
2711
+ signals: input.signals
2712
+ };
2713
+ }
2714
+ function createLangChainCer(input) {
2715
+ const executionId = input.executionId ?? crypto6.randomUUID();
2716
+ const bundle = certifyDecision(buildCertifyParams(input, executionId));
2717
+ return {
2718
+ bundle,
2719
+ certificateHash: bundle.certificateHash,
2720
+ executionId: bundle.snapshot.executionId
2721
+ };
2722
+ }
2723
+ function certifyLangChainRun(input, _options) {
2724
+ if (input.nodeUrl && input.apiKey) {
2725
+ const executionId = input.executionId ?? crypto6.randomUUID();
2726
+ const certifyParams = buildCertifyParams({ ...input, executionId }, executionId);
2727
+ const attestFn = _options?._attestFn ?? certifyAndAttestDecision;
2728
+ return attestFn(certifyParams, { nodeUrl: input.nodeUrl, apiKey: input.apiKey }).then(
2729
+ ({ bundle, receipt }) => ({
2730
+ bundle,
2731
+ receipt,
2732
+ certificateHash: bundle.certificateHash,
2733
+ executionId: bundle.snapshot.executionId,
2734
+ attested: true
2735
+ })
2736
+ );
2737
+ }
2738
+ return createLangChainCer(input);
2739
+ }
2625
2740
  // Annotate the CommonJS export names for ESM import in node:
2626
2741
  0 && (module.exports = {
2627
2742
  CerAttestationError,
@@ -2635,9 +2750,11 @@ function verifyAiCerBundleDetailed(bundle) {
2635
2750
  certifyAndAttestRun,
2636
2751
  certifyDecision,
2637
2752
  certifyDecisionFromProviderCall,
2753
+ certifyLangChainRun,
2638
2754
  computeInputHash,
2639
2755
  computeOutputHash,
2640
2756
  createClient,
2757
+ createLangChainCer,
2641
2758
  createSnapshot,
2642
2759
  exportCer,
2643
2760
  exportVerifiableRedacted,