npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@fullkontrol/ssi

v0.3.0

Published

Type-safe TypeScript client for the fullkontrol SSI API

Readme

@fullkontrol/ssi

Type-safe TypeScript client for the fullkontrol SSI API.

  • Fully typed — every method parameter and return value is typed from the live OpenAPI spec
  • Lightweight — built on openapi-fetch (~6 KB)
  • Auto-generated — no manual type definitions to maintain
  • Universal — ships ESM + CJS

Install

npm install @fullkontrol/ssi
# or
bun add @fullkontrol/ssi

Quick start

import { FullkontrolClient } from "@fullkontrol/ssi";

const fk = new FullkontrolClient({
  baseUrl: "https://alerts.fullkontrol.app",
  apiKey: "fk_abc123...",
});

// Send an alert — the most common operation
await fk.alert({
  workspace_id: "ws_1",
  place_id: "p_1",
  task_id: "t_1",
  to: "user_123",
  title: "Task reminder",
  body: "Your task is due in 30 minutes",
});

Constructor options

| Option | Type | Description | |--------|------|-------------| | baseUrl | string | Required — scheduler API base URL | | apiKey | string | Injected as x-service-token: Bearer <key> | | fetch | typeof fetch | Custom fetch implementation (useful for testing) |

API reference

fk.alert() — top-level alert primitive

A high-level primitive for the most common operation: send an alert to someone about a task. Handles field mapping, Date conversion, and fan-out internally.

// Send immediately to one user
const result = await fk.alert({
  workspace_id: "ws_1",
  place_id: "p_1",
  task_id: "t_1",
  to: "user_123",
  title: "Task reminder",
  body: "Your task is due in 30 minutes",
});
// result: { to, run_id, dispatched: true, dispatch: { accepted, outcome, ... } }

// Fan-out to multiple recipients — never throws, errors inline
import { isAlertError } from "@fullkontrol/ssi";

const results = await fk.alert({
  workspace_id: "ws_1",
  place_id: "p_1",
  task_id: "t_1",
  to: ["user_1", "user_2", "user_3"],
  title: "Team alert",
  body: "Task needs attention",
});

for (const r of results) {
  if (isAlertError(r)) {
    console.error(`Failed for ${r.to}: ${r.error}`);
  } else {
    console.log(`Sent to ${r.to}: run ${r.run_id}`);
  }
}

// Schedule for later
await fk.alert({
  workspace_id: "ws_1",
  place_id: "p_1",
  task_id: "t_1",
  to: "user_123",
  title: "Task reminder",
  body: "Your task is due in 30 minutes",
  at: "2025-06-15T14:00:00Z",
});

// RRULE schedule — creates a definition + materializes runs in one call
const scheduled = await fk.alert({
  workspace_id: "ws_1",
  place_id: "p_1",
  task_id: "t_1",
  to: "user_123",
  title: "Daily standup reminder",
  body: "Standup starts in 15 minutes",
  schedule: {
    rrule: "FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR;BYHOUR=9;BYMINUTE=0",
    timezone: "Europe/Oslo",
  },
});
// scheduled.schedule: { definition_id, created, already_exists, run_ids }

// WhatsApp instead of push
await fk.alert({
  workspace_id: "ws_1",
  place_id: "p_1",
  task_id: "t_1",
  to: "user_123",
  channel: "whatsapp",
  title: "Urgent",
  body: "Task overdue",
});

AlertInput fields

| Field | Type | Description | |-------|------|-------------| | workspace_id | string | Required | | place_id | string | Required | | task_id | string | Required | | to | string \| string[] | Required — recipient user ID(s). Arrays fan-out to parallel calls | | title | string | Required — notification title | | body | string | Required — notification body | | channel | "push" \| "whatsapp" | Delivery channel (default "push") | | at | string \| Date | Schedule for later. Omit for immediate dispatch | | timezone | string | IANA timezone for interpreting local at values | | schedule | AlertScheduleInput | RRULE schedule — creates a definition and materializes runs server-side |

AlertScheduleInput

| Field | Type | Description | |-------|------|-------------| | rrule | string | Required — RFC 5545 RRULE string (e.g. FREQ=DAILY;BYHOUR=9;BYMINUTE=0) | | timezone | string | Required — IANA timezone (e.g. Europe/Oslo) | | task_status | string | Task status for the materialization context (default "open") | | task_title | string | Task title for the materialization context (defaults to alert title) |

When schedule is provided, dispatch_immediately is forced to false. The backend creates an alert definition with trigger_type: "rrule" and immediately materializes runs. The result includes a schedule object with the created definition_id, run counts, and run_ids.

fk.alert.update() — update task alerts

Cancels existing alert runs for a task and re-materializes them based on updated task state. Wraps POST /v1/planner/replan-task into a flat, ergonomic interface.

// Task was rescheduled — cancel old runs and create new ones
const result = await fk.alert.update({
  workspace_id: "ws_1",
  place_id: "p_1",
  task_id: "t_1",
  timezone: "America/New_York",
  scheduled_at: new Date("2026-03-01T09:00:00Z"),
  task_status: "todo",
  task_title: "Morning check",
  assignee_user_id: "user_1",
});
// result: { cancelled_runs: 2, result: { created: 1, already_exists: 0, skipped: 1, run_ids: ["run_new"] }, inspected_definitions: 3 }

// Provide a reason for auditing
await fk.alert.update({
  workspace_id: "ws_1",
  place_id: "p_1",
  task_id: "t_1",
  timezone: "Europe/Oslo",
  scheduled_at: "2026-03-01T10:00:00Z",
  task_status: "open",
  task_title: "Weekly inspection",
  reason: "schedule_changed",
});

By default all matching definitions are replanned. Pass definition_ids to scope to specific ones (from fk.definitions.list() or fk.definitions.create()).

fk.alerts.send() — low-level direct send

// Send an alert (dispatches immediately by default)
await fk.alerts.send({
  workspace_id: "ws_1",
  place_id: "place_1",
  task_id: "task_1",
  recipient_user_id: "user_1",
  title: "Test notification",
  body: "This is a test alert",
});

// Schedule for later (dispatch_immediately defaults to false when scheduled_for is set)
await fk.alerts.send({
  workspace_id: "ws_1",
  place_id: "place_1",
  task_id: "task_1",
  recipient_user_id: "user_1",
  title: "Scheduled test",
  body: "Will be dispatched later",
  scheduled_for: "2025-06-15T14:00:00Z",
});

// RRULE schedule (creates definition + materializes in one call)
const result = await fk.alerts.send({
  workspace_id: "ws_1",
  place_id: "place_1",
  task_id: "task_1",
  recipient_user_id: "user_1",
  title: "Daily check",
  body: "Time for your daily check",
  schedule: {
    rrule: "FREQ=DAILY;BYHOUR=10;BYMINUTE=0",
    timezone: "America/New_York",
  },
});
// result.schedule: { definition_id, created, already_exists, run_ids }

fk.definitions

// Create a new alert definition
await fk.definitions.create({
  workspace_id: "ws_1",
  trigger_type: "relative_to_task_time",
  offsets: ["-30m"],
  condition: { allowed_statuses: ["open"], stop_statuses: ["done"] },
  recipients: { assignee: true },
  message_template: { title: "Reminder", body: "Task is due soon" },
  created_by: "admin",
});

// List definitions for a workspace
await fk.definitions.list({ workspace_id: "ws_1" });
await fk.definitions.list({ workspace_id: "ws_1", enabled: "true" });

// Update a definition
await fk.definitions.update("def_id", { enabled: false });
await fk.definitions.update("def_id", { offsets: ["-15m", "-1h"] });

fk.planner

// Materialize alert runs for a task
await fk.planner.materialize({
  task: {
    workspace_id: "ws_1",
    place_id: "place_1",
    task_id: "task_1",
    timezone: "Europe/Oslo",
    scheduled_at: "2025-06-15T14:00:00Z",
    task_status: "open",
    task_title: "Weekly inspection",
  },
});

// Replan after a task changes (cancels existing runs + re-materializes)
await fk.planner.replanTask({
  task: {
    workspace_id: "ws_1",
    place_id: "place_1",
    task_id: "task_1",
    timezone: "Europe/Oslo",
    scheduled_at: "2025-06-16T10:00:00Z",
    task_status: "open",
    task_title: "Weekly inspection",
  },
  reason: "schedule_changed",
});

fk.runs

// List runs with filters
await fk.runs.list({ workspace_id: "ws_1", limit: 50 });
await fk.runs.list({ workspace_id: "ws_1", status: "failed" });
await fk.runs.list({
  workspace_id: "ws_1",
  task_id: "task_1",
  from: "2025-06-01T00:00:00Z",
  to: "2025-06-30T23:59:59Z",
});

// Get a single run
const run = await fk.runs.get("run_id");

// Get delivery attempts for a run
const { attempts } = await fk.runs.attempts("run_id");
const { attempts: last5 } = await fk.runs.attempts("run_id", { limit: 5 });

// Get status transition events for a run
const { events } = await fk.runs.events("run_id");

// Cancel all pending runs for a task
await fk.runs.cancelByTask({
  workspace_id: "ws_1",
  task_id: "task_1",
  reason: "task_deleted",
});

fk.runs.deadLetter

// List dead-lettered runs
await fk.runs.deadLetter.list({ workspace_id: "ws_1" });

// Redrive a single run (resets to planned status)
await fk.runs.deadLetter.redrive({ run_id: "run_abc" });

// Bulk redrive
await fk.runs.deadLetter.redriveBulk({
  run_ids: ["run_abc", "run_def"],
});

fk.apiKeys

// Create a new API key (plaintext returned only once)
const { key, id } = await fk.apiKeys.create({
  label: "Firebase integration",
  created_by: "admin",
});
console.log(key); // fk_a1b2c3...

// List all keys (metadata only, no secrets)
const { keys } = await fk.apiKeys.list();

// Revoke a key
await fk.apiKeys.revoke("key_id");

fk.auth

// Issue a short-lived service JWT
const { token, expires_at } = await fk.auth.issueToken({ ttl_seconds: 900 });

// Use defaults
const jwt = await fk.auth.issueToken();

Error handling

All methods throw FullkontrolApiError on non-2xx responses:

import { FullkontrolClient, FullkontrolApiError } from "@fullkontrol/ssi";

try {
  await fk.definitions.update("nonexistent", { enabled: false });
} catch (e) {
  if (e instanceof FullkontrolApiError) {
    console.log(e.status);  // 404
    console.log(e.message); // "Definition not found: nonexistent"
    console.log(e.body);    // { error: "Definition not found: nonexistent" }
  }
}

| Status | Meaning | |--------|---------| | 400 | Validation error (check request body) | | 401 | Unauthorized (missing or invalid API key) | | 404 | Resource not found | | 500 | Server error | | 503 | Auth not configured / service unavailable |

Examples

Firebase Functions: materialize on task create

import { onDocumentCreated } from "firebase-functions/v2/firestore";
import { FullkontrolClient } from "@fullkontrol/ssi";

const fk = new FullkontrolClient({
  baseUrl: process.env.SCHEDULER_URL!,
  apiKey: process.env.SCHEDULER_API_KEY!,
});

export const onTaskCreated = onDocumentCreated(
  "workspaces/{workspaceId}/tasks/{taskId}",
  async (event) => {
    const task = event.data?.data();
    if (!task) return;

    await fk.planner.materialize({
      task: {
        workspace_id: event.params.workspaceId,
        place_id: task.place_id,
        task_id: event.params.taskId,
        timezone: task.timezone,
        scheduled_at: task.scheduled_at,
        task_status: task.status,
        task_title: task.title,
        assignee_user_id: task.assignee_id,
        owner_user_id: task.owner_id,
      },
    });
  }
);

Firebase Functions: replan on task update

export const onTaskUpdated = onDocumentUpdated(
  "workspaces/{workspaceId}/tasks/{taskId}",
  async (event) => {
    const before = event.data?.before.data();
    const after = event.data?.after.data();
    if (!before || !after) return;

    const scheduleChanged = before.scheduled_at !== after.scheduled_at;
    const statusChanged = before.status !== after.status;

    if (!scheduleChanged && !statusChanged) return;

    await fk.alert.update({
      workspace_id: event.params.workspaceId,
      place_id: after.place_id,
      task_id: event.params.taskId,
      timezone: after.timezone,
      scheduled_at: after.scheduled_at,
      task_status: after.status,
      task_title: after.title,
      assignee_user_id: after.assignee_id,
      owner_user_id: after.owner_id,
      reason: scheduleChanged ? "schedule_changed" : "status_changed",
    });
  }
);

Firebase Functions: cancel on task delete

export const onTaskDeleted = onDocumentDeleted(
  "workspaces/{workspaceId}/tasks/{taskId}",
  async (event) => {
    await fk.runs.cancelByTask({
      workspace_id: event.params.workspaceId,
      task_id: event.params.taskId,
      reason: "task_deleted",
    });
  }
);

Dead-letter monitoring cron

import { onSchedule } from "firebase-functions/v2/scheduler";

export const checkDeadLetters = onSchedule("every 1 hours", async () => {
  const { data } = await fk.runs.deadLetter.list({ limit: 500 });

  if (data.length === 0) return;

  console.warn(`${data.length} dead-lettered runs found`);

  // Auto-redrive all
  await fk.runs.deadLetter.redriveBulk({
    run_ids: data.map((run) => run.id),
  });
});

Using types for your own interfaces

import type {
  AlertDefinition,
  AlertRun,
  AlertRunAttempt,
  CreateDefinitionInput,
} from "@fullkontrol/ssi";

function formatRun(run: AlertRun): string {
  return `[${run.status}] ${run.id} scheduled for ${run.scheduled_for}`;
}

Development

bun install                                      # Install deps
bun run --filter @fullkontrol/ssi typecheck      # Type check
bun run --filter @fullkontrol/ssi test           # Run tests
bun run --filter @fullkontrol/ssi build          # Build ESM + CJS + declarations

Regenerating types

If the API schema changes, regenerate types from a running dev server:

# Start the scheduler
bun run --filter @fullkontrol/alerts-server dev

# Regenerate (defaults to http://localhost:8787)
bun run --filter @fullkontrol/ssi generate

# Or point to a different URL
OPENAPI_URL=https://alerts.fullkontrol.app bun run --filter @fullkontrol/ssi generate

The generated file at src/generated/openapi.ts is committed to git — consumers never need to run generation.

License

MIT