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

@t4h.framework/amber

v0.2.0

Published

Amber module for the T4H Framework

Readme

@t4h.framework/amber

Amber module for the T4H Framework. It drives an Amber webhook channel from a workflow: create tickets, act on them, and wait for what happens on the Amber side (an agent tabulating a ticket) without polling.

Installation

yarn add @t4h.framework/amber

Peer dependencies: @t4h.framework/core, @t4h.framework/http, @t4h.framework/fs, @t4h.framework/cache and typebox.

Configuration

import { Amber } from '@t4h.framework/amber'

const amber = new Amber({
  url: 'https://amber.example.com', // Amber API root
  tenant: 'your-tenant-id',
  channel: 'your-webhook-channel-id',
  clientId: 'channel-client-id',
  clientSecret: 'channel-client-secret',
})

Every call goes to {url}/v1/tenants/{tenant}/channels/{channel} with the channel credentials as basic auth.

Point the channel webhook at the workflow

Amber emits its events (service:tabulate, …) to the URL configured on the webhook channel. Set it to the events route of the workflow that uses this package, on the host that runs it:

POST {runtime}/tenants/{tenant}/projects/{project}/workflows/{workflow}/events

The host asks the waiting activities which event is theirs (toCorrelationKey, keyed by the Amber service id), verifies the signature Amber puts on every event (detached signature over timestamp.body, key resolved by kid at {url}/tenants/{tenant}/.well-known/jwks.json) and advances the right wait. Nothing is stored on the Amber side to find the run back: no metadata, no ids in the ticket.

One webhook channel serves one workflow. A project with several Amber workflows needs one channel per workflow.

Tickets

const ticket = await amber.createTicket({
  protocol: 'P-123',
  skill: 'skill-id',
  tabulationOption: 'option-id',
  customers: [{ isOwner: true, type: 'customer' }],
  fields: { customer: [{ name: 'email', value: '[email protected]' }] },
})

A ticket is born locked by the workflow (lock: { ref: <workflow id> }), so no agent picks it up while the workflow still works on it. Pass lock: false to create it unlocked, or lock: { expiresAt, metadata } to customize the lock.

The lock ref is the workflow id the host reports through GetWorkflowUniqueIdentifierClaim (amber-ticket for a workflow declared with id: 'amber-ticket'), not the id of the run. lock(), releaseLock() and the lock release of transfer() use the same ref, so every run of a workflow can release a lock another run of it took.

A locked ticket still takes comments, interactions and field updates from the workflow, but Amber answers 423 to a lock() while a lock exists (release first, lock again) and to a tabulate() while the ticket is locked, even by the workflow itself. Release the lock before tabulating or finishing the ticket. Transfers release it for you with status: 'in-queue' or lockRelease: true.

await ticket.comment({ type: 'public', content: 'Looking into it' })
await ticket.interact({ content: 'Hello', contentType: 'text/plain' })
await ticket.updateField({ field: 'field-id', value: 'new value' })
const fields = await ticket.fields()

await ticket.lock({ expiresAt: new Date(Date.now() + 3600_000) })
await ticket.releaseLock()

Handing the ticket to a human and waiting

const transfer = await ticket.transfer({
  skill: 'human-skill-id',
  status: 'in-queue',
})

const { tabulation } = await transfer.wait()

transfer returns an awaitable. Transferring to in-queue releases the workflow lock, and wait() suspends the workflow until an agent tabulates the ticket, resolving with the tabulation the agent chose (tabulation.tabulationOption, tabulation.notes). Informative tabulations are ignored. A transfer to a user or without status: 'in-queue' settles at once and wait() resolves without tabulation; add lockRelease: true to release the lock on those too.

The wait has no deadline by default. Pass timeoutInMs in the second argument, next to retry, to bound it; wait() then rejects with { reason: 'timeout', service, serviceTransfer } when the agent has not tabulated in time:

const transfer = await ticket.transfer(
  { skill: 'human-skill-id', status: 'in-queue' },
  { timeoutInMs: 24 * 60 * 60 * 1000, retry: true },
)

When the transfer released the lock (status: 'in-queue' or lockRelease: true), wait() on a ticket locks it back for the workflow before resolving, with no expiration, so no agent picks it up again while the workflow finishes it. Pass lock: false in the second argument to skip that, or lock: { expiresAt, metadata } to customize the lock. The lock is a separate AmberLockTicketActivity recorded after the transfer, and a refusal by Amber rejects wait() with its AmberRequestError.

const transfer = await ticket.transfer(
  { skill: 'human-skill-id', status: 'in-queue' },
  { lock: { expiresAt: new Date(Date.now() + 3600_000) } },
)

const { tabulation } = await transfer.wait() // ticket locked again here
await ticket.tabulate({
  tabulationOption: 'option-id',
  notes: [{ type: 'public', content: 'ok' }],
})
await ticket.finish()

Errors

Every call that Amber answers with a status other than 200 rejects with an AmberRequestError. Its message carries the status and the response body, which is where Amber says what it refused, so the reason shows up in any log that prints only the message:

Failed to create ticket: Amber responded 400 {"errors":{"$.tabulationOption":["Not found tabulation option"]}}

The same data is available as error.status, error.body and error.cause ({ httpStatusCode, body }). Bodies longer than 2000 characters are truncated. The activity that waits on Amber events, transfer(), rejects its awaitable with that message as the payload instead.

Activities

Every method above is an activity, recorded in the workflow history and replayable. Hosts register them by class: AmberCreateTicketActivity, AmberGetTicketActivity, AmberCommentTicketActivity, AmberInteractTicketActivity, AmberTabulateServiceActivity, AmberTransferServiceActivity, AmberUpdateServiceActivity, AmberLockTicketActivity, AmberReleaseTicketLockActivity, AmberFindTicketFieldsActivity, AmberUpdateTicketFieldValueActivity.

AmberTransferServiceActivity is the one that waits on Amber events; the schema it validates the events against (AmberServiceTabulateEventSchema) and the auth it declares (AmberWebhook.authorization(channelUrl)) are exported by the package.