npm package: @filed/printer-sdk
Description: A high-level SDK for building IPP (Internet Printing Protocol) printer servers
Author: Filed Engineering
Latest version: 0.2.3

# IPP Printer Server SDK

A high-level TypeScript SDK for building IPP (Internet Printing Protocol)
printer servers. This SDK acts as a **"File Acceptor"** that allows customers to
"print" documents from their computers directly to your backend infrastructure.

📐 **[Architecture Diagrams →](./docs/architecture.md#architecture-diagrams)**

## Overview

The IPP Printer Server SDK provides a minimalist, developer-friendly interface
for receiving print jobs from any IPP-compatible client (macOS, Windows, Linux).
Instead of printing to paper, documents are received by your backend for
processing, storage, or further handling.

**Key Design Principles:**

- **File Acceptor Focus**: Designed specifically for document ingestion, not
  traditional printing
- **Dynamic Virtual Queues**: Unlimited, on-the-fly queues based on URL paths
  (e.g., `/{customerId}/{authToken}`)
- **Stateless & Scalable**: Horizontal scaling with no hard dependencies on
  in-memory storage
- **IPP Abstraction**: Hide IPP protocol complexity behind a simple, intuitive
  API
- **Storage Adapter Pattern**: Use your own storage backend (Postgres, S3,
  DynamoDB, etc.)

## Features

- **Single Logical Printer**: One printer instance handles all queues
- **Dynamic Virtual Queues**: Unlimited queues based on URL paths - each
  customer gets their own queue (extracted from URL path)
- **Non-Blocking Processing**: Requests to different queues are processed
  independently and concurrently
- **Automatic Job Management**: Jobs are automatically completed or failed based on adapter operations
- **RFC Compliant**: Full IPP protocol support (RFC 8010, 8011, 3510, 7472)
- **Native OS Integration**: Works seamlessly with standard OS print drivers
- **TypeScript-First**: Full type safety and IntelliSense support

## Installation

This SDK is compatible with **Deno** and **Node.js 18+**. It uses standard Web
APIs (`Request`, `Response`) that are available in all modern runtimes.

### Deno

```bash
npm install @filed/printer-sdk
```

Then import using npm specifier:

```typescript
import { getJobDocument, IppPrinter } from "npm:@filed/printer-sdk@0.1.0";
```

### Node.js (18+)

```bash
npm install @filed/printer-sdk
```

```typescript
import { getJobDocument, IppPrinter } from "@filed/printer-sdk";
```

## Quick Start

### Deno Example

```typescript
// server.ts
import { getJobDocument, IppPrinter } from "npm:@filed/printer-sdk@0.1.0";
import type { PrinterStorageAdapter } from "npm:@filed/printer-sdk@0.1.0";

// Create a storage adapter (implement your own for production)
const adapter: PrinterStorageAdapter = {
  // ... implement adapter methods
};

// Create the printer
const printer = new IppPrinter({
  name: "My Printer",
  adapter,
});

// Initialize and start the server
Deno.serve({ port: 8631 }, async (req) => {
  return await printer.handle(req, async (req) => {
    // Extract queue ID from URL path (e.g., /customer123/authkey456 -> "customer123")
    const url = new URL(req.url);
    const queue = url.pathname.split("/")[1];
    return queue || "default";
  });
});
```

Run with:

```bash
deno run -A server.ts
```

### Node.js Example (18+)

Node.js 18+ includes native Web API support. Use the built-in `fetch` API or
convert Node.js http to Web APIs:

```typescript
// server.ts
import { getJobDocument, IppPrinter } from "@filed/printer-sdk";
import type { PrinterStorageAdapter } from "@filed/printer-sdk";
import { createServer } from "node:http";

// Create a storage adapter (implement your own for production)
const adapter: PrinterStorageAdapter = {
  // ... implement adapter methods
};

// Create the printer
const printer = new IppPrinter({
  name: "My Printer",
  adapter,
});

// Create HTTP server and convert to Web API Request/Response
const server = createServer(async (req, res) => {
  // Convert Node.js request to Web API Request
  const url = `http://${req.headers.host}${req.url}`;
  const body =
    req.method !== "GET" && req.method !== "HEAD"
      ? await streamToBuffer(req)
      : undefined;

  const request = new Request(url, {
    method: req.method,
    headers: req.headers as HeadersInit,
    body,
  });

  // Extract queue ID from URL path
  const extractQueue = async (req: Request) => {
    const url = new URL(req.url);
    return url.pathname.split("/")[1] || "default";
  };

  // Handle with printer
  const response = await printer.handle(request, extractQueue);

  // Convert Web API Response to Node.js response
  res.statusCode = response.status;
  response.headers.forEach((value, key) => {
    res.setHeader(key, value);
  });

  const responseBody = await response.arrayBuffer();
  res.end(Buffer.from(responseBody));
});

server.listen(8631, () => {
  console.log("IPP Printer Server running on http://localhost:8631");
});

// Helper to convert Node.js stream to buffer
async function streamToBuffer(stream: any): Promise<Uint8Array> {
  const chunks: Buffer[] = [];
  for await (const chunk of stream) {
    chunks.push(chunk);
  }
  return new Uint8Array(Buffer.concat(chunks));
}
```

Run with:

```bash
node server.js
```

### Node.js with Express

```typescript
// server.ts
import express from "express";
import { getJobDocument, IppPrinter } from "@filed/printer-sdk";

const app = express();
const printer = new IppPrinter({
  /* ... config ... */
});

// Important: IPP uses binary data (application/ipp)
// Use raw parser to handle the binary body
app.post(
  "/:customerId/:authToken",
  express.raw({ type: "application/ipp" }),
  async (req, res) => {
    // Convert Express Request to Web API Request
    const webReq = new Request(
      `${req.protocol}://${req.get("host")}${req.originalUrl}`,
      {
        method: req.method,
        headers: req.headers as HeadersInit,
        body: req.body, // req.body is a Buffer thanks to express.raw()
      }
    );

    const response = await printer.handle(webReq);

    // Send response
    res.status(response.status);
    response.headers.forEach((value, key) => res.setHeader(key, value));
    res.send(Buffer.from(await response.arrayBuffer()));
  }
);

app.listen(8631);
```

### Run the Repository Example

```bash
# Clone the repository
git clone https://github.com/filedcom/printer-sdk.git
cd printer-sdk

# Run the example server
deno run -A examples/deno.ts

# Or use the dev task with watch mode
deno task dev
```

## Architecture

### Dynamic Virtual Queues

The SDK supports unlimited, on-the-fly queues based on URL paths. Each customer
gets their own queue:

```
ipp://your-server:8631/{customerId}/{authToken}
```

- **Non-blocking**: Requests to different queues are processed independently and
  concurrently
- **Stateless**: No in-memory state dependencies - perfect for horizontal
  scaling
- **Automatic Extraction**: Queue ID is extracted from the URL path (first path segment)

### Storage Adapter Pattern

The SDK uses a storage adapter interface to abstract persistence. You can
implement your own adapter for any storage backend:

- **Postgres** - Store job metadata and document references
- **S3/GCS** - Store document files
- **DynamoDB** - Fully managed NoSQL storage
- **Redis** - Fast in-memory storage (for development)

### Queue Extraction

The SDK extracts queue IDs directly from the URL path. You provide a queue extraction function when calling `printer.handle()`:

```typescript
await printer.handle(req, async (req) => {
  const url = new URL(req.url);
  // Extract queue from path: /customer123/authkey456 -> "customer123"
  return url.pathname.split("/")[1] || "default";
});
```

### Automatic Job Management

Jobs are automatically managed by the SDK:
- **Success**: When all documents are successfully stored via the adapter, jobs are automatically marked as `completed`
- **Failure**: If any adapter operation fails (e.g., `addDocument` throws), the job is automatically marked as `aborted` with an error message
- **State Tracking**: All job state transitions are persisted through the storage adapter

## Usage

### Creating a Printer

```typescript
const printer = new IppPrinter({
  name: "My Printer", // Display name shown to users
  // uri is optional - will be derived from request URL if not provided
  adapter: myAdapter, // Storage adapter instance
});
```

### Handling Requests

```typescript
// Extract queue ID from URL path
const extractQueue = async (req: Request) => {
  const url = new URL(req.url);
  // /customer123/authkey456 -> "customer123"
  return url.pathname.split("/")[1] || "default";
};

// Handle requests
const response = await printer.handle(req, extractQueue);
```

### Storage Adapters

You must implement a `PrinterStorageAdapter` to handle job metadata and document
storage. The adapter interface allows you to use any storage backend.

**Development (using SimpleAdapter from examples):**

If you're working from the repository, you can import `SimpleAdapter`:

```typescript
import { SimpleAdapter } from "./src/services/sdk/examples/simple-adapter.ts";

// SimpleAdapter stores files on disk in print-jobs/{queue}/{jobId}/ structure
// Documents are automatically detected by MIME type (magic bytes)
const adapter = new SimpleAdapter("./print-jobs"); // Optional: specify output directory
```

**Production (implement your own):**

```typescript
import type {
  DocumentData,
  DocumentInfo,
  JobData,
  JobFilter,
  JobInfo,
  PrinterStateData,
  PrinterStorageAdapter,
} from "npm:@filed/printer-sdk@0.1.0";

class MyAdapter implements PrinterStorageAdapter {
  async createJob(data: JobData, printerUri: string): Promise<JobInfo> {
    // Store job metadata in your database
    // Return job info with unique ID
  }

  async addDocument(jobId: number, data: DocumentData): Promise<DocumentInfo> {
    // Store document data (S3, GCS, etc.)
    // Return document info
    // If this throws, the job will be automatically marked as aborted
  }

  async getJob(jobId: number): Promise<JobInfo | null> {
    // Retrieve job from storage
  }

  async getJobs(filter?: JobFilter): Promise<JobInfo[]> {
    // List jobs (optionally filtered by queue, state, etc.)
  }

  async updateJob(
    jobId: number,
    updates: Partial<JobInfo>
  ): Promise<JobInfo> {
    // Update job state
    // Called automatically by SDK when jobs complete/fail
  }

  async getPrinterState(): Promise<PrinterStateData> {
    // Return current printer state
  }
}
```

See
[`src/services/sdk/examples/simple-adapter.ts`](./src/services/sdk/examples/simple-adapter.ts)
for a complete reference implementation.

### Low-Level IPP Parser

For advanced use cases, you can use the low-level IPP parser (all exports are
available from the main package):

```typescript
import {
  decodeRequest,
  encodeResponse,
  Operation,
  StatusCode,
} from "npm:@filed/printer-sdk@0.1.0";

// Decode an IPP request
const request = await decodeRequest(requestBody);

// Create a response
const response = encodeResponse({
  version: { major: 2, minor: 0 },
  statusCode: StatusCode.SuccessfulOk,
  requestId: request.requestId,
  operationAttributes: [],
  groups: [],
});
```

## Customer Setup

Customers configure their printer URI as:

```
ipp://your-server:8631/{customerId}/{authToken}
```

### macOS Setup

**Command Line:**

```bash
lpadmin -p MyPrinter -E -v ipp://localhost:8631/customer123/authkey456 -o printer-is-shared=false
```

**GUI:**

1. Open **System Settings** → **Printers & Scanners**
2. Click **+** → **IP** tab
3. Address: `localhost:8631`
4. Protocol: **Internet Printing Protocol - IPP**
5. Queue: `customer123/authkey456`

### Windows Setup

1. **Settings** → **Printers & scanners** → **Add device** → **Add manually**
2. Select **Add a printer using an IP address or hostname**
3. Choose **Internet Printing Protocol (IPP)**
4. Enter:
   - Hostname: `localhost`
   - Port: `8631`
   - Queue: `customer123/authkey456`

## RFC Compliance

| RFC                                                       | Title                  | Status |
| --------------------------------------------------------- | ---------------------- | ------ |
| [RFC 8010](https://datatracker.ietf.org/doc/html/rfc8010) | Encoding and Transport | ✅     |
| [RFC 8011](https://datatracker.ietf.org/doc/html/rfc8011) | Model and Semantics    | ✅     |
| [RFC 3510](https://datatracker.ietf.org/doc/html/rfc3510) | IPP URL Scheme         | ✅     |
| [RFC 7472](https://datatracker.ietf.org/doc/html/rfc7472) | IPPS URL Scheme        | ✅     |

**Supported Operations:** Print-Job, Create-Job, Send-Document,
Get-Printer-Attributes, Get-Jobs, Get-Job-Attributes, Cancel-Job, Validate-Job

## API Reference

### Main Exports

- **`IppPrinter`**: Main printer class
- **`IppHandler`**: Low-level request handler (advanced)
- **`getJobDocument(doc)`**: Helper to get document data and filename

### Types

- **`PrinterConfig`**: Configuration for `IppPrinter` (name, adapter, optional uri)
- **`PrinterStorageAdapter`**: Interface for storage adapters
- **`JobInfo`**: Job metadata
- **`DocumentInfo`**: Document metadata

### Parser Exports

All parser exports are available from the main package:

- **`decodeRequest()`**, **`decodeResponse()`**: Decode IPP messages
- **`encodeRequest()`**, **`encodeResponse()`**: Encode IPP messages
- **`Operation`**, **`StatusCode`**: IPP constants
- All IPP types and utilities

## Examples

- [`examples/deno.ts`](./examples/deno.ts) - Complete production-ready example
  server

## Documentation

- **[AGENTS.md](./AGENTS.md)** - Context and instructions for AI coding agents
- [`docs/architecture.md`](./docs/architecture.md) - Detailed architecture and
  design
- [`docs/requirements.md`](./docs/requirements.md) - Requirements and design
  goals

## Development

```bash
# Run development server with watch mode
deno task dev

# Run tests
deno test

# Type check
deno task check

# Lint
deno task lint
```

## License

MIT