Jadpo is experimental and not ready for production. These docs follow the compiler's main branch as of 5 Nov 2026. What's built · Install 0.3.0
jadpo
ReferenceUsing packages

External servicesUsing packages

Where packages come from

partly builtSome of this isn't built yet. See What is built.

On this page

A package reaches your app in one of four ways. The first two install it in packages/, and the last two make it your app's own, in shared/providers/:

Where it comes from What you do Use it for
A folder jadpo package add ../postcodes A package you have on your machine, such as one of Jadpo's own
A git tag jadpo package add github.com/acme/postcodes@v2.1.0 A package in a repository of its own, with package.jadpo at the repository's root (Private packages come from git)
An OpenAPI document jadpo package import openapi orders.json --as Orders.v1 An API that publishes one (An importer)
By hand Write the files in shared/providers/<name>/<version>/ An API that publishes only HTML docs (Written by hand)

A folder or a tag is read once, when the package is added or updated. The package is then pinned in jadpo.lock with its source and digest, and your app holds its own copy to commit.

A name on its own doesn't install anything yet. jadpo package add stripe and jadpo package add @jadpo/resend ask the public registry, which isn't live, so they stop with registry.unreachable.

Jadpo's own packages

Four packages are kept in Jadpo's repository: Resend.v1, which sends your app's email, OpenAi.v1 and Anthropic.v1, which answer your app's model calls, and AmazonSqs.v2012_11_05, which receives from a queue. Jadpo has no package for Stripe, Twilio, GitHub or any other provider, so for those use the importer or write your own.

They can't be installed from a git tag, because the repository isn't a package itself: only each folder in it is. So clone the repository at your release, the one jadpo --version prints, and add the package's folder. This does it from your project's folder, with X.Y.Z standing for that release:

shell
git clone --depth 1 --branch vX.Y.Z https://github.com/jadpo-dev/jadpo ../jadpo-sourcejadpo package add ../jadpo-source/packages/resend
output
Added Resend.v1 1.0.0    folder   packages/resend/v1    source   path ../jadpo-source/packages/resend    surface  matches its codepackage added: Resend.v1 1.0.0, 1 operation, 3 types, 5 failuresnext: jadpo package show resend  shows what it offers

jadpo check and jadpo build read only your app's copy, in packages/resend/v1/, so you can delete the clone. jadpo package update resend reads the folder again, so it needs the clone back in the same place.

Resend's package has no fixed host, so its connection gives one as well as its key, each from a setting:

app.jadpo
// app.jadpoapplication {    connections: {        mail: {            package: Resend.v1            credential: config.resend_api_key            host: config.resend_address        }    }    email: {        connection: mail        from: config.mail_sender    }}// config.jadpoconfig {    mail_sender: Email    resend_address: Url {        default: "https://api.resend.com"    }    resend_api_key: Text {        secret: true    }}

The registry

The registry at jadpo.dev isn't live yet. When it is, it holds public Jadpo packages of both kinds, as Jadpo source with no native code and no install script. Nothing in a package runs when it is installed, and its code runs only when started (What package code may do). A malicious package could still name the wrong host or mark an unsafe call idempotent, so hosts, services and every package change appear in the approval report, the summary a person signs off before a release. How it works is in The public registry.

An importer

jadpo package import <format> <file> turns a published description into the app's own integration once, offline, in shared/providers/<name>/<version>/:

Format Typical source What it gives
openapi Most REST APIs (Stripe, Twilio, GitHub), and every Jadpo API Operations, types, error responses; from a Jadpo app, exact (its x-jadpo fields)
smithy AWS, one model per service The same, plus AWS's protocols and signing
postman A provider's published Postman collection Operations and example bodies; types inferred from examples
har A browser or proxy capture of real traffic Operations and real responses; values stripped, shapes kept
dts The provider's official TypeScript SDK (.d.ts files) Exact types and SDK method names; no HTTP method or path, so the agent adds those from the docs

Only openapi works today. The others stop with toolchain.not_built_yet.

openapi reads a local file: jadpo package import openapi ../orders/build/openapi/main.json, with --as Orders.v1 to name the package. "How the OpenAPI importer reads a document", further down this page, says how it reads one.

An agent then fills in what the source leaves out: which calls change something, idempotency, error names, and decoding (OpenAPI often says only integer for Unix seconds). A person reviews that diff. Corrections to a wrong specification live in the package and survive the next import, because the importer merges rather than overwrites, as an OpenAPI Overlay does.

Written by hand, when there is no specification

Many APIs publish only HTML docs. A package is Jadpo source, so for these an agent writes the package files directly from the docs, in exactly the format an importer produces. Nothing else changes: the same compiler checks, the same fakes, the same review. A package needs only the operations the app uses, so it can start with one operation and grow.

Mapping imported types to standard types

An imported description says little about what a field is: OpenAPI's integer may be pence, Unix seconds or a count. A package's public types use the standard types, such as Money and Instant, where one fits (type.standard_exists, Standard library). So an import maps fields to them in six layers, and nothing is guessed silently:

  1. Exact mappings. A description's own format hints map without judgement: OpenAPI's date-time to Instant, date to CalendarDate, email to Email, uri to Url, uuid to Uuid, ipv4 and ipv6 to IpAddress, ISO 4217 and ISO 3166 enumerations to Currency and Country.
  2. Shape and name detection, with the same matcher as type.standard_exists: amount with a sibling currency to Money, lat/lng to Point, an integer named like a time (created, *_at) as a candidate Instant from seconds. The provider's own shape becomes a named decoding at the edge (Stripe's amount: 1050, currency: "gbp" decodes to Money, with a field's wire_parts: under "Decoding"), so the app only ever sees the standard type.
  3. Findings on what was flagged. Every candidate the importer is not certain of is a declinable error on that field, package.standard_candidate, whose next: says what to check, marked (agent): confirm against the docs and swap, or decline with a reason that jadpo approval shows.
  4. One scoped review of the whole package. The import ends with one broad instruction to the agent: go through every imported type the app reaches against the provider's documentation, use the standard type wherever one fits, add a decoding wherever a format is undocumented (seconds, cents, comma-separated lists), and mark which operations change something and which are idempotent. The review covers the operations the app calls and every type they reach, never the whole description (Stripe's has 1,180 types), and grows when the app calls something new. Each imported type starts unreviewed; the agent marks it reviewed when it has gone through it; jadpo check lists reviewed and outstanding types for the operations the app calls, and jadpo approval shows a person any type shipped unreviewed.
  5. Confirmation by real responses. An operation the app calls stays unconfirmed until jadpo package record has checked a sandbox response against it ("Provenance and confirmation", below).
  6. The safety net. The package is Jadpo source, so type.standard_exists runs on it as on any code: a field that looks like a standard type and was not mapped blocks check until it is swapped or declined.

The import's own output lists what it mapped, the findings, the scoped review instruction and the operations still unconfirmed, with --json for agents. Corrections made in review survive the next import, because the importer merges rather than overwrites.

The agent marks a type reviewed by listing it in provider.jadpo's imported:, reviewed: [Charge, Shop], each one of the package's own types, once (package.provider_invalid). The review's scope is worked out from the calls on every check, so it grows when the app calls something new: the package's types each operation the app calls takes and gives reach, through fields, list items, payload variants and refinements. jadpo check, once it passes, lists the types still to review and the calls no recording confirms, counts both in its footer, and gives every call and type in --json as providers; neither stops it. With nothing left a passing check is one line; with something left it reads:

text
$ jadpo checkTo review  Go through each type against the provider's documentation, then add it to  `reviewed:` in `imported:` in its package's `provider.jadpo` (agent)    Tidepay.v2024_06_20  Charge, ChargesGetChargesInput, Shop; 0 of 3 reviewedNot yet confirmed  Record each with `jadpo package record`, which checks a sandbox answer  against its types (agent)    payments.charges.get_charges✓ checked  3 to review · 1 unconfirmed · 9 files

It writes the same to build/report/providers.md and .json, for jadpo approval to show a person what ships unreviewed or unconfirmed. The import ends with the scoped instruction: the types the app's calls reach still to review, or, while it calls none of the package's operations, when the review starts; and the calls not yet confirmed. Importing again keeps reviewed:, less any type the description added to, which the summary lists under "To review again". The choices are in "The scoped review and unconfirmed operations" below.

Provenance and confirmation

Every operation says where it came from, so a reviewer knows how far to trust it:

source: Meaning Also records
PackageSource.imported From a published description (OpenAPI, Smithy, Postman, .d.ts, GraphQL introspection later) provider.jadpo's imported: names the file and its digest
PackageSource.written Written from the provider's docs docs: with the page's URL

An operation is confirmed when a real response has been checked against it. jadpo package record <connection> <operation> calls the provider's sandbox once, then checks the operation's output type against the response, or writes the type if there is none yet. It saves the response, with values replaced by placeholders, as recordings/<resource>.<operation>.json in the package; every decoder must decode every recorded response, on every build of the app's own integration and at publish. An operation imported from a HAR capture is confirmed from the start. record refuses a connection whose environment is live.

Written operations that are not confirmed are allowed in every build. The approval report flags them, and CI requires a passing contract run against the sandbox for each one (see "Tests and fakes").

jadpo package record <connection> <resource>.<operation> calls the sandbox once through the connection, with its credential from .env.local and the operation's input, written as a route's body is, from --input. It checks the answer against the operation's output type and keeps it as the recording: along output_at:, only the members the type declares, each plain text or number replaced by a placeholder naming its built-in type ("{Text}"), and what the package's own encodings and decoders read kept as sent. The surface then marks the operation confirmed, and jadpo test decodes the recording ("Recording errors"):

text
$ jadpo package record couriers bookings.create --input '{"postcode": "N1 9GU"}'✓ Recorded `couriers.bookings.create` from the sandbox    sent    POST https://sandbox.acme-couriers.example/v1/bookings    answer  decodes as Booking    values  2 placeholders, 2 kept as sent    file    shared/providers/acme_couriers/v1/recordings/bookings.create.json✓ operation recorded  AcmeCouriers.v1 bookings.create · confirmed→ jadpo test  decodes the recording with every decoder
json
{  "data": {    "collectAt": "{Instant}",    "id": "{Text}",    "status": "booked",    "window": "PM"  }}

It refuses before anything is sent a connection whose environment is live, or whose credential nothing sets (recording.refused), and keeps nothing from an answer that doesn't decode or a refusal (recording.failed):

text
$ jadpo package record couriers bookings.create --input '{"postcode": "N1 9GU"}'error: The sandbox's answer to `couriers.bookings.create` doesn't decode as `Booking`  reason: Reading it found `variant` on `status`: the operation's types and the provider disagree, so a real call would fault. Nothing was recorded.  next: Correct `Booking` against the answer and the provider's documentation, then run it again (agent)  rule: recording.failed  help: https://jadpo.dev/docs/errors/recording.failed

The choices are in "Recordings" below.

Installing safely

  • Every package is verified at install, wherever it came from: jadpo package add and update regenerate the surface from the package's code and refuse one that differs from its committed surface.jadpo_record.

  • The lock records each package's version, digest and source: registry, git (the repository, the tag and the tag's commit), path (a folder, relative to your project) or link (a live checkout during development, below). A public package can never stand in for a git one. jadpo check and jadpo build read the lock and the committed folders only.

  • Publisher trust. A third party's package from git, rather than the registry, is unverified, and needs a person's acceptance in the change that installs it: AcmeCouriers.v1: PackageChange.git_source (Acceptances). Registry packages carry their verified publisher, and curated ones are marked official.

  • Your own company's repositories are listed in application { }'s packages: section, each a host and path ending in /:

    text
    packages: {    own_sources: ["github.com/acme/", "git.acme.example/platform/"]}

    A package locked from a repository under one of them needs no PackageChange.git_source; everything else about it is reviewed as for any package. Adding an entry, or widening one, is a decision: "github.com/acme/": PolicyChange.own_source_added, so an agent can't let a stranger's repository in. Removing one needs nothing, nor does narrowing one, or adding one below an entry already there, since no new repository gets in. The list is kept in access.jadpo_record as own_source lines, which gives the check its before side.

    A repository is under an entry by its host and its whole path parts, never by how its text starts, and an address that can't be told apart from someone else's is under nothing, which only costs a person's acceptance:

    Against github.com/acme/ Under it? Why
    github.com/acme/postcodes, github.com/acme/tools/postcodes Yes Below the entry
    github.com/acme/postcodes.git, https://github.com/acme/postcodes, ssh://git@github.com/acme/postcodes, git@github.com:acme/postcodes.git Yes The same repository, as git reads it
    GitHub.com/acme/postcodes Yes A host's name has no case
    github.com/Acme/postcodes No A path keeps its case, since many servers tell the two apart
    github.com/acme-evil/postcodes, github.com/acme No Whole path parts only, and strictly below the entry
    github.com:8443/acme/postcodes No Another server. An entry can name a port: git.acme.example:8443/platform/
    github.com@evil.example/acme/postcodes, https://token@github.com/acme/postcodes No An account in the address, other than ssh's git
    github.com/acme/../evil/postcodes, github.com/acme/%2e%2e/evil/postcodes No Nothing is resolved or unescaped
    http://github.com/acme/postcodes, git://…, file://… No They prove nothing about who answers

    An entry itself is refused unless it is a lower-case host and a path ending in /, package.own_source_invalid: github.com/ alone would let in every repository on the host.

  • Prose is unverified wherever it comes from, such as /// descriptions and GUIDE.md. Nothing safety-relevant relies on it.

jadpo package add from a folder (../postcodes) or from a git repository at a release tag (github.com/acme/postcodes@v2.1.0): the package is compiled as the app's own integration would be, the surface its code gives has to be its committed surface.jadpo_record byte for byte (install.surface_mismatch), and only then is it copied into packages/<name>/<version>/ and locked with its version, digest and source. update moves one to its newest release and shows what changed for review (Review and records). remove takes out one the app no longer uses (install.in_use), and link and unlink are under Developing and publishing a package. The choices are in "How packages are installed and linked" below.

text
$ jadpo package add github.com/acme/postcodes@v2.1.0✓ Added Postcodes.v2 2.1.0    folder   packages/postcodes/v2    source   git github.com/acme/postcodes tag v2.1.0    surface  matches its code✓ package added  Postcodes.v2 2.1.0 · 1 function · 2 types · 1 failure→ jadpo package show postcodes  shows what it offers
text
$ jadpo package update postcodes✓ Updated Postcodes.v2 from 2.0.0 to 2.1.0    folder   packages/postcodes/v2    source   git github.com/acme/postcodes tag v2.1.0    surface  matches its code  Changes since 2.0.0    + type OutwardCode✓ package updated  Postcodes.v2 2.1.0 · 1 added→ jadpo check  checks the app against 2.1.0

Not built yet: the public registry; importers other than openapi; adding another Jadpo app's OpenAPI document with jadpo package add; writing an operation's output type from the sandbox's answer when it has none; and a HAR capture's operations.

jadpo package add <name> is designed to look in a fixed order and record which step it used, so an agent never improvises where to look: the registry, the official package first; the provider's published specification, given a URL or found in its known places; an importer, from the official SDK's type definitions, a Postman collection or a HAR capture; then written from the provider's documentation by an agent. Today a name asks only the registry. Until it is live, use a folder, a git tag, jadpo package import openapi or write the package by hand, as the table at the top of this topic shows.

Markdown for agents: /docs/where-packages-come-from.md.Built from Jadpo main at aee2e8f.