AmLang registry https://tucan.amlang.net/api

Extension API

Everything needed to describe, package, publish and download an extension. Reads are public; writes need a credential.

The registry serves the catalog the IDE installs from. An extension is identity; each of its versions is a release, and a release owns the files it is made of. Nothing is publicly visible until both the extension and the version are approved.

Accounts, tokens and the Administrator role are covered separately in the User API. The examples below use:

New clients should use the /api/ prefix, for example GET /api/extensions. Existing paths such as /extensions remain supported and return the same JSON, regardless of the Accept header. The website catalog is separate at /add-extensions. All examples below use the preferred API base URL.

B=https://tucan.amlang.net/api
TOKEN=…            # your access token, from POST /users/login

The model

Four tables, each owning one thing:

tableownskey
extension identity: localName, name, description, type, approved localName, unique across both types
extension_version the release: date, notes, path, permissions, editors, menu items, approved (extension, version)
extension_resource a declaration: source, platform, name, folder, format (version, platform, name)
extension_file the bytes — only for source: registry one per resource

The separations that matter:

Extensions and applications

Every entry has a type, and it decides only which collection lists it.

typelisted atis
extension/extensionssomething a user installs into the IDE. The default
application/applicationsan app update — the IDE itself, say

Both use the same table, the same versions, the same resources and the same download URLs one level down; /applications/… mirrors /extensions/… exactly, filters included. The collections are disjoint: asking for an application under /extensions is a 404, not a redirect, and ids are unique across both, so nothing is reachable under the wrong one.

curl "$B/applications"                    # active applications, latest of each
curl "$B/applications/amstudio/v"         # its versions
curl -O "$B/applications/amstudio/v/2.1.0/amigaos/r/amstudio.lha"

A client never sends type. The collection decides it, on write exactly as on read, so there is no field to get right and no way to move an entry between collections by editing a manifest. It appears in responses, and is ignored if you send it.

Applications are published by whoever runs the registry, not by its users — an app update is not something a third party ships. Everything below is about extensions; applications only appear here because the IDE reads them through the same five endpoints.

Every endpoint

Reads are public and need nothing.

readreturns
GET /extensions?platform=&q= the catalog: published extensions, latest published version of each; filter by platform, search name and description
GET /extensions/{id}identity plus latest release info
GET /extensions/{id}/v?page=Nversions, 10 per page, newest first
GET /extensions/{id}/v/{version}release info plus a resources overview
GET /extensions/{id}/v/{version}/{platform}/r/{name}the file itself, if we host it
GET /applications…the same five reads for application updates

Who may write

There is no separate path for writing. An extension lives at one address, and the caller decides what is allowed there:

you areyou may
nobodyread the published catalog
signed in (Authorization: Bearer …) create an extension — and you own what you create
an owner of it everything about that extension: versions, uploads, deletions, who else works on it
writedoes
POST /extensionscreate one, or edit one you own
POST /extensions/{id}/vdeclare a version and the resources it is made of
PUT /extensions/{id}/v/{version}/{platform}/r/{name}upload one file you declared
DELETE /extensions/{id}/v/{version}drop a version and its files
DELETE /extensions/{id}/v/{version}/{platform}/r/{name}drop one uploaded file
GET / POST /extensions/{id}/userssee who works on it; invite someone
POST /extensions/{id}/users/{username}/acceptanswer an invitation — yours alone to send

Publishing is not yours to do, and that is what makes the rest open. A new extension and each new version are reviewed by whoever runs the registry before they reach the catalog. Until then your work answers 404 to everyone else and reads normally for you, so you can build, upload and correct at your own pace without anything half-finished being installable.

Tokens, sign-up and what each failure code means are in the User API.

extension.json — identity

{
  "localName":   "bebbossh",
  "name":        "BebboSSH",
  "description": "Bebbo's SSH2 suite for AmigaOS."
}

localName is the id: permanent, and in every URL. That is the whole shape — no version, no files, no platforms. Re-posting updates the name and description. No type here either; it comes from the collection you post to, so a manifest still carrying the old catalog's type works unchanged.

version.json — the release

{
  "version":      "1.0.0",
  "releaseNotes": "First release. Connection manager, key generation.",

  "path": ["vendor/bebbossh"],
  "permissions": ["system.execute", "internet"],

  "resources": [
    { "source": "registry", "platform": "amigaos",
      "name": "bebbossh-ext.lha", "format": "lha" },

    { "source": "aminet", "platform": "amigaos",
      "name": "bebbossh.lha", "folder": "vendor",
      "remoteIdentifier": "comm/net/bebbossh", "version": "2.4" }
  ],

  "menuItems": [
    { "name": "Add connection...", "script": "addConnection.js" },
    { "name": "Generate SSH key...", "command": "bebbosshkeygen -C \"${comment}\"",
      "form": "keygen.form.xml", "interactive": true }
  ],

  "fileEditors": [
    { "extension": ".dss", "viewTemplate": "pixler.view.xml",
      "onOpen": "openDss.js", "onSave": "saveDss.js" }
  ]
}
fieldnotes
versionrequired, unique within the extension
releaseNotesfree text
releaseDateread-only — stamped by the registry on creation, and kept if you re-post the version
pathfolders inside the extension folder to add to the IDE path
permissionse.g. system.execute, internet
resourcesreplaced wholesale on re-post, but a resource that survives keeps its uploaded bytes
menuItems, fileEditorsstored verbatim; references resolve inside the extension folder
platformsread-only — derived from the resources, so it cannot disagree with what is actually downloadable

Resources

A resource is one file: an archive, or a bare executable. The registry stores files, never directory trees — which is why a resource name may not contain a slash. Inside the archive, structure it as you please.

fieldapplies tonotes
sourceallregistry, aminet or url
platformallrequired; part of the identity and of the URL
nameallrequired; one file name, no / or \
folderallwhere it unpacks, relative to the extension folder. Blank means the root
formatallAny format hint up to 16 characters, lowercased; inferred from the suffix if omitted (including tar.*), otherwise raw. Any file type is accepted, including .deb and .apk
remoteIdentifieraminetrequired, e.g. comm/net/bebbossh
urlurlrequired, absolute
versionaminet, urlthe upstream version, unrelated to the extension's
sha256alloptional integrity check; worth setting for sources you do not control
size—read-only, present once bytes are uploaded

Split along ownership rather than size. Ship your own files as a registry resource; reference third-party archives as aminet or url instead of mirroring them. Per-platform builds are separate resources with distinct platform values — give them distinct names too, so the download URLs stay self-describing.

How the folder comes together

An extension installs into one folder of its own, extensions/<id>/, and every resource declares the folder inside it where the resource is unpacked. Several archives can contribute to the same tree:

extensions/bebbossh/
  addConnection.js          <- your archive, folder blank -> the root
  connections.json
  vendor/                   <- the Aminet archive, folder "vendor"
    bebbossh/               <- the directory that archive carries internally
      bebbossh
      libcryptossh.library

Whatever structure an archive contains nests below its folder: an archive carrying its own bebbossh/ directory, given folder: "vendor", lands in vendor/bebbossh/ — not in vendor/.

Resources sharing a destination share a namespace. Two archives containing the same path collide and one silently overwrites the other. Nothing detects it; you find out at runtime when a file is not the one you shipped. Generic root entries are the usual cause — README, install, LICENSE, Icons/, libs/. Give each third-party resource its own folder and the problem disappears by construction.

folder cannot point outside the extension: no leading /, no .., and no : — on AmigaOS amStudio:extensions/other is absolute, so a colon is an escape rather than a separator.

Manifest references are resolved relative to the extension folder, and a resource's folder counts: unpack your scripts under folder: "scripts" and the manifest has to say scripts/addConnection.js. The useful check before uploading is not "is it flat" but "does it match what I reference":

lha l dist/bebbossh-ext.lha
unzip -l dist/bebbossh-ext.zip

Packing the parent directory by mistake is the common slip — everything gains a prefix and the manifest's references stop resolving. Pack from inside the folder when you want its contents at the root:

cd src && lha a ../dist/bebbossh-ext.lha *

path: making binaries reachable

Folders inside the extension folder to add to the IDE's path.

"path": ["vendor/bebbossh"]                       -> extensions/bebbossh/vendor/bebbossh
"path": ["vendor/bebbossh", "vendor/bebbossh/bin"]
"path": []                                        -> nothing added (scripts-only extensions)

path is where the binaries ended up: a resource's folder plus whatever directory the archive carries inside it. Change the one and the other has to follow. A scripts-only extension needs no entry at all — scripts are named by the manifest, not executed off the path. The same three things are rejected here as in folder, for the same reason: a leading /, any .., and any :.

Publishing

Three calls, and order matters: the extension must exist before a version, and a version must declare a resource before its bytes can be uploaded. Everything here carries your access token.

# 1. identity, once. You own what you create.
curl -X POST "$B/extensions" -H "Authorization: Bearer $TOKEN" \
  -H 'Content-Type: application/json' --data-binary @extension.json

# 2. the release, and every resource it is made of
curl -X POST "$B/extensions/bebbossh/v" -H "Authorization: Bearer $TOKEN" \
  -H 'Content-Type: application/json' --data-binary @version.json

# 3. the bytes - one call per registry resource, named exactly as declared
curl -X PUT "$B/extensions/bebbossh/v/1.0.0/amigaos/r/bebbossh-ext.lha" \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/octet-stream' \
  --data-binary @dist/bebbossh-ext.lha

One upload per registry resource, addressed by platform and name — four registry resources means four PUTs, and a PUT for an undeclared name is a 400, not a silent create. aminet and url resources are only ever declared, never uploaded.

Always send Content-Type on uploads. Without it curl sends application/x-www-form-urlencoded, the body is parsed as a form and consumed, and the upload arrives empty.

Releasing a new version of something that already exists

The extension is already there, so skip step 1 — it is version, then resources:

curl -X POST "$B/extensions/am-git/v" -H "Authorization: Bearer $TOKEN" \
  -H 'Content-Type: application/json' --data-binary @version.json

# one PUT per registry resource - three platforms means three calls
for p in amigaos morphos-ppc macos-arm; do
  curl -X PUT "$B/extensions/am-git/v/1.0.3/$p/r/am-git-$p.lha" \
    -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/octet-stream' \
    --data-binary @dist/am-git-$p.lha
done

# what actually landed? a resource with size: null was declared but never uploaded
curl -s "$B/extensions/am-git/v/1.0.3" -H "Authorization: Bearer $TOKEN" \
  | jq '.resources[] | {platform, name, size}'

Each platform needs its own entry in the version's resources[] with a matching name. Re-posting a version you have already published is how you correct it: resources are replaced wholesale, but one that survives keeps its uploaded bytes, and releaseDate does not move.

What gets rejected

responsecause
400 no resource named X declareduploading a name the version does not list
400 X is a url/aminet resourceuploading bytes to something we do not host
400 name must name a single file/ or \ in a resource name
400 resource.format must be at most 16 charactersShorten the format hint or omit it; there is no file-type allowlist
400 folder/path would point outside.., a leading /, or a :
400 source must be one of …not registry, aminet or url
400 type may only be set by an administratortype sent in a body — the collection decides it
400 an aminet resource needs a remoteIdentifiermissing Aminet path
400 a url resource needs a urlmissing URL
400 empty bodyno Content-Type on the upload — see below
400 body is not valid JSONincluding a date the parser cannot read
404 creating a versionthe extension does not exist yet
404 on a writewrong collection: an application addressed under /extensions, or the reverse
401no token on a write, or an expired one
403a valid token, but you are not an owner of this extension

Always send Content-Type on uploads. Without it curl sends application/x-www-form-urlencoded, Play parses the body as a form and consumes it, and the upload arrives empty.

Reading it back

curl "$B/extensions"                      # active extensions, latest version of each
curl "$B/extensions/bebbossh"             # identity + latest release info
curl "$B/extensions/bebbossh/v?page=1"    # versions, 10 per page, newest first
curl "$B/extensions/bebbossh/v/1.0.0"     # release info + resources overview
curl -O "$B/extensions/bebbossh/v/1.0.0/amigaos/r/bebbossh-ext.lha"

/r/{name} serves only the files we host. An aminet or url resource is described in the version's resource list — with its remoteIdentifier or url — and 404s here; the client fetches those from where they actually live.

curl "$B/extensions?platform=amigaos"        # only what runs on amigaos
curl "$B/extensions?q=ssh"                   # search name and description
curl "$B/extensions?q=git&platform=amigaos"  # both

The listing is approved-only: an entry appears when the extension is approved and it has an approved version. Staged work never shows up, whoever is asking. ?all=1 includes unapproved entries and takes the newest version whatever its state, adding approved and versionApproved to each row — it needs the admin token, since a public all would defeat the point of approval, and returns 403 without one.

platform is matched against the latest version's resources — the same version the listing describes — so it cannot claim support the current release does not have. q is case-insensitive, and multiple words are AND: every term has to match something, so ?q=ssh+client narrows rather than widens. Results come back ranked, best first; without q they stay in name order.

what matchedscore
name is exactly the term100
name starts with it60
name contains it40
id is exactly the term30
id starts with it20
id contains it15
description contains it5

The name always outranks the id, and the id always outranks the description, so searching git puts the extension called git above one that merely mentions it in its blurb. The id is searched at all because the display name often is not what you would type — ?q=amlang-toolchain finds "AmLang Toolchain", which contains no such string. There is no search on the version endpoints; the listing is the search surface.

If a version is withdrawn

Approval says published; active says still in circulation. A release found to be malicious, or broken badly enough that nobody should install it, is withdrawn by the registry's operators rather than un-approved — un-approving would describe it as pending review, which it is not.

It takes effect immediately and everywhere: the version stops being the latestVersion and the next newest published one takes over, it disappears from /extensions/{id}/v, /extensions/{id}/v/{version} becomes a 404, and its downloads 404 too — including for anyone already holding the URL. Nothing is deleted, so it can be put back.

Releasing over it is the usual answer: bump the version, post a new version.json, upload its bytes. The previous version keeps its own metadata and files and stays installable at its own URL, which is the point of versioned metadata.