@mintid/engine
v0.5.0
Published
Mint engine SDK (ADR-0025 D1): the org-locked token-exchange client + typed Mint ops for BEING an engine. Depends on @mintid/client for the single-source domain canonicalizer + birth_seed.
Readme
@mintid/engine
The SDK for being a Mint engine: the org-locked token exchange, the typed Mint ops, and the org
wall. Depends only on @mintid/client (the single-source id grammar + domain canonicalizer +
birthSeed). Zero other runtime dependencies -- and that is deliberate (ADR-0034 D4): a shared
security primitive must be trivially adoptable, and a dependency in the security path is itself a
supply-chain surface.
pnpm add @mintid/engine # pulls @mintid/client transitivelyThe client PARSES every Mint response. It never casts. (ADR-0034 D1)
@mintid/engine 0.1.0 cast every response body it read -- seven sites -- and validated none of
them. A cast is a compile-time assertion with no runtime force, so a malformed 200 produced,
silently:
| symptom | consequence |
|---|---|
| token: undefined | every later call sent Bearer undefined |
| org_id: 'TSQ' (a string) | cached, then persisted by engines onto append-only rows keyed on a mutable identifier |
| Date.parse(undefined) -> NaN | the token cache silently NEVER hit |
0.2.0 parses. Every body is validated by a hand-rolled guard before use, and an unverifiable 200 is an error -- never a usable object, never a passed gate, never a written token cache.
The one behavioral change on upgrade
A previously-SILENT malformed 200 now THROWS (MintProtocolError). getOrgToken, mintFetch,
resolveByDomain, mintBrand and attachAlias keep their exact signatures; only their internals
changed. If your engine today accidentally depends on undefined flowing through a malformed
response, it will now fail loudly on upgrade. That is a fix, not a regression -- the silent
behavior was never a supported one.
MintProtocolError is NOT a MintError, on purpose
new MintProtocolError('...') instanceof Error // true
new MintProtocolError('...') instanceof MintError // FALSE — deliberately a siblingADR-CE-0006 D7 mandates that engines catch a Mint failure and degrade ("Mint-unreachable =
degraded mode, never a failed turn"). If a shape mismatch inherited MintError, an engine
faithfully following D7 would catch it and degrade past the org wall -- proceeding with no
membership proof. That is the wall failing open.
A protocol error is not an availability error. "Mint is down" (degrade, serve last-known-good) and
"Mint and I disagree about reality" (version skew, a proxy rewriting the body, a stale client) are
different categories, and degrading is never the right answer to the second. Do not "tidy"
MintProtocolError into the MintError hierarchy. IdNotInOrg is a sibling for the same reason:
a definitive "not in this org" must not be swallowed by a degrade-on-MintError handler.
Error messages name the field that failed (expires_at is not a parseable timestamp), never
the value -- the body may carry a token. No token and no raw body appears in any message, log, or
thrown object.
The org wall
import { assertIdInOrg, IdNotInOrg } from '@mintid/engine';
/** Proves `id` is in `orgCode`. Returns the authoritative, immutable org_seq to stamp on rows. */
export async function assertIdInOrg(
config: MintEngineConfig,
id: string,
orgCode: string, // REQUIRED. No default. Never falls back to config.org.
): Promise<{ orgId: number }>How it proves membership. It exchanges for a token locked to orgCode, then
GET /api/resolve/{id} under it. A foreign-org id returns a byte-identical unknown_id 404 --
the same 404 a genuine miss returns (ADR-0023 D7: no cross-org existence oracle). Therefore a
successful resolve under an org-locked token IS the proof. It opens no new oracle: zero new
endpoints, zero new server calls, and it returns strictly less information than you could get by
making the two calls yourself.
- 404 -> throws
IdNotInOrg. Foreign-org and unknown are indistinguishable, and must remain so.IdNotInOrgcarries no discriminator, no server body, no reason. Do not add one. - 200 -> returns
{ orgId }, the integer from the exchange -- never from the resolve body, never the code, and never a boolean. A boolean would force you to persist the org you were told, which is the exact vulnerability the wall exists to close. orgCodeis required.env.MINT_ORG ?? 'TSQ'in a security path is the wall failing open. An unset org is a hard failure here, before any network call.
import { assertIdInOrg, resolveByAlias } from '@mintid/engine';
/** Reverse-resolve an alias to its canonical Mint id. `null` = a genuine miss (byte-identical to a
* foreign-org miss). Works for EVERY alias kind — post-ADR-0032 `alias_hmac` is the universal match
* key; the old "join kinds only" limit is gone. */
export async function resolveByAlias(
config: MintEngineConfig,
alias: string,
aliasType: string,
orgCode: string, // REQUIRED.
): Promise<string | null>An IDENTITY alias is BYTE-EXACT. Mint folds only the JOIN kinds (
glb.domain+ the human-label kinds). Normalize an identity value yourself (lowercase an FQDN, strip a trailing dot) before you attach it and before you look it up -- or you will write one identity and query another.
org_seq is NEVER REUSED. Persist it as an INTEGER.
The exchange returns org_id = orgs.org_seq, the immutable surrogate PK -- not orgs.code.
Store that, in an INTEGER column, on every row you key to an org. Never the code.
Why the code is unsafe: orgs.code is a mutable TEXT UNIQUE request-header key with no
stability guarantee. Keying an immutable, unrewritable store (an append-only ledger) on a mutable
identifier means a future org-code reassignment silently re-points every historical row at a
different org -- one org inheriting another's history, with no correction possible. This is exactly
why the exchange returns the surrogate and the OpenAPI spec says "the target org_seq PK (NOT the
code)."
Why the surrogate is safe -- the guarantee, with its reasoning (ADR-0034 fact 2). Two independent
mechanisms, both machine-checked by this package's test/contract.test.ts:
orgs.org_seq INTEGER PRIMARY KEY AUTOINCREMENT(migrations/0014_org_tier.sql). TheAUTOINCREMENTkeyword is load-bearing, not decoration: a plainINTEGER PRIMARY KEYis a rowid alias, and SQLite REUSESmax(rowid)+1after a delete. WithAUTOINCREMENT, SQLite tracks a high-water mark insqlite_sequenceand never issues a value below it -- a seq, once issued, is never issued again.- Retirement is SOFT. Orgs carry
status/retired_atcolumns; the row stays and the seq is never freed. There is noDELETE FROM orgsand noUPDATE orgs SET code = ...anywhere in Mint'ssrc/ormigrations/-- both grep to zero hits, and a test in this package fails if that ever stops being true.
So: org_id is a stable foreign key. It is safe on an append-only row. Type the column INTEGER
-- if you type it TEXT, SQLite's affinity will happily coerce the integer to '3' and your ledger
will work by accident, on the wrong key, exactly as journal-engine's did.
What this package CANNOT do for you
Stated plainly, because the gaps are real (ADR-0034 D5):
- It pins the contract against Mint's source in Mint's monorepo, not against a live prod response. A hand-edited prod worker could still drift.
- It cannot force you to call
assertIdInOrginstead of trusting a caller-supplied org. Nothing in a client can make a wall be used -- that is a lint/review obligation on your side. - It cannot check that your
org_idcolumn is typedINTEGER. The journal bug had two halves; this package fixes the wire half. The schema half is yours.
Exports
| | |
|---|---|
| getOrgToken(config, orgCode?) | the org-locked exchange, cached per org within its ~5-min TTL |
| mintFetch(config, path, init?, orgCode?) | an authenticated call, refreshing once on a 401 |
| assertIdInOrg(config, id, orgCode) | the org wall |
| resolveByAlias(config, alias, aliasType, orgCode) | reverse-resolve an alias, oracle-closed |
| resolveByDomain(config, domain, orgCode?) | match-first domain lookup |
| mintBrand(config, input, orgCode?) | mint a GLB:BRD (minted / duplicate / dry_run) |
| attachAlias(config, input, orgCode?) | attach an alias (attached / identity_alias_conflict / duplicate) |
| MintAuthError MintDeniedError MintError | 401 / 403 / other non-2xx |
| MintProtocolError IdNotInOrg | siblings of MintError, not subclasses (see above) |
| canonicalizeDomain birthSeed birthSeedForDomain sha256Hex | re-exported from @mintid/client -- never re-implement these |
MintEngineConfig.org is a config-level default org for the convenience ops. It is deliberately
unreachable from assertIdInOrg / resolveByAlias.
Carries no secret: the standing SYS credential is a runtime env var (MINT_ENGINE_TOKEN).
