How a research institution exposes its biosafety records as a service: resources and data objects, versioning, errors, authentication, pagination, idempotency, webhooks, and a typed client for record operations.
Chapters 2 and 3 defined what a biosafety record contains: the registered facility and its certification block, the IBC authorization, training records, and the incident report with its state machine. A record that exists only as a data structure is still a spreadsheet with better column names. The value appears when every system that touches those records (the biosafety office's screens, the HR import job, the door-control system, the inspector's read-only view, a partner's exchange gateway) reads and writes them through one interface with the same rules. This chapter defines that interface.
The scope is record management. The service registers facilities, stores inspection and audit results, keeps training and access records, runs incident reports through their workflow, and keeps custody and inventory entries at the bookkeeping level. Its questions are administrative. Is this certification current? Does this person hold valid training for the facility they may enter? Which corrective actions are overdue? Who changed this record, and when? The service never decides what containment a piece of work needs and never judges whether a situation is safe. Those decisions arrive in the records as outcomes recorded by the people and bodies entitled to make them.
What is normative and what is proposed. The WIA-BIO-017 PHASE-2 API document states only a few interface facts: a base URL of https://api.wia-bio.org/bio-safety/v1, authentication by OAuth 2.0 plus API keys, and a rate limit of 1,000 requests per hour. The book treats the base URL as illustrative (its availability has not been verified) and the rate limit as an example configuration. The PHASE-2 annexes add non-normative notes on schema evolution and on declaring a deployment's operational scope in its OpenAPI document. Everything else here, including resource paths, headers, error bodies and the webhook format, is the book's proposed profile, designed to fit the SDK's types and the exchange rules of Chapter 5.
Five principles shape every resource in the profile. Most design disputes during implementation can be settled by returning to them.
status, change only through a dedicated sub-resource (/transitions, /receipt) that checks preconditions and records a reason. Chapter 3's transition table is enforced here, not in the user interface.
Paths are relative to the service's base URL, for example /v1 on the institution's own host. Every collection supports GET for listing (Section 4.5), and every record supports GET by ID. The table shows the write operations.
| Resource path | Record (ID prefix) | Write operations | Notes |
|---|---|---|---|
/facilities | BiosaftyFacility (FAC-) | POST register; PATCH with If-Match | Location detail is a separate field group released only on need-to-know |
/facilities/{id}/certifications | Certification entry | POST append | Each decision is appended; the facility's certification block shows the current one |
/authorizations | IBC authorization (AUT-) | POST; PATCH validity and status | The committee's outcome only; reasoning stays in the minutes |
/risk-assessment-records | Assessment metadata (RAR-) | POST; PATCH | Assessor, dates, IBC reference, approved level, document reference; the document itself is never parsed |
/inspections | Inspection or audit (INS-) | POST; POST .../findings | Internal audits and authority inspections; findings link to CAPA |
/training-records | TrainingRecord (TRN-) | POST; PATCH corrections | Bulk import uses the same endpoint with idempotency keys |
/access-grants | Access grant (ACG-) | POST; POST .../revocation | Who may enter which facility, on what basis, until when |
/access-reviews | Access-log review (ACR-) | POST | Door-system exports compared with the roster; raw events stay in the door system |
/incidents | IncidentReport (INC-) | POST; PATCH non-workflow fields | Status only through /incidents/{id}/transitions; CAPA and NotificationRecord as sub-resources |
/inventory-entries | Inventory entry (INV-) | POST; POST .../reconciliations | Bookkeeping only: registry reference, facility, custodian, status |
/custody-transfers | Transfer and receipt (TRF-) | POST; POST .../receipt | The record pair of Chapter 5 |
/compliance-checks | ComplianceResult (CHK-) | POST runs a check | Stored and immutable |
/audit-events | AuditEvent (AUD-) | None | Read-only; written by the service itself |
/webhook-subscriptions | Subscription (WHS-) | POST; DELETE | Event delivery to other systems (Section 4.8) |
Chapter 2's prefix list is extended here with INS-, ACG-, ACR-, INV- and WHS-, under the same rule: opaque, stable, never reused. Personnel are deliberately absent. The record service does not host a people directory; it resolves PER- identifiers against the institution's identity system, as Chapter 6 describes.
Every record returned by the service is wrapped in a small block of service metadata. Keeping that metadata outside the record body means the body stays exactly the SDK type or its profile extension, so client code can pass it to functions typed against BiosaftyFacility or IncidentReport without conversion.
GET /v1/facilities/FAC-EXU-B07-0314
Authorization: Bearer eyJhbGciOi…
HTTP/1.1 200 OK
Content-Type: application/json
ETag: "v7"
{
"meta": {
"id": "FAC-EXU-B07-0314",
"recordType": "BiosaftyFacility",
"schemaVersion": "1.2.0",
"version": 7,
"updatedAt": "2026-02-18T07:41:10Z",
"updatedBy": "PER-0B44",
"redactedGroups": ["location"]
},
"record": {
"facilityId": "FAC-EXU-B07-0314",
"name": "Example University Teaching Laboratory 3",
"bslLevel": "BSL-2",
"certification": {
"certified": true,
"certificationDate": "2025-03-20T00:00:00Z",
"expirationDate": "2027-03-19T00:00:00Z",
"certifyingAuthority": "Example Certifying Body",
"nextInspectionDate": "2026-03-20T00:00:00Z"
},
"authorizedPersonnel": ["PER-7F3A", "PER-2C91", "PER-0B44"]
}
}
All identifiers, names and dates are fictitious. version increases by one on every accepted change and is echoed in the ETag header (Section 4.3). schemaVersion names the schema the body conforms to. redactedGroups lists field groups that exist but were withheld from this caller. Here the caller may not see location detail, so the location block is absent and the metadata says so. Without that list, a client cannot tell "withheld" from "never entered" and may wrongly report the record as incomplete.
The facility's certification block holds the current decision. It does not hold the history of inspections and internal audits behind that decision, or what each one found. An inspector's first question is usually "what did the last inspection find, and what was done about it?", so the profile gives inspections their own record.
| Field | Type | Meaning |
|---|---|---|
inspectionId | string | Stable ID with INS- prefix |
facilityId | string | The registered facility inspected |
kind | enum | internal-audit, ibc-review, authority-inspection, certification-renewal |
conductedBy | object | Pseudonymous ID for internal staff, or the external body's name and reference number |
scheduledAt, conductedAt | ISO 8601 UTC | Planned and actual dates |
outcome | enum | no-findings, findings-open, findings-closed, certification-granted, certification-withheld, as reported by the inspecting body |
findings[] | array | findingId, optional code, short administrative summary, capaRef, dueAt |
documentRef | string | The signed report in the document management system |
When an authority inspection ends in a certification decision, two writes follow: the inspection record with its outcome, and a new entry appended to /facilities/{id}/certifications, both carrying the same documentRef. The service never infers a certification from an outcome. The biosafety officer enters the decision as the authority issued it, and the service only checks that the two records agree. Technical observations stay in the signed report. The summary field is for tracking ("door-closure log incomplete for February") and passes the redaction checklist of Chapter 6 before it is saved.
Training records are exposed exactly as Chapter 2 defined them. Two queries dominate: all current training for one person, and all records expiring within a window. The second drives the reminders that stop authorizations from lapsing unnoticed.
Access is split into two records. An access grant states that a person may enter a facility: grantId, facilityId, personnelId, basis (normally an AUT- authorization, or an escort arrangement approved by the biosafety officer), validFrom, validUntil, grantedBy and status. The facility's authorizedPersonnel list is derived from active grants. An access review records that someone compared the door system's log for a period against the roster: period, reviewer, a reference to the exported log, the number of mismatches, and links to any incidents opened. Raw door events never enter the record service.
Creating a grant is where the service does its most useful cross-record check. Before accepting POST /access-grants, it confirms that the facility's certification is current, that the basis authorization is active and covers this person and facility, and that the person holds valid training of the types institutional policy requires. A failure is refused with a code from Section 4.6. The required training types are institutional configuration; neither the standard nor this book prescribes them.
Institutions must be able to show which registered materials are held in which registered facility, and who is accountable for each. The inventory entry answers that and nothing more: inventoryEntryId, materialRef (the internal registry ID), the riskGroup label as recorded in the registry, facilityId, custodianId, status (held, in-transfer, transferred-out, disposal-recorded) and lastReconciledAt. It holds no descriptive or quantitative data about the material, and a schema with additionalProperties: false rejects any attempt to add such data (Chapter 2's data-minimization rule).
Other records drive the status. A custody transfer (Chapter 5) moves the entry to in-transfer. The receipt moves it to transferred-out and creates a new entry at the receiver. A disposal is recorded by reference to the document that attests it. A reconciliation records that the custodian confirmed the entry against the registry on a given date. A discrepancy is never fixed by editing the entry; it opens an incident or near-miss record, as Chapter 5 requires for custody receipts.
The major version appears in the path (/v1), as in the PHASE-2 base URL. Within a major version, the PHASE-2 annex allows only additive changes: new optional fields, new resources, new error codes. Renaming or removing a field, even in an error body, is breaking. The service reports its full version in a response header (WIA-API-Version: 1.3.0, a profile-defined name) and in its published OpenAPI document, which also declares the deployment's operational scope as the annex suggests.
When a new major version ships, the old one stays available for a parallel window. The PHASE annexes disagree on its length (Annex F says at least 90 days, Annex H at least 180). The profile uses 180 days, the stricter value; Chapter 7 discusses the conflict. During the window, old-version responses carry a Sunset header (RFC 8594) with the retirement date, so client operators see the deadline in their own logs rather than learning it from an outage.
Two people editing one facility record at once is an everyday event: the biosafety officer updates the roster while the secretariat enters a certification date. Without concurrency control the later save silently discards the earlier one. The profile uses HTTP conditional requests (RFC 9110): every response carries the record version as an ETag, and every PATCH must return it in If-Match.
PATCH /v1/facilities/FAC-EXU-B07-0314
If-Match: "v7"
Content-Type: application/merge-patch+json
{ "name": "Example University Teaching Laboratory 3 (Annex)" }
HTTP/1.1 412 Precondition Failed
Content-Type: application/problem+json
{
"type": "https://example.org/wia-bio-017/problems/version-conflict",
"title": "Record changed since it was read",
"status": 412,
"currentVersion": 8
}
The client re-reads the record, re-applies its change and tries again. A PATCH without If-Match is refused with 428 Precondition Required (RFC 6585), so no client can skip the check by accident. The merge-patch format (RFC 7396) replaces arrays whole, which is one reason the roster is changed through access grants rather than by patching the list.
Earlier versions remain readable. GET /v1/facilities/{id}?asOf=2026-01-10T00:00:00Z returns the version in force at that moment, and /versions lists all versions with their audit-event references. As Chapter 5 explains, checking whether a certification was valid on the date of a transfer must use the record as it was on that date.
Chapter 6 sets out how OAuth 2.0 and API keys divide the work. People sign in through the institution's identity provider with multi-factor authentication, and the client presents the resulting OAuth 2.0 access token (RFC 6749) as a bearer token (RFC 6750). Unattended integrations, such as the HR training import, use the client-credentials grant with short-lived tokens. An API key may identify the registered client application but is never sufficient on its own to read restricted or personal fields. For server-to-server links, certificate-bound tokens (mutual TLS, RFC 8705) are preferable where the institution can support them, because a stolen token is then useless without the client's private key.
Scopes describe what a token may do; roles describe who the person is. The service grants access only where both allow it.
| Scope | Allows | Typical holders (roles from Chapter 6) |
|---|---|---|
facilities:read | Facility summaries, certification history | All internal roles; inspector (time-boxed) |
facilities:location | The location field group | Biosafety officer; inspector in scope |
facilities:write | Register facilities; append certifications | Biosafety officer |
training:write | Create and correct training records | Training coordinator; HR import client |
access:write | Access grants and reviews | Biosafety officer; door-system client (reviews only) |
incidents:report | Create incident and near-miss reports | All registered personnel; anonymous-channel client |
incidents:manage | Transitions, CAPA, notifications | Biosafety officer; assigned investigator (own cases) |
audit:read | Audit events | Biosafety officer; auditor |
Some decisions depend on the data. A principal investigator may read the training status of people on their own approved projects, not of everyone, and an investigator may transition only the incidents assigned to them. The service evaluates these relationships at request time from the records themselves, on top of the role matrix.
Inspector access is modelled explicitly. The biosafety officer creates a grant naming the inspection record, the facilities in scope, read-only scopes and an expiry time. The inspector's token carries the grant ID, every read made with it is audited with that ID as the reason reference, and the token stops working when the grant expires, whatever its own lifetime.
When a caller is authenticated but not permitted, for example a token without facilities:location asking for the location group, the service answers 403 with code B003 "Unauthorized access" (specification §9.4) and writes an audit event with the action access-denied. Chapter 6 builds an alert on these events. A missing or expired token gets 401 with no B-code: a denial means someone reached the door and was turned away, while a 401 only means the client must sign in again.
Collections such as training records and audit events grow without limit, and offset paging (?page=40) misbehaves when data changes during reading: rows shift between pages and appear twice or not at all. The profile uses opaque cursors instead.
GET /v1/training-records?expiresBefore=2026-06-30T00:00:00Z&facilityId=FAC-EXU-B07-0314&limit=50
HTTP/1.1 200 OK
Link: </v1/training-records?cursor=eyJrIjoiVFJOLTIwMjYtMDA0ODEifQ>; rel="next"
{
"items": [
{ "meta": { "id": "TRN-2025-000731", "version": 2 },
"record": { "trainingRecordId": "TRN-2025-000731",
"personnelId": "PER-2C91",
"trainingType": "refresher",
"expirationDate": "2026-05-12T00:00:00Z" } }
],
"nextCursor": "eyJrIjoiVFJOLTIwMjYtMDA0ODEifQ",
"snapshotAt": "2026-03-02T01:00:00Z"
}
The cursor is also offered in a Link header (RFC 8288) that generic HTTP tools understand. snapshotAt states the moment the listing reflects. A client assembling a report such as "everyone whose training expires this quarter" passes that value as asOf on every later page, so the whole report describes one moment even while records change. For audit events the cursor follows the stream's seq from Chapter 5, so paging never skips or repeats an event.
Filters are named query parameters, not a general query language. A small fixed set is easier to secure and index, and it stops clients from combining conditions to infer what a redaction hides.
| Collection | Filters | Typical question |
|---|---|---|
/facilities | bslLevel, certificationExpiresBefore, building (location scope only) | Which certifications lapse before the next committee meeting? |
/training-records | personnelId, trainingType, expiresBefore, facilityId | Who needs a refresher before their grant renews? |
/access-grants | facilityId, personnelId, status, validAt | Who could enter this room on a given date? |
/incidents | status, severity, eventClass, facilityId, occurredFrom/occurredTo | Which serious incidents are still under investigation? |
/audit-events | recordRef, actor, action, from/to | Who changed this record, and when? |
Filters respect field-level permissions: a caller without the location scope cannot filter by building, and only roles that may resolve personnel IDs may filter incidents by them. At the rate limit, which PHASE-2 gives as 1,000 requests per hour and the book treats as an example, the service answers 429 with a Retry-After header. Bulk jobs such as a nightly import should have their own client identity and quota, so a backlog never locks the biosafety officer out of the incident screen.
Errors are returned as problem details (RFC 9457, application/problem+json) with a type URI, a short title, the HTTP status and a human-readable detail. The profile adds code for a WIA-BIO-017 code where one applies, and findings[] when a request fails several checks at once, so that the first problem does not hide the others.
POST /v1/access-grants
Idempotency-Key: grant-PER-5D10-FAC-EXU-B07-0314-2026-03
Content-Type: application/json
{ "facilityId": "FAC-EXU-B07-0314", "personnelId": "PER-5D10",
"basis": "AUT-2026-0019", "validFrom": "2026-03-04T00:00:00Z",
"validUntil": "2026-08-31T23:59:59Z" }
HTTP/1.1 409 Conflict
Content-Type: application/problem+json
{
"type": "https://example.org/wia-bio-017/problems/grant-preconditions",
"title": "Access grant preconditions not met",
"status": 409,
"detail": "2 of 3 preconditions failed for PER-5D10 at FAC-EXU-B07-0314.",
"findings": [
{ "code": "B009", "subject": "PER-5D10",
"message": "No valid training record of required type 'initial'." },
{ "code": "B008", "subject": "AUT-2026-0019",
"message": "Authorization approvedLevel does not match facility bslLevel." }
]
}
The person has no current initial training, and the authorization meant to justify the grant carries an approved level that differs from the level recorded for the facility. Both are record inconsistencies, not technical judgments. The response tells the biosafety officer which records to fix; it does not say what the work requires, which the committee settled when it approved the authorization.
Specification §9.4 lists six codes, B001 to B006. The SDK's BiosaftyErrorCode enumeration contains those six and four more, B007 to B010, which the specification's table does not mention. The profile uses all ten identifiers, because clients already import the enumeration, but gives them different roles.
| Code | BiosaftyErrorCode member | Source | Role in the record service | HTTP |
|---|---|---|---|---|
| B003 | UNAUTHORIZED_ACCESS | Spec §9.4, SDK | Authenticated but not permitted; audit event always written | 403 |
| B007 | INVALID_PARAMETERS | SDK only | Schema validation failure (bad enum value, wrong ID prefix, unknown field in a closed object) | 422 |
| B008 | BSL_MISMATCH | SDK only | Recorded level labels disagree across records (authorization vs. facility, assessment record vs. authorization) | 409 on write; finding in a check |
| B009 | TRAINING_EXPIRED | SDK only | Required training missing or expired for a person on a roster or grant | 409 on write; finding in a check |
| B010 | CERTIFICATION_REQUIRED | SDK only | Facility certification absent or past its expiration date | 409 on write; finding in a check |
| B001, B002, B004, B005, B006 | Other members | Spec §9.4, SDK | Stored only as a label on an inspection finding or incident, entered from the responsible body's report; never produced by the service's own checks | None (data, not an error) |
The last row matters most. A check that "detected" one of those five conditions from the booleans in a facility's engineering checklist would be a technical determination made by software, and it would be wrong exactly when it counts, because the checklist records what a certifying body attested at one moment. The profile keeps the codes as labels, so findings and incidents can be counted by category, and forbids the service from producing them.
Protocol problems are kept apart from B-codes: version-conflict (412), precondition-required (428), illegal-transition (409, an incident transition outside Chapter 3's table), idempotency-key-reuse (409) and rate-limited (429). Otherwise a dashboard would count a stale browser tab as a compliance violation.
The SDK exports BiosaftyError with the constructor (code: BiosaftyErrorCode, message: string, details?: Record<string, unknown>). The spelling is the SDK's own and stays verbatim at the import boundary, with correctly spelled aliases re-exported as Chapter 2 recommends. A client can convert coded problem responses into this class, so errors from the service and from local validation are handled the same way:
import { BiosaftyError, BiosaftyErrorCode } from '@wia/bio-017';
interface Problem {
type: string; title: string; status: number; detail?: string;
code?: string; findings?: { code: string; subject: string; message: string }[];
}
const KNOWN = new Set<string>(Object.values(BiosaftyErrorCode));
export function toBiosafetyError(p: Problem): BiosaftyError | undefined {
const first = p.code ?? p.findings?.[0]?.code;
if (!first || !KNOWN.has(first)) return undefined; // protocol problem, not a finding
return new BiosaftyError(first as BiosaftyErrorCode, p.detail ?? p.title, {
status: p.status, type: p.type, findings: p.findings ?? [],
});
}
Chapter 5 applies idempotency keys to messages between institutions. The same problem arises inside one institution whenever a client retries a request whose response it never received: a laptop drops off the network after "submit incident", or the nightly import restarts halfway. Duplicates in biosafety records are not harmless. Two copies of one incident inflate statistics sent to an authority, and two copies of one training record can hide a correction applied to only one of them.
The profile therefore requires an Idempotency-Key header on every POST that creates a record or triggers an action. The header name follows a draft in the IETF HTTP working group, and the semantics are those of Chapter 5:
import-HR-2026-03-01-row-4410) are easier to audit than random ones.idempotency-key-reuse. The service does not guess which body was meant.
PATCH needs no key, because If-Match already makes it safe to retry: a repeat against an advanced version fails with 412, showing the first attempt succeeded.
Much biosafety administration is triggered by dates and state changes, and other systems need to react. The door-control system should revoke access when a grant ends, the learning-management system should book a refresher, and the on-call pager should fire for a severity 3 incident. Polling every collection is wasteful and slow, so the profile offers webhook subscriptions.
| Event type | Emitted when | Typical subscriber |
|---|---|---|
facility.certification.lapsed | Expiration date passes with no new entry | Door-control adapter; dashboard |
training.expiring | A training record enters its reminder window | Learning-management system |
access-grant.ended | A grant expires or is revoked | Door-control adapter |
incident.created | A new incident or near-miss report is accepted | Notification service (Chapter 3 SLA timer) |
incident.transitioned | A state transition is accepted | IBC secretariat queue |
custody-transfer.overdue | No receipt within the agreed window | Sending and receiving biosafety offices |
Reminder-window lengths are institutional configuration; the profile names the events, not the number of days.
Payloads carry identifiers, not record content. The subscriber fetches the record through the API with its own credentials. Access control is then applied once, in the API. A payload logged by mistake reveals almost nothing, and a subscriber that falls behind reads the current state rather than a stale copy.
POST /hooks/biosafety (subscriber's endpoint)
Content-Type: application/json
WIA-Webhook-Id: 0192f7b1-4d20-7a33-8e11-6b2c9d0e7f45
WIA-Webhook-Timestamp: 1772500000
WIA-Webhook-Signature: v1=5e0f…c29a
{
"eventType": "incident.transitioned",
"occurredAt": "2026-03-03T01:06:40Z",
"subscriptionId": "WHS-0007",
"recordRef": { "type": "IncidentReport", "id": "INC-2026-0042", "version": 6 },
"change": { "from": "under-investigation", "to": "resolved" },
"auditEventId": "AUD-2026-0005102"
}
timestamp + "." + body with a per-subscription secret. Subscribers verify it in constant time and reject timestamps outside a tolerance they choose (a few minutes, for example). During secret rotation both signatures are sent.WIA-Webhook-Id, which subscribers use to discard duplicates.version in recordRef lets a subscriber ignore events older than the state it holds. Strict order comes from reading /audit-events by seq.
The /compliance-checks resource gathers the cross-record rules of this chapter. A client posts a scope (a facility, an authorization or a department) and an asOf time. The service evaluates its rules against the record versions in force at that time and stores a ComplianceResult under a CHK- ID. The result reuses the SDK's shape (isCompliant, violations, warnings, recommendations, complianceScore, requiredUpgrades). The profile adds structured findings[] and the ID and version of every record consulted.
The rules are few and entirely administrative. Is the certification present and unexpired at asOf? Does every active authorization for the facility carry an approved level label equal to the facility's recorded level? Does everyone on the roster hold an active grant and the training types policy requires? Are inspection findings, CAPA items or notifications past due? Each rule compares recorded values with other recorded values or with a date.
Consider two record sets. In the first, facility FAC-EXU-B07-0314 is recorded at BSL-2 with certification valid until 2027, and its one active authorization, AUT-2026-0011, was approved at BSL-2. In the second, the facility also carries authorization AUT-2026-0019, whose approved level was entered as BSL-3. That may be a typing error or a project linked to the wrong facility. The service cannot tell which, and does not try.
POST /v1/compliance-checks
Idempotency-Key: chk-FAC-EXU-B07-0314-2026-03-05
Content-Type: application/json
{ "scope": { "facilityId": "FAC-EXU-B07-0314" }, "asOf": "2026-03-05T00:00:00Z" }
HTTP/1.1 201 Created
Location: /v1/compliance-checks/CHK-2026-00188
{
"checkId": "CHK-2026-00188",
"asOf": "2026-03-05T00:00:00Z",
"result": {
"isCompliant": false,
"violations": ["AUT-2026-0019: approved level differs from facility level"],
"warnings": [],
"recommendations": ["Confirm AUT-2026-0019 with the IBC secretariat"],
"complianceScore": 50,
"requiredUpgrades": []
},
"findings": [
{ "code": "B008", "subject": "AUT-2026-0019", "related": ["FAC-EXU-B07-0314"] }
],
"consulted": [
{ "id": "FAC-EXU-B07-0314", "version": 7 },
{ "id": "AUT-2026-0011", "version": 2 },
{ "id": "AUT-2026-0019", "version": 1 }
]
}
For the first record set the same call returns isCompliant: true with no violations or findings. The recommendation in the second case is to confirm the record with the people who own the decision. The service never recommends changing the facility or the work, and requiredUpgrades stays empty by design. complianceScore is kept for compatibility with the SDK type; as Chapter 7 explains, it is a heuristic, not a percentage of checks passed. Because each result lists the versions it consulted, a check is reproducible, and an inspector can see exactly which record versions the system looked at on 5 March.
According to its package.json, the reference package is named @wia/bio-017, version 1.0.0. Its availability on the public npm registry has not been verified, so the examples can equally be built from the source in the standard's repository. For record work the package contributes the record and result types (BiosaftyFacility, TrainingRecord, IncidentReport, ComplianceResult and the enumerations of Chapter 2), the BiosaftyErrorCode enumeration and BiosaftyError class, and the BioSafetySDK class, whose getVersion() reports the library version for the evidence package of Chapter 7. The shell tool's wia-bio-017 version subcommand does the same in CI logs.
The legacy v1.0.0 SDK also ships calculation helpers for risk scores, exposure, containment and protective equipment, and these lie outside the scope of this record standard, because technical safety determinations belong to institutional biosafety committees and official authorities.
The package has no HTTP client, so a deployment writes one or generates it from its OpenAPI document. The interface below is the book's proposal, not part of the v1.0.0 package. It wraps this chapter's conventions so that application code does not repeat them.
// records/client.ts - book profile, not part of @wia/bio-017 v1.0.0
import type { TrainingRecord, IncidentReport, ComplianceResult } from '@wia/bio-017';
import type { BiosafetyFacility } from './sdk-aliases'; // Chapter 2 alias file
export interface Envelope<T> {
meta: { id: string; version: number; schemaVersion: string; redactedGroups?: string[] };
record: T;
}
export interface Page<T> { items: Envelope<T>[]; nextCursor?: string; snapshotAt: string }
export interface RecordClient {
getFacility(id: string, opts?: { asOf?: string }): Promise<Envelope<BiosafetyFacility>>;
patchFacility(id: string, ifMatch: number, patch: Partial<BiosafetyFacility>)
: Promise<Envelope<BiosafetyFacility>>;
listTraining(filter: { personnelId?: string; facilityId?: string; expiresBefore?: string },
page?: { cursor?: string; limit?: number; asOf?: string })
: Promise<Page<TrainingRecord>>;
createTraining(rec: TrainingRecord, idempotencyKey: string)
: Promise<Envelope<TrainingRecord>>;
reportIncident(rec: Partial<IncidentReport>, idempotencyKey: string)
: Promise<Envelope<IncidentReport>>;
transitionIncident(id: string, to: IncidentReport['status'], reason: string,
idempotencyKey: string): Promise<Envelope<IncidentReport>>;
runComplianceCheck(scope: { facilityId: string }, asOf: string, idempotencyKey: string)
: Promise<{ checkId: string; result: ComplianceResult }>;
}
Two usage patterns cover much application code: iterating a listing at a fixed snapshot, and changing a record under optimistic concurrency with one retry on conflict.
import type { RecordClient } from './records/client';
// 1. Every training record expiring before a date, as one consistent snapshot.
export async function* expiringTraining(c: RecordClient, before: string) {
let cursor: string | undefined;
let asOf: string | undefined;
do {
const page = await c.listTraining({ expiresBefore: before }, { cursor, limit: 100, asOf });
asOf ??= page.snapshotAt; // pin later pages to the first snapshot
yield* page.items;
cursor = page.nextCursor;
} while (cursor);
}
// 2. Rename a facility with optimistic concurrency.
export async function renameFacility(c: RecordClient, id: string, name: string) {
for (let attempt = 0; attempt < 2; attempt++) {
const cur = await c.getFacility(id);
try {
return await c.patchFacility(id, cur.meta.version, { name });
} catch (e) {
if ((e as { status?: number }).status !== 412) throw e; // re-read and retry once
}
}
throw new Error(`${id} kept changing; try again later`);
}
Imports follow Section 4.7: one stable key per source row, so a rerun creates nothing twice, and a B007 rejection of one row is queued for the training coordinator instead of stopping the batch. Where records must reach code typed against the strict SDK interface, which still requires an agent object on IncidentReport (Chapter 3), a single adapter module makes the substitution explicit. Type assertions should not be scattered through the application.
ETag/If-Match protects concurrent edits, Idempotency-Key protects retries, and cursors with snapshots keep listings consistent.Technical safety content: consult official biosafety authorities. This chapter defines interfaces for storing and checking administrative records. Containment levels, risk assessment, protective measures and every other technical safety matter are decided by the institutional biosafety committee, the biosafety officer and qualified personnel, following official guidance from the KDCA and national biosafety authorities, the WHO Laboratory Biosafety Manual and the BMBL. The record service only stores their outcomes.
/incidents/{id}/transitions rather than by patching the status field?POST /training-records with the same idempotency key but a corrected expiration date. What does the service return, and why?snapshotAt and asOf keep a multi-page report consistent?