@fabricorg/ports
v0.5.7
Published
Vendor-neutral port interfaces, a DTCG token resolver, and the contract-test kit every adapter must pass.
Downloads
1,420
Readme
@fabricorg/ports
Vendor-neutral port interfaces, a W3C DTCG token resolver, and the contract-test kit every adapter must pass.
A capability declares that it needs a port — CapabilityPortRequirement in its manifest. Until now that was a name resolving against nothing. This package supplies the interface behind the name, checks that a registered adapter actually satisfies the declaration, and gives every adapter the same suite to pass.
pnpm add @fabricorg/portsWhere a standard exists, the port speaks it
FlagsPort is structurally compatible with an OpenFeature provider's evaluation surface, so an OpenFeature provider is a thin adapter rather than a translation layer. DesignTokensPort consumes W3C DTCG documents, so a design tool is one exporter among any. IdentityPort is OAuth2/OIDC-shaped, so a gateway that already validates JWTs implements it without a second identity model.
Identity is where the platform stops trusting its caller
Every governed action, projection decision, grant and audit record derives from actor context. IdentityPort.verify turns a presented credential into ActorClaims, and returns null for anything it cannot positively verify rather than partially trusted claims.
import { actorContextFromClaims } from "@fabricorg/ports";
const claims = await identity.verify(credential);
if (!claims) throw new Error("unauthenticated");
// Refuses expired, not-yet-valid, cross-tenant and out-of-space claims.
const actor = actorContextFromClaims(claims, { tenantId, spaceId });Verification proves who the caller is. assertActorClaimsCoverScope enforces the half it does not: that they may act in the tenant and space this request names. A credential valid for one tenant replayed against another is refused there, not later. Space coverage is spelled "tenant-wide" rather than left absent, so claims that simply omit coverage can never be read as covering everything.
Nothing here imports a vendor SDK. A port is a shape; an adapter is anything that satisfies it.
Turning a declaration into a gate
import { assertPortRequirementsSatisfied } from "@fabricorg/ports";
assertPortRequirementsSatisfied({ capability, adapters });Fails when a declared port has no registered adapter, when no adapter speaks a required standard, or when none offers a required version — with every unsatisfied port named. Version matching is exact; range negotiation belongs to whatever installs the adapters, and half-implemented semver would be worse than none.
The adapter contract
import { flagsPortChecks, runPortContract } from "@fabricorg/ports";
const { passed, failures } = await runPortContract(adapter, flagsPortChecks());Every flags adapter must default rather than throw on an unknown key, return each resolver's own type, evaluate repeatably, and tolerate an absent context. Every design-token adapter must resolve a theme to at least one token, follow every alias, and keep names unique. runPortContract reports every failure rather than stopping at the first, so one run tells an adapter author everything to fix.
That is what makes swapping a vendor an adapter build plus a configuration change rather than a migration.
DTCG resolution
resolveDesignTokens flattens a DTCG document to dotted names, follows {alias} references to their concrete value, inherits $type from the nearest ancestor group that declares one, and refuses both a dangling alias and an alias cycle.
Payment observations (additive contract)
PaymentObservationsPort and PAYMENT_OBSERVATIONS_PORT (from @fabricorg/ports/catalog)
let an external settlement source report an exact payment allocation without implementing
PaymentsPort.charge. The existing charge interface and fabric.payments 1.0.0 are unchanged.
Call assertPaymentObservationRequest before a provider lookup and
assertPaymentObservationMatches before recording its result. Supply server-derived tenant,
merchant and payer references, canonical source namespace/account/payment/allocation identity,
currency, and a bounded observation age. Connection aliases are not source identity. Missing,
unavailable, unmapped and stale observations remain unknown. Results carry cumulative minor-unit
facts, never webhook deltas; authorization or capture alone is not settlement.
Refunded and reversed amounts classify distinct returned debits; adverse corrections can make their sum exceed gross settlement. Such observations are retained for deficit reconciliation. Pending refunds and disputes can overlap; these are observations, not an available-balance calculation. Applications retain durable allocation ownership and adverse history, and govern every resulting state change through Host. An observation cannot reserve funds, authorize a refund, guarantee settlement until a supplier commits, or authenticate its own source.
Certification uses independently seeded unpaid, authorized, captured, partially settled, settled, refunded, reversed, disputed and pending-refund cases; every identity dimension has a denial. An independent effect counter checks that reads and repeated reads do not move money. Fixture truth must not be obtained from the adapter under certification. The suite does not authenticate hand-written certificates: deployment admission still needs trusted issuer/provenance and exact adapter, source account and capability bindings. No production adapter ships with this contract.
Hosted collection creation, refund requests and refund lookup remain separate lifecycle work. This observation-only addition neither implements those operations nor changes Host completion or retry semantics.
Observations normalize timestamps to UTC ISO format with milliseconds (YYYY-MM-DDTHH:mm:ss.sssZ).
PaymentObservationContractError.rejection is a typed, stable reason (for example foreign_scope,
stale_observation, invalid_amount or unexpected_effect); its message contains no raw source values.
The observation definition is included in REFERENCE_PORTS, while assemblies require it only for
capabilities that declare the optional observation dependency.
Payment lifecycle and exact runtime admission
HostedCollectionsPort creates/looks up a hosted collection under the complete immutable request.
A hosted URL is never authorization, capture or settlement evidence. PaymentRefundsPort requests
and looks up a separately approved exact refund; a timeout or missing lookup remains unknown.
The provider must atomically enforce the stable key and remaining refundable amount, including
concurrent requests. These additive interfaces preserve PaymentsPort.charge.
assertHostedCollectionResult allows only HTTPS destinations on server-certified origins;
assertPaymentRefundResult checks the exact original amount/currency. They are safe decoders,
not effect certification. Implementers must also prove dropped-response/restart recovery,
changed-key refusal, separate refund authority and concurrent over-refund denial against independent
effect counts before enabling mutation capabilities. No mutation adapter is certified by these types.
admitPaymentRuntime reads a current PaymentRuntimeAuthority, requires an exact tenant/space,
port/contract, adapter/provider versions, canonical source/account and merchant, and checks issuance,
expiry, well-formed evidence digest and revocation. The authority authenticates its issuing source; caller JSON
is never trusted authority. A transactional consumer holds authority and connection revocation locks
through commit, or uses a separately certified fenced dispatch protocol for remote stores. Recheck
at each consequential boundary. This does not replace Host business authorization or payer mandates.
assertPaymentRequestAdmission checks the request against the admitted tenant, space, merchant and canonical source account before dispatch. Its required contract argument pins the operation port and version; observation admission cannot authorize a refund operation.
Closing collection requests and resolving refunds
HostedCollectionClosurePort.close requires separate governed closure authority and the entire
original hosted-collection request. lookupClosure reads that same immutable operation. A closed
result binds the exact request and final gross amount collected by that page. It guarantees that all
original URLs and every future retry of the original creation key cannot collect again—even when
closing a request whose creation has not yet arrived. An in-flight capture must independently
settle, be irrevocably voided, or expire at the provider first. Closure never voids it itself. Until
then close and lookupClosure return the same pending reference, replay is idempotent, and the page
cannot accept new payments. Closure neither refunds money nor cancels an order. Missing lookup, URL
expiry, rejection and timeout never prove closure. Use assertHostedCollectionClosureRequest and
assertHostedCollectionClosureResult before dispatch and before retaining proof.
PaymentRefundResolutionPort.resolve is read-only and binds the exact original refund request. A
final result identifies the precise returned amount (full, partial or zero) and guarantees that
the remainder can never execute through an original-key retry. A positive amount requires an opaque
refund reference; zero requires null. Missing operations remain unknown until the provider has an
independent terminal fence. Validate with assertPaymentRefundResolution. Ordinary refund request
rejection is not this proof, and source balance reconciliation remains separately required.
The closure and refund-finality certification helpers require independent provider truth and effect counters, including never-opened pages, pending captures, exact identity denials and full/partial/zero refund outcomes. Run them alongside application-owned create/refund-request certification. These contracts do not provide a production adapter, authorize a business action, or replace current runtime admission. Existing lifecycle interfaces and versions remain unchanged.
The closure fence key includes the admitted tenant, space, merchant, namespace and account plus the
original creation key. The complete request remains immutable; one closure operation claims that
collection key. Changed operations return rejected/changed_request or rejected/not_authorized
without poisoning a valid request; foreign bindings always return rejected/not_authorized. Host and
trusted dispatch enforce exact closure authority; opaque references are not self-authenticating.
After closure, the old lifecycle create/lookup return rejected/unsupported, while lookupClosure
retains the immutable proof. Lookup never returns closed or pending for a changed or foreign-binding
request: it returns unknown, rejected/not_authorized or rejected/changed_request without effects.
After refund finality, old request/lookup cannot return pending; only a full refund may still return
the matching confirmed result, otherwise they return unsupported.
Certification includes independently partial/full pages, zero-effect tombstones, concurrent close/create, concurrent pending-refund request/resolve, read-only and denied-operation checks, changed-key/authority/source replays, and operation, closure and monetary effect counters. A create that wins the closure race may create one page; it must not remain collectible after closure.
Applications calculate replacement amounts from independent source observations, retaining closure amount as evidence rather than treating it as the balance. Closure reports gross settlement, not capture or net funds. Providers unable to fence bank-transfer instructions against late incoming funds are unsupported. An amount above the original request cannot be represented as a valid closure proof and must stay unresolved. A refund that never reached the provider likewise remains reserved until independently proved final; this read-only resolution port cannot create its abandonment fence.
