npm package: @nanograph/sdk
Description: Official Volted Node.js SDK
Author: Nanograph
Latest version: 0.2.1

# Nano SDK for NodeJS

This package provides the NodeJS implementation of the Nanograph SDK, which allows you to create node servers that can communicate with frontend clients like Volted Studio.

## Installation

```bash
npm i @nanograph/sdk
```

## Usage

### Creating a Server

```typescript
import { NanoSDK } from '@nanograph/sdk'

// Initialize SDK
const sdk = new NanoSDK({
  serverName: 'My Node Server',
  serverUid: 'my-node-server',
  port: 3016,
  nodesPath: 'src/nodes'
})

// Start the server
async function main() {
  await sdk.start()
  console.log('Server started')
}

// Handle shutdown
sdk.onShutdown(() => {
  console.log('Server is shutting down')
})

// Graceful shutdown
process.on('SIGINT', () => sdk.stop())
process.on('SIGTERM', () => sdk.stop())

// Run the server
main().catch(console.error)
```

### Creating Nodes

```typescript
import { NanoSDK, NodeDefinition, NodeInstance } from '@nanograph/sdk'

// Define the node
const nodeDef: NodeDefinition = {
  uid: 'my-unique-node-id',
  name: 'My Node',
  category: 'Processing',
  version: '1.0.0',
  type: 'server',
  description: 'Description of my node',
  inputs: [
    {
      name: 'input1',
      type: 'string',
      description: 'First input'
    }
  ],
  outputs: [
    {
      name: 'output1',
      type: 'string',
      description: 'First output'
    }
  ],
  parameters: [
    {
      name: 'param1',
      type: 'boolean',
      value: true,
      default: true,
      label: 'Parameter 1',
      description: 'Description of parameter 1'
    }
  ]
}

// Register the node
const myNode: NodeInstance = NanoSDK.registerNode(nodeDef)

// Implement the execution logic
myNode.execute = async ({ inputs, parameters, context }) => {
  // Get input values (inputs are now arrays)
  const input1 = (inputs.input1 && inputs.input1[0]) || ''
  
  // Send status update
  context.sendStatus({ type: 'running', message: 'Processing...' })
  
  // Check for abort
  if (context.isAborted()) {
    throw new Error('Execution aborted')
  }
  
  // Process the inputs
  const output1 = `Processed: ${input1}`
  
  // Return the outputs (outputs must be arrays)
  return { output1: [output1] }
}

export default myNode
```

Multiple nodes can be defined in a single file, or you can organize your nodes in a directory structure.

## Folder Structure

Recommended project structure:

```
my-node-server/
├── src/
│   ├── index.ts          # Entry point
│   └── nodes/            # Nodes directory (automatically scanned)
│       ├── processing/   # Category directory
│       │   ├── TextUppercase.ts  # Single node file
│       │   └── complex/   # Complex node with multiple files
│       │       ├── index.ts      # Main node definition
│       │       └── utils.ts      # Helper functions
│       └── math/         # Another category
└── package.json         # Dependencies
```

## Configuration

The SDK accepts the following configuration options:

| Key                   | Type      | Default   | Description                                                        |
|-----------------------|-----------|-----------|--------------------------------------------------------------------|
| `serverName`          | `string`  | (required) | Human-readable name of your server                                |
| `serverUid`           | `string`  | (required) | Unique identifier for your server (no spaces or special chars)   |
| `port`                | `number`  | `3016`     | HTTP port for server to listen on                                |
| `nodesPath`           | `string`  | `"nodes"`  | Directory path containing node definition files                   |
| `autoWatch`           | `boolean` | `true`     | Automatically reload nodes when files change                     |
| `watchDebounceTime`   | `number`  | `500`      | Debounce time (ms) for file change detection                     |

**Example configuration:**

```typescript
const sdk = new NanoSDK({
  serverName: 'My Processing Server',
  serverUid: 'my-processing-server',
  port: 3016,
  nodesPath: 'src/nodes',
  autoWatch: true,
  watchDebounceTime: 1000
})
```

## API Reference

### `NanoSDK`

The main SDK class.

#### `constructor(config: NanoSDKConfig)`

Create a new SDK instance.

#### `static registerNode(definition: NodeDefinition): NodeInstance`

Register a node with the SDK.

#### `async start(): Promise<void>`

Start the node server.

#### `async stop(): Promise<void>`

Stop the node server.

#### `onShutdown(handler: () => void | Promise<void>): void`

Register a handler to be called when the server is shutting down.

### `NodeInstance`

Represents a registered node.

#### `definition: NodeDefinition`

The node definition.

#### `execute: ExecuteFunction`

The function to execute when the node is called.

### `ExecuteFunction`

```typescript
type ExecuteFunction = (ctx: ExecutionContext) => Promise<NodeOutputs> | NodeOutputs
```

### `ExecutionContext`

```typescript
interface ExecutionContext {
  inputs: NodeInputs
  parameters: Parameter[]
  context: {
    sendStatus: (status: NodeStatus) => void
    isAborted: () => boolean
    graphNode: GraphNode
    instanceId: string
  }
}
```

## NodeStatus Reference

The `NodeStatus` object is used to communicate the current status, progress, or result of a node execution back to the orchestrator. You send it using `context.sendStatus(status)` from within your node's `execute` function.

**NodeStatus fields:**

| Field      | Type                | Description                                                          |
|------------|---------------------|----------------------------------------------------------------------|
| `type`     | `string`            | One of: `'idle'`, `'running'`, `'complete'`, `'error'`, `'missing'`  |
| `message`  | `string` (optional) | Human-readable status or error message                               |
| `progress` | `object` (optional) | Progress info, e.g. `{ step: 2, total: 5 }`                          |
| `outputs`  | `object` (optional) | Output values (only for `'complete'` status)                         |

**Example: Sending progress updates from a node**

```typescript
myNode.execute = async ({ inputs, parameters, context }) => {
  const totalSteps = 5
  for (let step = 1; step <= totalSteps; step++) {
    if (context.isAborted()) {
      throw new Error('Aborted!')
    }

    // Simulate work
    await new Promise(res => setTimeout(res, 1000))
    // Send progress update
    context.sendStatus({
      type: 'running',
      message: `Processing step ${step}/${totalSteps}`,
      progress: { step, total: totalSteps }
    })
  }
  // Just return the outputs as arrays; the SDK will send the 'complete' status automatically
  return { result: ['done'] }
}
```

> **Note:** You do **not** need to manually send a `'complete'` status at the end. The SDK will automatically send a `'complete'` status with the outputs you return from your `execute` function.

## Asset Management (NanoCore integration)

> Configuration: Set `NANOCORE_HTTP_ENDPOINT` (e.g., `https://host:3001`) and `NANOCORE_TOKEN` in your environment; the SDK uses these to communicate with NanoCore.

### Asset Reference Format

Assets are referenced by their SHA-256 content hash:

```
sha256:<64-hex-chars>
```
- Example: `sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855`

The hash is the canonical identifier used across all NanoCore APIs, SDKs, and frontends. To resolve/download, clients use the server’s HTTP endpoint (`nanocoreHttpEndpoint`, e.g., `https://host:3001`). In this SDK, configure it via the `NANOCORE_HTTP_ENDPOINT` environment variable.

### Node Output Convention

Nodes should output asset hashes (not blobs/base64):

```js
return { output1: [ "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" ] }
```

---

### 1. Uploading an Asset

Use the `uploadAsset` function to upload a file (from a file path, Buffer, or stream) to NanoCore.

```typescript
import { uploadAsset } from '@nanograph/sdk';

// Upload from a file path
const result = await uploadAsset('/path/to/myfile.png', {
  type: 'image', // optional
  meta: { description: 'My image' } // optional
});

// Result:
console.log(result.hash);  // Asset hash (e.g. sha256:e3b0c44298fc1c14...)
console.log(result.name);  // Original filename
console.log(result.size);  // File size in bytes
```

**You can also upload a Buffer or a stream:**
```typescript
const buffer = Buffer.from(...);
await uploadAsset(buffer, { type: 'image' });
```

---

### 2. Downloading an Asset

To download an asset by its hash:

```typescript
import { resolveAsset } from '@nanograph/sdk';

const hash = 'sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855';
const stream = await resolveAsset(hash);
// `stream` is a Node.js Readable Stream

// To get the content as a Buffer (for small files only):
const buffer = await resolveAsset(hash, { asBuffer: true });
```

---

### 3. Get the Direct Download URL (Backend Use)

If you need the direct download URL (with backend authentication):

```typescript
import { getAssetDownloadUrl } from '@nanograph/sdk';

const url = getAssetDownloadUrl(hash);
// url = 'https://.../assets/sha256:<hex>/download'
```

---

### 4. Generate a Temporary Signed URL (Frontend Use)

To allow a client (e.g. browser) to download an asset without exposing your API token, generate a temporary signed URL:

```typescript
import { getAssetPresignedUrl } from '@nanograph/sdk';

const { url, expiresIn } = await getAssetPresignedUrl(hash);
// `url` is a temporary direct-access URL to the asset (valid for `expiresIn` seconds)
```

This URL can be safely sent to the frontend, which can download the asset without any further authentication.

---

### 5. Asset Reference Format

The asset reference format is always:
```
sha256:<64-hex-chars>
```
Resolution uses the configured `NANOCORE_HTTP_ENDPOINT` (NodeServer `nanocoreHttpEndpoint`).

---

**Note:**  
All SDK functions automatically use the configuration provided by NanoCore.  
No sensitive information (such as API tokens) is ever exposed to the frontend, thanks to the use of signed URLs.

## License

MIT