Chapter 5

Chapter 5: The API Interface (PHASE 2)

Chapter overview: PHASE 2 of WIA-SPACE-016 turns the component record from Chapter 4 into something that organisations can exchange over the network. This chapter walks through the five operation groups the specification names, the authentication model, every endpoint of the reference TypeScript SDK, the error and discovery conventions, and the reliability patterns (idempotency, concurrency control, rate limiting) that a parts-records API needs. It closes with an honest audit of what the 1.0.0 reference implementation does and does not do.

5.1 Five Operation Groups

The PHASE 2 document is short on normative detail, but it is precise about scope. It says the API exposes five operation groups, each to be described in an OpenAPI document: componentCatalog, maintenanceEvent, certificationPackage, traceability and serviceBulletin. An implementation is not obliged to offer all five. It declares the subset it supports in its interoperability profile (Annex I, discussed in Chapter 8), so a repair station and a component manufacturer can both be conformant while exposing quite different surfaces.

Grouping by business capability rather than by URL is the right choice for this domain. The parties who exchange component data — suppliers, maintenance organisations, operators and certification authorities — each own a different slice of the part's history, and each needs a different set of permissions. A group is therefore also the natural unit for authorisation scopes, for rate limits and for conformance claims.

What each group is for

Operation groupSpecification wordingTypical writersTypical readers
componentCatalogList, retrieve and search component type designationsDesign holder, supplierOperators, MROs, parts distributors
maintenanceEventRecord, retrieve and query maintenance actionsMRO, operator's line maintenanceOperator technical records, lessors, authority auditors
certificationPackageSubmit certification evidence to FAA, EASA or the Korean authorityDesign holder, supplier with design approvalCertification authority
traceabilityQuery by serial number across the component lifetime(derived from the other groups)Anyone accepting a used part; investigators
serviceBulletinIssue and acknowledge airworthiness directivesAuthority (ADs), design holder (SBs)Operators and MROs, who acknowledge and record compliance

Two details in the specification's wording deserve correction before anyone designs against them. First, the certificationPackage line in PHASE 2 names the Korean authority as "KCAA". The body responsible for airworthiness certification in Korea is the Ministry of Land, Infrastructure and Transport (MOLIT), which acts through its aviation organisation and the Korean Airworthiness Standards; an implementation should use that name in any authority identifier. Second, the serviceBulletin group is described as issuing and acknowledging "airworthiness directives". An airworthiness directive is a legally binding instruction from an authority; a service bulletin is a recommendation from the design holder that becomes mandatory only if an AD or an operator's own programme calls it up. The group can carry both, but every message must say which one it is, because the compliance obligation is completely different. Chapter 6 follows that distinction through the full acknowledgement workflow.

Catalogue entries versus serialised parts

The specification says componentCatalog handles type designations — that is, part-number-level facts such as the approved configuration, the applicable material specification and the interchangeability of dash numbers. The reference SDK, however, attaches its create/read/update calls to /components, and the object it moves is AircraftComponent, which carries a serial number, a manufacturing date and an inspection history. That is an instance, not a type.

Both levels are needed, and mixing them causes real damage. A catalogue change (for example, a new dash number that supersedes an old one) must not rewrite the history of every serial number built to the old configuration, and a single part's inspection finding must not alter the catalogue. A practical design keeps two resources: a catalogue resource keyed by manufacturer plus part number, and a component resource keyed by a server-assigned record identifier with part number, serial number and manufacturer as a unique triple. The SDK's /components endpoints map onto the second resource; this book files them under componentCatalog only because the specification offers no other group for instance registration.

Where each actor enters

Following one part through the groups shows how they interlock. The supplier registers the part and its first-article evidence; the design holder or supplier submits a certification package; the operator reads the catalogue to confirm the part is eligible for its fleet; the MRO writes maintenance events whenever the part is removed, inspected or repaired; the authority, or the design holder, issues bulletins against a range of part and serial numbers; and anyone who is offered a used part queries traceability to see the whole trail before accepting it. None of those actors needs write access to more than two groups. That observation drives the authorisation model in the next section.

5.2 Authentication, Authorisation and Transport Security

PHASE 2 states that authentication uses OAuth with mutual TLS for type-certificate authority callers, and it cites "OAuth 2.1 (RFC 9700)". That citation mixes up two documents. RFC 9700 is the Best Current Practice for OAuth 2.0 Security, published by the IETF in January 2025. OAuth 2.1 is a separate consolidation effort that was still an Internet-Draft at the time of writing. The practical reading is: use OAuth 2.0 (RFC 6749) with bearer tokens (RFC 6750), and follow the hardening rules in RFC 9700.

What RFC 9700 changes in practice

Why the authority channel uses mTLS

The specification singles out certification authority callers for mutual TLS. The reasoning is sound: those calls carry or approve type-design evidence, and both sides need strong assurance of who is on the other end. With RFC 8705, the client presents an X.509 certificate during the TLS handshake, the authorisation server records a hash of that certificate in the token (the cnf claim with an x5t#S256 member), and the resource server rejects the token if it arrives over a connection authenticated with any other certificate. The exchange below shows a client-credentials request of that kind; the client certificate does not appear in the HTTP text because it is presented in the TLS layer.

Client-credentials token request over mTLS (scope names illustrative)
POST /oauth2/token HTTP/1.1
Host: auth.mro.example.net
Content-Type: application/x-www-form-urlencoded

grant_type=client_credentials&client_id=hangar-7-records&scope=maintenance.write%20traceability.read

HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: no-store

{
  "access_token": "eyJhbGciOiJFUzI1NiIs...",
  "token_type": "Bearer",
  "expires_in": 600,
  "scope": "maintenance.write traceability.read"
}

A short token lifetime (ten minutes in the example) limits the damage of a leak, and Cache-Control: no-store keeps the token out of intermediary caches, as RFC 6749 requires for token responses.

Scopes that follow the operation groups

The specification does not define scope names. A layout that mirrors the operation groups is easy to audit: catalog.read, catalog.write, maintenance.read, maintenance.write, certification.submit, certification.review, traceability.read, bulletin.issue and bulletin.acknowledge. These names are this book's proposal, not part of WIA-SPACE-016. Whatever names a deployment chooses, it should publish them in its OpenAPI document so that auditors can check that, for instance, no MRO client holds certification.review.

The SDK's simplification

The reference SDK does none of this. Its WIAConfig has a single apiKey string, and the constructor throws "API key is required" if it is empty. The key is never placed in a header, because the request function never performs a network call (Section 5.7). Treat apiKey as a placeholder for "some credential" and wrap token acquisition behind a function, as the transport sketch later in this chapter does, so that moving from a static key to OAuth tokens changes one place in the code.

5.3 The SDK Endpoints

The package @wia/aircraft-component (version 1.0.0, Node.js 18 or later) exports WIAAircraftComponentClient, the ComponentAnalyzer helper class and all the types from Chapter 4. The client's default base URL is https://api.wia-standards.org/v1/aircraft-component and its default timeout is 60,000 ms. The base URL is only a default string in the source; this book does not assume that a production service answers there, and every example below points at a deployment of your own.

Endpoint map

MethodPathSDK methodOperation groupRequest bodyResponse data
GET/components/{id}getComponentcomponentCatalog—AircraftComponent
POST/componentscreateComponentcomponentCatalogPartial<AircraftComponent>AircraftComponent
PUT/components/{id}updateComponentcomponentCatalogPartial<AircraftComponent>AircraftComponent
POST/components/searchsearchComponentscomponentCatalogComponentSearchQueryAircraftComponent[]
POST/components/{id}/analysisperformStructuralAnalysisnone named (engineering evidence){ appliedLoad, analyst }StructuralAnalysis
POST/components/{id}/inspectionsaddInspectionmaintenanceEventOmit<Inspection, 'id'>Inspection
GET/components/{id}/inspectionsgetInspectionHistorymaintenanceEvent—Inspection[]
POST/components/{id}/certifyrequestCertificationcertificationPackage{ standard }{ requestId }
GET/certifications/{certificateNumber}/verifyverifyCertificationcertificationPackage—{ valid, details }

Reading the table against Section 5.1 reveals two gaps. There is no SDK method for traceability — nothing that takes a serial number and returns the part's life — and there is none for serviceBulletin. The only way to find a part by serial number today is to already know its record identifier, because ComponentSearchQuery filters on type, status, manufacturer, certificationRequired and materialType only. For a standard whose central promise is back-to-birth traceability, a query such as GET /traceability?manufacturer=…&partNumber=…&serialNumber=… is the first extension an implementer should add; it needs all three keys because serial numbers are unique only within one manufacturer's part number.

Design notes on individual calls

Search uses POST. searchComponents sends its filter as a JSON body. That keeps complex filters out of URLs (and out of access logs), at the cost of HTTP caching. For a records system that is usually the right trade.

Update is partial but uses PUT. updateComponent accepts Partial<AircraftComponent>. Under HTTP semantics (RFC 9110), PUT replaces the target's state with the enclosed representation, so a partial body sent with PUT is ambiguous: does an absent certification mean "unchanged" or "remove"? A server should either require complete records on PUT or offer PATCH with JSON Merge Patch (RFC 7396), where omitted members are unchanged and null means delete. Whichever it chooses, the OpenAPI document must say so.

Analysis is a request, not a calculation. performStructuralAnalysis sends only an applied load (kN) and the analyst's identity. The server — or rather the stress team behind it — is expected to return a StructuralAnalysis with stress, safety factor and fatigue life. In a real programme those results come from an approved stress report with its own document number and revision; the API record should cite that report in metadata rather than pretend the number was computed on the fly.

Inspections are append-only. addInspection omits id, so the server assigns it. There is no update or delete for inspections, which matches regulatory practice: a wrong entry in a maintenance record is corrected by a new entry that references it, not by erasing it.

Certification is asynchronous. requestCertification returns a requestId, not a certificate. Authority review takes much longer than any HTTP timeout, so the natural HTTP pattern is 202 Accepted with a Location header pointing to a status resource. verifyCertification later answers the question "is this certificate number genuine and in force?", which is exactly what a receiving inspector needs when a part arrives with paperwork.

History is unpaged. getInspectionHistory returns an array. A structural part in service for decades can accumulate a long history; a production API should page it with a cursor and let clients filter by date range.

Worked example: register a part, then record an inspection

The listing below uses only the public SDK surface. It registers a flight-control fitting, then attaches an eddy-current inspection whose result is conditional with a re-inspection date. All part data are illustrative. Note the unwrap helper, which refuses to continue unless the response is both successful and carries data; the reason becomes clear in Section 5.7.

TypeScript: createComponent followed by addInspection
import {
  WIAAircraftComponentClient,
  APIResponse,
} from '@wia/aircraft-component';

const client = new WIAAircraftComponentClient({
  apiKey: process.env.WIA_API_KEY ?? '',   // constructor throws if empty
  baseURL: 'https://mro.example.net/wia/v1/aircraft-component',
  timeout: 30_000,                         // overrides the 60 s default
  debug: false,
});

// Audit hook: fires once per call, before the (mock) transport returns.
client.on('request', ({ method, path }) => {
  console.info(`[audit] ${method} ${path}`);
});

// Never trust `success` alone: also require `data`.
function unwrap<T>(res: APIResponse<T>, what: string): T {
  if (!res.success || res.data === undefined) {
    const code = res.error?.code ?? 'NO_DATA';
    throw new Error(`${what} failed: ${code} ${res.error?.message ?? ''}`);
  }
  return res.data;
}

export async function registerAndInspect(): Promise<void> {
  // 1. Register a serialized part (all values illustrative).
  const part = unwrap(
    await client.createComponent({
      type: 'flight-control',
      status: 'maintenance',
      partNumber: 'EX-FC-2210-07',
      serialNumber: 'SN-000123',
      manufacturer: 'Example Actuation Ltd',
      manufacturingDate: '2019-04-11',
      dimensions: { length: 412, width: 96, height: 88, weight: 3.4 },
      material: {
        type: 'aluminum-7075',
        grade: '7075-T6 (illustrative)',
        strength: 570,          // MPa, typical handbook UTS
        fatigueLimit: 159,      // MPa, illustrative
        density: 2810,          // kg/m3 (2.81 g/cm3 x 1000)
        corrosionResistance: 'medium',
      },
      inspectionHistory: [],
      metadata: { cageCode: 'X0000', cyclesSinceNew: 18250 },
    }),
    'createComponent',
  );

  if (!part.id) {
    throw new Error('server did not assign a record id');
  }

  // 2. Attach an eddy-current inspection with a conditional result.
  const inspection = unwrap(
    await client.addInspection(part.id, {
      inspectionDate: '2026-03-02',
      inspector: 'NDT-L2-0457',
      method: 'eddy-current',
      result: 'conditional',
      findings: 'Indication at lug bore below reject level; re-inspect',
      nextInspectionDate: '2026-09-02',
    }),
    'addInspection',
  );

  console.info(`inspection ${inspection.id} stored for ${part.serialNumber}`);
}

Two points in the example are deliberate. The material density is expressed in kg/m³, as the type definition requires, even though handbooks usually quote g/cm³; Chapter 2 explains the conversion. And cycle counts and the manufacturer's CAGE code travel in metadata, because the 1.0 types have no fields for them — an additive extension that Chapter 4 recommends formalising.

5.4 Responses and Errors

Two error models

The SDK wraps every result in APIResponse<T>: a boolean success, an optional data of type T, and an optional error object with a string code and a human-readable message. The specification, on the other hand, says error payloads follow Problem Details for HTTP APIs, RFC 9457, which obsoletes RFC 7807. The two are not in conflict if they are placed at different layers. Problem Details is the wire format between server and client; APIResponse is the in-process envelope that the SDK hands to application code. The SDK's job is to translate the former into the latter.

RFC 9457 defines a JSON object served with the media type application/problem+json and five standard members: type (a URI identifying the problem type; about:blank if none), title (a short summary that should not change between occurrences), status (the HTTP status code), detail (an explanation of this occurrence) and instance (a URI identifying this occurrence). Extension members are allowed, which is where the SDK-style code can live.

Problem Details response for a duplicate serial number (type URI illustrative)
HTTP/1.1 409 Conflict
Content-Type: application/problem+json
Content-Language: en

{
  "type": "https://problems.example.org/wia-aircraft-component/duplicate-serial",
  "title": "Serial number already registered for this part number",
  "status": 409,
  "detail": "S/N SN-000123 is already registered under P/N EX-FC-2210-07 (CAGE X0000).",
  "instance": "urn:uuid:5b0e7f5c-2d1a-4d6e-9c3b-8a1f0e2d4c77",
  "code": "DUPLICATE_SERIAL",
  "existingId": "cmp-7f3a91"
}

Carrying both type and code is redundant on purpose. The type URI is the stable, globally unique identifier that RFC 9457 wants; the short code is convenient for switch statements and log searches. A deployment should publish the mapping table once and never reuse a code for a different type. Annex E of PHASE 2 adds a relevant rule: adding new error codes is non-breaking, but renaming or removing fields, even in error payloads, requires a version bump. Client code should therefore treat an unknown code as a generic failure of its HTTP status class rather than crash.

A proposed code table

Only one code, REQUEST_FAILED, appears in the SDK. The table below is a proposal for a complete set that maps cleanly onto HTTP status codes and onto problems that a parts-records service actually meets.

codeHTTP statusWhen it occursClient should
VALIDATION_FAILED422Body is well-formed JSON but breaks the schema (bad enum, missing serial number, date not ISO 8601)Fix the data; do not retry unchanged
UNAUTHENTICATED401Token missing, expired or bound to another certificateObtain a new token, retry once
FORBIDDEN_SCOPE403Token valid but lacks the group's scopeStop; request access through governance
NOT_FOUND404Unknown record identifier or certificate numberCheck the identifier source
DUPLICATE_SERIAL409Manufacturer + P/N + S/N already registeredInvestigate; may indicate a data-migration error or a suspect part
PRECONDITION_FAILED412If-Match revision is staleRe-read, merge, resubmit
PRECONDITION_REQUIRED428Update sent without If-MatchAdd the revision header
RATE_LIMITED429Client exceeded its quotaWait for Retry-After, then retry
REQUEST_FAILED502/503, or none (network)Upstream or transport failureRetry with backoff if the call is idempotent

The DUPLICATE_SERIAL row deserves emphasis. In most APIs a conflict is a nuisance; here it can be a safety signal. Two physical parts claiming the same identity is one of the classic indicators of a suspected unapproved part, and Chapter 6 treats it as such. The server should log the attempt with the caller's identity instead of silently returning the existing record.

Rate limiting

The specification points to RFC 6585 for rate limiting. That RFC defines status code 429 Too Many Requests and allows the response to include a Retry-After header, whose syntax is defined in HTTP Semantics (RFC 9110) as either a number of seconds or an HTTP date.

429 response with Retry-After
HTTP/1.1 429 Too Many Requests
Retry-After: 30
Content-Type: application/problem+json

{
  "type": "https://problems.example.org/wia-aircraft-component/rate-limited",
  "title": "Request rate exceeded for this client",
  "status": 429,
  "code": "RATE_LIMITED"
}

A well-behaved client honours Retry-After when present and otherwise uses exponential backoff with random jitter, so that a fleet of hangar terminals reconnecting after an outage does not stampede the server in lockstep. Retries are safe only for idempotent calls; Section 5.6 shows how to make the important POSTs idempotent.

5.5 Discovery and Signed Manifests

Well-known discovery

Because implementations may support different subsets of the five groups, a client needs a way to ask a server what it offers before it starts sending data. PHASE 2 specifies the IETF well-known URI mechanism, RFC 8615, for operation discovery. RFC 8615 reserves the path prefix /.well-known/ for site-wide metadata and requires that each suffix be registered with IANA. The specification does not name a suffix, so the one used below, wia-aircraft-component, is illustrative; a production deployment should use whatever suffix the WIA working group registers.

Discovery document (all values illustrative)
GET /.well-known/wia-aircraft-component HTTP/1.1
Host: mro.example.net
Accept: application/json

HTTP/1.1 200 OK
Content-Type: application/json

{
  "standard": "WIA-SPACE-016",
  "document": "PHASE-2-API-INTERFACE",
  "specVersion": "1.0.0",
  "apiBase": "https://mro.example.net/wia/v1/aircraft-component",
  "openapi": "https://mro.example.net/wia/v1/aircraft-component/openapi.json",
  "tokenEndpoint": "https://auth.mro.example.net/oauth2/token",
  "operationGroups": {
    "componentCatalog":     { "status": "supported" },
    "maintenanceEvent":     { "status": "supported" },
    "traceability":         { "status": "partial",
                              "reason": "history limited to parts overhauled at this site" },
    "serviceBulletin":      { "status": "supported" },
    "certificationPackage": { "status": "excluded",
                              "justification": "repair station; does not submit type-design evidence" }
  },
  "profile": "WIA-P2-API-INTERFACE-Edge-only/2",
  "manifest": "https://mro.example.net/wia/v1/aircraft-component/manifest.dsse.json"
}

The document tells the caller where the API and its OpenAPI description live, where to obtain tokens, and which groups are supported, partial or excluded — using the same three statuses that Annex I prescribes for profile manifests. A client written against this document can fail early and clearly ("this repair station does not accept certification packages") instead of discovering the gap through a 404 halfway through a workflow.

Why the manifest is signed

Discovery data is a tempting target. If an attacker could alter the discovery document, they could redirect a certification authority client to a counterfeit token endpoint or a counterfeit API. TLS protects the document in transit, but not against a compromised web server or a stale copy served from a mirror. PHASE 2 therefore requires that manifest endpoints be signed with Sigstore using a DSSE envelope.

DSSE (Dead Simple Signing Envelope) wraps a payload of any type. The envelope carries a payloadType string, the base64-encoded payload, and one or more signatures, each with a keyid and a sig. The signature is computed not over the raw payload but over a pre-authentication encoding that binds the payload type and length to the bytes, which prevents an attacker from reinterpreting a signed document as a different kind of document.

DSSE envelope around the manifest (payload type and key id illustrative)
{
  "payloadType": "application/vnd.example.wia-manifest+json",
  "payload": "eyJzdGFuZGFyZCI6IldJQS1TUEFDRS0wMTYiLCJzcGVjVmVyc2lvbiI6...",
  "signatures": [
    {
      "keyid": "mro-example-manifest-2026q3",
      "sig": "MEUCIQDx...kA=="
    }
  ]
}

Sigstore adds the Rekor transparency log. When the manifest is signed, an entry is recorded in Rekor; a verifier can then check not only that the signature is valid, but also that it was publicly logged at a specific time. The same Sigstore key signs the profile manifest and the SBOM in the evidence package (Annexes G and I), so one verification routine serves all three.

Key rotation

The specification says only that the signing key is rotated "per the deployment policy". A workable policy has four parts. Publish the next public key before it is used, so verifiers can learn it in advance. Sign with the new key from a stated date and keep the old public key available for verification. Rely on the Rekor timestamp to show that each old signature was made while its key was valid, so historical manifests stay verifiable after the key retires. And keep an emergency revocation path for a compromised key, which invalidates signatures logged after the compromise time rather than all history. Sigstore also supports "keyless" signing, in which a short-lived certificate is issued against an OIDC identity; that removes long-lived private keys altogether, at the cost of depending on the identity provider.

5.6 Events, Idempotency and Concurrency

SDK events

The client exposes on(event, callback), backed by the eventemitter3 package. Internally it emits 'request' with { method, path, body } each time a call is made and 'error' if the transport throws. These are in-process hooks, useful for audit logging, metrics and debugging. They are not a subscription to server-side events: nothing in 1.0.0 tells a client that another organisation has recorded a maintenance event or issued a bulletin. For that, a deployment needs webhooks or a message stream, which the specification leaves to implementers. Note also that the 'request' event receives the full request body; if the audit sink is less trusted than the API, redact personal data such as inspector names before logging.

Why maintenance writes need idempotency keys

Hangars are poor network environments. A technician's tablet posts an inspection, the Wi-Fi drops before the response arrives, and the app cannot know whether the server stored the record. If it retries a plain POST, the part may end up with two identical inspections; if it does not retry, the part may have none. Both outcomes corrupt the maintenance record, and the second one can hide an inspection that was actually performed.

The standard remedy is an idempotency key: the client generates a unique value (typically a UUID) for each logical operation and sends it with every attempt. The server stores the key together with a fingerprint of the request and the response it produced. A retry with the same key and the same body gets the stored response replayed; a reuse of the key with a different body is rejected. The IETF HTTP API working group has an Internet-Draft that defines an Idempotency-Key header for exactly this; it had not been published as an RFC at the time of writing, so the status codes in the example below are this book's convention.

Idempotent inspection POST with retry
POST /components/cmp-7f3a91/inspections HTTP/1.1
Host: mro.example.net
Authorization: Bearer eyJhbGciOiJFUzI1NiIs...
Content-Type: application/json
Idempotency-Key: "0f8e2b9a-6a51-4c1e-a7d3-3b5c9e1d2f40"

{ "inspectionDate": "2026-03-02", "method": "eddy-current", "result": "conditional", ... }

--- first delivery ---
HTTP/1.1 201 Created
Location: /components/cmp-7f3a91/inspections/insp-0192

--- retry after a dropped connection, same key, same body ---
HTTP/1.1 201 Created
Location: /components/cmp-7f3a91/inspections/insp-0192
(stored response replayed; no second record)

--- same key, different body ---
HTTP/1.1 422 Unprocessable Content
Content-Type: application/problem+json

Three operations in this API should always carry a key: addInspection, any future maintenanceEvent write, and requestCertification (a duplicated certification request wastes authority time and confuses the audit trail). The server must keep keys for longer than the longest plausible offline period of a client; a tablet that syncs a day's work at the end of a shift needs at least that window. The key also gives Chapter 6's at-least-once delivery model its safety: at-least-once plus idempotent receipt behaves like exactly-once.

Optimistic concurrency on updates

Component records are edited by more than one organisation. A supplier may be correcting a manufacturing date while an MRO changes the status to maintenance. With a plain PUT, the second writer silently overwrites the first. HTTP already has the tool to prevent this: the server returns an ETag with every representation, and the client sends it back in If-Match when it updates. If the record has changed in between, the server answers 412 Precondition Failed. RFC 6585 adds 428 Precondition Required, which lets a server refuse updates that omit the precondition altogether.

Conditional update with ETag and If-Match
GET /components/cmp-7f3a91 HTTP/1.1
...
HTTP/1.1 200 OK
ETag: "rev-14"

PUT /components/cmp-7f3a91 HTTP/1.1
If-Match: "rev-14"
Content-Type: application/json

{ ...full record with status "maintenance"... }

HTTP/1.1 412 Precondition Failed      (someone else wrote rev-15 first)
HTTP/1.1 428 Precondition Required    (If-Match header missing)

For status changes this is more than hygiene. A ComponentStatus moving from maintenance back to production is effectively a return-to-service statement; it must be based on the latest record, not on a copy that predates a failed inspection.

Timeouts and clocks

The SDK's 60-second default timeout is generous for record lookups and too short for uploading a large certification package. A sensible split is short timeouts for reads and small writes, and an asynchronous upload pattern (upload to a staging location, then reference it in the request) for evidence files. Annex E of PHASE 2 adds two operational notes that matter here: deadlines that depend on timestamps need synchronised clocks (NTPv4 is sufficient for most, PTP where deterministic interlocks are required), and implementations should publish a monthly error-budget summary with p95 latency, error rate and violation hours so that operators can compare providers without exposing tenant data. Both are non-normative, but both are cheap to adopt.

5.7 Limits of the Reference Implementation

A reader who installs @wia/aircraft-component 1.0.0 and runs the example in Section 5.3 will see every call succeed. That is not evidence that anything works. This section explains why and what to do about it.

The mock transport

Every public method ends in the private makeRequest function. Its body contains a comment saying that in production it would make actual HTTP requests; for now it emits the 'request' event and returns { success: true, data: body } — the request body itself, cast to whatever type the caller expects. There is no network call, no header, no use of the configured timeout and no use of the API key. The consequences are easy to demonstrate.

What the mock returns
const c = await client.createComponent({ partNumber: '', serialNumber: '' });
// { success: true, data: { partNumber: '', serialNumber: '' } }   <- no id, no validation

const r = await client.requestCertification('cmp-7f3a91', 'CS-25');
// { success: true, data: { standard: 'CS-25' } }   <- r.data.requestId is undefined

const v = await client.verifyCertification('XX-0000');
// { success: true, data: undefined }               <- "success" with nothing verified

In the worked example, createComponent returns the part without an id, because the request did not contain one and no server assigned one. Without the explicit check, the next call would post the inspection to /components/undefined/inspections and still report success. That is why the example tests for the identifier and why unwrap insists on data. Code written this way keeps working unchanged when a real transport replaces the mock.

The companion simulator has the same character. Its communication-protocol tab prints four endpoint tests with fixed "200 OK" and "201 Created" results, and two of those paths (/api/components/AC-001/certification and /api/components/AC-001/stress-analysis) do not match the SDK's paths at all. It is a teaching illustration, not a test. Likewise, the PHASE 2 text refers to api/openapi.yaml, which is not present in the repository; until it exists, the SDK's type definitions and the endpoint table in this chapter are the closest thing to an interface contract.

Replacing the transport

The sketch below shows what a real makeRequest has to do: attach a bearer token from a token provider, apply the configured timeout with AbortController, send JSON, translate RFC 9457 problem responses into APIResponse, and distinguish timeouts from other network failures. Node.js 18 and later provide fetch and AbortController globally. Retries, idempotency keys and ETag handling sit one layer above this function.

A real transport (sketch)
// Sketch of a real transport. NOT part of @wia/aircraft-component 1.0.0.
async function httpRequest<T>(
  cfg: { baseURL: string; timeout: number },
  getToken: () => Promise<string>,
  method: string,
  path: string,
  body?: unknown,
  extraHeaders: Record<string, string> = {},
): Promise<APIResponse<T>> {
  const ctrl = new AbortController();
  const timer = setTimeout(() => ctrl.abort(), cfg.timeout);
  try {
    const res = await fetch(cfg.baseURL + path, {
      method,
      signal: ctrl.signal,
      headers: {
        Authorization: `Bearer ${await getToken()}`,
        Accept: 'application/json, application/problem+json',
        ...(body === undefined ? {} : { 'Content-Type': 'application/json' }),
        ...extraHeaders,
      },
      body: body === undefined ? undefined : JSON.stringify(body),
    });
    if (res.ok) {
      const data = res.status === 204 ? undefined : ((await res.json()) as T);
      return { success: true, data };
    }
    const type = res.headers.get('content-type') ?? '';
    if (type.startsWith('application/problem+json')) {
      const p = await res.json();
      return {
        success: false,
        error: { code: p.code ?? String(p.type), message: p.detail ?? p.title ?? '' },
      };
    }
    return { success: false, error: { code: `HTTP_${res.status}`, message: res.statusText } };
  } catch (e: any) {
    const code = e?.name === 'AbortError' ? 'TIMEOUT' : 'REQUEST_FAILED';
    return { success: false, error: { code, message: String(e?.message ?? e) } };
  } finally {
    clearTimeout(timer);
  }
}

// Path segments built from part data must be encoded:
const path = `/components/${encodeURIComponent(id)}/inspections`;

The last line addresses a subtle defect in the SDK: it interpolates identifiers straight into paths. Record identifiers are usually safe, but certificate numbers and serial numbers from legacy systems can contain slashes, spaces or other reserved characters. encodeURIComponent prevents a serial number from silently turning into a different path.

ComponentAnalyzer: what to use and what not to use

The SDK also ships four static helpers. Two of them are simple and correct as far as they go: calculateStress(force, area) returns force divided by area, and calculateSafetyFactor(materialStrength, appliedStress) returns their ratio. isSafe(safetyFactor, minimumSafetyFactor = 1.5) compares the ratio against a threshold. The fourth, estimateFatigueLife, should never be used.

ComponentAnalyzer behaviour
import { ComponentAnalyzer as CA } from '@wia/aircraft-component';

// Units are the caller's problem: N / mm^2 = MPa.
const stress = CA.calculateStress(50_000, 400);        // 125 (MPa)
const wrong  = CA.calculateStress(50, 400);            // 0.125 -- kN / mm^2 = GPa!

const fs = CA.calculateSafetyFactor(570, stress);      // 4.56
CA.isSafe(fs);                                         // true (threshold 1.5)

// Anti-example: do not use for engineering.
CA.estimateFatigueLife(570, 570);   // 1.0e8 cycles at S = Su  (physically absurd)
CA.estimateFatigueLife(285, 570);   // about 8.0e8 cycles

Units. The helpers are unit-agnostic, which means the caller must be consistent. StructuralAnalysis.appliedLoad is in kN and calculatedStress in MPa; one megapascal is one newton per square millimetre, so the load must be converted to newtons before dividing by an area in mm². Forgetting the conversion yields a stress a thousand times too small, and a safety factor a thousand times too large.

The 1.5 threshold. The default of isSafe echoes the factor of safety of 1.5 between limit and ultimate load in the transport-category rules (14 CFR / CS 25.303). That factor is applied to loads. Comparing a material strength to a stress and asking for a ratio of at least 1.5 is equivalent only under specific conditions: linear behaviour, the stress computed at limit load, and a strength value that is a statistically based design allowable rather than a typical handbook tensile strength. Even then it says nothing about the separate requirement that the structure withstand limit load without detrimental permanent deformation (25.305), about fatigue, or about damage tolerance. isSafe is a convenience for demonstrations, not a compliance finding.

The fatigue estimate. estimateFatigueLife computes N = 108 − 3·log10(S/Su). At a stress equal to the ultimate strength it predicts 108 cycles; a real specimen loaded to its ultimate strength fails on the first load application. At half the ultimate strength it predicts roughly 8 × 108 cycles, again far beyond what any aluminium alloy test would support. The formula has the shape of a Basquin-type S–N relation but the constants are anchored at the wrong end of the curve, and it ignores mean stress, stress concentration, surface finish and environment. Fatigue lives in a WIA record must come from test-based S–N data or from the approved fatigue and damage-tolerance analysis (Chapter 7), with the source cited. A value produced by this helper should be treated as a defect if it ever appears in a component record.

Checklist before calling an implementation "PHASE 2 ready"

ItemReference SDK 1.0.0Needed for production
Real HTTP transport with timeoutNo (mock)Yes
OAuth 2.0 tokens per RFC 9700; mTLS for authority callersNo (unused API key)Yes
RFC 9457 problem parsingNoYes
Traceability query by manufacturer + P/N + S/NNo methodYes
Service bulletin / AD operationsNo methodIf the profile claims the group
Idempotency keys on maintenance and certification writesNoYes
ETag / If-Match on updatesNoYes
Well-known discovery and DSSE-signed manifestNoYes
Published OpenAPI documentReferenced but absentYes
Fatigue estimationPhysically wrong helperRemove; cite approved analysis

None of these gaps makes the SDK useless. Its types are a faithful expression of the PHASE 1 data model, its method list is a reasonable first draft of the resource layout, and its APIResponse envelope is a convenient boundary for application code. Used as a skeleton — with the transport, security and reliability layers filled in as described in this chapter — it is a sound starting point. Chapter 6 now puts these operations in motion, following the messages that pass between supplier, MRO, operator and authority over one part's life.