Changelog¶
All notable changes to this package are recorded here. The format follows Keep a Changelog; versions follow Semantic Versioning.
The first release is 1.0.0-beta1, cut when the roadmap is complete: both API
editions served and consumed, the compliance collection and the NE:ONE
interoperability suite green. Pre-release versions (-betaN, -rcN) may still
change the public API; every such change is listed under the release that makes
it. From 1.0.0 on, breaking changes need a new major version.
[Unreleased]¶
Added¶
Model\GraphValidator: one validator for a whole logistics-object or event graph, root and every embedded node, used byChangeApplier,POST /logistics-objectsand the logistics-events endpoints alike, so no ingestion path accepts what another refuses (adversarial review 10).
Fixed¶
- A change could introduce an embedded node of an unknown class, which then
escaped validation along with any property attached to it; a node of a
logistics object class; or a node of the wrong class for the property.
ChangeApplierrefuses the class up front and the validator judges the rest (R10-001, R10-002). Comparernormalised only five of the twelve XSD integer types, so a delete spelled asxsd:bytedid not match a storedxsd:integerand an add of the same number was not a duplicate; it now takes the integer family fromRdf\Xsd(R10-003).POST /logistics-objectsvalidated the root's properties only; embedded nodes, property kinds and ranges are now validated (R10-004). Posted events are validated the same way, embedded locations included (D10-001). One exception stays deliberate: a nested logistics object in a POST body (the spec's own example A2) is still accepted and kept embedded, since splitting it into an object of its own is not implemented (spec question 33); a change or an event may not introduce one.- An
xsd:dateoffset is bounded to 14:00 like adateTime's (D10-002). - An embedded node a client identified itself (an
https:orurn:id, NE:ONE'sneone:scheme) escaped validation on creation and on posted events, could not be addressed by a change afterwards and was never cleansed when unlinked. Every non-root subject of a graph is now an embedded node: the validator judges it,withEmbeddedIds()gives it a server-mintedinternal:id when the object is stored, and the change applier addresses and cleanses it whatever id it carries (adversarial review 11). - A typed link, a reference that states the class of what it points to and nothing else, escaped validation when its class was unknown to the ontology (R12-001), and an unrelated change removed its class as an orphan (R12-002). Its class must now exist, as a class of the data model or as a code list, and whatever a reachable node links to is kept. A type-only node with an id of its own is a reference by design, not an embedded node: a change cannot describe it in place, and the guide says how to embed instead (R12-003).
- A code list did not count as a class when a property's range was checked, so a unit typed with the wrong list, or a code list where a class was expected, passed (R13-001). It counts now, and an untyped member IRI of the wrong list is caught by its IRI when the property expects a list.
- A nested logistics object that creation had accepted (spec question 33) made every later change to the object fail, since the change applier judged the finished graph without that allowance (R14-001). It is allowed there now; a change still cannot introduce one, and the type of any node, embedded nodes included, can no longer be changed through a change.
- A language-tagged literal skipped the datatype range check, so
"many"@enentered an integer property (R14-002). Tagged text now fits a string range and nothing else. - The bulk event route dropped a malformed
cargo:eventForvalue and reported no failure (R14-003); it answers 400 like the single-object route. - A non-string or empty
@idon creation was treated as absent and a URI with a space became a 500 (R14-004); every malformed@idis a 400. JwksKeyResolverignoredkey_ops; a key published for encryption only could verify tokens (D14-001). A presentkey_opsmust includeverify, and the selection policy is in the guide.
[1.0.0-beta6] - 2026-10-04¶
Added¶
ServerConfig::problems(array $settings): what is wrong with a set of settings, in plain sentences, without constructing anything, for a host's status page on an install that is not configured yet. The constructor throws the first of them.tools/consumer-smoke.phpand a CI job that installs the package into an empty project without development dependencies and exercises the runtime.Server\Deprecationslogs, at notice level, every deprecated term an object carries when it is created or revised, as the compatibility guide promised.
Changed¶
ActionRequests::create()queues the Pending notification before dispatchingActionRequestCreated, and every decision queues its notification before its event, so a listener that decides a request synchronously cannot put its decision's notification ahead of the state it decided;create()and every decision return the stored request, which a listener may have advanced.DataHolder::change()and the holder's HTTP change path no longer decide a request a creation listener already decided;ChangeFailedis also thrown when such a listener rejected it (adversarial review 7, R7-001).- An integral
xsd:doubleis written as an explicit value object, since a JSON number without a fraction reads back asxsd:integerin any JSON-LD processor; the client uses the package encoder (R7-002). - The expander applies a term's datatype coercion to native numbers and
booleans, and an explicit value object without
@languageis a plain string under a default language (R7-003). A term definition never expands a document-relative@id(R7-004). - JWT time claims are NumericDate with their fraction or an absolute RFC 3339
instant with a zone; anything else present is refused with
JwtException::INVALID_CLAIMrather than read as absent (R7-005). - A posted logistics event may name only the object it is posted on in
cargo:eventFor, however many values and in any order (R7-006). - Changes validate literals by each datatype's own grammar and against the property's range, as the checked builder does (R7-007).
DeliveryVerdictrejects a request PSR-18 reports as unusable (RequestExceptionInterface) instead of retrying it (R7-008).- The token endpoint refuses two client authentication methods in one
request and repeated form parameters with
invalid_request(R7-009). Servicesvalidates against the newest configured data model version (ServerConfig::validationModel()), as the configuration documented; a server configured for 3.2 alone refuses 3.3-only terms (D7-001).Claims::expiresAt(),notBefore()andissuedAt()return?floatinstead of?int, so a fractional NumericDate keeps its fraction. A consumer that needs whole seconds decides its own rounding, for example(int) floor($claims->expiresAt())beforeDateTimeImmutable::setTimestamp(); understrict_typespassing the float directly is aTypeError.-
DataHolder::subscribe()honours a creation listener's decision instead of accepting a second time (R8-001). Range checks follow XSD derivation (Rdf\Xsd): anxsd:intsatisfies anxsd:integerrange, any integer or decimal type satisfies a double, and bounded integer types are checked against their bounds; the builder and the change applier share the rules (R8-002). A native number or boolean under an@id-coerced term expands to the value it is rather than being refused (R8-003). The JWT verifier compares claims against the clock with its fraction (R8-004), refuses a presentnullor malformednbf/iat, and validates the hour, minute, second and offset ranges of an RFC 3339 claim before parsing it (R8-005). -
ActionRequestStore::save()states its envelope: the server saves a request once and moves it on withtransition(); a latersave()under the same IRI may replace status, history and errors, while type, payload, objects, requester and request time are fixed by the first save. The contract no longer asks a store to move a re-saved request to another object (beta5's test is withdrawn), since the SDK never does that and two hosts' query columns had been caught between the two readings (Laravel integration review, round 3). AccessDelegationkeeps each permission, delegate and logistics object once, in order, so a store projecting one row per (request, object) never sees a repeat.
[1.0.0-beta5] - 2026-10-04¶
Added¶
Client\TokenEndpointException(extendsClientException) with the status and the OAutherrorcode, thrown byClientCredentialsTokenProviderwhen the token endpoint answers without a token.- The
ActionRequestStorecontract checks that replacing a request under the same IRI moves it to the object it now concerns, inauditTrail()andpendingChanges().
Fixed¶
Client\DeliveryVerdict::of()walks the chain of previous exceptions, so a transport failure the SDK client wrapped in aClientException, or a token-endpoint outage, isRetryrather thanReject; refused credentials stay final (Drupal integration review, round 3).
[1.0.0-beta4] - 2026-10-04¶
Added¶
Spi\Volatile, a marker for stores with nothing to roll back; the in-memory stores carry it and the identity-unit-of-work warning is decided by it rather than by class name.ServerBuilder::check(Services)returns the same findings for a host's status page.Testing\RacingActionRequestStore: stages a decision lost to another worker, outright or through a host callback that flips its own row first.Testing\FixedClock::set()for jumping to an absolute instant.Client\DeliveryVerdict: the retry classification every outbox worker needs (Retryfor transport failures, 5xx, 408 and 429;Rejectotherwise), and a guide section specifying the outbox row, claim and outcome model two hosts converged on.
Changed¶
ActionRequestStatusChangedand the status notification fire after the decision's side effects (grants, revision, revocation) are in place, once per decision, with the status the request had before it; a change that fails to apply reportsFailedfromPendingeven though the store recordedAcceptedin between (Drupal integration review, round 2).
[1.0.0-beta3] - 2026-10-04¶
Changed¶
- Every decision on an action request stores its new status (the compare-and-set) before any side effect: grants, the new revision, notifications. A decision that loses the race against another worker now writes nothing even under a host without a transactional unit of work; it used to commit the grants first (Laravel integration review, round 2).
Serviceslogs a warning when noUnitOfWorkis given and a store is not one of the SDK's in-memory ones, since operations are then not atomic.- The contract traits' fixture constants are prefixed (
CONTRACT_OBJECT,CONTRACT_PARTNER, ...) so a host test case with constants of its own can use the traits. NotificationOutboxand the SPI guide state whenenqueue()runs relative to the unit of work, that delivery is at least once, and that PSR-14 listeners run inside the unit of work before the fan-out.
[1.0.0-beta2] - 2026-10-04¶
Added¶
Testing\Contract\*ContractTests: every store contract is also a trait, for hosts whose test cases must extend a framework base class (Testbench,KernelTestBase) and so cannot extend the abstract contract; the abstract class is now the trait on a bareTestCase, so the two cannot drift.
Changed¶
-
LogisticsEventStorestates that every event it receives carriescargo:eventDate(the server and the checked builder both refuse one without), so stores need no semantics for date-less events. -
A status-change notification for an action request over several logistics objects (an access delegation, typically) no longer names an arbitrary first object in
api:hasLogisticsObject, whose cardinality is at most one; it names none and the request inapi:isTriggeredBylists them all (spec question 32, IATA-Cargo/ONE-Record#437). Type subscriptions keep matching declared@typevalues only, now recorded as spec question 31 (IATA-Cargo/ONE-Record#412).
[1.0.0-beta1] - 2026-10-04¶
First release: the server and the client for API 2.2.0 and 2.3.0 with data model 3.2 and 3.3, the compliance collection green for both editions, the NE:ONE interoperability suite green, and five independent adversarial review rounds folded in. Public API changes remain possible between betas and are recorded here.
Added¶
- Repository skeleton: Composer package, PHPUnit 11, PHPStan (level max) with an SDK-boundary rule, php-cs-fixer (PER-CS 2.0), DDEV configuration, GitHub Actions, MkDocs documentation site.
Spec\Edition,Spec\ApiVersion,Spec\DataModelVersionandSpec\Namespaces: the supported editions (2025-07: API 2.2.0 / data model 3.2; 2026-07: API 2.3.0 / data model 3.3) and the IRIs ONE Record is built from.Rdf\Graphand its terms (Iri,BlankNode,Literal,Triple): the RDF layer everything else works on.- Generated vocabulary (
Vocabulary\Generated): every class, property and named individual of the cargo and API ontologies and every code list, merged across both editions with per-termsince/deprecatedIn/removedInmetadata, plusVocabulary\Vocabularyfor runtime questions (accepted properties per class, logistics-object classes, most specific type, code-list membership) and version-limited views. bin/generate-vocabulary: regenerates the vocabulary from IATA's ontologies at pinned commits; CI fails on a diff.JsonLd: the restricted JSON-LD processor ONE Record needs.Expanderturns a compacted document into triples (inline object contexts with prefixes,@vocab,@base,@languageand@type-coercing term definitions;@id,@type, value objects, arrays, embedded objects and references) and rejects everything outside that subset with a message naming the construct and its path.Writercompacts a graph back to deterministic JSON-LD.Comparerdecides graph isomorphism modulo blank-node labels, with embedded-object IRIs (internal:,neone:) treated as blank nodes and numeric literals normalised;Diffreports the differing triples.Model:LogisticsObject,LocalGraph(objects linked by local key, resolved to URIs with anIriMinter;UuidIriMintermints deterministic UUIDs under a base URL),ObjectBuilder/Embedded/Valuesbuilders validated against the ontology (with an optional data model version ceiling), andEmbeddedIdMinterfor the spec's stableinternal:<uuid5>embedded ids.Change: theapi:Changemodel with JSON-LD read/write,ChangeBuilder(diff two versions of an object into ADD/DELETE operations, editing embedded objects in place or replacing them via blank nodes) andChangeApplier(apply a change atomically with the spec's rules: revision check, no event edits, deletes before adds, ontology validation, orphan cleanup).Api\Error,Api\ErrorDetail,Api\Severityvalue objects.Auth:Rs256Verifier(RS256 only, keys from a resolver, issuer/expiry/ not-before/audience checks),Rs256Signer,StaticKeyResolver,JwksKeyResolver(PSR-18 + PSR-16, refresh on unknown key id),Jwk(RSA JWK to PEM),JwtAuthenticatorfor the server'sAuthenticatorSPI, andTokenEndpoint, a PSR-15 OAuth 2.0 client-credentials endpoint with aClientCredentialsVerifierSPI and an in-memory reference implementation.Server: the PSR-15 ONE Record server.OneRecordServernegotiates the API version, routes, authenticates and answers every failure as anapi:Error;ServerConfigholds base URL, base path, data holder, versions, languages and limits. Endpoints for server information, logistics objects (read with?at=and?embedded=, create, change, verify), audit trail, logistics events (single and 2.3 bulk), notifications, subscriptions, access delegations and action requests.ActionRequestsimplements the lifecycle (accept applies changes, grants delegations, rejects stale pending changes);DataHolderis the host's PHP API (create,update,publish,announce,accept/reject/acknowledge/revoke,subscribe,forget);Notification\Fanoutqueues notifications in the outbox. PSR-14 events for created/revised objects, received events and notifications, and action-request changes.Server\Spi: the interfaces a host implements (LogisticsObjectStore,LogisticsEventStore,ActionRequestStore,SubscriptionStore,AccessDelegationStore,NotificationOutbox,Authenticator,AccessPolicy) with in-memory implementations inServer\InMemoryandInMemoryServerwiring them all;SystemClock.Api: value objects for every API document (Subscription,AccessDelegation,Verification,Notification,ServerInformation,ActionRequest,Collection,ErrorDocument) andSpec\ApiFeatures, the table of properties gated by API version.- Compliance collection (
tests/Compliance/): a newman collection generated from the specification's examples with assertions from its MUST tables, run in CI againstbin/servefor API 2.2.0 and 2.3.0 (ddev compliancelocally). bin/serve: the in-memory server under PHP's built-in web server with an OAuth 2.0 token endpoint, for development and the compliance collection.- Review round with the Laravel and Drupal wrapper teams:
Server\Spi\UnitOfWork(the host's transaction boundary around every mutating request and holder operation);Server\Spi\IdGeneratoras an interface withUuidIdGenerator; anidonOutboundNotification;SubscriptionStore::offer()/withdraw(),ActionRequestStore::accepted(),LogisticsEventStore::eraseFor(),AccessDelegationStore::eraseFor()and a scopedDataHolder::forget();Server\GrantAccessPolicy(public grants stored as grants toEVERYONE;InMemoryAccessPolicydeprecated); millisecondSystemClock,ActionRequest::toStorageJsonLd(),LogisticsEvent::fromStored()andObjectBuilder::ofEvent();Auth\Jwt\ChainKeyResolver, an optional, fault-tolerant cache onJwksKeyResolver,Auth\JwksEndpoint;ServerBuilder::routes(); objects created over HTTP fan out to subscribers; and theTestingnamespace with the doubles andTesting\Contractstore contract tests. - Adversarial review round (2026-10-03), one regression test per finding:
client credentials bound to the partner's origin (
additionalOriginsfor multi-host partners);ActionRequestStore::transition()compare-and-set and lifecycle decisions made on stored state; shared visited set in graph collectors; strictxsd:dateTimeparsing (a malformed expiry is a 400, not "no expiry"); the client refuses to request an expiring delegation from a 2.2 partner; fan-out consults the access policy before disclosing a body;DataHolder::change()/update()/publish()throwChangeFailed; language-tagged literals refused in changes; exactxsd:decimalcomparison; sound canonical blank-node labels; order-independent change validation; discovery falls back through API versions on 406; publishers may query offers for their own objects; bulk events validated like single ones; client checks the answer is about the requested object; typed links rewritten in historical reads; RFC 6749 form-encoding of Basic credentials;q=0exclusions and configured body versions honoured;isTriggeredBynever names an organisation; global event IRI uniqueness; malformed?at=and non-finite numbers answer 400;Idempotency-Keyon sent and received notifications; JWKS refresh cooldown; only the requestor or the policy may revoke; reference stores keep snapshots; position-aware JSON-LD compaction, RFC 3986@baseresolution, term-scoped coercion and labelled blank roots. - Second adversarial round (2026-10-03), again one regression test per
finding: canonical blank-node labels by exhaustive individualisation search
(
ComparisonBudgetExceededwhen a graph is too symmetric, instead of a guess); the writer uses a term alias only when its coercion fits the values and never writes a node id as a bare prefix; historical reads withembedded=trueaccepted by the client; a subscriber may revoke a SubscriptionRequest a third party created (spec question 30); the in-memory event store and outbox keep snapshots, with contract tests; changes may not write API-namespace properties and final validation tolerates only the two revision properties; content negotiation evaluates every served version against the most specific matching range;xsd:dateTimeranges checked,24:00:00accepted as the next midnight; dot-segment removal for network-path references; the JWKS refresh cooldown kept in the shared cache; a shared embedded node introduced once in a change; a store's status conflict answers 409. - Third adversarial round (2026-10-03): the change builder plans deletions
over the whole before/after graphs, so a shared embedded node is edited
once, keeps its triples while any link reaches it, and loses them once when
none does; a node under several root properties reports every one of them
as changed; every compact key the writer emits must read back as its IRI,
so shadowed
@vocabnames and wrong coercions are skipped instead of dropping a predicate or turning a string into an IRI; the client names both identities it accepts before a flattened answer's root is chosen; in-memory outbox reads hand out copies and the outbox contract states ownership. - Fourth adversarial round (2026-10-03): the change builder now builds a
correspondence between old and new embedded nodes over the whole graphs
before emitting any operation (unchanged content first in every slot, then
one-to-one in-place pairs), so an unchanged branch is never edited to serve
another, shared nodes split and merge exactly as the target does, and
topology-only changes are changes; term definitions in a written
@contextuse prefixes only and the reader resolves definitions through other terms, so a datatype alias round-trips instead of failing or changing the datatype; the JWKS rotation test checks the rotated key itself. - Fifth adversarial round (2026-10-04): a term spelled as a compact IRI of a
defined prefix, or as an absolute IRI, cannot be redefined to mean another
IRI (JSON-LD's invalid IRI mapping); a definition that only repeats the
prefix mapping is left out of a written
@contextinstead of appearing as an empty array. Server\Spi\AuthenticatorandServer\Spi\Agent.- Interoperability suite (
tests/Interop/): NE:ONE built from source at a pinned commit as a comparison oracle; the same graph published to both servers and compared as RDF, plus changes, events and access delegations. Findings: the JSON-LD reader accepts a top-level@graph(NE:ONE's flattened answers), the comparer canonicalisesxsd:dateTimeand the bounded integer types, andChangeBuilderignores inferred superclass types when diffing. Client:OneRecordClientover PSR-18 for every endpoint of a partner's server, with server-information discovery, API version negotiation (highest common version, 2.3-only properties optional on read), typed errors carrying the partner'sapi:Error(OneRecordHttpException), a bulk-event call that falls back to per-object posts, andClientCredentialsTokenProvider/StaticTokenProviderbehind theTokenProviderinterface.- Architecture rule: a table-driven PHPStan rule (
tools/PhpStan/LayerRule.php) enforces the layering insidesrc/(documented in the SDK boundary page), next to the existing rule that keepssrc/free of anything but PSR and PHP.Api\Nodesmoved toJsonLd\NodesandModel\LogisticsEventthrowsModelExceptionso the model no longer depends on the API layer.