TypeScript integration starter
© Pi Soft, 2018-2026 · Tricryption Engine 8.1
The TypeScript edition of the Python integration starter, for an
application developer whose backend is TypeScript. It needs Node.js 22.18 or newer (on the 23
line, 23.6 or newer; it is qualified on v24.22.2, the Node the product ships) and nothing else at run time: no package from
a registry, no build step, no native SDK. Node runs the .ts files as they are (it strips the types),
and every import is a node: built-in. It talks to a Tricryption
REST Gateway at contract 1.7.1 (every path under /api/v1) and proves the same two things on a
live deployment:
- selective field sharing -- two protected JSON fields under two keys; the owner grants a recipient READ on one; the recipient reads that field and is refused the other;
- same-session revocation -- the owner revokes the grant and the recipient is refused that field on the same session it just read it on (and on a fresh one).
Every request is a real certificate login and a real DPoP proof (RFC 9449, plus the Gateway's bth
body-hash claim). Both identities are fresh for each run (run.<uuid>.owner, run.<uuid>.agent),
and the run removes what it created. The behaviour, the error codes and the configuration file are
the Python edition's: the same config.json works for both, and one operator bootstrap serves
both -- your operator follows the operator bootstrap
exactly as for the Python edition.
| file | what it is |
|---|---|
client.ts | GatewayClient -- certificate login, DPoP on every request, delegated sessions, scoped sessions bound to a per-task ES256 key (HolderKey, scopedMint, scopedRevoke), bounded read retries |
example.ts | the selective-sharing and revocation run, its clean-up, and --cleanup for an interrupted run |
preflight.ts | checks Node.js, its OpenSSL, the trust file, GET /api/v1/ready and the registration |
workflow/ | the workflow access lifecycle adapter: adapter.ts, its journal (journal.ts), the security-event outbox (export_outbox.ts) and example_workflow.ts -- see "The workflow adapter" below |
config.example.json | the configuration -- paths only, no secret |
package.json, tsconfig.json | "type": "module" and no dependencies; the compiler settings if you type-check it |
test/ | the driver the starter's own qualification runs it through -- not part of its API |
The nine commands
Your operator registers your service and gives you: a certificate and its key, the Gateway's CA
file, the role id for your principals and the clean-up recipient -- the bootstrap prints the
last two as principal_roles [...] and cleanup_recipient '...'. You give the operator one thing:
your sender PUBLIC key (step 6 below).
cd examples/typescript-integration-starter
node --version # v22.18 or newer; qualified on v24.22.2 (Windows: the same)
command -v openssl # only to MAKE your keys (Windows: where openssl); the starter never runs it
mkdir -p keys && chmod 700 keys # keys/ is not in the starter; it holds your keys, private
openssl genrsa -out keys/sender.key 2048 # your request-signing key -- NOT the certificate's key
openssl pkey -in keys/sender.key -pubout -out keys/sender.pub # send keys/sender.pub to your operator
cp config.example.json config.json # then edit it -- see "Filling in config.json"
node preflight.ts --config config.json # five lines; it stops at the first that fails
node example.ts --config config.json # the run, narrated; --json for one verdict line per step
Node.js 22.18 is the first release that runs .ts files without a flag and has node:sqlite (the
workflow adapter's journal) on by default, so it is the minimum (on the 23 line types are stripped
without a flag only from 23.6, so 23.0-23.5 are refused too); an older Node refuses the first
.ts file with ERR_UNKNOWN_FILE_EXTENSION before preflight.ts can say so. The starter is
qualified -- its own tests run it end to end -- on v24.22.2, the Node the product ships; 22.18 itself
is the documented minimum, not a qualified version. A file that used
TypeScript syntax Node cannot strip (an enum, a namespace, a parameter property) would be refused
by Node at start-up -- the starter uses none, and keeps it that way.
Filling in config.json
It is the Python edition's file, key for key. Put what the operator gave you in keys/ under the
names config.example.json already uses -- keys/service.pem (the certificate), keys/service.key
(its key) and keys/gateway-ca.pem (the Gateway's CA) -- or change those three paths. Paths are
resolved against the directory of config.json. Then set endpoint (the Gateway's origin, no path)
and replace the two <...> placeholders with the role id and the clean-up recipient. The openssl
key is read by the Python edition, which signs through that executable; this edition signs with
node:crypto and ignores it, so a file written for one works for the other. For a loopback
evaluation Gateway on http://127.0.0.1:<port> there is no TLS: set "trust_file": null and
"allow_http_loopback": true.
preflight.ts's last line is the registration itself: your certificate logs in as a registered
service, and one read signed with keys/sender.key is served. PROOF_SIGNATURE_REJECTED there means
the operator registered a different sender key; "NOT a registered service" means the registration is
missing or the Gateway has not re-read its registry yet (it polls every 2 s by default).
A good run prints PASS for OWNER_ENCRYPT, AGENT_READ_A, AGENT_DENIED_B,
REVOKED_SAME_SESSION, REVOKED_FRESH_SESSION and CLEANUP, and exits 0 -- the Python edition's
verdict lines, in the same format (--json writes one JSON object per line, keys sorted).
If a run is interrupted (Ctrl-C, or Ctrl-Break on Windows) it makes no further calls and writes
runs/run.<uuid>/cleanup-report.json, naming what it left behind (identities by id, keys by a hash
prefix). Remove exactly that, and nothing of any other run's:
node example.ts --config config.json --cleanup runs/run.<uuid>/run-record.json
Keys are removed (POST /api/v1/keys/remove: each key, its access control list and every cached
copy are deleted from the key server, and each one is read back absent); the principals are
removed, and the key server hands a removed principal's remaining keys to cleanup_recipient.
Using the client from your code
import { Config, GatewayClient, GatewayError, AmbiguousOutcome } from "./client.ts";
const cfg = Config.load("config.json");
const service = new GatewayClient(cfg);
await service.certificateLogin(); // the registered service's own session
const alice = await service.delegate("alice"); // a session ACTING AS your user "alice"
const out = await alice.request("POST", "/api/v1/crypto/encrypt", { data: "aGVsbG8=" });
await alice.close();
await service.close();
Every call is async. The names are the Python edition's in camelCase (certificateLogin,
scopedMint, expireKeys, removeKeys, importKeys, retryAfter, readBack). Run your own
.ts the same way (node your-file.ts), or compile it with your own toolchain: tsconfig.json sets
allowImportingTsExtensions, erasableSyntaxOnly and noEmit, and looks for Node's types in
./node_modules/@types -- install @types/node there as a development dependency of YOUR project if
you type-check it. The starter itself needs none.
Behind a proxy
The Gateway itself terminates no TLS; in a deployment a reverse proxy does. endpoint is where you
connect and trust_file is what you verify it against -- the ONLY roots trusted for that connection.
external_origin is the origin the GATEWAY believes it has, and it is what the DPoP htu claim is
built from. When the two are the same (the usual case) leave it null; when you reach the Gateway by
a different name than the one it was configured with, set it to the configured one. A mismatch is
refused 401 PROOF_BINDING_MISMATCH naming htu. Plain http:// is accepted only for a loopback
evaluation Gateway with "allow_http_loopback": true. Redirects are never followed.
What is retried, and what is not
| request | retried? |
|---|---|
GET, POST /api/v1/crypto/decrypt, POST /api/v1/keys/acl/get | yes -- on a transport error, a 429 or a 503, up to retry.max_attempts, honouring Retry-After (never longer than retry.max_retry_after_seconds), with a NEW proof (jti, signature) each time |
| every other request -- encrypt (which MINTS a key), grant, revoke, expire, key removal and import, principal creation and removal, logins | never. A write that was sent and got no answer throws AmbiguousOutcome, carrying what was sent (sent: method, path, body size and hash) and how to read back what happened (readBack). It is not replayed: a second encrypt would mint a second key. |
| a certificate login | never -- a refused login is thrown on the first refusal |
A refusal is a GatewayError with status, a stable code (ACL_DENIED, PROOF_REPLAYED,
RATE_LIMITED, ... or the client's own TLS_VERIFY_FAILED, TRANSPORT) and a sanitized detail.
The workflow adapter
workflow/ is the TypeScript edition of the workflow access lifecycle adapter:
a trusted backend ties protected top-level JSON fields, and temporary key-server-enforced READ
access to them, to a step of a workflow, and ends that access when the step closes. Its contract is
the Python edition's -- the same seven operations, the same WF_* refusals, the same policy file, the
same endTime stamp, the same crash points and restart behaviour, the same security-event outbox --
so that page is its manual. In TypeScript:
import { Config } from "../client.ts";
import { WorkflowAdapter, WorkflowError } from "./adapter.ts";
const wf = await WorkflowAdapter.open(Config.load("config.json"), "policy.json", "workflow.sqlite"); // logs in; reconciles
const w = await wf.beginWorkflow(Date.now() / 1000 + 3600);
const [docId, frags] = await wf.protectFields(w, claim, ["field_a", "field_b"]);
await wf.grantStep(w, "review", "a1", "claims-review", 300); // -> the deadline (UTC seconds)
const data = await wf.decryptFragment(w, "review", "a1", frags.field_a); // -> plaintext bytes
await wf.closeStep(w, "review"); // -> "closed"
await wf.closeWorkflow(w); // -> "closed"
await wf.status(w); // steps, deadlines, pending_cleanup
node workflow/example_workflow.ts --config config.json --json # one JSON verdict per step
node workflow/export_outbox.ts runs/security-events.db drain --to events.jsonl
The journal is yours to place. The adapter talks to a JournalStore -- three methods (tx, q,
close) over plain SQL with ? placeholders on the seven tables journal.ts declares. The default,
SqliteJournalStore, keeps them in one SQLite file through node:sqlite (WAL, synchronous=FULL) --
the very file the Python edition writes, so either edition reads the other's. To keep the journal in
your own database, implement JournalStore over it (translate the placeholders, run each tx as one
transaction, return an inserted row's id) and pass it to WorkflowAdapter.open in place of the path,
with exportOutbox set to a path, an ExportOutbox or null.
What this is not
No key export, no offline mode. Every key stays in the key server; every encrypt and decrypt is a request. This is a starter, not a general SDK: it implements the routes the example and the workflow adapter use and the transport they share.
Credentials
Nothing in config.json is a secret -- the keys are named by path. The login key and the sender key
are each read once into a node:crypto key object held in memory, used to sign, and never printed or
written anywhere; a scoped session's holder key is generated in memory and never touches the disk.
Bearers and proofs exist only in memory and are never printed; an error never carries a header.
Every behaviour on this page is qualified end to end against a live key server and Gateway before a release, by the same checks that qualify the Python edition.