npm package: @fatagnus/dink-sdk
Description: TypeScript SDK for Dink edge mesh platform
Author: Ozan Turksever
Latest version: 2.32.0

# @fatagnus/dink-sdk

TypeScript SDK for the Dink edge mesh platform.

This SDK is for **Lite Edge** - edges that require constant connectivity. For offline-first applications, use `@fatagnus/dink-sync` instead.

| Edge Type | SDK | Use Case |
|-----------|-----|----------|
| **Lite Edge** | ✅ This SDK | Always-connected IoT, RPC services |
| **Offline-First Edge** | `@fatagnus/dink-sync` | Offline data with auto-sync |
| **Full Edge** | Not yet available | Local NATS services (planned) |

## Installation

```bash
npm install @fatagnus/dink-sdk
```

## Quick Start (createEdgeWorker)

The fastest way to start an edge service:

```typescript
import { createEdgeWorker } from '@fatagnus/dink-sdk/edge';
import { createServices } from './generated/registry.js';
import { SensorServiceImpl } from './sensor-impl.js';

await createEdgeWorker({
	services: createServices({ sensor: new SensorServiceImpl() }),
	labels: { region: 'us-east', type: 'sensor' },
});
```

Credentials resolve automatically from `DINK_EDGE_KEY` or `DINK_API_KEY` environment variables. Graceful shutdown on SIGINT/SIGTERM is built-in.

For more control, see the [EdgeClient](#quick-start-edgeclient) section below.

## Quick Start (EdgeClient)

### Edge Client

Connect an edge device to the Dink platform:

```typescript
import { EdgeClient } from '@fatagnus/dink-sdk';

// With embedded URL keys (recommended), no serverUrl needed!
const edge = new EdgeClient({
  apiKey: process.env.DINK_API_KEY!, // Contains embedded URL + credentials
});

// Edge ID and App ID are extracted from the API key
console.log(`Edge ID: ${edge.getEdgeId()}`);  // e.g., "my-edge-device"
console.log(`App ID: ${edge.getAppId()}`);    // e.g., "my-app"

await edge.connect();

// Expose a service
await edge.exposeService({
  name: 'sensor',
  version: '1.0.0',
  definition: {
    name: 'sensor',
    version: '1.0.0',
    handleRequest: async (method, data) => {
      if (method === 'read') {
        return { temperature: 22.5 };
      }
      throw new Error(`Unknown method: ${method}`);
    },
  },
});
```

### Center Client

Connect from a center/cloud service to discover and call edge services:

```typescript
import { CenterClient } from '@fatagnus/dink-sdk';

const center = new CenterClient({
  serverUrl: 'nats://localhost:4222',
  apiKey: process.env.DINK_API_KEY,
});

await center.connect();

// Discover edges
const edges = await center.discoverEdges();

// Call a service on an edge (edge ID comes from discovery)
const result = await center.call({
  edgeId: edges[0].id, // e.g., "my-edge-device" 
  service: 'sensor',
  method: 'read',
  data: {},
});

console.log(result); // { temperature: 22.5 }
```

### Error Types

Typed errors for structured error handling:

```typescript
import { ConnectionError, AuthError, ServiceError, TimeoutError, ConfigError } from '@fatagnus/dink-sdk';
```

| Error | Code | Fields | Use Case |
|-------|------|--------|----------|
| `DinkError` | varies | `code` | Base class for all SDK errors |
| `ConnectionError` | `CONNECTION_ERROR` | | NATS connection issues |
| `AuthError` | `AUTH_ERROR` | | Invalid/expired/revoked keys |
| `ServiceError` | `SERVICE_ERROR` | `status`, `service`, `method` | RPC call failures |
| `TimeoutError` | `TIMEOUT` | `timeoutMs` | Request timeouts |
| `ConfigError` | `CONFIG_ERROR` | | Missing required configuration |

### Peer RPC (Edge-to-Edge)

Edges in the same group can call each other's services directly:

```typescript
import { EdgeClient } from '@fatagnus/dink-sdk';

const edge = new EdgeClient({ apiKey: process.env.DINK_EDGE_KEY! });
await edge.connect();

// Expose service for peer calls
await edge.exposePeerService(handler);

// Call a specific peer's service
const caller = edge.groupCaller('my-group');
const client = new SensorServiceClient('target-edge', caller);
const temp = await client.ReadTemperature({});

// Call any peer in the group (load-balanced)
const broadcastCaller = edge.groupBroadcastCaller('my-group');
```

Generated typed clients work unchanged with `groupCaller()` — same `ServiceCaller` interface as `CenterClient`.

## Loading Credentials

Use `loadDinkEnv()` to load credentials from the `.dink/` directory (managed by the `dink env` CLI) with automatic fallback to `DINK_SERVER` / `DINK_API_KEY` environment variables.

> **Note:** This is Node.js only (uses `node:fs`, `node:path`). Available via subpath import `@fatagnus/dink-sdk/env` or from the main entry point.

```typescript
import { loadDinkEnv } from '@fatagnus/dink-sdk/env';
import { CenterClient, EdgeClient } from '@fatagnus/dink-sdk';

// Load current environment (from .dink/.current) or fall back to env vars
const env = await loadDinkEnv();

const center = new CenterClient({ serverUrl: env.server, apiKey: env.apiKey });
const edge = new EdgeClient({ apiKey: env.apiKey });
```

Load a specific named environment:

```typescript
const env = await loadDinkEnv('staging'); // reads .dink/staging.yaml
```

Credential resolution order:
1. **Named env** — `.dink/<name>.yaml` (when a name is passed)
2. **Current env** — `.dink/.current` → `.dink/<current>.yaml`
3. **Env vars** — `DINK_SERVER` / `DINK_API_KEY` (CI/Docker fallback)

Error handling:

```typescript
import { DinkEnvError } from '@fatagnus/dink-sdk/env';

try {
  const env = await loadDinkEnv();
} catch (e) {
  if (e instanceof DinkEnvError) {
    // e.code === 'NO_ENVIRONMENT' — no .dink/ dir and no env vars set
    // e.code === 'NOT_FOUND'      — named env file missing
  }
}
```

This integrates with `dink env` CLI commands for managing environments. In CI/Docker where no `.dink/` directory exists, set `DINK_SERVER` and `DINK_API_KEY` as environment variables — the same code works without changes.

## API Key Formats

Dink supports two API key formats for edge authentication:

### Embedded URL Format (Recommended)

The new format embeds the dinkd server URL directly in the key, enabling easier onboarding where edge agents only need a single API key to connect:

```typescript
import { EdgeClient } from '@fatagnus/dink-sdk';

// No serverUrl needed - it's embedded in the key!
const edge = new EdgeClient({
  apiKey: process.env.DINK_API_KEY!,
});
```

The embedded key format is a base64-encoded JSON object:
```
base64({"url": "nats://dinkd.example.com:4222", "secret": "dk_..."})
```

Keys created via the Convex SDK (`createEdgeKey`) or the test utilities automatically use this format.

### Legacy Format

The legacy format is a plain string without embedded URL:
```
dk_<env>_ed_<appID>_<edgeID>_<random>
```

For legacy keys, you must provide `serverUrl` explicitly:

```typescript
const edge = new EdgeClient({
  serverUrl: 'nats://localhost:4222',
  apiKey: 'dk_prod_ed_myapp_myedge_abc123',
});
```

### Parsing Keys

Use `parseEdgeKey` to inspect a key and extract its components:

```typescript
import { parseEdgeKey } from '@fatagnus/dink-sdk';

// Parse an embedded URL key
const parsed = parseEdgeKey(embeddedKey);
console.log(parsed.serverUrl); // 'nats://dinkd.example.com:4222'
console.log(parsed.secret);    // 'dk_prod_ed_myapp_myedge_abc123'
console.log(parsed.appId);     // 'myapp'
console.log(parsed.edgeId);    // 'myedge'

// Parse a legacy key
const legacy = parseEdgeKey('dk_prod_ed_myapp_myedge_abc123');
console.log(legacy.serverUrl); // undefined (no embedded URL)
console.log(legacy.secret);    // 'dk_prod_ed_myapp_myedge_abc123'
```

### URL Override

Even with embedded URL keys, you can override the URL in the config:

```typescript
// Override the embedded URL (e.g., for local development)
const edge = new EdgeClient({
  apiKey: productionKey,
  serverUrl: 'nats://localhost:4222', // Takes precedence over embedded URL
});
```

## Handling Kick Commands

Edges can be forcibly disconnected by the server (e.g., for key revocation, maintenance, or security). Register a callback to handle kick events:

```typescript
import { EdgeClient } from '@fatagnus/dink-sdk';

const edge = new EdgeClient({ apiKey: process.env.DINK_API_KEY! });
await edge.connect();

// Register kick handler (optional but recommended)
const unsubscribe = edge.onKick((reason: string) => {
  console.log(`Received kick command: ${reason}`);
  // Clean up resources, save state, etc.
  // The edge will automatically disconnect after this callback
});

// Later, if you want to stop listening:
unsubscribe();
```

When kicked, the edge client will:
1. Call your `onKick` callback (if registered)
2. Gracefully disconnect from the server
3. Clean up all service subscriptions

**Note:** To kick an edge from your Convex backend, use `kickEdge` from `@fatagnus/dink-convex`.

## Subpath Exports

You can also import specific clients directly:

```typescript
import { EdgeClient } from '@fatagnus/dink-sdk/edge';
import { CenterClient } from '@fatagnus/dink-sdk/center';
import { loadDinkEnv } from '@fatagnus/dink-sdk/env';
```

## Naming Rules

App IDs, edge IDs, app names, and service names must follow these rules to ensure compatibility with NATS subjects:

**Allowed characters:**
- Alphanumeric: `a-z`, `A-Z`, `0-9`
- Hyphens: `-`
- Colons: `:`

**Not allowed:**
- Underscore `_` (used as key delimiter)
- Dot `.` (NATS hierarchy separator)
- Asterisk `*` (NATS wildcard)
- Greater than `>` (NATS tail wildcard)
- Dollar sign `$` (NATS system subjects)
- Spaces and whitespace

**Examples:**
```typescript
// ✓ Valid names
'my-app'
'sensor-42'
'namespace:service'
'MyApp123'

// ✗ Invalid names
'my_app'      // underscore not allowed
'my.app'      // dot not allowed
'my app'      // space not allowed
'sensor*'     // asterisk not allowed
```

## Requirements

- Node.js >= 18.0.0
- A running dinkd server

## Documentation

For full documentation, visit [https://github.com/ozanturksever/dink](https://github.com/ozanturksever/dink)

## License

MIT