From 85c9cf4b07fe1d7f7deedee61e9d7578118b9c58 Mon Sep 17 00:00:00 2001 From: ernest-nowacki Date: Wed, 25 Feb 2026 16:07:02 +0100 Subject: [PATCH 01/18] remove node types and bun types from workflows --- bun.lock | 9 +++------ packages/cre-sdk-examples/package.json | 4 +--- .../workflows/http-fetch-no-sugar/index.ts | 5 +++-- .../src/workflows/proof-of-reserve/index.ts | 3 ++- packages/cre-sdk-examples/tsconfig.json | 3 +++ packages/cre-sdk/scripts/src/build-types.ts | 20 ++++++++++++++++++- 6 files changed, 31 insertions(+), 13 deletions(-) diff --git a/bun.lock b/bun.lock index 73d21f08..cc6f00b6 100644 --- a/bun.lock +++ b/bun.lock @@ -26,7 +26,7 @@ }, "packages/cre-sdk": { "name": "@chainlink/cre-sdk", - "version": "1.0.9", + "version": "1.1.2", "bin": { "cre-compile": "bin/cre-compile.ts", }, @@ -51,20 +51,17 @@ }, "packages/cre-sdk-examples": { "name": "@chainlink/cre-sdk-examples", - "version": "1.0.9", + "version": "1.1.2", "dependencies": { "@bufbuild/protobuf": "2.6.3", "@chainlink/cre-sdk": "workspace:*", "viem": "2.34.0", "zod": "3.25.76", }, - "devDependencies": { - "@types/bun": "1.3.8", - }, }, "packages/cre-sdk-javy-plugin": { "name": "@chainlink/cre-sdk-javy-plugin", - "version": "1.0.9", + "version": "1.1.1", "bin": { "cre-setup": "bin/setup.ts", "cre-compile-workflow": "bin/compile-workflow.ts", diff --git a/packages/cre-sdk-examples/package.json b/packages/cre-sdk-examples/package.json index ff1515bf..ed73f4d9 100644 --- a/packages/cre-sdk-examples/package.json +++ b/packages/cre-sdk-examples/package.json @@ -20,9 +20,7 @@ "viem": "2.34.0", "zod": "3.25.76" }, - "devDependencies": { - "@types/bun": "1.3.8" - }, + "devDependencies": {}, "engines": { "bun": ">=1.2.21" } diff --git a/packages/cre-sdk-examples/src/workflows/http-fetch-no-sugar/index.ts b/packages/cre-sdk-examples/src/workflows/http-fetch-no-sugar/index.ts index 5579ce2e..293987ad 100644 --- a/packages/cre-sdk-examples/src/workflows/http-fetch-no-sugar/index.ts +++ b/packages/cre-sdk-examples/src/workflows/http-fetch-no-sugar/index.ts @@ -6,6 +6,7 @@ import { type NodeRuntime, Runner, type Runtime, + text, } from '@chainlink/cre-sdk' import { z } from 'zod' @@ -38,8 +39,8 @@ const fetchMathResult = (nodeRuntime: NodeRuntime): bigint => { const resp = httpClient.sendRequest(nodeRuntime, req).result() // The mathjs.org API returns the result as a raw string in the body. // We need to parse it into a bigint. - const bodyText = new TextDecoder().decode(resp.body) - const val = BigInt(bodyText.trim()) + const bodyText = text(resp) + const val = BigInt(bodyText) return val } diff --git a/packages/cre-sdk-examples/src/workflows/proof-of-reserve/index.ts b/packages/cre-sdk-examples/src/workflows/proof-of-reserve/index.ts index 1ac90d2c..6135bc2f 100644 --- a/packages/cre-sdk-examples/src/workflows/proof-of-reserve/index.ts +++ b/packages/cre-sdk-examples/src/workflows/proof-of-reserve/index.ts @@ -19,6 +19,7 @@ import { Runner, type Runtime, TxStatus, + text, } from '@chainlink/cre-sdk' import { type Address, decodeFunctionResult, encodeFunctionData, zeroAddress } from 'viem' import { z } from 'zod' @@ -66,7 +67,7 @@ const fetchReserveInfo = (sendRequester: HTTPSendRequester, config: Config): Res throw new Error(`HTTP request failed with status: ${response.statusCode}`) } - const responseText = Buffer.from(response.body).toString('utf-8') + const responseText = text(response) const porResp: PORResponse = JSON.parse(responseText) if (porResp.ripcord) { diff --git a/packages/cre-sdk-examples/tsconfig.json b/packages/cre-sdk-examples/tsconfig.json index d8cb82fa..68da0445 100644 --- a/packages/cre-sdk-examples/tsconfig.json +++ b/packages/cre-sdk-examples/tsconfig.json @@ -21,6 +21,9 @@ "skipLibCheck": true, "noFallthroughCasesInSwitch": true, + // Do not auto-include @types/* — mirrors the customer WASM environment + "types": [], + // Some stricter flags (disabled by default) "noUnusedLocals": false, "noUnusedParameters": false, diff --git a/packages/cre-sdk/scripts/src/build-types.ts b/packages/cre-sdk/scripts/src/build-types.ts index 4172cefd..598d1165 100644 --- a/packages/cre-sdk/scripts/src/build-types.ts +++ b/packages/cre-sdk/scripts/src/build-types.ts @@ -1,5 +1,5 @@ import { glob } from 'fast-glob' -import { copyFile, mkdir } from 'fs/promises' +import { copyFile, mkdir, readFile, writeFile } from 'fs/promises' import { join } from 'path' const buildTypes = async () => { @@ -28,6 +28,24 @@ const buildTypes = async () => { } console.log(`✅ Copied ${typeFiles.length} type definition file(s) to dist/sdk/types`) + + // Prepend triple-slash references to dist/index.d.ts so consumers pick up + // global type augmentations (e.g. restricted-apis.d.ts) automatically. + // tsc strips these from the emitted .d.ts, so we add them back here. + const indexDts = join(packageRoot, 'dist/index.d.ts') + const sourceIndex = join(packageRoot, 'src/index.ts') + const sourceContent = await readFile(sourceIndex, 'utf-8') + + const tripleSlashRefs = sourceContent + .split('\n') + .filter((line) => line.startsWith('/// Date: Wed, 25 Feb 2026 16:12:13 +0100 Subject: [PATCH 02/18] Update comment --- packages/cre-sdk-examples/tsconfig.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cre-sdk-examples/tsconfig.json b/packages/cre-sdk-examples/tsconfig.json index 68da0445..4905e437 100644 --- a/packages/cre-sdk-examples/tsconfig.json +++ b/packages/cre-sdk-examples/tsconfig.json @@ -21,7 +21,7 @@ "skipLibCheck": true, "noFallthroughCasesInSwitch": true, - // Do not auto-include @types/* — mirrors the customer WASM environment + // Do not auto-include @types/* — mirrors the customer cre cli initialized environment "types": [], // Some stricter flags (disabled by default) From e62a01fe9ecbb1654b5de0e32474ff8ce5029785 Mon Sep 17 00:00:00 2001 From: ernest-nowacki Date: Wed, 25 Feb 2026 16:28:08 +0100 Subject: [PATCH 03/18] Expose console types through global and add examples that serve as test that this setup actually works --- .../src/restricted-apis-example.ts | 59 +++++++++++++++++++ packages/cre-sdk/src/sdk/types/global.d.ts | 12 ++++ 2 files changed, 71 insertions(+) create mode 100644 packages/cre-sdk-examples/src/restricted-apis-example.ts diff --git a/packages/cre-sdk-examples/src/restricted-apis-example.ts b/packages/cre-sdk-examples/src/restricted-apis-example.ts new file mode 100644 index 00000000..8576cec2 --- /dev/null +++ b/packages/cre-sdk-examples/src/restricted-apis-example.ts @@ -0,0 +1,59 @@ +/** + * This example shows how CRE workflows mark restricted APIs as deprecated in TS. + * + * The restricted APIs covered in this example are: + * - fetch + * - window + * - document + * - XMLHttpRequest + * - localStorage + * - sessionStorage + * - setTimeout + * - setInterval + * + * There are also NodeJS APIs that do work with the QuickJS runtime, like console.log, which this file covers. + */ + +export const testFetch = async () => { + // @ts-expect-error - fetch is not available in the CRE SDK + fetch('https://api.chain.link/v1/price?symbol=ETH/USD') +} + +export const testWindow = async () => { + // @ts-expect-error - window is not available in the CRE SDK + window.alert('Hello, world!') +} + +export const testDocument = async () => { + // @ts-expect-error - document is not available in the CRE SDK + document.body.innerHTML = 'Hello, world!' +} + +export const testXMLHttpRequest = async () => { + // @ts-expect-error - XMLHttpRequest is not available in the CRE SDK + new XMLHttpRequest() +} + +export const testLocalStorage = async () => { + // @ts-expect-error - localStorage is not available in the CRE SDK + localStorage.setItem('test', 'Hello, world!') +} + +export const testSessionStorage = async () => { + // @ts-expect-error - sessionStorage is not available in the CRE SDK + sessionStorage.setItem('test', 'Hello, world!') +} + +export const testSetTimeout = async () => { + // @ts-expect-error - setTimeout is not available in the CRE SDK + setTimeout(() => { + console.log('Hello, world!') + }, 1000) +} + +export const testSetInterval = async () => { + // @ts-expect-error - setInterval is not available in the CRE SDK + setInterval(() => { + console.log('Hello, world!') + }, 1000) +} diff --git a/packages/cre-sdk/src/sdk/types/global.d.ts b/packages/cre-sdk/src/sdk/types/global.d.ts index e8036a46..b6c820e8 100644 --- a/packages/cre-sdk/src/sdk/types/global.d.ts +++ b/packages/cre-sdk/src/sdk/types/global.d.ts @@ -78,6 +78,18 @@ declare global { * @returns Unix timestamp in milliseconds */ function now(): number + + /** + * Console API available in the QuickJS runtime + */ + interface Console { + log(...args: unknown[]): void + warn(...args: unknown[]): void + error(...args: unknown[]): void + info(...args: unknown[]): void + debug(...args: unknown[]): void + } + var console: Console } export {} From 227700f2b88c65a82d971fb2d60fbf1bab3d6792 Mon Sep 17 00:00:00 2001 From: ernest-nowacki Date: Thu, 26 Feb 2026 11:56:39 +0100 Subject: [PATCH 04/18] Add node modules restrictions --- packages/cre-sdk/src/index.ts | 1 + packages/cre-sdk/src/sdk/types/global.d.ts | 29 ++++++++++++++ .../sdk/types/restricted-node-modules.d.ts | 40 +++++++++++++++++++ 3 files changed, 70 insertions(+) create mode 100644 packages/cre-sdk/src/sdk/types/restricted-node-modules.d.ts diff --git a/packages/cre-sdk/src/index.ts b/packages/cre-sdk/src/index.ts index d0fec607..8095eaa5 100644 --- a/packages/cre-sdk/src/index.ts +++ b/packages/cre-sdk/src/index.ts @@ -1,4 +1,5 @@ /// /// +/// export * from './sdk' diff --git a/packages/cre-sdk/src/sdk/types/global.d.ts b/packages/cre-sdk/src/sdk/types/global.d.ts index b6c820e8..4482d996 100644 --- a/packages/cre-sdk/src/sdk/types/global.d.ts +++ b/packages/cre-sdk/src/sdk/types/global.d.ts @@ -90,6 +90,35 @@ declare global { debug(...args: unknown[]): void } var console: Console + + /** + * TextEncoder/TextDecoder APIs available via Javy's text_encoding support + */ + interface TextEncoderEncodeIntoResult { + read: number + written: number + } + + interface TextEncoder { + readonly encoding: string + encode(input?: string): Uint8Array + encodeInto(input: string, dest: Uint8Array): TextEncoderEncodeIntoResult + } + var TextEncoder: { + prototype: TextEncoder + new (): TextEncoder + } + + interface TextDecoder { + readonly encoding: string + readonly fatal: boolean + readonly ignoreBOM: boolean + decode(input?: ArrayBuffer | ArrayBufferView, options?: { stream?: boolean }): string + } + var TextDecoder: { + prototype: TextDecoder + new (label?: string, options?: { fatal?: boolean; ignoreBOM?: boolean }): TextDecoder + } } export {} diff --git a/packages/cre-sdk/src/sdk/types/restricted-node-modules.d.ts b/packages/cre-sdk/src/sdk/types/restricted-node-modules.d.ts new file mode 100644 index 00000000..e5c9389b --- /dev/null +++ b/packages/cre-sdk/src/sdk/types/restricted-node-modules.d.ts @@ -0,0 +1,40 @@ +// Restricted Node.js modules that are not available in CRE WASM workflows. +// These modules require native bindings or system access that cannot run in WebAssembly. +// Importing from these modules will produce a TypeScript error at development time, +// preventing cryptic WASM runtime failures. + +/** @deprecated node:crypto is not available in CRE WASM workflows. It requires native bindings that cannot run in WebAssembly. */ +declare module 'node:crypto' {} + +/** @deprecated node:fs is not available in CRE WASM workflows. It requires filesystem access that is not available in WebAssembly. */ +declare module 'node:fs' {} + +/** @deprecated node:fs/promises is not available in CRE WASM workflows. It requires filesystem access that is not available in WebAssembly. */ +declare module 'node:fs/promises' {} + +/** @deprecated node:net is not available in CRE WASM workflows. It requires network access. Use cre.capabilities.HTTPClient instead. */ +declare module 'node:net' {} + +/** @deprecated node:http is not available in CRE WASM workflows. It requires network access. Use cre.capabilities.HTTPClient instead. */ +declare module 'node:http' {} + +/** @deprecated node:https is not available in CRE WASM workflows. It requires network access. Use cre.capabilities.HTTPClient instead. */ +declare module 'node:https' {} + +/** @deprecated node:child_process is not available in CRE WASM workflows. It requires OS process spawning that is not available in WebAssembly. */ +declare module 'node:child_process' {} + +/** @deprecated node:os is not available in CRE WASM workflows. It requires OS access that is not available in WebAssembly. */ +declare module 'node:os' {} + +/** @deprecated node:stream is not available in CRE WASM workflows. It requires native bindings that cannot run in WebAssembly. */ +declare module 'node:stream' {} + +/** @deprecated node:worker_threads is not available in CRE WASM workflows. It requires threading support that is not available in WebAssembly. */ +declare module 'node:worker_threads' {} + +/** @deprecated node:dns is not available in CRE WASM workflows. It requires network access that is not available in WebAssembly. */ +declare module 'node:dns' {} + +/** @deprecated node:zlib is not available in CRE WASM workflows. It requires native compression bindings that cannot run in WebAssembly. */ +declare module 'node:zlib' {} From a77661008d7698c5fb34a1abf27321d9d2bd4c8a Mon Sep 17 00:00:00 2001 From: ernest-nowacki Date: Thu, 26 Feb 2026 13:41:05 +0100 Subject: [PATCH 05/18] Add more restricted node APIs --- packages/cre-sdk/scripts/src/build-types.ts | 21 +- packages/cre-sdk/src/index.ts | 1 - .../sdk/types/restricted-node-modules.d.ts | 356 +++++++++++++++++- packages/cre-sdk/tsconfig.json | 7 +- 4 files changed, 366 insertions(+), 19 deletions(-) diff --git a/packages/cre-sdk/scripts/src/build-types.ts b/packages/cre-sdk/scripts/src/build-types.ts index 598d1165..d2f272ae 100644 --- a/packages/cre-sdk/scripts/src/build-types.ts +++ b/packages/cre-sdk/scripts/src/build-types.ts @@ -32,18 +32,33 @@ const buildTypes = async () => { // Prepend triple-slash references to dist/index.d.ts so consumers pick up // global type augmentations (e.g. restricted-apis.d.ts) automatically. // tsc strips these from the emitted .d.ts, so we add them back here. + // + // Note: restricted-node-modules is intentionally NOT referenced from src/index.ts + // because the SDK's own build scripts legitimately use Node.js modules. It is + // added here explicitly so that workflow consumers still receive the restrictions. const indexDts = join(packageRoot, 'dist/index.d.ts') const sourceIndex = join(packageRoot, 'src/index.ts') const sourceContent = await readFile(sourceIndex, 'utf-8') - const tripleSlashRefs = sourceContent + const refsFromSource = sourceContent .split('\n') .filter((line) => line.startsWith('/// '] + + const tripleSlashRefs = [...refsFromSource, ...distributionOnlyRefs].join('\n') if (tripleSlashRefs) { const indexContent = await readFile(indexDts, 'utf-8') - await writeFile(indexDts, `${tripleSlashRefs}\n${indexContent}`) + // Strip any existing triple-slash references from the top of the file + // so that re-running build-types is idempotent. + const withoutExistingRefs = indexContent + .split('\n') + .filter((line) => !line.startsWith('/// /// -/// export * from './sdk' diff --git a/packages/cre-sdk/src/sdk/types/restricted-node-modules.d.ts b/packages/cre-sdk/src/sdk/types/restricted-node-modules.d.ts index e5c9389b..2454d3f6 100644 --- a/packages/cre-sdk/src/sdk/types/restricted-node-modules.d.ts +++ b/packages/cre-sdk/src/sdk/types/restricted-node-modules.d.ts @@ -1,40 +1,368 @@ // Restricted Node.js modules that are not available in CRE WASM workflows. // These modules require native bindings or system access that cannot run in WebAssembly. -// Importing from these modules will produce a TypeScript error at development time, -// preventing cryptic WASM runtime failures. +// Importing from these modules is allowed by TypeScript, but all exports are typed as +// `never` so any usage produces a clear error at the call site. /** @deprecated node:crypto is not available in CRE WASM workflows. It requires native bindings that cannot run in WebAssembly. */ -declare module 'node:crypto' {} +declare module 'node:crypto' { + export const randomBytes: never + export const randomUUID: never + export const randomInt: never + export const randomFillSync: never + export const randomFill: never + export const createHash: never + export const createHmac: never + export const createCipheriv: never + export const createDecipheriv: never + export const createSign: never + export const createVerify: never + export const createDiffieHellman: never + export const createDiffieHellmanGroup: never + export const createECDH: never + export const generateKey: never + export const generateKeySync: never + export const generateKeyPair: never + export const generateKeyPairSync: never + export const createPrivateKey: never + export const createPublicKey: never + export const createSecretKey: never + export const pbkdf2: never + export const pbkdf2Sync: never + export const scrypt: never + export const scryptSync: never + export const timingSafeEqual: never + export const publicEncrypt: never + export const publicDecrypt: never + export const privateDecrypt: never + export const privateEncrypt: never + export const getCiphers: never + export const getHashes: never + export const getCurves: never + export const getFips: never + export const setFips: never + export const getRandomValues: never + export const Hash: never + export const Hmac: never + export const Sign: never + export const Verify: never + export const KeyObject: never + export const Certificate: never + export const ECDH: never + export const DiffieHellman: never + export const DiffieHellmanGroup: never + export const Cipheriv: never + export const Decipheriv: never + export const webcrypto: never + export const subtle: never + export const crypto: never + export const fips: never + export const constants: never +} /** @deprecated node:fs is not available in CRE WASM workflows. It requires filesystem access that is not available in WebAssembly. */ -declare module 'node:fs' {} +declare module 'node:fs' { + export const readFile: never + export const readFileSync: never + export const writeFile: never + export const writeFileSync: never + export const appendFile: never + export const appendFileSync: never + export const readdir: never + export const readdirSync: never + export const mkdir: never + export const mkdirSync: never + export const mkdtemp: never + export const mkdtempSync: never + export const rm: never + export const rmSync: never + export const rmdir: never + export const rmdirSync: never + export const unlink: never + export const unlinkSync: never + export const stat: never + export const statSync: never + export const lstat: never + export const lstatSync: never + export const fstat: never + export const fstatSync: never + export const statfs: never + export const statfsSync: never + export const exists: never + export const existsSync: never + export const copyFile: never + export const copyFileSync: never + export const cp: never + export const cpSync: never + export const rename: never + export const renameSync: never + export const readlink: never + export const readlinkSync: never + export const symlink: never + export const symlinkSync: never + export const link: never + export const linkSync: never + export const open: never + export const openSync: never + export const close: never + export const closeSync: never + export const read: never + export const readSync: never + export const write: never + export const writeSync: never + export const truncate: never + export const truncateSync: never + export const ftruncate: never + export const ftruncateSync: never + export const chmod: never + export const chmodSync: never + export const chown: never + export const chownSync: never + export const utimes: never + export const utimesSync: never + export const access: never + export const accessSync: never + export const createReadStream: never + export const createWriteStream: never + export const watch: never + export const watchFile: never + export const unwatchFile: never + export const realpath: never + export const realpathSync: never + export const promises: never + export const constants: never + export const Dir: never + export const Dirent: never + export const Stats: never + export const ReadStream: never + export const WriteStream: never + export const FileHandle: never + export const FSWatcher: never +} /** @deprecated node:fs/promises is not available in CRE WASM workflows. It requires filesystem access that is not available in WebAssembly. */ -declare module 'node:fs/promises' {} +declare module 'node:fs/promises' { + export const readFile: never + export const writeFile: never + export const appendFile: never + export const readdir: never + export const mkdir: never + export const mkdtemp: never + export const rm: never + export const rmdir: never + export const unlink: never + export const stat: never + export const lstat: never + export const statfs: never + export const copyFile: never + export const cp: never + export const rename: never + export const readlink: never + export const symlink: never + export const link: never + export const open: never + export const truncate: never + export const chmod: never + export const chown: never + export const utimes: never + export const access: never + export const realpath: never + export const watch: never + export const constants: never + export const FileHandle: never +} /** @deprecated node:net is not available in CRE WASM workflows. It requires network access. Use cre.capabilities.HTTPClient instead. */ -declare module 'node:net' {} +declare module 'node:net' { + export const createServer: never + export const createConnection: never + export const connect: never + export const isIP: never + export const isIPv4: never + export const isIPv6: never + export const getDefaultAutoSelectFamily: never + export const setDefaultAutoSelectFamily: never + export const Socket: never + export const Server: never + export const BlockList: never + export const SocketAddress: never +} /** @deprecated node:http is not available in CRE WASM workflows. It requires network access. Use cre.capabilities.HTTPClient instead. */ -declare module 'node:http' {} +declare module 'node:http' { + export const createServer: never + export const request: never + export const get: never + export const validateHeaderName: never + export const validateHeaderValue: never + export const setMaxIdleHTTPParsers: never + export const Server: never + export const ClientRequest: never + export const IncomingMessage: never + export const ServerResponse: never + export const OutgoingMessage: never + export const Agent: never + export const globalAgent: never + export const METHODS: never + export const STATUS_CODES: never + export const maxHeaderSize: never +} /** @deprecated node:https is not available in CRE WASM workflows. It requires network access. Use cre.capabilities.HTTPClient instead. */ -declare module 'node:https' {} +declare module 'node:https' { + export const createServer: never + export const request: never + export const get: never + export const Server: never + export const Agent: never + export const globalAgent: never +} /** @deprecated node:child_process is not available in CRE WASM workflows. It requires OS process spawning that is not available in WebAssembly. */ -declare module 'node:child_process' {} +declare module 'node:child_process' { + export const spawn: never + export const spawnSync: never + export const exec: never + export const execSync: never + export const execFile: never + export const execFileSync: never + export const fork: never + export const ChildProcess: never +} /** @deprecated node:os is not available in CRE WASM workflows. It requires OS access that is not available in WebAssembly. */ -declare module 'node:os' {} +declare module 'node:os' { + export const hostname: never + export const platform: never + export const arch: never + export const type: never + export const release: never + export const version: never + export const machine: never + export const cpus: never + export const availableParallelism: never + export const freemem: never + export const totalmem: never + export const uptime: never + export const loadavg: never + export const homedir: never + export const tmpdir: never + export const userInfo: never + export const networkInterfaces: never + export const endianness: never + export const getPriority: never + export const setPriority: never + export const EOL: never + export const devNull: never + export const constants: never +} /** @deprecated node:stream is not available in CRE WASM workflows. It requires native bindings that cannot run in WebAssembly. */ -declare module 'node:stream' {} +declare module 'node:stream' { + export const Readable: never + export const Writable: never + export const Duplex: never + export const Transform: never + export const PassThrough: never + export const Stream: never + export const pipeline: never + export const finished: never + export const promises: never + export const addAbortSignal: never + export const compose: never + export const isErrored: never + export const isReadable: never +} /** @deprecated node:worker_threads is not available in CRE WASM workflows. It requires threading support that is not available in WebAssembly. */ -declare module 'node:worker_threads' {} +declare module 'node:worker_threads' { + export const Worker: never + export const MessageChannel: never + export const MessagePort: never + export const BroadcastChannel: never + export const isMainThread: never + export const isInternalThread: never + export const parentPort: never + export const workerData: never + export const threadId: never + export const resourceLimits: never + export const SHARE_ENV: never + export const receiveMessageOnPort: never + export const moveMessagePortToContext: never + export const getEnvironmentData: never + export const setEnvironmentData: never + export const markAsUntransferable: never + export const markAsUncloneable: never + export const isMarkedAsUntransferable: never + export const postMessageToThread: never +} /** @deprecated node:dns is not available in CRE WASM workflows. It requires network access that is not available in WebAssembly. */ -declare module 'node:dns' {} +declare module 'node:dns' { + export const lookup: never + export const lookupService: never + export const resolve: never + export const resolve4: never + export const resolve6: never + export const resolveCname: never + export const resolveMx: never + export const resolveNs: never + export const resolvePtr: never + export const resolveSrv: never + export const resolveTxt: never + export const resolveNaptr: never + export const resolveSoa: never + export const resolveAny: never + export const reverse: never + export const setServers: never + export const getServers: never + export const setDefaultResultOrder: never + export const getDefaultResultOrder: never + export const promises: never + export const Resolver: never +} /** @deprecated node:zlib is not available in CRE WASM workflows. It requires native compression bindings that cannot run in WebAssembly. */ -declare module 'node:zlib' {} +declare module 'node:zlib' { + export const createGzip: never + export const createGunzip: never + export const createDeflate: never + export const createInflate: never + export const createDeflateRaw: never + export const createInflateRaw: never + export const createUnzip: never + export const createBrotliCompress: never + export const createBrotliDecompress: never + export const createZstdCompress: never + export const createZstdDecompress: never + export const gzip: never + export const gzipSync: never + export const gunzip: never + export const gunzipSync: never + export const deflate: never + export const deflateSync: never + export const inflate: never + export const inflateSync: never + export const deflateRaw: never + export const deflateRawSync: never + export const inflateRaw: never + export const inflateRawSync: never + export const unzip: never + export const unzipSync: never + export const brotliCompress: never + export const brotliCompressSync: never + export const brotliDecompress: never + export const brotliDecompressSync: never + export const crc32: never + export const constants: never + export const Gzip: never + export const Gunzip: never + export const Deflate: never + export const Inflate: never + export const DeflateRaw: never + export const InflateRaw: never + export const Unzip: never + export const BrotliCompress: never + export const BrotliDecompress: never + export const ZstdCompress: never + export const ZstdDecompress: never +} diff --git a/packages/cre-sdk/tsconfig.json b/packages/cre-sdk/tsconfig.json index 1a5841ff..49fc05c0 100644 --- a/packages/cre-sdk/tsconfig.json +++ b/packages/cre-sdk/tsconfig.json @@ -34,5 +34,10 @@ "experimentalDecorators": true, "emitDecoratorMetadata": true }, - "include": ["src/**/*", "scripts/**/*.ts", "bin/**/*.ts"] + "include": ["src/**/*", "scripts/**/*.ts", "bin/**/*.ts"], + // restricted-node-modules.d.ts is excluded from the SDK's own compilation + // because the SDK's build scripts legitimately use Node.js modules. It is + // injected into dist/index.d.ts by build-types.ts so workflow consumers + // still receive the restrictions. + "exclude": ["src/sdk/types/restricted-node-modules.d.ts"] } From 1e8ca6af8a0fe62d3ea7aedf24542727642f63a5 Mon Sep 17 00:00:00 2001 From: ernest-nowacki Date: Thu, 26 Feb 2026 18:07:53 +0100 Subject: [PATCH 06/18] Expose more apis through runtime --- packages/cre-sdk/src/sdk/types/global.d.ts | 53 +++++++++++++++++++ .../cre-sdk/src/sdk/utils/prepare-runtime.ts | 11 ++-- 2 files changed, 61 insertions(+), 3 deletions(-) diff --git a/packages/cre-sdk/src/sdk/types/global.d.ts b/packages/cre-sdk/src/sdk/types/global.d.ts index 4482d996..3fb9e68d 100644 --- a/packages/cre-sdk/src/sdk/types/global.d.ts +++ b/packages/cre-sdk/src/sdk/types/global.d.ts @@ -119,6 +119,59 @@ declare global { prototype: TextDecoder new (label?: string, options?: { fatal?: boolean; ignoreBOM?: boolean }): TextDecoder } + + /** + * Base64 encoding/decoding — exposed via prepareRuntime() from node:buffer + */ + function atob(encodedData: string): string + function btoa(stringToEncode: string): string + + /** + * URL and URLSearchParams — exposed via prepareRuntime() from node:url + */ + interface URLSearchParams { + append(name: string, value: string): void + delete(name: string): void + get(name: string): string | null + getAll(name: string): string[] + has(name: string): boolean + set(name: string, value: string): void + sort(): void + toString(): string + forEach(callback: (value: string, key: string, parent: URLSearchParams) => void): void + entries(): IterableIterator<[string, string]> + keys(): IterableIterator + values(): IterableIterator + [Symbol.iterator](): IterableIterator<[string, string]> + readonly size: number + } + var URLSearchParams: { + prototype: URLSearchParams + new ( + init?: string | Record | [string, string][] | URLSearchParams, + ): URLSearchParams + } + + interface URL { + hash: string + host: string + hostname: string + href: string + readonly origin: string + password: string + pathname: string + port: string + protocol: string + search: string + readonly searchParams: URLSearchParams + username: string + toString(): string + toJSON(): string + } + var URL: { + prototype: URL + new (url: string, base?: string | URL): URL + } } export {} diff --git a/packages/cre-sdk/src/sdk/utils/prepare-runtime.ts b/packages/cre-sdk/src/sdk/utils/prepare-runtime.ts index dbb8faba..1c94f453 100644 --- a/packages/cre-sdk/src/sdk/utils/prepare-runtime.ts +++ b/packages/cre-sdk/src/sdk/utils/prepare-runtime.ts @@ -1,10 +1,15 @@ -import { Buffer } from 'node:buffer' +import { atob, Buffer, btoa } from 'node:buffer' +import { URL, URLSearchParams } from 'node:url' /** * This function is used to prepare the runtime for the SDK to work. * It should be called as a part of SDK initialization. - * It exposes NodeJS Buffer in global namespace, so it can be bundled and used in workflow code. + * It exposes Node.js APIs in global namespace, so they can be bundled and used in workflow code. */ export const prepareRuntime = () => { - globalThis.Buffer = Buffer as any + globalThis.Buffer = Buffer + globalThis.atob = atob + globalThis.btoa = btoa + globalThis.URL = URL as any + globalThis.URLSearchParams = URLSearchParams } From 87fa5a85c4acbc71cf7b79fa32d7a6ccfe043726 Mon Sep 17 00:00:00 2001 From: ernest-nowacki Date: Mon, 2 Mar 2026 16:35:11 +0100 Subject: [PATCH 07/18] TS fixes --- .../src/restricted-apis-example.ts | 33 +- packages/cre-sdk/README.md | 13 +- packages/cre-sdk/scripts/run.ts | 6 +- packages/cre-sdk/scripts/src/build-types.ts | 9 +- packages/cre-sdk/scripts/src/compile-to-js.ts | 2 + .../src/validate-workflow-runtime-compat.ts | 378 ++++++++++++++++++ packages/cre-sdk/src/sdk/types/global.d.ts | 71 ++-- .../src/sdk/types/restricted-apis.d.ts | 26 +- 8 files changed, 447 insertions(+), 91 deletions(-) create mode 100644 packages/cre-sdk/scripts/src/validate-workflow-runtime-compat.ts diff --git a/packages/cre-sdk-examples/src/restricted-apis-example.ts b/packages/cre-sdk-examples/src/restricted-apis-example.ts index 8576cec2..dc823bfd 100644 --- a/packages/cre-sdk-examples/src/restricted-apis-example.ts +++ b/packages/cre-sdk-examples/src/restricted-apis-example.ts @@ -3,15 +3,11 @@ * * The restricted APIs covered in this example are: * - fetch - * - window - * - document - * - XMLHttpRequest - * - localStorage - * - sessionStorage * - setTimeout * - setInterval * - * There are also NodeJS APIs that do work with the QuickJS runtime, like console.log, which this file covers. + * Other unsupported globals/modules are enforced by cre-compile runtime checks. + * There are also NodeJS APIs that do work with the QuickJS runtime, like console.log. */ export const testFetch = async () => { @@ -19,31 +15,6 @@ export const testFetch = async () => { fetch('https://api.chain.link/v1/price?symbol=ETH/USD') } -export const testWindow = async () => { - // @ts-expect-error - window is not available in the CRE SDK - window.alert('Hello, world!') -} - -export const testDocument = async () => { - // @ts-expect-error - document is not available in the CRE SDK - document.body.innerHTML = 'Hello, world!' -} - -export const testXMLHttpRequest = async () => { - // @ts-expect-error - XMLHttpRequest is not available in the CRE SDK - new XMLHttpRequest() -} - -export const testLocalStorage = async () => { - // @ts-expect-error - localStorage is not available in the CRE SDK - localStorage.setItem('test', 'Hello, world!') -} - -export const testSessionStorage = async () => { - // @ts-expect-error - sessionStorage is not available in the CRE SDK - sessionStorage.setItem('test', 'Hello, world!') -} - export const testSetTimeout = async () => { // @ts-expect-error - setTimeout is not available in the CRE SDK setTimeout(() => { diff --git a/packages/cre-sdk/README.md b/packages/cre-sdk/README.md index 95ad9648..c4fb8771 100644 --- a/packages/cre-sdk/README.md +++ b/packages/cre-sdk/README.md @@ -8,6 +8,7 @@ The Chainlink Runtime Environment (CRE) SDK for TypeScript enables developers to - [Examples](#examples) - [Simulate locally with CRE CLI](#simulate-locally-with-cre-cli) - [Installation](#installation) +- [Runtime Compatibility Constraints](#runtime-compatibility-constraints) - [Core Concepts](#core-concepts) - [Workflows](#workflows) - [Runtime Modes](#runtime-modes) @@ -47,10 +48,20 @@ This package must be used along with the [CRE CLI tool](https://github.com/smart ## Prerequisites -1. the [bun runtime](https://bun.com/). The wasm compilation currently is only supported by the bun runtime which has near-complete NodeJS compatibility. +1. the [bun runtime](https://bun.com/) for local tooling and workflow compilation. 2. the [CRE CLI tool](https://github.com/smartcontractkit/cre-cli) installed. +## Runtime Compatibility Constraints + +CRE workflows are compiled to WASM and executed through Javy (QuickJS). This is **not** a full Node.js runtime. + +- Node built-ins like `node:fs`, `node:crypto`, `node:http`, `node:net`, etc. are not supported in workflows. +- Browser globals like `fetch`, `window`, and `setTimeout` are also not available in workflow runtime. +- `cre compile:workflow` / `cre-compile` now validates workflow source and fails fast when unsupported APIs are used. + +Use CRE capabilities (for example, `cre.capabilities.HTTPClient`) instead of direct Node/browser APIs. + ## Getting Started We recommend you consult the [getting started docs](https://docs.chain.link/cre/getting-started/cli-installation) and install the CRE CLI. diff --git a/packages/cre-sdk/scripts/run.ts b/packages/cre-sdk/scripts/run.ts index a4a1afd3..11c801a6 100644 --- a/packages/cre-sdk/scripts/run.ts +++ b/packages/cre-sdk/scripts/run.ts @@ -37,7 +37,11 @@ const main = async () => { process.exit(1) } } catch (error) { - console.error(`Failed to load script ${scriptName}:`, error) + if (error instanceof Error && error.name === 'WorkflowRuntimeCompatibilityError') { + console.error(error.message) + } else { + console.error(`Failed to run script ${scriptName}:`, error) + } process.exit(1) } } diff --git a/packages/cre-sdk/scripts/src/build-types.ts b/packages/cre-sdk/scripts/src/build-types.ts index d2f272ae..2c483f2c 100644 --- a/packages/cre-sdk/scripts/src/build-types.ts +++ b/packages/cre-sdk/scripts/src/build-types.ts @@ -32,10 +32,6 @@ const buildTypes = async () => { // Prepend triple-slash references to dist/index.d.ts so consumers pick up // global type augmentations (e.g. restricted-apis.d.ts) automatically. // tsc strips these from the emitted .d.ts, so we add them back here. - // - // Note: restricted-node-modules is intentionally NOT referenced from src/index.ts - // because the SDK's own build scripts legitimately use Node.js modules. It is - // added here explicitly so that workflow consumers still receive the restrictions. const indexDts = join(packageRoot, 'dist/index.d.ts') const sourceIndex = join(packageRoot, 'src/index.ts') const sourceContent = await readFile(sourceIndex, 'utf-8') @@ -44,10 +40,7 @@ const buildTypes = async () => { .split('\n') .filter((line) => line.startsWith('/// '] - - const tripleSlashRefs = [...refsFromSource, ...distributionOnlyRefs].join('\n') + const tripleSlashRefs = refsFromSource.join('\n') if (tripleSlashRefs) { const indexContent = await readFile(indexDts, 'utf-8') diff --git a/packages/cre-sdk/scripts/src/compile-to-js.ts b/packages/cre-sdk/scripts/src/compile-to-js.ts index 250b36ef..8ef9f97d 100644 --- a/packages/cre-sdk/scripts/src/compile-to-js.ts +++ b/packages/cre-sdk/scripts/src/compile-to-js.ts @@ -2,6 +2,7 @@ import { existsSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs' import { mkdir } from 'node:fs/promises' import path from 'node:path' import { $ } from 'bun' +import { assertWorkflowRuntimeCompatibility } from './validate-workflow-runtime-compat' import { wrapWorkflowCode } from './workflow-wrapper' export const main = async (tsFilePath?: string, outputFilePath?: string) => { @@ -19,6 +20,7 @@ export const main = async (tsFilePath?: string, outputFilePath?: string) => { } const resolvedInput = path.resolve(inputPath) + assertWorkflowRuntimeCompatibility(resolvedInput) console.info(`📁 Using input file: ${resolvedInput}`) // If no explicit output path → same dir, swap extension to .js diff --git a/packages/cre-sdk/scripts/src/validate-workflow-runtime-compat.ts b/packages/cre-sdk/scripts/src/validate-workflow-runtime-compat.ts new file mode 100644 index 00000000..cbf42905 --- /dev/null +++ b/packages/cre-sdk/scripts/src/validate-workflow-runtime-compat.ts @@ -0,0 +1,378 @@ +import { existsSync, readFileSync } from 'node:fs' +import path from 'node:path' +import * as ts from 'typescript' + +type Violation = { + filePath: string + line: number + column: number + message: string +} + +const restrictedModuleSpecifiers = new Set([ + 'crypto', + 'node:crypto', + 'fs', + 'node:fs', + 'fs/promises', + 'node:fs/promises', + 'net', + 'node:net', + 'http', + 'node:http', + 'https', + 'node:https', + 'child_process', + 'node:child_process', + 'os', + 'node:os', + 'stream', + 'node:stream', + 'worker_threads', + 'node:worker_threads', + 'dns', + 'node:dns', + 'zlib', + 'node:zlib', +]) + +const restrictedGlobalApis = new Set([ + 'fetch', + 'window', + 'document', + 'XMLHttpRequest', + 'localStorage', + 'sessionStorage', + 'setTimeout', + 'setInterval', +]) + +const sourceExtensions = ['.ts', '.tsx', '.mts', '.cts', '.js', '.jsx', '.mjs', '.cjs'] + +class WorkflowRuntimeCompatibilityError extends Error { + constructor(violations: Violation[]) { + const sortedViolations = [...violations].sort((a, b) => { + if (a.filePath !== b.filePath) return a.filePath.localeCompare(b.filePath) + if (a.line !== b.line) return a.line - b.line + return a.column - b.column + }) + + const formattedViolations = sortedViolations + .map((violation) => { + const relativePath = path.relative(process.cwd(), violation.filePath) + return `- ${relativePath}:${violation.line}:${violation.column} ${violation.message}` + }) + .join('\n') + + super( + 'Unsupported API usage found in workflow source.\n' + + 'CRE workflows run on Javy (QuickJS), not full Node.js.\n' + + 'Use CRE capabilities instead (for example, HTTPClient instead of fetch/node:http).\n\n' + + formattedViolations, + ) + this.name = 'WorkflowRuntimeCompatibilityError' + } +} + +const toAbsolutePath = (filePath: string) => path.resolve(filePath) + +const getScriptKind = (filePath: string): ts.ScriptKind => { + switch (path.extname(filePath).toLowerCase()) { + case '.js': + return ts.ScriptKind.JS + case '.jsx': + return ts.ScriptKind.JSX + case '.mjs': + return ts.ScriptKind.JS + case '.cjs': + return ts.ScriptKind.JS + case '.tsx': + return ts.ScriptKind.TSX + case '.mts': + return ts.ScriptKind.TS + case '.cts': + return ts.ScriptKind.TS + default: + return ts.ScriptKind.TS + } +} + +const createViolation = ( + filePath: string, + pos: number, + sourceFile: ts.SourceFile, + message: string, +): Violation => { + const { line, character } = sourceFile.getLineAndCharacterOfPosition(pos) + return { + filePath: toAbsolutePath(filePath), + line: line + 1, + column: character + 1, + message, + } +} + +const isRelativeImport = (specifier: string) => { + return specifier.startsWith('./') || specifier.startsWith('../') || specifier.startsWith('/') +} + +const resolveRelativeImport = (fromFilePath: string, specifier: string): string | null => { + const basePath = specifier.startsWith('/') + ? path.resolve(specifier) + : path.resolve(path.dirname(fromFilePath), specifier) + + if (existsSync(basePath)) { + return toAbsolutePath(basePath) + } + + for (const extension of sourceExtensions) { + const withExtension = `${basePath}${extension}` + if (existsSync(withExtension)) { + return toAbsolutePath(withExtension) + } + } + + for (const extension of sourceExtensions) { + const asIndex = path.join(basePath, `index${extension}`) + if (existsSync(asIndex)) { + return toAbsolutePath(asIndex) + } + } + + return null +} + +const getStringLiteralFromCall = (node: ts.CallExpression): string | null => { + const [firstArg] = node.arguments + if (!firstArg || !ts.isStringLiteral(firstArg)) return null + return firstArg.text +} + +const collectModuleUsage = ( + sourceFile: ts.SourceFile, + filePath: string, + violations: Violation[], + enqueueFile: (nextFile: string) => void, +) => { + const checkModuleSpecifier = (specifier: string, pos: number) => { + if (restrictedModuleSpecifiers.has(specifier)) { + violations.push( + createViolation( + filePath, + pos, + sourceFile, + `'${specifier}' is not available in CRE workflow runtime.`, + ), + ) + } + + if (!isRelativeImport(specifier)) return + const resolved = resolveRelativeImport(filePath, specifier) + if (resolved) { + enqueueFile(resolved) + } + } + + const visit = (node: ts.Node) => { + if (ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier)) { + checkModuleSpecifier(node.moduleSpecifier.text, node.moduleSpecifier.getStart(sourceFile)) + } + + if ( + ts.isExportDeclaration(node) && + node.moduleSpecifier && + ts.isStringLiteral(node.moduleSpecifier) + ) { + checkModuleSpecifier(node.moduleSpecifier.text, node.moduleSpecifier.getStart(sourceFile)) + } + + if ( + ts.isImportEqualsDeclaration(node) && + ts.isExternalModuleReference(node.moduleReference) && + node.moduleReference.expression && + ts.isStringLiteral(node.moduleReference.expression) + ) { + checkModuleSpecifier( + node.moduleReference.expression.text, + node.moduleReference.expression.getStart(sourceFile), + ) + } + + if (ts.isCallExpression(node)) { + if (ts.isIdentifier(node.expression) && node.expression.text === 'require') { + const requiredModule = getStringLiteralFromCall(node) + if (requiredModule) { + checkModuleSpecifier(requiredModule, node.arguments[0].getStart(sourceFile)) + } + } + + if (node.expression.kind === ts.SyntaxKind.ImportKeyword) { + const importedModule = getStringLiteralFromCall(node) + if (importedModule) { + checkModuleSpecifier(importedModule, node.arguments[0].getStart(sourceFile)) + } + } + } + + ts.forEachChild(node, visit) + } + + visit(sourceFile) +} + +const isDeclarationName = (identifier: ts.Identifier): boolean => { + const parent = identifier.parent + + if ( + (ts.isFunctionDeclaration(parent) && parent.name === identifier) || + (ts.isFunctionExpression(parent) && parent.name === identifier) || + (ts.isClassDeclaration(parent) && parent.name === identifier) || + (ts.isClassExpression(parent) && parent.name === identifier) || + (ts.isInterfaceDeclaration(parent) && parent.name === identifier) || + (ts.isTypeAliasDeclaration(parent) && parent.name === identifier) || + (ts.isEnumDeclaration(parent) && parent.name === identifier) || + (ts.isModuleDeclaration(parent) && parent.name === identifier) || + (ts.isTypeParameterDeclaration(parent) && parent.name === identifier) || + (ts.isVariableDeclaration(parent) && parent.name === identifier) || + (ts.isParameter(parent) && parent.name === identifier) || + (ts.isBindingElement(parent) && parent.name === identifier) || + (ts.isImportClause(parent) && parent.name === identifier) || + (ts.isImportSpecifier(parent) && parent.name === identifier) || + (ts.isNamespaceImport(parent) && parent.name === identifier) || + (ts.isImportEqualsDeclaration(parent) && parent.name === identifier) || + (ts.isNamespaceExport(parent) && parent.name === identifier) || + (ts.isEnumMember(parent) && parent.name === identifier) || + (ts.isPropertyDeclaration(parent) && parent.name === identifier) || + (ts.isPropertySignature(parent) && parent.name === identifier) || + (ts.isMethodDeclaration(parent) && parent.name === identifier) || + (ts.isMethodSignature(parent) && parent.name === identifier) || + (ts.isGetAccessorDeclaration(parent) && parent.name === identifier) || + (ts.isSetAccessorDeclaration(parent) && parent.name === identifier) || + (ts.isPropertyAssignment(parent) && parent.name === identifier) || + (ts.isShorthandPropertyAssignment(parent) && parent.name === identifier) || + (ts.isLabeledStatement(parent) && parent.label === identifier) + ) { + return true + } + + if ( + (ts.isPropertyAccessExpression(parent) && parent.name === identifier) || + (ts.isQualifiedName(parent) && parent.right === identifier) || + (ts.isTypeReferenceNode(parent) && parent.typeName === identifier) + ) { + return true + } + + return false +} + +const collectGlobalApiUsage = ( + program: ts.Program, + localSourceFiles: Set, + violations: Violation[], +) => { + const checker = program.getTypeChecker() + + for (const sourceFile of program.getSourceFiles()) { + const resolvedSourcePath = toAbsolutePath(sourceFile.fileName) + if (!localSourceFiles.has(resolvedSourcePath)) continue + + const visit = (node: ts.Node) => { + if ( + ts.isIdentifier(node) && + restrictedGlobalApis.has(node.text) && + !isDeclarationName(node) + ) { + const symbol = checker.getSymbolAtLocation(node) + const hasLocalDeclaration = + symbol?.declarations?.some((declaration) => + localSourceFiles.has(toAbsolutePath(declaration.getSourceFile().fileName)), + ) ?? false + + if (!hasLocalDeclaration) { + violations.push( + createViolation( + resolvedSourcePath, + node.getStart(sourceFile), + sourceFile, + `'${node.text}' is not available in CRE workflow runtime.`, + ), + ) + } + } + + if ( + ts.isPropertyAccessExpression(node) && + ts.isIdentifier(node.expression) && + node.expression.text === 'globalThis' && + restrictedGlobalApis.has(node.name.text) + ) { + violations.push( + createViolation( + resolvedSourcePath, + node.name.getStart(sourceFile), + sourceFile, + `'globalThis.${node.name.text}' is not available in CRE workflow runtime.`, + ), + ) + } + + ts.forEachChild(node, visit) + } + + visit(sourceFile) + } +} + +export const assertWorkflowRuntimeCompatibility = (entryFilePath: string) => { + const rootFile = toAbsolutePath(entryFilePath) + const filesToScan = [rootFile] + const scannedFiles = new Set() + const localSourceFiles = new Set() + const violations: Violation[] = [] + + while (filesToScan.length > 0) { + const currentFile = filesToScan.pop() + if (!currentFile || scannedFiles.has(currentFile)) continue + scannedFiles.add(currentFile) + + if (!existsSync(currentFile)) continue + localSourceFiles.add(currentFile) + + const fileContents = readFileSync(currentFile, 'utf-8') + const sourceFile = ts.createSourceFile( + currentFile, + fileContents, + ts.ScriptTarget.Latest, + true, + getScriptKind(currentFile), + ) + + collectModuleUsage(sourceFile, currentFile, violations, (nextFile) => { + if (!scannedFiles.has(nextFile)) { + filesToScan.push(nextFile) + } + }) + } + + const program = ts.createProgram({ + rootNames: [...localSourceFiles], + options: { + allowJs: true, + checkJs: true, + noEmit: true, + skipLibCheck: true, + target: ts.ScriptTarget.ESNext, + module: ts.ModuleKind.ESNext, + moduleResolution: ts.ModuleResolutionKind.Bundler, + }, + }) + + collectGlobalApiUsage(program, localSourceFiles, violations) + + if (violations.length > 0) { + throw new WorkflowRuntimeCompatibilityError(violations) + } +} + +export { WorkflowRuntimeCompatibilityError } diff --git a/packages/cre-sdk/src/sdk/types/global.d.ts b/packages/cre-sdk/src/sdk/types/global.d.ts index 3fb9e68d..a93c895c 100644 --- a/packages/cre-sdk/src/sdk/types/global.d.ts +++ b/packages/cre-sdk/src/sdk/types/global.d.ts @@ -1,5 +1,8 @@ -// Global type declarations for the CRE SDK runtime -// Those are the methods that the Host exposes to the Guest. +// Global type declarations for the CRE SDK runtime. +// These are the methods and globals exposed by the Host to the Guest. + +type ExistingGlobal = + typeof globalThis extends Record ? T : Fallback /** * Host functions exposed by the CRE runtime to WASM guests @@ -82,43 +85,46 @@ declare global { /** * Console API available in the QuickJS runtime */ - interface Console { + type CreConsole = { log(...args: unknown[]): void warn(...args: unknown[]): void error(...args: unknown[]): void info(...args: unknown[]): void debug(...args: unknown[]): void } - var console: Console + var console: ExistingGlobal<'console', CreConsole> /** * TextEncoder/TextDecoder APIs available via Javy's text_encoding support */ - interface TextEncoderEncodeIntoResult { + interface CreTextEncoderEncodeIntoResult { read: number written: number } - interface TextEncoder { + interface CreTextEncoder { readonly encoding: string encode(input?: string): Uint8Array - encodeInto(input: string, dest: Uint8Array): TextEncoderEncodeIntoResult - } - var TextEncoder: { - prototype: TextEncoder - new (): TextEncoder + encodeInto(input: string, dest: Uint8Array): CreTextEncoderEncodeIntoResult } + var TextEncoder: ExistingGlobal< + 'TextEncoder', + { prototype: CreTextEncoder; new (): CreTextEncoder } + > - interface TextDecoder { + interface CreTextDecoder { readonly encoding: string readonly fatal: boolean readonly ignoreBOM: boolean decode(input?: ArrayBuffer | ArrayBufferView, options?: { stream?: boolean }): string } - var TextDecoder: { - prototype: TextDecoder - new (label?: string, options?: { fatal?: boolean; ignoreBOM?: boolean }): TextDecoder - } + var TextDecoder: ExistingGlobal< + 'TextDecoder', + { + prototype: CreTextDecoder + new (label?: string, options?: { fatal?: boolean; ignoreBOM?: boolean }): CreTextDecoder + } + > /** * Base64 encoding/decoding — exposed via prepareRuntime() from node:buffer @@ -129,7 +135,7 @@ declare global { /** * URL and URLSearchParams — exposed via prepareRuntime() from node:url */ - interface URLSearchParams { + interface CreURLSearchParams { append(name: string, value: string): void delete(name: string): void get(name: string): string | null @@ -138,21 +144,24 @@ declare global { set(name: string, value: string): void sort(): void toString(): string - forEach(callback: (value: string, key: string, parent: URLSearchParams) => void): void + forEach(callback: (value: string, key: string, parent: CreURLSearchParams) => void): void entries(): IterableIterator<[string, string]> keys(): IterableIterator values(): IterableIterator [Symbol.iterator](): IterableIterator<[string, string]> readonly size: number } - var URLSearchParams: { - prototype: URLSearchParams - new ( - init?: string | Record | [string, string][] | URLSearchParams, - ): URLSearchParams - } - - interface URL { + var URLSearchParams: ExistingGlobal< + 'URLSearchParams', + { + prototype: CreURLSearchParams + new ( + init?: string | Record | [string, string][] | CreURLSearchParams, + ): CreURLSearchParams + } + > + + interface CreURL { hash: string host: string hostname: string @@ -163,15 +172,15 @@ declare global { port: string protocol: string search: string - readonly searchParams: URLSearchParams + readonly searchParams: CreURLSearchParams username: string toString(): string toJSON(): string } - var URL: { - prototype: URL - new (url: string, base?: string | URL): URL - } + var URL: ExistingGlobal< + 'URL', + { prototype: CreURL; new (url: string, base?: string | CreURL): CreURL } + > } export {} diff --git a/packages/cre-sdk/src/sdk/types/restricted-apis.d.ts b/packages/cre-sdk/src/sdk/types/restricted-apis.d.ts index 91196ec7..a39dfe4d 100644 --- a/packages/cre-sdk/src/sdk/types/restricted-apis.d.ts +++ b/packages/cre-sdk/src/sdk/types/restricted-apis.d.ts @@ -1,27 +1,15 @@ declare global { - /** @deprecated fetch is not available in CRE WASM workflows. Use cre.capabilities.HTTPClient instead. */ - const fetch: never - - /** @deprecated window is not available in CRE WASM workflows. */ - const window: never - - /** @deprecated document is not available in CRE WASM workflows. */ - const document: never - - /** @deprecated XMLHttpRequest is not available in CRE WASM workflows. Use cre.capabilities.HTTPClient instead. */ - const XMLHttpRequest: never - - /** @deprecated localStorage is not available in CRE WASM workflows. */ - const localStorage: never - - /** @deprecated sessionStorage is not available in CRE WASM workflows. */ - const sessionStorage: never + /** + * @deprecated fetch is not available in CRE WASM workflows. + * Use cre.capabilities.HTTPClient instead. + */ + function fetch(_notAvailable: never): never /** @deprecated setTimeout is not available in CRE WASM workflows. Use cre.capabilities.CronCapability for scheduling. */ - const setTimeout: never + function setTimeout(_notAvailable: never, ..._args: never[]): never /** @deprecated setInterval is not available in CRE WASM workflows. Use cre.capabilities.CronCapability for scheduling. */ - const setInterval: never + function setInterval(_notAvailable: never, ..._args: never[]): never } export {} From afd12704b58facd0de3d4d422de827d4fb34ce69 Mon Sep 17 00:00:00 2001 From: ernest-nowacki Date: Mon, 2 Mar 2026 17:26:07 +0100 Subject: [PATCH 08/18] Fix prepare runtime --- packages/cre-sdk/src/sdk/utils/prepare-runtime.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/cre-sdk/src/sdk/utils/prepare-runtime.ts b/packages/cre-sdk/src/sdk/utils/prepare-runtime.ts index 1c94f453..f48841fb 100644 --- a/packages/cre-sdk/src/sdk/utils/prepare-runtime.ts +++ b/packages/cre-sdk/src/sdk/utils/prepare-runtime.ts @@ -10,6 +10,8 @@ export const prepareRuntime = () => { globalThis.Buffer = Buffer globalThis.atob = atob globalThis.btoa = btoa - globalThis.URL = URL as any - globalThis.URLSearchParams = URLSearchParams + // node:url constructor types are slightly narrower than lib.dom/global types. + // Runtime behavior is compatible; cast to the global constructor shapes. + globalThis.URL = URL as unknown as typeof globalThis.URL + globalThis.URLSearchParams = URLSearchParams as unknown as typeof globalThis.URLSearchParams } From 8352563ca35ba5dbf47110d45d590838113c3afd Mon Sep 17 00:00:00 2001 From: ernest-nowacki Date: Mon, 2 Mar 2026 17:56:12 +0100 Subject: [PATCH 09/18] Add links to the docs --- .../src/validate-workflow-runtime-compat.ts | 3 +- .../src/sdk/types/restricted-apis.d.ts | 11 +++- .../sdk/types/restricted-node-modules.d.ts | 61 +++++++++++++++---- 3 files changed, 60 insertions(+), 15 deletions(-) diff --git a/packages/cre-sdk/scripts/src/validate-workflow-runtime-compat.ts b/packages/cre-sdk/scripts/src/validate-workflow-runtime-compat.ts index cbf42905..dc108d25 100644 --- a/packages/cre-sdk/scripts/src/validate-workflow-runtime-compat.ts +++ b/packages/cre-sdk/scripts/src/validate-workflow-runtime-compat.ts @@ -67,7 +67,8 @@ class WorkflowRuntimeCompatibilityError extends Error { super( 'Unsupported API usage found in workflow source.\n' + 'CRE workflows run on Javy (QuickJS), not full Node.js.\n' + - 'Use CRE capabilities instead (for example, HTTPClient instead of fetch/node:http).\n\n' + + 'Use CRE capabilities instead (for example, HTTPClient instead of fetch/node:http).\n' + + 'See https://docs.chain.link/cre/concepts/typescript-wasm-runtime\n\n' + formattedViolations, ) this.name = 'WorkflowRuntimeCompatibilityError' diff --git a/packages/cre-sdk/src/sdk/types/restricted-apis.d.ts b/packages/cre-sdk/src/sdk/types/restricted-apis.d.ts index a39dfe4d..434d3e01 100644 --- a/packages/cre-sdk/src/sdk/types/restricted-apis.d.ts +++ b/packages/cre-sdk/src/sdk/types/restricted-apis.d.ts @@ -2,13 +2,20 @@ declare global { /** * @deprecated fetch is not available in CRE WASM workflows. * Use cre.capabilities.HTTPClient instead. + * @see https://docs.chain.link/cre/concepts/typescript-wasm-runtime */ function fetch(_notAvailable: never): never - /** @deprecated setTimeout is not available in CRE WASM workflows. Use cre.capabilities.CronCapability for scheduling. */ + /** + * @deprecated setTimeout is not available in CRE WASM workflows. Use cre.capabilities.CronCapability for scheduling. + * @see https://docs.chain.link/cre/concepts/typescript-wasm-runtime + */ function setTimeout(_notAvailable: never, ..._args: never[]): never - /** @deprecated setInterval is not available in CRE WASM workflows. Use cre.capabilities.CronCapability for scheduling. */ + /** + * @deprecated setInterval is not available in CRE WASM workflows. Use cre.capabilities.CronCapability for scheduling. + * @see https://docs.chain.link/cre/concepts/typescript-wasm-runtime + */ function setInterval(_notAvailable: never, ..._args: never[]): never } diff --git a/packages/cre-sdk/src/sdk/types/restricted-node-modules.d.ts b/packages/cre-sdk/src/sdk/types/restricted-node-modules.d.ts index 2454d3f6..83538a4d 100644 --- a/packages/cre-sdk/src/sdk/types/restricted-node-modules.d.ts +++ b/packages/cre-sdk/src/sdk/types/restricted-node-modules.d.ts @@ -2,8 +2,12 @@ // These modules require native bindings or system access that cannot run in WebAssembly. // Importing from these modules is allowed by TypeScript, but all exports are typed as // `never` so any usage produces a clear error at the call site. +// See https://docs.chain.link/cre/concepts/typescript-wasm-runtime -/** @deprecated node:crypto is not available in CRE WASM workflows. It requires native bindings that cannot run in WebAssembly. */ +/** + * @deprecated node:crypto is not available in CRE WASM workflows. It requires native bindings that cannot run in WebAssembly. + * @see https://docs.chain.link/cre/concepts/typescript-wasm-runtime + */ declare module 'node:crypto' { export const randomBytes: never export const randomUUID: never @@ -59,7 +63,10 @@ declare module 'node:crypto' { export const constants: never } -/** @deprecated node:fs is not available in CRE WASM workflows. It requires filesystem access that is not available in WebAssembly. */ +/** + * @deprecated node:fs is not available in CRE WASM workflows. It requires filesystem access that is not available in WebAssembly. + * @see https://docs.chain.link/cre/concepts/typescript-wasm-runtime + */ declare module 'node:fs' { export const readFile: never export const readFileSync: never @@ -139,7 +146,10 @@ declare module 'node:fs' { export const FSWatcher: never } -/** @deprecated node:fs/promises is not available in CRE WASM workflows. It requires filesystem access that is not available in WebAssembly. */ +/** + * @deprecated node:fs/promises is not available in CRE WASM workflows. It requires filesystem access that is not available in WebAssembly. + * @see https://docs.chain.link/cre/concepts/typescript-wasm-runtime + */ declare module 'node:fs/promises' { export const readFile: never export const writeFile: never @@ -171,7 +181,10 @@ declare module 'node:fs/promises' { export const FileHandle: never } -/** @deprecated node:net is not available in CRE WASM workflows. It requires network access. Use cre.capabilities.HTTPClient instead. */ +/** + * @deprecated node:net is not available in CRE WASM workflows. It requires network access. Use cre.capabilities.HTTPClient instead. + * @see https://docs.chain.link/cre/concepts/typescript-wasm-runtime + */ declare module 'node:net' { export const createServer: never export const createConnection: never @@ -187,7 +200,10 @@ declare module 'node:net' { export const SocketAddress: never } -/** @deprecated node:http is not available in CRE WASM workflows. It requires network access. Use cre.capabilities.HTTPClient instead. */ +/** + * @deprecated node:http is not available in CRE WASM workflows. It requires network access. Use cre.capabilities.HTTPClient instead. + * @see https://docs.chain.link/cre/concepts/typescript-wasm-runtime + */ declare module 'node:http' { export const createServer: never export const request: never @@ -207,7 +223,10 @@ declare module 'node:http' { export const maxHeaderSize: never } -/** @deprecated node:https is not available in CRE WASM workflows. It requires network access. Use cre.capabilities.HTTPClient instead. */ +/** + * @deprecated node:https is not available in CRE WASM workflows. It requires network access. Use cre.capabilities.HTTPClient instead. + * @see https://docs.chain.link/cre/concepts/typescript-wasm-runtime + */ declare module 'node:https' { export const createServer: never export const request: never @@ -217,7 +236,10 @@ declare module 'node:https' { export const globalAgent: never } -/** @deprecated node:child_process is not available in CRE WASM workflows. It requires OS process spawning that is not available in WebAssembly. */ +/** + * @deprecated node:child_process is not available in CRE WASM workflows. It requires OS process spawning that is not available in WebAssembly. + * @see https://docs.chain.link/cre/concepts/typescript-wasm-runtime + */ declare module 'node:child_process' { export const spawn: never export const spawnSync: never @@ -229,7 +251,10 @@ declare module 'node:child_process' { export const ChildProcess: never } -/** @deprecated node:os is not available in CRE WASM workflows. It requires OS access that is not available in WebAssembly. */ +/** + * @deprecated node:os is not available in CRE WASM workflows. It requires OS access that is not available in WebAssembly. + * @see https://docs.chain.link/cre/concepts/typescript-wasm-runtime + */ declare module 'node:os' { export const hostname: never export const platform: never @@ -256,7 +281,10 @@ declare module 'node:os' { export const constants: never } -/** @deprecated node:stream is not available in CRE WASM workflows. It requires native bindings that cannot run in WebAssembly. */ +/** + * @deprecated node:stream is not available in CRE WASM workflows. It requires native bindings that cannot run in WebAssembly. + * @see https://docs.chain.link/cre/concepts/typescript-wasm-runtime + */ declare module 'node:stream' { export const Readable: never export const Writable: never @@ -273,7 +301,10 @@ declare module 'node:stream' { export const isReadable: never } -/** @deprecated node:worker_threads is not available in CRE WASM workflows. It requires threading support that is not available in WebAssembly. */ +/** + * @deprecated node:worker_threads is not available in CRE WASM workflows. It requires threading support that is not available in WebAssembly. + * @see https://docs.chain.link/cre/concepts/typescript-wasm-runtime + */ declare module 'node:worker_threads' { export const Worker: never export const MessageChannel: never @@ -296,7 +327,10 @@ declare module 'node:worker_threads' { export const postMessageToThread: never } -/** @deprecated node:dns is not available in CRE WASM workflows. It requires network access that is not available in WebAssembly. */ +/** + * @deprecated node:dns is not available in CRE WASM workflows. It requires network access that is not available in WebAssembly. + * @see https://docs.chain.link/cre/concepts/typescript-wasm-runtime + */ declare module 'node:dns' { export const lookup: never export const lookupService: never @@ -321,7 +355,10 @@ declare module 'node:dns' { export const Resolver: never } -/** @deprecated node:zlib is not available in CRE WASM workflows. It requires native compression bindings that cannot run in WebAssembly. */ +/** + * @deprecated node:zlib is not available in CRE WASM workflows. It requires native compression bindings that cannot run in WebAssembly. + * @see https://docs.chain.link/cre/concepts/typescript-wasm-runtime + */ declare module 'node:zlib' { export const createGzip: never export const createGunzip: never From f55557d066f74408c6f21801ed36a3a4fd82b60e Mon Sep 17 00:00:00 2001 From: ernest-nowacki Date: Mon, 2 Mar 2026 18:00:13 +0100 Subject: [PATCH 10/18] Update versions --- packages/cre-sdk-examples/package.json | 2 +- packages/cre-sdk/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/cre-sdk-examples/package.json b/packages/cre-sdk-examples/package.json index ed73f4d9..fbf719f6 100644 --- a/packages/cre-sdk-examples/package.json +++ b/packages/cre-sdk-examples/package.json @@ -1,7 +1,7 @@ { "name": "@chainlink/cre-sdk-examples", "private": true, - "version": "1.1.2", + "version": "1.1.3", "type": "module", "author": "Ernest Nowacki", "license": "BUSL-1.1", diff --git a/packages/cre-sdk/package.json b/packages/cre-sdk/package.json index 9c540220..9a8a90d9 100644 --- a/packages/cre-sdk/package.json +++ b/packages/cre-sdk/package.json @@ -1,6 +1,6 @@ { "name": "@chainlink/cre-sdk", - "version": "1.1.2", + "version": "1.1.3", "type": "module", "main": "dist/index.js", "types": "dist/index.d.ts", From d809ee7d2d2582e2c099274ae329a525bc442340 Mon Sep 17 00:00:00 2001 From: ernest-nowacki Date: Mon, 2 Mar 2026 18:34:25 +0100 Subject: [PATCH 11/18] Add node modules examples --- .../src/restricted-node-modules-example.ts | 133 ++++++++++++++++++ packages/cre-sdk/scripts/src/build-types.ts | 6 +- 2 files changed, 138 insertions(+), 1 deletion(-) create mode 100644 packages/cre-sdk-examples/src/restricted-node-modules-example.ts diff --git a/packages/cre-sdk-examples/src/restricted-node-modules-example.ts b/packages/cre-sdk-examples/src/restricted-node-modules-example.ts new file mode 100644 index 00000000..f68290fc --- /dev/null +++ b/packages/cre-sdk-examples/src/restricted-node-modules-example.ts @@ -0,0 +1,133 @@ +/** + * This example shows how CRE workflows mark restricted Node.js modules as `never` in TS. + * + * CRE workflows run on QuickJS (via Javy/WASM), not full Node.js. + * All exports from restricted modules are typed as `never`, so any usage + * produces a clear TypeScript error at the call site. + * + * The restricted modules covered in this example are: + * - node:crypto + * - node:fs + * - node:fs/promises + * - node:net + * - node:http + * - node:https + * - node:child_process + * - node:os + * - node:stream + * - node:worker_threads + * - node:dns + * - node:zlib + * + * For HTTP requests, use cre.capabilities.HTTPClient instead of node:http/node:https/node:net. + * For scheduling, use cre.capabilities.CronCapability instead of setTimeout/setInterval. + * + * @see https://docs.chain.link/cre/concepts/typescript-wasm-runtime + */ + +import { exec } from 'node:child_process' +import { createHash, randomBytes } from 'node:crypto' +import { lookup } from 'node:dns' +import { readFileSync } from 'node:fs' +import { readFile } from 'node:fs/promises' +import { request as httpRequest } from 'node:http' +import { request as httpsRequest } from 'node:https' +import { createConnection } from 'node:net' +import { cpus, hostname } from 'node:os' +import { Readable } from 'node:stream' +import { Worker } from 'node:worker_threads' +import { createGzip } from 'node:zlib' + +// --- node:crypto --- + +export const testCryptoRandomBytes = () => { + // @ts-expect-error - node:crypto is not available in CRE WASM workflows + randomBytes(32) +} + +export const testCryptoCreateHash = () => { + // @ts-expect-error - node:crypto is not available in CRE WASM workflows + createHash('sha256') +} + +// --- node:fs --- + +export const testFsReadFileSync = () => { + // @ts-expect-error - node:fs is not available in CRE WASM workflows + readFileSync('/etc/passwd', 'utf-8') +} + +// --- node:fs/promises --- + +export const testFsPromisesReadFile = async () => { + // @ts-expect-error - node:fs/promises is not available in CRE WASM workflows + await readFile('/etc/passwd', 'utf-8') +} + +// --- node:net --- + +export const testNetCreateConnection = () => { + // @ts-expect-error - node:net is not available in CRE WASM workflows + createConnection({ host: 'localhost', port: 8080 }) +} + +// --- node:http --- + +export const testHttpRequest = () => { + // @ts-expect-error - node:http is not available in CRE WASM workflows + httpRequest('http://example.com') +} + +// --- node:https --- + +export const testHttpsRequest = () => { + // @ts-expect-error - node:https is not available in CRE WASM workflows + httpsRequest('https://example.com') +} + +// --- node:child_process --- + +export const testChildProcessExec = () => { + // @ts-expect-error - node:child_process is not available in CRE WASM workflows + exec('ls -la') +} + +// --- node:os --- + +export const testOsHostname = () => { + // @ts-expect-error - node:os is not available in CRE WASM workflows + hostname() +} + +export const testOsCpus = () => { + // @ts-expect-error - node:os is not available in CRE WASM workflows + cpus() +} + +// --- node:stream --- + +export const testStreamReadable = () => { + // @ts-expect-error - node:stream is not available in CRE WASM workflows + new Readable() +} + +// --- node:worker_threads --- + +export const testWorkerThreads = () => { + // @ts-expect-error - node:worker_threads is not available in CRE WASM workflows + new Worker('./worker.js') +} + +// --- node:dns --- + +export const testDnsLookup = () => { + // @ts-expect-error - node:dns is not available in CRE WASM workflows + lookup('example.com', () => {}) +} + +// --- node:zlib --- + +export const testZlibCreateGzip = () => { + // @ts-expect-error - node:zlib is not available in CRE WASM workflows + createGzip() +} diff --git a/packages/cre-sdk/scripts/src/build-types.ts b/packages/cre-sdk/scripts/src/build-types.ts index 2c483f2c..082d80e0 100644 --- a/packages/cre-sdk/scripts/src/build-types.ts +++ b/packages/cre-sdk/scripts/src/build-types.ts @@ -40,7 +40,11 @@ const buildTypes = async () => { .split('\n') .filter((line) => line.startsWith('/// '] + + const tripleSlashRefs = [...refsFromSource, ...consumerOnlyRefs].join('\n') if (tripleSlashRefs) { const indexContent = await readFile(indexDts, 'utf-8') From 8934ae78ba891fd70f7e02c5322a034a7456200f Mon Sep 17 00:00:00 2001 From: ernest-nowacki Date: Mon, 2 Mar 2026 18:45:07 +0100 Subject: [PATCH 12/18] Add documentation around workflow validation --- .../src/validate-workflow-runtime-compat.ts | 193 ++++++++++++++++++ 1 file changed, 193 insertions(+) diff --git a/packages/cre-sdk/scripts/src/validate-workflow-runtime-compat.ts b/packages/cre-sdk/scripts/src/validate-workflow-runtime-compat.ts index dc108d25..239fe21b 100644 --- a/packages/cre-sdk/scripts/src/validate-workflow-runtime-compat.ts +++ b/packages/cre-sdk/scripts/src/validate-workflow-runtime-compat.ts @@ -1,14 +1,96 @@ +/** + * Workflow Runtime Compatibility Validator + * + * CRE (Compute Runtime Environment) workflows are compiled from TypeScript to + * WebAssembly and executed inside a Javy/QuickJS sandbox — NOT a full Node.js + * runtime. This means many APIs that developers take for granted (filesystem, + * network sockets, crypto, child processes, etc.) are simply not available at + * runtime and will silently fail or crash. + * + * This module performs **static analysis** on workflow source code to catch + * these issues at build time, before the workflow is compiled to WASM. It + * operates in two passes: + * + * 1. **Module import analysis** — walks the AST of every reachable source file + * (starting from the workflow entry point) and flags imports from restricted + * Node.js built-in modules (e.g. `node:fs`, `node:crypto`, `node:http`). + * This catches `import`, `export ... from`, `require()`, and dynamic + * `import()` syntax. + * + * 2. **Global API analysis** — uses the TypeScript type-checker to detect + * references to browser/Node globals that don't exist in QuickJS (e.g. + * `fetch`, `setTimeout`, `window`, `document`). Only flags identifiers + * that resolve to non-local declarations, so user-defined variables with + * the same name (e.g. `const fetch = cre.capabilities.HTTPClient`) are + * not flagged. + * + * The validator follows relative imports transitively so that violations in + * helper files reachable from the entry point are also caught. + * + * ## How it's used + * + * This validator runs automatically as part of the `cre-compile` build pipeline: + * + * ``` + * cre-compile [path/to/output.wasm] + * ``` + * + * The pipeline is: `cre-compile` (CLI) -> `compile-workflow` -> `compile-to-js` + * -> **`assertWorkflowRuntimeCompatibility()`** -> bundle -> compile to WASM. + * + * The validation happens before any bundling or WASM compilation, so developers + * get fast, actionable error messages pointing to exact file:line:column + * locations instead of cryptic WASM runtime failures. + * + * ## Layers of protection + * + * This validator is one of two complementary mechanisms that prevent usage of + * unavailable APIs: + * + * 1. **Compile-time types** (`restricted-apis.d.ts` and `restricted-node-modules.d.ts`) + * — mark restricted APIs as `never` so the TypeScript compiler flags them + * with red squiggles in the IDE. This gives instant feedback while coding. + * + * 2. **Build-time validation** (this module) — performs AST-level static + * analysis during `cre-compile`. This catches cases that type-level + * restrictions can't cover, such as `require()` calls, dynamic `import()`, + * and usage inside plain `.js` files that don't go through `tsc`. + * + * @example + * ```ts + * import { assertWorkflowRuntimeCompatibility } from './validate-workflow-runtime-compat' + * + * // Throws WorkflowRuntimeCompatibilityError if violations are found + * assertWorkflowRuntimeCompatibility('./src/workflow.ts') + * ``` + * + * @see https://docs.chain.link/cre/concepts/typescript-wasm-runtime + */ + import { existsSync, readFileSync } from 'node:fs' import path from 'node:path' import * as ts from 'typescript' +/** + * A single detected violation: a location in the source code where a + * restricted API is referenced. + */ type Violation = { + /** Absolute path to the file containing the violation. */ filePath: string + /** 1-based line number. */ line: number + /** 1-based column number. */ column: number + /** Human-readable description of the violation. */ message: string } +/** + * Node.js built-in module specifiers that are not available in the QuickJS + * runtime. Both bare (`fs`) and prefixed (`node:fs`) forms are included + * because TypeScript/bundlers accept either. + */ const restrictedModuleSpecifiers = new Set([ 'crypto', 'node:crypto', @@ -36,6 +118,11 @@ const restrictedModuleSpecifiers = new Set([ 'node:zlib', ]) +/** + * Global identifiers (browser and Node.js) that do not exist in the QuickJS + * runtime. For network requests, workflows should use `cre.capabilities.HTTPClient`; + * for scheduling, `cre.capabilities.CronCapability`. + */ const restrictedGlobalApis = new Set([ 'fetch', 'window', @@ -47,8 +134,14 @@ const restrictedGlobalApis = new Set([ 'setInterval', ]) +/** File extensions treated as scannable source code. */ const sourceExtensions = ['.ts', '.tsx', '.mts', '.cts', '.js', '.jsx', '.mjs', '.cjs'] +/** + * Error thrown when one or more runtime-incompatible API usages are detected. + * The message includes a docs link and a formatted list of every violation + * with file path, line, column, and description. + */ class WorkflowRuntimeCompatibilityError extends Error { constructor(violations: Violation[]) { const sortedViolations = [...violations].sort((a, b) => { @@ -75,8 +168,13 @@ class WorkflowRuntimeCompatibilityError extends Error { } } +/** Resolves a file path to an absolute path using the current working directory. */ const toAbsolutePath = (filePath: string) => path.resolve(filePath) +/** + * Maps a file extension to the appropriate TypeScript {@link ts.ScriptKind} + * so the parser handles JSX, CommonJS, and ESM files correctly. + */ const getScriptKind = (filePath: string): ts.ScriptKind => { switch (path.extname(filePath).toLowerCase()) { case '.js': @@ -98,6 +196,10 @@ const getScriptKind = (filePath: string): ts.ScriptKind => { } } +/** + * Creates a {@link Violation} with 1-based line and column numbers derived + * from a character position in the source file. + */ const createViolation = ( filePath: string, pos: number, @@ -113,10 +215,17 @@ const createViolation = ( } } +/** Returns `true` if the specifier looks like a relative or absolute file path. */ const isRelativeImport = (specifier: string) => { return specifier.startsWith('./') || specifier.startsWith('../') || specifier.startsWith('/') } +/** + * Attempts to resolve a relative import specifier to an absolute file path. + * Tries the path as-is first, then appends each known source extension, then + * looks for an index file inside the directory. Returns `null` if nothing is + * found on disk. + */ const resolveRelativeImport = (fromFilePath: string, specifier: string): string | null => { const basePath = specifier.startsWith('/') ? path.resolve(specifier) @@ -143,12 +252,32 @@ const resolveRelativeImport = (fromFilePath: string, specifier: string): string return null } +/** + * Extracts a string literal from the first argument of a call expression. + * Used for `require('node:fs')` and `import('node:fs')` patterns. + * Returns `null` if the first argument is not a static string literal. + */ const getStringLiteralFromCall = (node: ts.CallExpression): string | null => { const [firstArg] = node.arguments if (!firstArg || !ts.isStringLiteral(firstArg)) return null return firstArg.text } +/** + * **Pass 1 — Module import analysis.** + * + * Walks the AST of a single source file and: + * - Flags any import/export/require/dynamic-import of a restricted module. + * - Enqueues relative imports for recursive scanning so the validator + * transitively covers the entire local dependency graph. + * + * Handles all module import syntaxes: + * - `import ... from 'node:fs'` + * - `export ... from 'node:fs'` + * - `import fs = require('node:fs')` + * - `require('node:fs')` + * - `import('node:fs')` + */ const collectModuleUsage = ( sourceFile: ts.SourceFile, filePath: string, @@ -175,10 +304,12 @@ const collectModuleUsage = ( } const visit = (node: ts.Node) => { + // import ... from 'specifier' if (ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier)) { checkModuleSpecifier(node.moduleSpecifier.text, node.moduleSpecifier.getStart(sourceFile)) } + // export ... from 'specifier' if ( ts.isExportDeclaration(node) && node.moduleSpecifier && @@ -187,6 +318,7 @@ const collectModuleUsage = ( checkModuleSpecifier(node.moduleSpecifier.text, node.moduleSpecifier.getStart(sourceFile)) } + // import fs = require('specifier') if ( ts.isImportEqualsDeclaration(node) && ts.isExternalModuleReference(node.moduleReference) && @@ -200,6 +332,7 @@ const collectModuleUsage = ( } if (ts.isCallExpression(node)) { + // require('specifier') if (ts.isIdentifier(node.expression) && node.expression.text === 'require') { const requiredModule = getStringLiteralFromCall(node) if (requiredModule) { @@ -207,6 +340,7 @@ const collectModuleUsage = ( } } + // import('specifier') if (node.expression.kind === ts.SyntaxKind.ImportKeyword) { const importedModule = getStringLiteralFromCall(node) if (importedModule) { @@ -221,9 +355,20 @@ const collectModuleUsage = ( visit(sourceFile) } +/** + * Checks whether an identifier AST node is the **name being declared** (as + * opposed to a reference/usage). For example, in `const fetch = ...` the + * `fetch` token is a declaration name, while in `fetch(url)` it is a usage. + * + * This distinction is critical so that user-defined variables that shadow + * restricted global names are not flagged as violations. + */ const isDeclarationName = (identifier: ts.Identifier): boolean => { const parent = identifier.parent + // Variable, function, class, interface, type alias, enum, module, + // type parameter, parameter, binding element, import names, enum member, + // property/method declarations, property assignments, and labels. if ( (ts.isFunctionDeclaration(parent) && parent.name === identifier) || (ts.isFunctionExpression(parent) && parent.name === identifier) || @@ -256,6 +401,9 @@ const isDeclarationName = (identifier: ts.Identifier): boolean => { return true } + // Property access (obj.fetch), qualified names (Ns.fetch), and type + // references (SomeType) — the right-hand identifier is not a standalone + // usage of the global name. if ( (ts.isPropertyAccessExpression(parent) && parent.name === identifier) || (ts.isQualifiedName(parent) && parent.right === identifier) || @@ -267,6 +415,19 @@ const isDeclarationName = (identifier: ts.Identifier): boolean => { return false } +/** + * **Pass 2 — Global API analysis.** + * + * Uses the TypeScript type-checker to find references to restricted global + * identifiers (e.g. `fetch`, `setTimeout`, `window`). An identifier is only + * flagged if: + * - It matches a name in {@link restrictedGlobalApis}. + * - It is **not** a declaration name (see {@link isDeclarationName}). + * - Its symbol resolves to a declaration outside the local source files, + * meaning it comes from the global scope rather than user code. + * + * This also catches `globalThis.fetch`-style access patterns. + */ const collectGlobalApiUsage = ( program: ts.Program, localSourceFiles: Set, @@ -279,6 +440,7 @@ const collectGlobalApiUsage = ( if (!localSourceFiles.has(resolvedSourcePath)) continue const visit = (node: ts.Node) => { + // Direct usage: fetch(...), setTimeout(...) if ( ts.isIdentifier(node) && restrictedGlobalApis.has(node.text) && @@ -302,6 +464,7 @@ const collectGlobalApiUsage = ( } } + // Property access on globalThis: globalThis.fetch(...) if ( ts.isPropertyAccessExpression(node) && ts.isIdentifier(node.expression) && @@ -325,6 +488,34 @@ const collectGlobalApiUsage = ( } } +/** + * Validates that a workflow entry file (and all local files it transitively + * imports) only uses APIs available in the CRE QuickJS/WASM runtime. + * + * The check runs in two passes: + * + * 1. **Module import scan** — starting from `entryFilePath`, recursively + * parses every reachable local source file and flags imports from + * restricted Node.js built-in modules. + * + * 2. **Global API scan** — creates a TypeScript program from the collected + * source files and uses the type-checker to flag references to restricted + * global identifiers that resolve to non-local (i.e. global) declarations. + * + * @param entryFilePath - Path to the workflow entry file (absolute or relative). + * @throws {WorkflowRuntimeCompatibilityError} If any violations are found. + * The error message includes a link to the CRE runtime docs and a formatted + * list of every violation with file:line:column and description. + * + * @example + * ```ts + * // During the cre-compile build step: + * assertWorkflowRuntimeCompatibility('./src/workflow.ts') + * // Throws if the workflow (or any file it imports) uses fetch, node:fs, etc. + * ``` + * + * @see https://docs.chain.link/cre/concepts/typescript-wasm-runtime + */ export const assertWorkflowRuntimeCompatibility = (entryFilePath: string) => { const rootFile = toAbsolutePath(entryFilePath) const filesToScan = [rootFile] @@ -332,6 +523,7 @@ export const assertWorkflowRuntimeCompatibility = (entryFilePath: string) => { const localSourceFiles = new Set() const violations: Violation[] = [] + // Pass 1: Walk the local import graph and collect module-level violations. while (filesToScan.length > 0) { const currentFile = filesToScan.pop() if (!currentFile || scannedFiles.has(currentFile)) continue @@ -356,6 +548,7 @@ export const assertWorkflowRuntimeCompatibility = (entryFilePath: string) => { }) } + // Pass 2: Use the type-checker to detect restricted global API usage. const program = ts.createProgram({ rootNames: [...localSourceFiles], options: { From 504996a95c75f44814db3816c92f898ba8da8592 Mon Sep 17 00:00:00 2001 From: ernest-nowacki Date: Tue, 3 Mar 2026 12:33:26 +0100 Subject: [PATCH 13/18] Add tests and fixed bug caught up by the test --- .../src/restricted-node-modules-example.ts | 1 - .../validate-workflow-runtime-compat.test.ts | 412 ++++++++++++++++++ .../src/validate-workflow-runtime-compat.ts | 15 +- 3 files changed, 420 insertions(+), 8 deletions(-) create mode 100644 packages/cre-sdk/scripts/src/validate-workflow-runtime-compat.test.ts diff --git a/packages/cre-sdk-examples/src/restricted-node-modules-example.ts b/packages/cre-sdk-examples/src/restricted-node-modules-example.ts index f68290fc..a97ac831 100644 --- a/packages/cre-sdk-examples/src/restricted-node-modules-example.ts +++ b/packages/cre-sdk-examples/src/restricted-node-modules-example.ts @@ -20,7 +20,6 @@ * - node:zlib * * For HTTP requests, use cre.capabilities.HTTPClient instead of node:http/node:https/node:net. - * For scheduling, use cre.capabilities.CronCapability instead of setTimeout/setInterval. * * @see https://docs.chain.link/cre/concepts/typescript-wasm-runtime */ diff --git a/packages/cre-sdk/scripts/src/validate-workflow-runtime-compat.test.ts b/packages/cre-sdk/scripts/src/validate-workflow-runtime-compat.test.ts new file mode 100644 index 00000000..b1cb92a7 --- /dev/null +++ b/packages/cre-sdk/scripts/src/validate-workflow-runtime-compat.test.ts @@ -0,0 +1,412 @@ +import { afterEach, beforeEach, describe, expect, test } from 'bun:test' +import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs' +import { tmpdir } from 'node:os' +import path from 'node:path' +import { + assertWorkflowRuntimeCompatibility, + WorkflowRuntimeCompatibilityError, +} from './validate-workflow-runtime-compat' + +let tempDir: string + +beforeEach(() => { + tempDir = mkdtempSync(path.join(tmpdir(), 'cre-validate-test-')) +}) + +afterEach(() => { + rmSync(tempDir, { recursive: true, force: true }) +}) + +/** Write a file in the temp directory and return its absolute path. */ +const writeTemp = (filename: string, content: string): string => { + const filePath = path.join(tempDir, filename) + const dir = path.dirname(filePath) + mkdirSync(dir, { recursive: true }) + writeFileSync(filePath, content, 'utf-8') + return filePath +} + +/** Assert that the validator throws with violations matching the given patterns. */ +const expectViolations = (entryPath: string, expectedPatterns: (string | RegExp)[]) => { + try { + assertWorkflowRuntimeCompatibility(entryPath) + throw new Error('Expected WorkflowRuntimeCompatibilityError but none was thrown') + } catch (error) { + expect(error).toBeInstanceOf(WorkflowRuntimeCompatibilityError) + const message = (error as Error).message + for (const pattern of expectedPatterns) { + if (typeof pattern === 'string') { + expect(message).toContain(pattern) + } else { + expect(message).toMatch(pattern) + } + } + } +} + +/** Assert that the validator does NOT throw. */ +const expectNoViolations = (entryPath: string) => { + expect(() => assertWorkflowRuntimeCompatibility(entryPath)).not.toThrow() +} + +// --------------------------------------------------------------------------- +// Pass 1: Module import analysis +// --------------------------------------------------------------------------- + +describe('module import analysis', () => { + test("detects import ... from 'node:fs'", () => { + const entry = writeTemp('workflow.ts', `import { readFileSync } from 'node:fs';\n`) + expectViolations(entry, ["'node:fs' is not available"]) + }) + + test("detects bare module specifier 'fs' (without node: prefix)", () => { + const entry = writeTemp('workflow.ts', `import { readFileSync } from 'fs';\n`) + expectViolations(entry, ["'fs' is not available"]) + }) + + test('detects export ... from restricted module', () => { + const entry = writeTemp('workflow.ts', `export { createHash } from 'node:crypto';\n`) + expectViolations(entry, ["'node:crypto' is not available"]) + }) + + test('detects import = require() syntax', () => { + const entry = writeTemp('workflow.ts', `import fs = require('node:fs');\n`) + expectViolations(entry, ["'node:fs' is not available"]) + }) + + test('detects require() call', () => { + const entry = writeTemp('workflow.js', `const fs = require('node:fs');\n`) + expectViolations(entry, ["'node:fs' is not available"]) + }) + + test('detects dynamic import()', () => { + const entry = writeTemp('workflow.ts', `const fs = await import('node:fs');\n`) + expectViolations(entry, ["'node:fs' is not available"]) + }) + + test('detects all restricted modules in a single file', () => { + const modules = [ + 'crypto', + 'node:crypto', + 'fs', + 'node:fs', + 'fs/promises', + 'node:fs/promises', + 'net', + 'node:net', + 'http', + 'node:http', + 'https', + 'node:https', + 'child_process', + 'node:child_process', + 'os', + 'node:os', + 'stream', + 'node:stream', + 'worker_threads', + 'node:worker_threads', + 'dns', + 'node:dns', + 'zlib', + 'node:zlib', + ] + + const imports = modules.map((mod, i) => `import m${i} from '${mod}';`).join('\n') + const entry = writeTemp('workflow.ts', `${imports}\n`) + expectViolations( + entry, + modules.map((mod) => `'${mod}' is not available`), + ) + }) + + test('does NOT flag allowed third-party modules', () => { + const entry = writeTemp( + 'workflow.ts', + `import { something } from '@chainlink/cre-sdk';\nimport lodash from 'lodash';\n`, + ) + expectNoViolations(entry) + }) + + test('does NOT flag relative imports themselves', () => { + const helper = writeTemp('helper.ts', `export const add = (a: number, b: number) => a + b;\n`) + const entry = writeTemp( + 'workflow.ts', + `import { add } from './helper';\nconsole.log(add(1, 2));\n`, + ) + expectNoViolations(entry) + }) + + test('follows relative imports transitively and detects violations in them', () => { + writeTemp('deep.ts', `import { readFileSync } from 'node:fs';\nexport const x = 1;\n`) + writeTemp('middle.ts', `import { x } from './deep';\nexport const y = x;\n`) + const entry = writeTemp('workflow.ts', `import { y } from './middle';\nconsole.log(y);\n`) + expectViolations(entry, ["'node:fs' is not available"]) + }) + + test('handles circular relative imports without infinite loop', () => { + writeTemp('a.ts', `import { b } from './b';\nexport const a = 'a';\n`) + writeTemp('b.ts', `import { a } from './a';\nexport const b = 'b';\n`) + const entry = writeTemp('workflow.ts', `import { a } from './a';\n`) + expectNoViolations(entry) + }) + + test('resolves imports without file extension', () => { + writeTemp('utils.ts', `import { cpus } from 'node:os';\nexport const x = 1;\n`) + const entry = writeTemp('workflow.ts', `import { x } from './utils';\nconsole.log(x);\n`) + expectViolations(entry, ["'node:os' is not available"]) + }) + + test('resolves index file imports', () => { + writeTemp('lib/index.ts', `import { hostname } from 'node:os';\nexport const name = 'test';\n`) + const entry = writeTemp('workflow.ts', `import { name } from './lib';\nconsole.log(name);\n`) + expectViolations(entry, ["'node:os' is not available"]) + }) + + test('reports multiple violations from multiple files', () => { + writeTemp('helper.ts', `import { exec } from 'node:child_process';\nexport const run = exec;\n`) + const entry = writeTemp( + 'workflow.ts', + `import { run } from './helper';\nimport { readFileSync } from 'node:fs';\n`, + ) + expectViolations(entry, ["'node:child_process' is not available", "'node:fs' is not available"]) + }) +}) + +// --------------------------------------------------------------------------- +// Pass 2: Global API analysis +// --------------------------------------------------------------------------- + +describe('global API analysis', () => { + test('detects bare fetch() usage', () => { + const entry = writeTemp('workflow.ts', `const res = fetch('https://example.com');\n`) + expectViolations(entry, ["'fetch' is not available"]) + }) + + test('detects setTimeout usage', () => { + const entry = writeTemp('workflow.ts', `setTimeout(() => {}, 1000);\n`) + expectViolations(entry, ["'setTimeout' is not available"]) + }) + + test('detects setInterval usage', () => { + const entry = writeTemp('workflow.ts', `setInterval(() => {}, 1000);\n`) + expectViolations(entry, ["'setInterval' is not available"]) + }) + + test('detects window reference', () => { + const entry = writeTemp('workflow.ts', `const w = window;\n`) + expectViolations(entry, ["'window' is not available"]) + }) + + test('detects document reference', () => { + const entry = writeTemp('workflow.ts', `const el = document.getElementById('app');\n`) + expectViolations(entry, ["'document' is not available"]) + }) + + test('detects XMLHttpRequest usage', () => { + const entry = writeTemp('workflow.ts', `const xhr = new XMLHttpRequest();\n`) + expectViolations(entry, ["'XMLHttpRequest' is not available"]) + }) + + test('detects localStorage usage', () => { + const entry = writeTemp('workflow.ts', `localStorage.setItem('key', 'value');\n`) + expectViolations(entry, ["'localStorage' is not available"]) + }) + + test('detects sessionStorage usage', () => { + const entry = writeTemp('workflow.ts', `sessionStorage.getItem('key');\n`) + expectViolations(entry, ["'sessionStorage' is not available"]) + }) + + test('detects globalThis.fetch access', () => { + const entry = writeTemp('workflow.ts', `const res = globalThis.fetch('https://example.com');\n`) + expectViolations(entry, ["'globalThis.fetch' is not available"]) + }) + + test('detects globalThis.setTimeout access', () => { + const entry = writeTemp('workflow.ts', `globalThis.setTimeout(() => {}, 100);\n`) + expectViolations(entry, ["'globalThis.setTimeout' is not available"]) + }) + + test('does NOT flag user-defined variable named fetch', () => { + const entry = writeTemp( + 'workflow.ts', + `export const fetch = (url: string) => url;\nconst result = fetch('test');\n`, + ) + expectNoViolations(entry) + }) + + test('does NOT flag user-defined function named fetch', () => { + const entry = writeTemp( + 'workflow.ts', + `export function fetch(url: string) { return url; }\nconst result = fetch('test');\n`, + ) + expectNoViolations(entry) + }) + + test('does NOT flag function parameter named fetch', () => { + const entry = writeTemp( + 'workflow.ts', + `export function doRequest(fetch: (url: string) => void) { fetch('test'); }\n`, + ) + expectNoViolations(entry) + }) + + test('does NOT flag property access obj.fetch', () => { + const entry = writeTemp('workflow.ts', `const obj = { fetch: () => {} };\nobj.fetch();\n`) + expectNoViolations(entry) + }) + + test('does NOT flag interface property named fetch', () => { + const entry = writeTemp('workflow.ts', `interface Client { fetch: (url: string) => void; }\n`) + expectNoViolations(entry) + }) + + test('does NOT flag destructured property named fetch from local object', () => { + const entry = writeTemp( + 'workflow.ts', + `const capabilities = { fetch: (url: string) => url };\nconst { fetch } = capabilities;\nexport const result = fetch('test');\n`, + ) + expectNoViolations(entry) + }) + + test('does NOT flag class method named fetch', () => { + const entry = writeTemp( + 'workflow.ts', + `class HttpClient {\n fetch(url: string) { return url; }\n}\nnew HttpClient().fetch('test');\n`, + ) + expectNoViolations(entry) + }) + + test('detects global APIs in transitively imported files', () => { + writeTemp('helper.ts', `export const doFetch = () => fetch('https://example.com');\n`) + const entry = writeTemp('workflow.ts', `import { doFetch } from './helper';\ndoFetch();\n`) + expectViolations(entry, ["'fetch' is not available"]) + }) +}) + +// --------------------------------------------------------------------------- +// Combined / integration tests +// --------------------------------------------------------------------------- + +describe('integration', () => { + test('clean workflow passes validation', () => { + const entry = writeTemp( + 'workflow.ts', + ` +import { Runner, cre } from '@chainlink/cre-sdk'; + +export async function main() { + const runner = await Runner.newRunner(); + console.log('Hello from CRE'); +} +`, + ) + expectNoViolations(entry) + }) + + test('detects both module and global API violations in same file', () => { + const entry = writeTemp( + 'workflow.ts', + ` +import { readFileSync } from 'node:fs'; +const data = readFileSync('/tmp/data.json', 'utf-8'); +const res = fetch('https://api.example.com'); +`, + ) + expectViolations(entry, ["'node:fs' is not available", "'fetch' is not available"]) + }) + + test('error message includes file path and line/column info', () => { + const entry = writeTemp('workflow.ts', `import { readFileSync } from 'node:fs';\n`) + try { + assertWorkflowRuntimeCompatibility(entry) + throw new Error('Expected error') + } catch (error) { + expect(error).toBeInstanceOf(WorkflowRuntimeCompatibilityError) + const msg = (error as Error).message + // Should contain relative or absolute path to the file + expect(msg).toContain('workflow.ts') + // Should contain line:column format + expect(msg).toMatch(/:\d+:\d+/) + } + }) + + test('error message includes docs link', () => { + const entry = writeTemp('workflow.ts', `import { readFileSync } from 'node:fs';\n`) + try { + assertWorkflowRuntimeCompatibility(entry) + throw new Error('Expected error') + } catch (error) { + const msg = (error as Error).message + expect(msg).toContain('https://docs.chain.link/cre/concepts/typescript-wasm-runtime') + } + }) + + test('handles .js files', () => { + const entry = writeTemp('workflow.js', `const fs = require('node:fs');\n`) + expectViolations(entry, ["'node:fs' is not available"]) + }) + + test('handles .mjs files', () => { + const entry = writeTemp('workflow.mjs', `import { readFileSync } from 'node:fs';\n`) + expectViolations(entry, ["'node:fs' is not available"]) + }) + + test('handles .cjs files', () => { + const entry = writeTemp('workflow.cjs', `const fs = require('node:fs');\n`) + expectViolations(entry, ["'node:fs' is not available"]) + }) + + test('violations are sorted by file path, then line, then column', () => { + writeTemp( + 'b-helper.ts', + `import { exec } from 'node:child_process';\nexport const run = exec;\n`, + ) + const entry = writeTemp( + 'a-workflow.ts', + `import { run } from './b-helper';\nimport { readFileSync } from 'node:fs';\nimport { cpus } from 'node:os';\n`, + ) + try { + assertWorkflowRuntimeCompatibility(entry) + throw new Error('Expected error') + } catch (error) { + const msg = (error as Error).message + const violationLines = msg.split('\n').filter((line) => line.startsWith('- ')) + + // Should have 3 violations minimum + expect(violationLines.length).toBeGreaterThanOrEqual(3) + + // Extract file paths from violation lines + const filePaths = violationLines.map((line) => line.split(':')[0].replace('- ', '')) + + // Verify sorted order: a-workflow.ts violations before b-helper.ts + const aIndexes = filePaths + .map((f, i) => (f.includes('a-workflow') ? i : -1)) + .filter((i) => i >= 0) + const bIndexes = filePaths + .map((f, i) => (f.includes('b-helper') ? i : -1)) + .filter((i) => i >= 0) + + if (aIndexes.length > 0 && bIndexes.length > 0) { + expect(Math.max(...aIndexes)).toBeLessThan(Math.min(...bIndexes)) + } + } + }) + + test('empty file passes validation', () => { + const entry = writeTemp('workflow.ts', '') + expectNoViolations(entry) + }) + + test('file with only comments passes validation', () => { + const entry = writeTemp('workflow.ts', `// This is a comment\n/* Block comment */\n`) + expectNoViolations(entry) + }) + + test('non-existent entry file does not throw', () => { + const nonExistent = path.join(tempDir, 'does-not-exist.ts') + // Should not throw since the file doesn't exist - it just won't find violations + expectNoViolations(nonExistent) + }) +}) diff --git a/packages/cre-sdk/scripts/src/validate-workflow-runtime-compat.ts b/packages/cre-sdk/scripts/src/validate-workflow-runtime-compat.ts index 239fe21b..9f2bee32 100644 --- a/packages/cre-sdk/scripts/src/validate-workflow-runtime-compat.ts +++ b/packages/cre-sdk/scripts/src/validate-workflow-runtime-compat.ts @@ -67,7 +67,7 @@ * @see https://docs.chain.link/cre/concepts/typescript-wasm-runtime */ -import { existsSync, readFileSync } from 'node:fs' +import { existsSync, readFileSync, statSync } from 'node:fs' import path from 'node:path' import * as ts from 'typescript' @@ -158,11 +158,12 @@ class WorkflowRuntimeCompatibilityError extends Error { .join('\n') super( - 'Unsupported API usage found in workflow source.\n' + - 'CRE workflows run on Javy (QuickJS), not full Node.js.\n' + - 'Use CRE capabilities instead (for example, HTTPClient instead of fetch/node:http).\n' + - 'See https://docs.chain.link/cre/concepts/typescript-wasm-runtime\n\n' + - formattedViolations, + `Unsupported API usage found in workflow source. +CRE workflows run on Javy (QuickJS), not full Node.js. +Use CRE capabilities instead (for example, HTTPClient instead of fetch/node:http). +See https://docs.chain.link/cre/concepts/typescript-wasm-runtime + +${formattedViolations}`, ) this.name = 'WorkflowRuntimeCompatibilityError' } @@ -231,7 +232,7 @@ const resolveRelativeImport = (fromFilePath: string, specifier: string): string ? path.resolve(specifier) : path.resolve(path.dirname(fromFilePath), specifier) - if (existsSync(basePath)) { + if (existsSync(basePath) && statSync(basePath).isFile()) { return toAbsolutePath(basePath) } From 28414a3c49c053084c5c627c882a0be8a1ec02a2 Mon Sep 17 00:00:00 2001 From: ernest-nowacki Date: Tue, 3 Mar 2026 13:14:05 +0100 Subject: [PATCH 14/18] Set version for alpha --- packages/cre-sdk-examples/package.json | 4 ++-- packages/cre-sdk/package.json | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/cre-sdk-examples/package.json b/packages/cre-sdk-examples/package.json index fbf719f6..011f23b5 100644 --- a/packages/cre-sdk-examples/package.json +++ b/packages/cre-sdk-examples/package.json @@ -1,7 +1,7 @@ { "name": "@chainlink/cre-sdk-examples", "private": true, - "version": "1.1.3", + "version": "1.1.3-alpha.1", "type": "module", "author": "Ernest Nowacki", "license": "BUSL-1.1", @@ -16,7 +16,7 @@ }, "dependencies": { "@bufbuild/protobuf": "2.6.3", - "@chainlink/cre-sdk": "workspace:*", + "@chainlink/cre-sdk": "1.1.3-alpha.1", "viem": "2.34.0", "zod": "3.25.76" }, diff --git a/packages/cre-sdk/package.json b/packages/cre-sdk/package.json index 9a8a90d9..105a7ac5 100644 --- a/packages/cre-sdk/package.json +++ b/packages/cre-sdk/package.json @@ -1,6 +1,6 @@ { "name": "@chainlink/cre-sdk", - "version": "1.1.3", + "version": "1.1.3-alpha.1", "type": "module", "main": "dist/index.js", "types": "dist/index.d.ts", @@ -60,7 +60,7 @@ "dependencies": { "@bufbuild/protobuf": "2.6.3", "@bufbuild/protoc-gen-es": "2.6.3", - "@chainlink/cre-sdk-javy-plugin": "workspace:*", + "@chainlink/cre-sdk-javy-plugin": "1.1.1", "@standard-schema/spec": "1.0.0", "viem": "2.34.0", "zod": "3.25.76" From e742d67ad77ea7bd1fbc0c7a326b86425a494d9a Mon Sep 17 00:00:00 2001 From: ernest-nowacki Date: Tue, 3 Mar 2026 14:07:53 +0100 Subject: [PATCH 15/18] Restore 1.1.3 version --- packages/cre-sdk-examples/package.json | 4 ++-- packages/cre-sdk/package.json | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/cre-sdk-examples/package.json b/packages/cre-sdk-examples/package.json index 011f23b5..fbf719f6 100644 --- a/packages/cre-sdk-examples/package.json +++ b/packages/cre-sdk-examples/package.json @@ -1,7 +1,7 @@ { "name": "@chainlink/cre-sdk-examples", "private": true, - "version": "1.1.3-alpha.1", + "version": "1.1.3", "type": "module", "author": "Ernest Nowacki", "license": "BUSL-1.1", @@ -16,7 +16,7 @@ }, "dependencies": { "@bufbuild/protobuf": "2.6.3", - "@chainlink/cre-sdk": "1.1.3-alpha.1", + "@chainlink/cre-sdk": "workspace:*", "viem": "2.34.0", "zod": "3.25.76" }, diff --git a/packages/cre-sdk/package.json b/packages/cre-sdk/package.json index 105a7ac5..9a8a90d9 100644 --- a/packages/cre-sdk/package.json +++ b/packages/cre-sdk/package.json @@ -1,6 +1,6 @@ { "name": "@chainlink/cre-sdk", - "version": "1.1.3-alpha.1", + "version": "1.1.3", "type": "module", "main": "dist/index.js", "types": "dist/index.d.ts", @@ -60,7 +60,7 @@ "dependencies": { "@bufbuild/protobuf": "2.6.3", "@bufbuild/protoc-gen-es": "2.6.3", - "@chainlink/cre-sdk-javy-plugin": "1.1.1", + "@chainlink/cre-sdk-javy-plugin": "workspace:*", "@standard-schema/spec": "1.0.0", "viem": "2.34.0", "zod": "3.25.76" From ccc00e36a1d95b14764586d20c0a3a5401c6ed04 Mon Sep 17 00:00:00 2001 From: ernest-nowacki Date: Tue, 3 Mar 2026 14:27:08 +0100 Subject: [PATCH 16/18] Simplify error messages --- packages/cre-sdk/bin/cre-compile.ts | 7 ++++++- packages/cre-sdk/scripts/src/compile-workflow.ts | 11 +++++++---- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/packages/cre-sdk/bin/cre-compile.ts b/packages/cre-sdk/bin/cre-compile.ts index 5e23ca18..38a34a19 100755 --- a/packages/cre-sdk/bin/cre-compile.ts +++ b/packages/cre-sdk/bin/cre-compile.ts @@ -1,6 +1,7 @@ #!/usr/bin/env bun import { main as compileWorkflow } from "../scripts/src/compile-workflow"; +import { WorkflowRuntimeCompatibilityError } from "../scripts/src/validate-workflow-runtime-compat"; const main = async () => { const cliArgs = process.argv.slice(2); @@ -26,6 +27,10 @@ const main = async () => { // CLI entry point main().catch((e) => { - console.error(e); + if (e instanceof WorkflowRuntimeCompatibilityError) { + console.error(`\n❌ ${e.message}`); + } else { + console.error(e); + } process.exit(1); }); diff --git a/packages/cre-sdk/scripts/src/compile-workflow.ts b/packages/cre-sdk/scripts/src/compile-workflow.ts index ebe6775b..7e11eab9 100644 --- a/packages/cre-sdk/scripts/src/compile-workflow.ts +++ b/packages/cre-sdk/scripts/src/compile-workflow.ts @@ -3,6 +3,7 @@ import { mkdir } from 'node:fs/promises' import path from 'node:path' import { main as compileToJs } from './compile-to-js' import { main as compileToWasm } from './compile-to-wasm' +import { WorkflowRuntimeCompatibilityError } from './validate-workflow-runtime-compat' export const main = async (inputFile?: string, outputWasmFile?: string) => { const cliArgs = process.argv.slice(3) @@ -41,9 +42,7 @@ export const main = async (inputFile?: string, outputWasmFile?: string) => { await mkdir(path.dirname(resolvedJsOutput), { recursive: true }) console.info(`🚀 Compiling workflow`) - console.info(`📁 Input: ${resolvedInput}`) - console.info(`🧪 JS out: ${resolvedJsOutput}`) - console.info(`🎯 WASM out:${resolvedWasmOutput}\n`) + console.info(`📁 Input: ${resolvedInput}\n`) // Step 1: TS/JS → JS (bundled) console.info('📦 Step 1: Compiling JS...') @@ -60,7 +59,11 @@ export const main = async (inputFile?: string, outputWasmFile?: string) => { // Optional: allow direct CLI usage if (import.meta.main) { main().catch((e) => { - console.error(e) + if (e instanceof WorkflowRuntimeCompatibilityError) { + console.error(`\n❌ ${e.message}`) + } else { + console.error(e) + } process.exit(1) }) } From 3a1136d6806eca90a7d6f582ba5cedbde8baa56e Mon Sep 17 00:00:00 2001 From: ernest-nowacki Date: Tue, 3 Mar 2026 14:37:26 +0100 Subject: [PATCH 17/18] Alpha release 2 --- packages/cre-sdk-examples/package.json | 4 ++-- packages/cre-sdk/package.json | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/cre-sdk-examples/package.json b/packages/cre-sdk-examples/package.json index fbf719f6..641e784f 100644 --- a/packages/cre-sdk-examples/package.json +++ b/packages/cre-sdk-examples/package.json @@ -1,7 +1,7 @@ { "name": "@chainlink/cre-sdk-examples", "private": true, - "version": "1.1.3", + "version": "1.1.3-alpha.2", "type": "module", "author": "Ernest Nowacki", "license": "BUSL-1.1", @@ -16,7 +16,7 @@ }, "dependencies": { "@bufbuild/protobuf": "2.6.3", - "@chainlink/cre-sdk": "workspace:*", + "@chainlink/cre-sdk": "1.1.3-alpha.2", "viem": "2.34.0", "zod": "3.25.76" }, diff --git a/packages/cre-sdk/package.json b/packages/cre-sdk/package.json index 9a8a90d9..8011db02 100644 --- a/packages/cre-sdk/package.json +++ b/packages/cre-sdk/package.json @@ -1,6 +1,6 @@ { "name": "@chainlink/cre-sdk", - "version": "1.1.3", + "version": "1.1.3-alpha.2", "type": "module", "main": "dist/index.js", "types": "dist/index.d.ts", @@ -60,7 +60,7 @@ "dependencies": { "@bufbuild/protobuf": "2.6.3", "@bufbuild/protoc-gen-es": "2.6.3", - "@chainlink/cre-sdk-javy-plugin": "workspace:*", + "@chainlink/cre-sdk-javy-plugin": "1.1.1", "@standard-schema/spec": "1.0.0", "viem": "2.34.0", "zod": "3.25.76" From f4cf52791553873481d86af905b497f1da4fb1c7 Mon Sep 17 00:00:00 2001 From: ernest-nowacki Date: Tue, 3 Mar 2026 15:23:59 +0100 Subject: [PATCH 18/18] Restore correct versions --- packages/cre-sdk-examples/package.json | 4 ++-- packages/cre-sdk/package.json | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/cre-sdk-examples/package.json b/packages/cre-sdk-examples/package.json index 641e784f..fbf719f6 100644 --- a/packages/cre-sdk-examples/package.json +++ b/packages/cre-sdk-examples/package.json @@ -1,7 +1,7 @@ { "name": "@chainlink/cre-sdk-examples", "private": true, - "version": "1.1.3-alpha.2", + "version": "1.1.3", "type": "module", "author": "Ernest Nowacki", "license": "BUSL-1.1", @@ -16,7 +16,7 @@ }, "dependencies": { "@bufbuild/protobuf": "2.6.3", - "@chainlink/cre-sdk": "1.1.3-alpha.2", + "@chainlink/cre-sdk": "workspace:*", "viem": "2.34.0", "zod": "3.25.76" }, diff --git a/packages/cre-sdk/package.json b/packages/cre-sdk/package.json index 8011db02..9a8a90d9 100644 --- a/packages/cre-sdk/package.json +++ b/packages/cre-sdk/package.json @@ -1,6 +1,6 @@ { "name": "@chainlink/cre-sdk", - "version": "1.1.3-alpha.2", + "version": "1.1.3", "type": "module", "main": "dist/index.js", "types": "dist/index.d.ts", @@ -60,7 +60,7 @@ "dependencies": { "@bufbuild/protobuf": "2.6.3", "@bufbuild/protoc-gen-es": "2.6.3", - "@chainlink/cre-sdk-javy-plugin": "1.1.1", + "@chainlink/cre-sdk-javy-plugin": "workspace:*", "@standard-schema/spec": "1.0.0", "viem": "2.34.0", "zod": "3.25.76"