tassyir.app.toml,
manages client secrets, creates seeded dev stores and sends test webhooks.
Every command prints --json, and every refusal has a stable code, a
message and a one-sentence fix, so an AI coding agent can drive it as
well as you can.
Phase 1 serves apps on your own server (hosting.mode = "external").
Tassyir Hosting, logs --tail and submit come later; their commands
already exist and say so.
Install
Node 22.18 or later:npx tassyir@latest <command>.
Example: from nothing to a test webhook
--json, as an agent would run it (the CLI prints each
document on one line; here they are spread out to read):
Commands
tassyir <command> --help explains each one and its options.
tassyir --help --json is the whole catalogue as data. The commands that act
on an app take --app <handle or id>, and otherwise use the handle in
./tassyir.app.toml.
Signing in
tassyir login uses the OAuth 2.0 Device Authorization Grant
(RFC 8628):
- It asks Tassyir for a code and prints it with the approval page,
https://app.tassyir.io/{ar|en}/developers/cli?code=XXXX-XXXX. In a terminal it offers to open the page (press Enter); it always prints the code, so you can check that the page shows the same one. - You approve on the page, signed in to the dashboard with developer mode on. Approve only a sign-in you started yourself.
- Meanwhile the CLI asks every
intervalseconds (5), slowing down by 5 seconds each time the API saysslow_down. It stops on a denial, when the code expires (10 minutes), or when the API no longer knows the sign-in.
tsy_dt_…). The token works on the
developer API only: it cannot read a business, place an order or approve
another sign-in. It lapses after 90 days without use and after 365 days in
any case, and stops working when you change your password. Signing in again
replaces this machine’s token and revokes the old one. tassyir logout
revokes it on Tassyir (at once) and forgets it. Signing out of the dashboard
does not sign the CLI out.
The CLI never prints the token or the sign-in’s device code, in any mode,
with TASSYIR_DEBUG too; as a last resort, anything shaped like one is
replaced by tsy_dt_[redacted] before it is printed. Control characters
in text from the API (escape sequences) are dropped the same way.
Where the token is stored
Why this choice: a keychain keeps the token encrypted at rest and out of
backups and dotfile repositories, as
gh does. The CLI talks to the
keychain through the programs the OS ships, so it needs no native module and
no install script; the token goes to them on stdin, never as an argument, so
no process listing shows it. When no keychain answers (a server without a
desktop session, a locked keychain, a container), the token goes to the
file, and login says so. On Windows the CLI uses the file, in
%APPDATA%\tassyir, a folder only your account can read: Credential Manager
has no command that gives a secret back without PowerShell, and that path is
not built yet.
credentials.json also holds what is not secret, per API origin: the email,
the user id and the token’s id. So whoami can say who you are, and a
staging sign-in (--api-origin) sits next to production.
Client secrets
A client secret (tsy_cs_…) is shown once: Tassyir keeps only its hash.
secrets createmints the app’s first secret and prints it with the client id and the app id. It refuses when the app already has a live secret:secrets rotateis the rotation, which gives the current secrets 24 more hours so a deployed server keeps working while you roll the new one out.- With
--write-env(or--env-file <path>), the three values go to.envand the secret is not printed. A value the file already holds is never replaced silently: the file is checked before the secret is minted, and only--forcereplaces it. The starter’s placeholders (app_your-client-id, …) are filled in. A new file is created 0600; in a starter project, runnpm run setupfirst (the CLI refuses otherwise, since setup never touches an existing.env). A folder that does not exist or cannot be written is refused before minting too. - If writing
.envstill fails after the secret was minted, the error shows the three values once (revealin--json): the secret is never lost. - The CLI writes a secret nowhere else, ever.
--json, errors and exit codes
With --json, a command prints one JSON document on one line on stdout:
{"ok":true,…}, or {"ok":false,"error":{…}}. login prints two lines:
first the code to approve ("step":"authorize", userCode,
verificationUriComplete), then the result. Without --json, results go to
stdout and refusals to stderr.
An error has a stable code, a message and a one-sentence fix, and when
they apply status (the API’s HTTP status), requestId,
retryAfterSeconds, errors (every manifest problem: code, path,
message, fix), details (what the refusal is about: limit,
handle, …) and reveal (values you must see although the command failed,
such as a secret minted before its .env write broke). The API’s own codes are passed on as they are. For example,
tassyir validate on a manifest with handle = "API" and no reason for its
scope (exit code 4):
--json, the same (on one line when printed):
Codes of the CLI’s own (the API’s pass through as they are):
NOT_LOGGED_IN, LOGIN_DENIED, LOGIN_EXPIRED, LOGIN_INVALID_GRANT,
LOGIN_RATE_LIMITED, LOGIN_FAILED, TOKEN_ENV_INVALID, TOKEN_FROM_ENV,
CREDENTIALS_UNAVAILABLE, CREDENTIALS_UNREADABLE, CREDENTIALS_UNWRITABLE,
KEYCHAIN_UNAVAILABLE, MANIFEST_NOT_FOUND, MANIFEST_UNREADABLE,
MANIFEST_TOML_INVALID, MANIFEST_INVALID, APP_REQUIRED, APP_NOT_FOUND,
APP_FIELDS_REQUIRED, APP_SECRET_EXISTS, ENV_VALUE_EXISTS,
ENV_FILE_MISSING, ENV_FILE_UNREADABLE, ENV_FILE_UNWRITABLE,
INSTALLATION_REQUIRED, WEBHOOK_ENDPOINT_FAILED, WEBHOOK_REPLAY_SKIPPED,
COMMAND_NOT_AVAILABLE, UNKNOWN_COMMAND, INVALID_ARGUMENT,
API_ORIGIN_INVALID, API_REDIRECTED, NETWORK_ERROR, TIMEOUT,
SERVER_ERROR, INVALID_RESPONSE,
NODE_VERSION_UNSUPPORTED, INTERNAL_ERROR. A refusal the API sends
without a code (a malformed request, the throttler) reads as BAD_REQUEST,
RATE_LIMITED, SERVER_ERROR, …, by status.
The manifest
validate and deploy parse tassyir.app.toml and check it with
@tassyir/app-manifest, the Tassyir API’s own validator:
the same normalised manifest, and the same errors in the same order. One
check comes on top: no field of the manifest takes a TOML date or time, so
the CLI refuses one wherever it is (the validator would read a date where a
table goes as an empty table, and the API would see a string there).
deploy uploads only a manifest that passed, and the same manifest twice is
answered unchanged (exit 0). A hosted app (hosting.mode = "tassyir") is
answered by the API: APP_HOSTING_NOT_AVAILABLE until Tassyir Hosting opens.
From code
The package’s entry runs a command line in-process, with your own I/O (the specs do this against a fake API):io is a CliIo: stdout, stderr, the environment, the working folder,
fetch, a clock, and how to run a program. DeveloperApi is the typed
developer-API client the commands use. Its routes, bodies and answers are
typed from the developer API’s OpenAPI document, vendored in openapi/ at a
recorded backend commit (SOURCE.json); the package exports the types
(DeveloperApp, Installation, Delivery, WebhookTopic, …, and the raw
DeveloperApiPaths, DeveloperApiOperations, DeveloperApiComponents). In
this repo, with a clone of tassyir/backend in TASSYIR_BACKEND_DIR,
npm run openapi:check -w tassyir compares the copy with the backend’s and
npm run openapi:update -w tassyir vendors the new file and regenerates the
types.