npm package: @marmarteam/sdk
Description: TypeScript client for the Marmar Clinical Decision Support API
Latest version: 0.1.4

# Marmar CDS TypeScript SDK

Typed client for integrating with the Marmar Clinical Decision Support (CDS) API.
The SDK provides:

- A strongly-typed `MarmarClient` over the core REST API
- Built‑in error handling via `ApiError`
- Webhook signature helpers for secure event handling
- Generated TypeScript types for all OpenAPI and FHIR schemas

## Installation

```bash
npm install @marmarteam/sdk
```

> The package targets modern Node and browser environments with `fetch` available.
> For Node < 18 or custom runtimes, see **Client configuration** below.

## Quickstart

```ts
import { createMarmarClient } from '@marmarteam/sdk';

const client = createMarmarClient({
  baseUrl: 'https://cds.marmar.life/v1',
  apiKey: process.env.MARMAR_API_KEY!,
  tenantCode: process.env.MARMAR_TENANT_CODE!,
});

const patient = await client.createOrUpdatePatient({
  externalId: 'mrn-123',
  demographics: {
    firstName: 'Ada',
    lastName: 'Lovelace',
    sex: 'FEMALE',
  },
});

const assessmentQueued = await client.createAssessment({
  patient: {
    patientId: patient.patientId,
  },
  medications: [
    {
      name: 'Metformin',
      dosage: { amount: 500, unit: 'mg', frequency: 'BID' },
    },
  ],
});

console.log(`Assessment ${assessmentQueued.assessmentId} queued at ${assessmentQueued.createdAt}`);

const roster = await client.listPatients();
console.log(`Loaded ${roster.patients.length} patients`);
```

## Client configuration

`createMarmarClient` accepts the following configuration:

```ts
import type { CreateMarmarClientConfig } from '@marmarteam/sdk';

const config: CreateMarmarClientConfig = {
  baseUrl: 'https://cds.marmar.life/v1',
  apiKey: process.env.MARMAR_API_KEY!,
  tenantCode: process.env.MARMAR_TENANT_CODE!,
  // Optional: custom fetch implementation (for Node < 18, tests, or edge runtimes)
  // fetch: yourCustomFetch,
};
```

- `baseUrl`: Base URL for the Marmar CDS API. The SDK appends relative paths such as `/patients` and `/assessments`.
- `apiKey`: Tenant API key. Sent as `X-API-Key` and `X-Marmar-Api-Key`.
- `tenantCode`: Human‑readable tenant code used to scope tenant‑specific operations.
- `fetch`: Optional custom `fetch` implementation. If omitted, the global `fetch` is used.

Per‑request overrides and options are available via `RequestOptions`:

```ts
const roster = await client.listPatients({
  tenantCode: 'another-tenant', // override configured tenant for this call
  signal: abortController.signal,
  headers: {
    'X-Request-Id': 'example-id',
  },
});
```

## MarmarClient methods

### Tenants

#### `registerTenant`

Register a new tenant and obtain credentials (primarily for sandbox / provisioning flows).

```ts
const registration = await client.registerTenant({
  organizationName: 'Example Health',
  contactEmail: 'ops@example.org',
});

console.log(registration.tenantCode, registration.apiKey);
```

### Patients

#### `createOrUpdatePatient`

Create or update a patient profile for the current tenant.

```ts
const patient = await client.createOrUpdatePatient({
  externalId: 'mrn-123',
  demographics: {
    firstName: 'Ada',
    lastName: 'Lovelace',
    sex: 'FEMALE',
  },
});

console.log(patient.patientId, patient.created ? 'created' : 'updated');
```

#### `listPatients`

List patients for the current tenant.

```ts
const patients = await client.listPatients();

for (const item of patients.patients) {
  console.log(item.patientId, item.externalId);
}
```

### Assessments

#### `createAssessment`

Queue a medication safety assessment for a patient.

```ts
const assessmentQueued = await client.createAssessment({
  patient: {
    patientId: patient.patientId,
  },
  medications: [
    {
      name: 'Metformin',
      dosage: { amount: 500, unit: 'mg', frequency: 'BID' },
    },
  ],
});
```

#### `getAssessment`

Retrieve an assessment by identifier.

```ts
const assessment = await client.getAssessment(assessmentQueued.assessmentId);

console.log(assessment.assessmentId, assessment.summary?.text);
```

### Webhooks

#### `configureTenantWebhook`

Configure outbound webhook delivery for the tenant.

```ts
await client.configureTenantWebhook({
  url: 'https://example.org/webhooks/marmar',
  secret: process.env.MARMAR_WEBHOOK_SECRET!,
  events: ['assessment.completed'],
});
```

The same `secret` is used to generate and verify webhook signatures (see below).

## Webhook verification

The SDK includes helpers for securely verifying webhook signatures and generating signatures for tests:

- `verifyWebhookSignature`
- `signWebhookPayload`

### Verifying incoming webhooks

```ts
import { verifyWebhookSignature } from '@marmarteam/sdk';

const webhookSecret = process.env.MARMAR_WEBHOOK_SECRET!;

// Example using Express with a raw body parser
app.post(
  '/webhooks/marmar',
  express.raw({ type: 'application/json' }),
  (req, res) => {
    const signature = req.header('X-Marmar-Signature')!;
    const timestamp = req.header('X-Marmar-Timestamp')!;
    const payload = req.body; // Buffer or string

    const result = verifyWebhookSignature({
      payload,
      signature,
      secret: webhookSecret,
      timestamp,
      // Optional: toleranceSeconds (defaults to 300 seconds)
      // toleranceSeconds: 600,
    });

    if (!result.valid) {
      console.warn('Invalid Marmar webhook:', result.reason);
      return res.sendStatus(400);
    }

    const eventJson = payload.toString('utf-8');
    const event = JSON.parse(eventJson);

    // Handle AssessmentCompletedEvent
    // ...

    return res.sendStatus(204);
  },
);
```

### Generating signatures (tests and local tooling)

```ts
import { signWebhookPayload } from '@marmarteam/sdk';

const secret = 'test-secret';
const timestamp = Math.floor(Date.now() / 1000).toString();
const payload = { type: 'assessment.completed' };

const signature = signWebhookPayload(secret, payload, timestamp);
```

## Error handling

API errors throw an instance of `ApiError`, which includes the HTTP status and parsed response body when available.

```ts
import { ApiError } from '@marmarteam/sdk';

try {
  const assessment = await client.getAssessment('non-existent-id');
  console.log(assessment);
} catch (error) {
  if (error instanceof ApiError) {
    console.error('Marmar API error', {
      status: error.status,
      statusText: error.statusText,
      body: error.body,
    });
  } else {
    console.error('Unexpected error', error);
  }
}
```

## Using generated types and FHIR helpers

The SDK exports the full OpenAPI types from the Marmar CDS API so you can type your own HTTP calls and data pipelines.

```ts
import type { components, operations } from '@marmarteam/sdk';

type PatientRequest = components['schemas']['PatientRequest'];
type PatientResponse = components['schemas']['PatientResponse'];
type AssessmentQueuedResponse = components['schemas']['AssessmentQueuedResponse'];

type SubmitFhirBundleResponse =
  operations['submitFhirBundle']['responses'][200]['content']['application/json'];
```

You can also use the FHIR types when constructing bundles for ingestion:

```ts
import type { components } from '@marmarteam/sdk';

type FhirBundle = components['schemas']['FhirBundle'];

const bundle: FhirBundle = {
  resourceType: 'Bundle',
  type: 'collection',
  entry: [
    // Patient, MedicationStatement, Condition, etc.
  ],
};
```

The FHIR endpoints are currently accessed via standard HTTP clients; the SDK provides the types so that your requests and responses remain type‑safe.

## Environment support

- **Node**: Recommended on Node 18+ where `fetch` is available by default. For older Node versions, supply a `fetch` implementation.
- **Browser / front‑end**: Works with modern browsers that provide `fetch`. Do not expose secret API keys directly in public clients.

## Further reading

For full API reference, FHIR details, and end‑to‑end integration guides, see the Marmar CDS documentation at `https://docs.cds.marmar.life`.