From f58893bd44725afbd2c27e624b7329e7d344aec5 Mon Sep 17 00:00:00 2001 From: Javid Date: Sat, 25 Jul 2026 15:31:48 +0330 Subject: [PATCH 1/4] docs: add draft public and event contracts --- README.md | 14 ++- asyncapi/corelink-events-v1.yaml | 46 ++++++++ openapi/corelink-public-v1.yaml | 184 +++++++++++++++++++++++++++++++ 3 files changed, 239 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 59a8a90..519b081 100644 --- a/README.md +++ b/README.md @@ -18,16 +18,20 @@ SDK, CLI, mock server, MCP server and external integration. ## Current status -The repository structure and JSON Schemas are present, but the OpenAPI and -AsyncAPI specification files are currently empty. They are not usable as -generated-client or mock-server inputs yet. Do not publish an SDK or claim API -compatibility until a reviewed, versioned specification exists. +The public OpenAPI document now contains a reviewed draft for the locally +verified Tenant → Device → Provisioning → Command slice, and AsyncAPI contains +a draft command-event envelope. The admin and internal documents remain +incomplete. These drafts are not stable release +contract and must not be used to publish an SDK or claim full API +compatibility until API-01/API-02 acceptance and cross-repository validation +are complete. ## Contract rules - Public device identity is `corelink_device_id`; integration IDs remain internal implementation details. -- Model CoreLink resources, not raw Traccar, OpenRemote or Keycloak payloads. +- Model CoreLink resources, not raw upstream-provider payloads. Public + terminology uses neutral CoreLink names and the `tc`/`or` codes only. - Keep public, admin and internal audiences in separate documents. - Define authentication, tenant scope, authorization failures, pagination, idempotency and problem responses for every operation. diff --git a/asyncapi/corelink-events-v1.yaml b/asyncapi/corelink-events-v1.yaml index e69de29..1b01e18 100644 --- a/asyncapi/corelink-events-v1.yaml +++ b/asyncapi/corelink-events-v1.yaml @@ -0,0 +1,46 @@ +asyncapi: 2.6.0 +info: + title: CoreLink Event Contracts + version: 1.0.0-draft + description: Draft event envelope for the locally verified command flow. Not a stable release contract. + x-maturity: draft +defaultContentType: application/json +channels: + corelink.device.command.v1: + description: Versioned command events partitioned by canonical device_id. + publish: + operationId: publishDeviceCommandEvent + message: + $ref: '#/components/messages/DeviceCommandEvent' +components: + messages: + DeviceCommandEvent: + name: DeviceCommandEvent + title: Device command event + summary: Immutable event envelope for a device command lifecycle. + contentType: application/json + payload: + $ref: '#/components/schemas/EventEnvelope' + schemas: + EventEnvelope: + type: object + required: [id, type, version, occurred_at, tenant_id, correlation_id, payload, metadata] + properties: + id: {type: string, format: uuid} + type: {type: string, enum: [device.command.requested, device.command.acknowledged]} + version: {type: string, const: v1} + occurred_at: {type: string, format: date-time} + tenant_id: {type: string, format: uuid} + device_id: {type: string, format: uuid} + actor_id: {type: string, format: uuid, nullable: true} + correlation_id: {type: string, format: uuid} + causation_id: {type: string, format: uuid, nullable: true} + source: + type: object + required: [system, connector, instance] + properties: + system: {type: string} + connector: {type: string} + instance: {type: string} + payload: {type: object, additionalProperties: true} + metadata: {type: object, additionalProperties: true} diff --git a/openapi/corelink-public-v1.yaml b/openapi/corelink-public-v1.yaml index e69de29..abdd923 100644 --- a/openapi/corelink-public-v1.yaml +++ b/openapi/corelink-public-v1.yaml @@ -0,0 +1,184 @@ +openapi: 3.1.0 +info: + title: CoreLink Public API + version: 1.0.0-draft + description: Draft contract for the locally verified connected-device foundation. Not a stable release contract. + x-maturity: draft +servers: + - url: http://localhost:8000 + description: Local development only +security: + - bearerAuth: [] +paths: + /api/v1/tenants: + post: + summary: Create a tenant + operationId: createTenant + tags: [tenants] + requestBody: + required: true + content: + application/json: + schema: {$ref: '#/components/schemas/TenantCreate'} + responses: + '201': + description: Tenant created + content: {application/json: {schema: {$ref: '#/components/schemas/Tenant'}}} + '401': {$ref: '#/components/responses/Unauthorized'} + /api/v1/tenants/{tenant_id}/devices: + parameters: + - $ref: '#/components/parameters/TenantId' + post: + summary: Register a canonical device + operationId: createDevice + tags: [devices] + requestBody: + required: true + content: + application/json: + schema: {$ref: '#/components/schemas/DeviceCreate'} + responses: + '201': + description: Device created + content: {application/json: {schema: {$ref: '#/components/schemas/Device'}}} + '401': {$ref: '#/components/responses/Unauthorized'} + '403': {$ref: '#/components/responses/Forbidden'} + /api/v1/tenants/{tenant_id}/devices/{device_id}/provisioning: + parameters: + - $ref: '#/components/parameters/TenantId' + - $ref: '#/components/parameters/DeviceId' + post: + summary: Start or replay device provisioning + operationId: startProvisioning + tags: [provisioning] + parameters: + - $ref: '#/components/parameters/IdempotencyKey' + requestBody: + content: + application/json: + schema: {$ref: '#/components/schemas/ProvisioningStart'} + responses: + '201': + description: Provisioning record created or replayed + content: {application/json: {schema: {$ref: '#/components/schemas/Provisioning'}}} + '401': {$ref: '#/components/responses/Unauthorized'} + '409': {$ref: '#/components/responses/Conflict'} + /api/v1/tenants/{tenant_id}/devices/{device_id}/commands: + parameters: + - $ref: '#/components/parameters/TenantId' + - $ref: '#/components/parameters/DeviceId' + post: + summary: Queue a device command + operationId: createCommand + tags: [commands] + parameters: + - $ref: '#/components/parameters/IdempotencyKey' + requestBody: + required: true + content: + application/json: + schema: {$ref: '#/components/schemas/CommandCreate'} + responses: + '201': + description: Command queued + content: {application/json: {schema: {$ref: '#/components/schemas/Command'}}} + '401': {$ref: '#/components/responses/Unauthorized'} + '403': {$ref: '#/components/responses/Forbidden'} + '409': {$ref: '#/components/responses/Conflict'} +components: + securitySchemes: + bearerAuth: {type: http, scheme: bearer, bearerFormat: JWT} + parameters: + TenantId: {name: tenant_id, in: path, required: true, schema: {type: string, format: uuid}} + DeviceId: {name: device_id, in: path, required: true, schema: {type: string, format: uuid}} + IdempotencyKey: {name: Idempotency-Key, in: header, required: true, schema: {type: string, minLength: 1, maxLength: 255}} + responses: + Unauthorized: + description: Bearer authentication is required + content: {application/json: {schema: {$ref: '#/components/schemas/Problem'}}} + Forbidden: + description: The actor lacks the tenant permission + content: {application/json: {schema: {$ref: '#/components/schemas/Problem'}}} + Conflict: + description: Idempotency or lifecycle conflict + content: {application/json: {schema: {$ref: '#/components/schemas/Problem'}}} + schemas: + TenantCreate: + type: object + required: [name, slug] + properties: + name: {type: string, minLength: 2, maxLength: 255} + slug: {type: string, minLength: 2, maxLength: 100, pattern: '^[a-z0-9-]+$'} + tenant_type: {type: string, enum: [customer, partner, reseller, platform], default: customer} + Tenant: + allOf: + - {$ref: '#/components/schemas/TenantCreate'} + - type: object + required: [id, status] + properties: + id: {type: string, format: uuid} + status: {type: string, enum: [active, suspended, archived]} + DeviceCreate: + type: object + required: [device_model_id] + properties: + device_model_id: {type: string, format: uuid} + name: {type: string, maxLength: 255, nullable: true} + metadata: {type: object, additionalProperties: true} + identities: + type: array + items: + type: object + required: [identity_type, value] + properties: + identity_type: {type: string, enum: [imei, iccid, mac, eui64, serial, external]} + value: {type: string, minLength: 1, maxLength: 255} + is_primary: {type: boolean, default: false} + Device: + type: object + required: [id, tenant_id, device_model_id, status] + properties: + id: {type: string, format: uuid} + tenant_id: {type: string, format: uuid} + device_model_id: {type: string, format: uuid} + name: {type: string, nullable: true} + status: {type: string, enum: [inventory, provisioning, active, suspended, retired]} + metadata: {type: object, additionalProperties: true} + ProvisioningStart: + type: object + properties: + metadata: {type: object, additionalProperties: true} + Provisioning: + type: object + required: [id, tenant_id, device_id, state, metadata] + properties: + id: {type: string, format: uuid} + tenant_id: {type: string, format: uuid} + device_id: {type: string, format: uuid} + state: {type: string, enum: [unprovisioned, pending, provisioning, active, failed, revoked]} + metadata: {type: object, additionalProperties: true} + CommandCreate: + type: object + required: [command_type, provider] + properties: + command_type: {type: string, minLength: 1, maxLength: 150} + provider: {type: string, enum: [tc, or]} + payload: {type: object, additionalProperties: true} + metadata: {type: object, additionalProperties: true} + Command: + type: object + required: [id, tenant_id, device_id, command_type, provider, status] + properties: + id: {type: string, format: uuid} + tenant_id: {type: string, format: uuid} + device_id: {type: string, format: uuid} + command_type: {type: string} + provider: {type: string, enum: [tc, or]} + status: {type: string, enum: [queued, dispatching, sent, acknowledged, succeeded, failed, timed_out, cancelled]} + payload: {type: object, additionalProperties: true} + metadata: {type: object, additionalProperties: true} + Problem: + type: object + required: [detail] + properties: + detail: {type: string} From 790a2142d45eefc20c1fc6e1956785a4ad02b0dc Mon Sep 17 00:00:00 2001 From: Javid Date: Sat, 25 Jul 2026 15:35:00 +0330 Subject: [PATCH 2/4] fix: preserve compatible public contract baseline --- README.md | 24 +-- asyncapi/corelink-events-v1.yaml | 67 ++++---- openapi/corelink-public-v1.yaml | 284 +++++++++++++++++-------------- 3 files changed, 198 insertions(+), 177 deletions(-) diff --git a/README.md b/README.md index 519b081..d3f999e 100644 --- a/README.md +++ b/README.md @@ -13,31 +13,32 @@ SDK, CLI, mock server, MCP server and external integration. | `openapi/corelink-internal-v1.yaml` | Internal service contract; never expose as public API | | `asyncapi/corelink-events-v1.yaml` | Published event channels and payloads | | `schemas/` | Reusable JSON Schemas for device, command, event envelope and errors | -| `postman/` | Collections, environments and runnable examples | +| `postman/` | Versioned collection, sandbox environment and runnable examples | | `docs/terminology.md` | Shared public-contract vocabulary | ## Current status -The public OpenAPI document now contains a reviewed draft for the locally -verified Tenant → Device → Provisioning → Command slice, and AsyncAPI contains -a draft command-event envelope. The admin and internal documents remain -incomplete. These drafts are not stable release -contract and must not be used to publish an SDK or claim full API -compatibility until API-01/API-02 acceptance and cross-repository validation -are complete. +P3.1 introduces a reviewed `1.0.0-draft` public contract for the proven Device +and Command slice, plus a canonical event envelope. It is intentionally a +small boundary: tenant provisioning, integration callbacks and privileged +administration remain out of public v1 until they have their own reviewed +contract. SDKs and the mock server may consume this draft only in prerelease +channels; it is not a release claim until runtime parity and CI checks land. ## Contract rules - Public device identity is `corelink_device_id`; integration IDs remain internal implementation details. -- Model CoreLink resources, not raw upstream-provider payloads. Public - terminology uses neutral CoreLink names and the `tc`/`or` codes only. +- Model CoreLink resources, not raw integration-provider payloads. - Keep public, admin and internal audiences in separate documents. - Define authentication, tenant scope, authorization failures, pagination, idempotency and problem responses for every operation. - Make breaking changes through an explicit versioned contract and coordinated platform/SDK release. +Read [the compatibility policy](docs/compatibility-policy.md) before changing a +public operation. + ## Before merging a contract change 1. Check that the change matches the CoreLink ownership boundaries in the @@ -46,3 +47,6 @@ are complete. 3. Add representative request, response and error examples. 4. Update affected SDK, mock-server, developer-docs and website references in the same delivery plan. +5. Let the contract-compatibility workflow classify the public diff. It rejects + breaking v1 changes; publish a new major document with migration guidance + for any such change. diff --git a/asyncapi/corelink-events-v1.yaml b/asyncapi/corelink-events-v1.yaml index 1b01e18..59f39c9 100644 --- a/asyncapi/corelink-events-v1.yaml +++ b/asyncapi/corelink-events-v1.yaml @@ -1,46 +1,43 @@ -asyncapi: 2.6.0 +asyncapi: 3.0.0 info: - title: CoreLink Event Contracts + title: CoreLink Event Contract version: 1.0.0-draft - description: Draft event envelope for the locally verified command flow. Not a stable release contract. - x-maturity: draft -defaultContentType: application/json + description: Canonical event envelopes for approved public and partner deliveries. channels: - corelink.device.command.v1: - description: Versioned command events partitioned by canonical device_id. - publish: - operationId: publishDeviceCommandEvent - message: - $ref: '#/components/messages/DeviceCommandEvent' + device.lifecycle: + address: device.lifecycle + messages: + deviceLifecycle: + $ref: '#/components/messages/DeviceLifecycle' + command.status: + address: command.status + messages: + commandStatus: + $ref: '#/components/messages/CommandStatus' +operations: + receiveDeviceLifecycle: + action: receive + channel: {$ref: '#/channels/device.lifecycle'} + receiveCommandStatus: + action: receive + channel: {$ref: '#/channels/command.status'} components: messages: - DeviceCommandEvent: - name: DeviceCommandEvent - title: Device command event - summary: Immutable event envelope for a device command lifecycle. - contentType: application/json - payload: - $ref: '#/components/schemas/EventEnvelope' + DeviceLifecycle: + name: DeviceLifecycle + payload: {$ref: '#/components/schemas/EventEnvelope'} + CommandStatus: + name: CommandStatus + payload: {$ref: '#/components/schemas/EventEnvelope'} schemas: EventEnvelope: type: object - required: [id, type, version, occurred_at, tenant_id, correlation_id, payload, metadata] + additionalProperties: false + required: [event_id, event_type, occurred_at, tenant_id, data] properties: - id: {type: string, format: uuid} - type: {type: string, enum: [device.command.requested, device.command.acknowledged]} - version: {type: string, const: v1} + event_id: {type: string, format: uuid} + event_type: {type: string, examples: [command.completed]} occurred_at: {type: string, format: date-time} tenant_id: {type: string, format: uuid} - device_id: {type: string, format: uuid} - actor_id: {type: string, format: uuid, nullable: true} - correlation_id: {type: string, format: uuid} - causation_id: {type: string, format: uuid, nullable: true} - source: - type: object - required: [system, connector, instance] - properties: - system: {type: string} - connector: {type: string} - instance: {type: string} - payload: {type: object, additionalProperties: true} - metadata: {type: object, additionalProperties: true} + correlation_id: {type: string} + data: {type: object, additionalProperties: true} diff --git a/openapi/corelink-public-v1.yaml b/openapi/corelink-public-v1.yaml index abdd923..11cd703 100644 --- a/openapi/corelink-public-v1.yaml +++ b/openapi/corelink-public-v1.yaml @@ -1,184 +1,204 @@ -openapi: 3.1.0 +openapi: 3.1.1 info: title: CoreLink Public API version: 1.0.0-draft - description: Draft contract for the locally verified connected-device foundation. Not a stable release contract. - x-maturity: draft + description: | + Versioned public contract for the proven connected-device slice. Integration + details are deliberately excluded. All resource identifiers are canonical + CoreLink identifiers. servers: - - url: http://localhost:8000 - description: Local development only + - url: https://api.corelink.example security: - bearerAuth: [] +tags: + - name: Devices + - name: Commands paths: - /api/v1/tenants: - post: - summary: Create a tenant - operationId: createTenant - tags: [tenants] - requestBody: - required: true - content: - application/json: - schema: {$ref: '#/components/schemas/TenantCreate'} + /health/live: + get: + operationId: getLiveHealth + security: [] + x-corelink-stability: stable responses: - '201': - description: Tenant created - content: {application/json: {schema: {$ref: '#/components/schemas/Tenant'}}} - '401': {$ref: '#/components/responses/Unauthorized'} + '200': + description: Process is live. + content: + application/json: + schema: {$ref: '#/components/schemas/Health'} + /health/ready: + get: + operationId: getReadiness + security: [] + x-corelink-stability: stable + responses: + '200': + description: Dependencies required for traffic are ready. + content: + application/json: + schema: {$ref: '#/components/schemas/Health'} + '503': {$ref: '#/components/responses/ServiceUnavailable'} /api/v1/tenants/{tenant_id}/devices: parameters: - $ref: '#/components/parameters/TenantId' + get: + tags: [Devices] + operationId: listDevices + x-corelink-stability: stable + parameters: + - $ref: '#/components/parameters/Limit' + - $ref: '#/components/parameters/Offset' + - name: status + in: query + schema: {$ref: '#/components/schemas/DeviceStatus'} + - name: device_model_id + in: query + schema: {type: string, format: uuid} + responses: + '200': + description: Tenant-scoped device page. + content: {application/json: {schema: {$ref: '#/components/schemas/DevicePage'}}} + '401': {$ref: '#/components/responses/Unauthorized'} + '403': {$ref: '#/components/responses/Forbidden'} post: - summary: Register a canonical device + tags: [Devices] operationId: createDevice - tags: [devices] + x-corelink-stability: stable requestBody: required: true - content: - application/json: - schema: {$ref: '#/components/schemas/DeviceCreate'} + content: {application/json: {schema: {$ref: '#/components/schemas/DeviceCreate'}}} responses: - '201': - description: Device created - content: {application/json: {schema: {$ref: '#/components/schemas/Device'}}} + '201': {description: Device created, content: {application/json: {schema: {$ref: '#/components/schemas/Device'}}}} + '400': {$ref: '#/components/responses/BadRequest'} '401': {$ref: '#/components/responses/Unauthorized'} '403': {$ref: '#/components/responses/Forbidden'} - /api/v1/tenants/{tenant_id}/devices/{device_id}/provisioning: + '409': {$ref: '#/components/responses/Conflict'} + /api/v1/tenants/{tenant_id}/devices/{corelink_device_id}: parameters: - $ref: '#/components/parameters/TenantId' - - $ref: '#/components/parameters/DeviceId' - post: - summary: Start or replay device provisioning - operationId: startProvisioning - tags: [provisioning] - parameters: - - $ref: '#/components/parameters/IdempotencyKey' + - $ref: '#/components/parameters/CoreLinkDeviceId' + get: + tags: [Devices] + operationId: getDevice + x-corelink-stability: stable + responses: + '200': {description: Device, content: {application/json: {schema: {$ref: '#/components/schemas/Device'}}}} + '401': {$ref: '#/components/responses/Unauthorized'} + '403': {$ref: '#/components/responses/Forbidden'} + '404': {$ref: '#/components/responses/NotFound'} + patch: + tags: [Devices] + operationId: updateDevice + x-corelink-stability: stable requestBody: - content: - application/json: - schema: {$ref: '#/components/schemas/ProvisioningStart'} + required: true + content: {application/json: {schema: {$ref: '#/components/schemas/DeviceUpdate'}}} responses: - '201': - description: Provisioning record created or replayed - content: {application/json: {schema: {$ref: '#/components/schemas/Provisioning'}}} + '200': {description: Updated device, content: {application/json: {schema: {$ref: '#/components/schemas/Device'}}}} + '400': {$ref: '#/components/responses/BadRequest'} '401': {$ref: '#/components/responses/Unauthorized'} - '409': {$ref: '#/components/responses/Conflict'} - /api/v1/tenants/{tenant_id}/devices/{device_id}/commands: + '403': {$ref: '#/components/responses/Forbidden'} + '404': {$ref: '#/components/responses/NotFound'} + /api/v1/tenants/{tenant_id}/devices/{corelink_device_id}/commands: parameters: - $ref: '#/components/parameters/TenantId' - - $ref: '#/components/parameters/DeviceId' + - $ref: '#/components/parameters/CoreLinkDeviceId' + get: + tags: [Commands] + operationId: listDeviceCommands + x-corelink-stability: stable + parameters: + - $ref: '#/components/parameters/Limit' + - $ref: '#/components/parameters/Offset' + responses: + '200': {description: Tenant-scoped command page, content: {application/json: {schema: {$ref: '#/components/schemas/CommandPage'}}}} + '401': {$ref: '#/components/responses/Unauthorized'} + '403': {$ref: '#/components/responses/Forbidden'} + '404': {$ref: '#/components/responses/NotFound'} post: - summary: Queue a device command - operationId: createCommand - tags: [commands] + tags: [Commands] + operationId: createDeviceCommand + x-corelink-stability: stable parameters: - - $ref: '#/components/parameters/IdempotencyKey' + - name: Idempotency-Key + in: header + required: true + schema: {type: string, minLength: 1, maxLength: 255} requestBody: required: true - content: - application/json: - schema: {$ref: '#/components/schemas/CommandCreate'} + content: {application/json: {schema: {$ref: '#/components/schemas/CommandCreate'}}} responses: - '201': - description: Command queued - content: {application/json: {schema: {$ref: '#/components/schemas/Command'}}} + '201': {description: Command accepted, content: {application/json: {schema: {$ref: '#/components/schemas/Command'}}}} + '400': {$ref: '#/components/responses/BadRequest'} '401': {$ref: '#/components/responses/Unauthorized'} '403': {$ref: '#/components/responses/Forbidden'} + '404': {$ref: '#/components/responses/NotFound'} '409': {$ref: '#/components/responses/Conflict'} + /api/v1/tenants/{tenant_id}/devices/{corelink_device_id}/commands/{command_id}: + parameters: + - $ref: '#/components/parameters/TenantId' + - $ref: '#/components/parameters/CoreLinkDeviceId' + - $ref: '#/components/parameters/CommandId' + get: + tags: [Commands] + operationId: getDeviceCommand + x-corelink-stability: stable + responses: + '200': {description: Command, content: {application/json: {schema: {$ref: '#/components/schemas/Command'}}}} + '401': {$ref: '#/components/responses/Unauthorized'} + '403': {$ref: '#/components/responses/Forbidden'} + '404': {$ref: '#/components/responses/NotFound'} components: securitySchemes: bearerAuth: {type: http, scheme: bearer, bearerFormat: JWT} parameters: TenantId: {name: tenant_id, in: path, required: true, schema: {type: string, format: uuid}} - DeviceId: {name: device_id, in: path, required: true, schema: {type: string, format: uuid}} - IdempotencyKey: {name: Idempotency-Key, in: header, required: true, schema: {type: string, minLength: 1, maxLength: 255}} + CoreLinkDeviceId: {name: corelink_device_id, in: path, required: true, schema: {type: string, format: uuid}} + CommandId: {name: command_id, in: path, required: true, schema: {type: string, format: uuid}} + Limit: {name: limit, in: query, schema: {type: integer, minimum: 1, maximum: 100, default: 50}} + Offset: {name: offset, in: query, schema: {type: integer, minimum: 0, default: 0}} responses: - Unauthorized: - description: Bearer authentication is required - content: {application/json: {schema: {$ref: '#/components/schemas/Problem'}}} - Forbidden: - description: The actor lacks the tenant permission - content: {application/json: {schema: {$ref: '#/components/schemas/Problem'}}} - Conflict: - description: Idempotency or lifecycle conflict - content: {application/json: {schema: {$ref: '#/components/schemas/Problem'}}} + BadRequest: {description: Invalid request, content: {application/problem+json: {schema: {$ref: '#/components/schemas/Problem'}}}} + Unauthorized: {description: Authentication required, content: {application/problem+json: {schema: {$ref: '#/components/schemas/Problem'}}}} + Forbidden: {description: Tenant access or permission denied, content: {application/problem+json: {schema: {$ref: '#/components/schemas/Problem'}}}} + NotFound: {description: Resource not found, content: {application/problem+json: {schema: {$ref: '#/components/schemas/Problem'}}}} + Conflict: {description: Conflicting request, content: {application/problem+json: {schema: {$ref: '#/components/schemas/Problem'}}}} + ServiceUnavailable: {description: Dependency unavailable, content: {application/problem+json: {schema: {$ref: '#/components/schemas/Problem'}}}} schemas: - TenantCreate: - type: object - required: [name, slug] - properties: - name: {type: string, minLength: 2, maxLength: 255} - slug: {type: string, minLength: 2, maxLength: 100, pattern: '^[a-z0-9-]+$'} - tenant_type: {type: string, enum: [customer, partner, reseller, platform], default: customer} - Tenant: - allOf: - - {$ref: '#/components/schemas/TenantCreate'} - - type: object - required: [id, status] - properties: - id: {type: string, format: uuid} - status: {type: string, enum: [active, suspended, archived]} - DeviceCreate: - type: object - required: [device_model_id] - properties: - device_model_id: {type: string, format: uuid} - name: {type: string, maxLength: 255, nullable: true} - metadata: {type: object, additionalProperties: true} - identities: - type: array - items: - type: object - required: [identity_type, value] - properties: - identity_type: {type: string, enum: [imei, iccid, mac, eui64, serial, external]} - value: {type: string, minLength: 1, maxLength: 255} - is_primary: {type: boolean, default: false} + Health: {type: object, required: [status], properties: {status: {type: string, enum: [ok, ready]}}} + DeviceStatus: {type: string, enum: [inventory, provisioning, active, suspended, retired]} Device: type: object - required: [id, tenant_id, device_model_id, status] + required: [corelink_device_id, tenant_id, device_model_id, status, metadata, created_at, updated_at] properties: - id: {type: string, format: uuid} + corelink_device_id: {type: string, format: uuid} tenant_id: {type: string, format: uuid} device_model_id: {type: string, format: uuid} - name: {type: string, nullable: true} - status: {type: string, enum: [inventory, provisioning, active, suspended, retired]} - metadata: {type: object, additionalProperties: true} - ProvisioningStart: - type: object - properties: - metadata: {type: object, additionalProperties: true} - Provisioning: - type: object - required: [id, tenant_id, device_id, state, metadata] - properties: - id: {type: string, format: uuid} - tenant_id: {type: string, format: uuid} - device_id: {type: string, format: uuid} - state: {type: string, enum: [unprovisioned, pending, provisioning, active, failed, revoked]} + partner_sku_id: {type: [string, 'null'], format: uuid} + firmware_release_id: {type: [string, 'null'], format: uuid} + name: {type: [string, 'null'], maxLength: 255} + status: {$ref: '#/components/schemas/DeviceStatus'} metadata: {type: object, additionalProperties: true} - CommandCreate: + created_at: {type: string, format: date-time} + updated_at: {type: string, format: date-time} + DeviceCreate: type: object - required: [command_type, provider] + required: [device_model_id] properties: - command_type: {type: string, minLength: 1, maxLength: 150} - provider: {type: string, enum: [tc, or]} - payload: {type: object, additionalProperties: true} + device_model_id: {type: string, format: uuid} + partner_sku_id: {type: string, format: uuid} + firmware_release_id: {type: string, format: uuid} + name: {type: string, maxLength: 255} metadata: {type: object, additionalProperties: true} - Command: + DeviceUpdate: type: object - required: [id, tenant_id, device_id, command_type, provider, status] + minProperties: 1 properties: - id: {type: string, format: uuid} - tenant_id: {type: string, format: uuid} - device_id: {type: string, format: uuid} - command_type: {type: string} - provider: {type: string, enum: [tc, or]} - status: {type: string, enum: [queued, dispatching, sent, acknowledged, succeeded, failed, timed_out, cancelled]} - payload: {type: object, additionalProperties: true} + firmware_release_id: {type: [string, 'null'], format: uuid} + name: {type: [string, 'null'], maxLength: 255} metadata: {type: object, additionalProperties: true} - Problem: - type: object - required: [detail] - properties: - detail: {type: string} + DevicePage: {type: object, required: [items, total, limit, offset], properties: {items: {type: array, items: {$ref: '#/components/schemas/Device'}}, total: {type: integer}, limit: {type: integer}, offset: {type: integer}}} + CommandStatus: {type: string, enum: [queued, dispatching, sent, acknowledged, succeeded, failed, timed_out, cancelled]} + CommandCreate: {type: object, required: [command_type], properties: {command_type: {type: string, minLength: 1, maxLength: 150}, payload: {type: object, additionalProperties: true}, timeout_at: {type: string, format: date-time}, metadata: {type: object, additionalProperties: true}}} + Command: {type: object, required: [command_id, tenant_id, corelink_device_id, command_type, payload, status, attempt_count, metadata, created_at, updated_at], properties: {command_id: {type: string, format: uuid}, tenant_id: {type: string, format: uuid}, corelink_device_id: {type: string, format: uuid}, command_type: {type: string}, payload: {type: object, additionalProperties: true}, status: {$ref: '#/components/schemas/CommandStatus'}, attempt_count: {type: integer}, timeout_at: {type: [string, 'null'], format: date-time}, acknowledged_at: {type: [string, 'null'], format: date-time}, completed_at: {type: [string, 'null'], format: date-time}, error_code: {type: [string, 'null'], maxLength: 100}, metadata: {type: object, additionalProperties: true}, created_at: {type: string, format: date-time}, updated_at: {type: string, format: date-time}}} + CommandPage: {type: object, required: [items, total, limit, offset], properties: {items: {type: array, items: {$ref: '#/components/schemas/Command'}}, total: {type: integer}, limit: {type: integer}, offset: {type: integer}}} + Problem: {type: object, required: [type, title, status, code, correlation_id], properties: {type: {type: string}, title: {type: string}, status: {type: integer}, code: {type: string}, detail: {type: string}, diagnostics: {type: object, additionalProperties: true}, correlation_id: {type: string}}} From e38c941665042e4b3cd5d21ef7398a5a155696ed Mon Sep 17 00:00:00 2001 From: Javid Date: Sat, 25 Jul 2026 17:05:46 +0330 Subject: [PATCH 3/4] docs: publish contract release matrix --- CHANGELOG.md | 23 +++++++++++++++++++++++ README.md | 4 ++++ docs/compatibility-matrix.md | 23 +++++++++++++++++++++++ 3 files changed, 50 insertions(+) create mode 100644 CHANGELOG.md create mode 100644 docs/compatibility-matrix.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..09a2822 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,23 @@ +# Changelog + +All notable contract releases are recorded here. Contract tags are immutable; +corrections require a new patch tag and must not be moved in place. + +## [1.0.0-draft] - 2026-07-25 + +Initial reviewed draft for the proven Device and Command public slice. + +### Added + +- canonical public device and command identifiers; +- tenant-scoped device listing, lookup, creation and command operations; +- `application/problem+json` error responses with correlation IDs; +- canonical event envelope and device lifecycle event definitions; +- compatibility policy and public OpenAPI compatibility gate. + +### Explicitly not released + +Tenant provisioning, partner credentials, webhooks, telemetry, billing and +privileged administration remain outside this draft until their contracts and +runtime parity evidence are reviewed. + diff --git a/README.md b/README.md index d3f999e..037e986 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,10 @@ administration remain out of public v1 until they have their own reviewed contract. SDKs and the mock server may consume this draft only in prerelease channels; it is not a release claim until runtime parity and CI checks land. +The immutable baseline tag is `v1.0.0-draft`. See the +[changelog](CHANGELOG.md) and [compatibility matrix](docs/compatibility-matrix.md) +for the exact release boundary and consumer status. + ## Contract rules - Public device identity is `corelink_device_id`; integration IDs remain diff --git a/docs/compatibility-matrix.md b/docs/compatibility-matrix.md new file mode 100644 index 0000000..191ddd5 --- /dev/null +++ b/docs/compatibility-matrix.md @@ -0,0 +1,23 @@ +# Contract compatibility matrix + +This matrix records the contract version and verification boundary for each +consumer. A draft tag is immutable, but it is not a production support claim. + +| Surface | Contract source | Version | Compatibility gate | Current status | +| --- | --- | --- | --- | --- | +| Public HTTP API | `openapi/corelink-public-v1.yaml` | `1.0.0-draft` | OpenAPI syntax + public diff checker | Draft reviewed; runtime parity is a separate gate | +| Admin HTTP API | `openapi/corelink-admin-v1.yaml` | `1.0.0-draft` | OpenAPI syntax + authorization review | Internal draft; not a public release | +| Internal HTTP API | `openapi/corelink-internal-v1.yaml` | `1.0.0-draft` | OpenAPI syntax + service ownership review | Internal draft; not a public release | +| Events | `asyncapi/corelink-events-v1.yaml` | `1.0.0-draft` | AsyncAPI validation + event envelope review | Draft; delivery/replay evidence remains platform-owned | +| Python/TypeScript/Java SDKs | Generated or hand-written consumers | N/A | Contract version pinned per release | No stable SDK release claim for this draft | +| CLI, mock server and MCP server | Consumer repositories | N/A | Runtime parity and examples | Must consume a reviewed tag before beta | + +## Release rules + +1. Each released row must point to an immutable Git tag. +2. Additive changes within the same major version require a new minor or patch + tag and a changelog entry. +3. Breaking changes require a new major contract document and migration notes. +4. A contract tag is not a runtime release until the corresponding consumer + parity checks and operational evidence are attached to the release record. + From 2c0199212573dcb76fdd496f45cfb394fa4a79fb Mon Sep 17 00:00:00 2001 From: Javid Date: Sat, 25 Jul 2026 17:10:24 +0330 Subject: [PATCH 4/4] ci: add contract lint and example gate --- .github/workflows/contract-compatibility.yml | 4 ++ docs/runtime-parity.md | 4 ++ scripts/lint_contracts.rb | 45 ++++++++++++++++++++ 3 files changed, 53 insertions(+) create mode 100644 scripts/lint_contracts.rb diff --git a/.github/workflows/contract-compatibility.yml b/.github/workflows/contract-compatibility.yml index e4a710a..e50f62f 100644 --- a/.github/workflows/contract-compatibility.yml +++ b/.github/workflows/contract-compatibility.yml @@ -7,6 +7,8 @@ on: - "asyncapi/**" - "schemas/**" - "scripts/check_openapi_compatibility.rb" + - "scripts/lint_contracts.rb" + - "postman/**" - ".github/workflows/contract-compatibility.yml" push: branches: [main] @@ -30,3 +32,5 @@ jobs: run: | git show "origin/${{ github.base_ref }}:openapi/corelink-public-v1.yaml" > /tmp/base-public.yaml || true ruby scripts/check_openapi_compatibility.rb /tmp/base-public.yaml openapi/corelink-public-v1.yaml + - name: Lint public contract and examples + run: ruby scripts/lint_contracts.rb diff --git a/docs/runtime-parity.md b/docs/runtime-parity.md index 5041833..5aa6df1 100644 --- a/docs/runtime-parity.md +++ b/docs/runtime-parity.md @@ -16,3 +16,7 @@ No TypeScript or Python package may be promoted beyond prerelease until these items, a contract-diff check and generated-client compatibility tests are green. This gate preserves current consumers while the public boundary is normalized. + +The repository workflow also runs `scripts/lint_contracts.rb` and parses the +versioned Postman collection/environment. This is an early structural gate; it +does not replace runtime parity or generated-client tests. diff --git a/scripts/lint_contracts.rb b/scripts/lint_contracts.rb new file mode 100644 index 0000000..4152cc4 --- /dev/null +++ b/scripts/lint_contracts.rb @@ -0,0 +1,45 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +# Small dependency-free contract lint. It catches incomplete operations before +# a full OpenAPI/AsyncAPI validator is introduced in the contract toolchain. + +require "json" +require "yaml" + +PUBLIC_SPEC = "openapi/corelink-public-v1.yaml" +OPERATIONS = %w[get put post patch delete head options].freeze + +spec = YAML.safe_load(File.read(PUBLIC_SPEC), permitted_classes: [], aliases: false) +errors = [] +paths = spec.fetch("paths", {}) +errors << "public contract must declare paths" if paths.empty? + +paths.each do |path, path_item| + OPERATIONS.each do |method| + operation = path_item[method] + next unless operation + + location = "#{method.upcase} #{path}" + errors << "#{location}: missing operationId" if operation["operationId"].to_s.empty? + errors << "#{location}: missing x-corelink-stability" if operation["x-corelink-stability"].to_s.empty? + errors << "#{location}: missing responses" if operation.fetch("responses", {}).empty? + end +end + +%w[ + postman/corelink-public-v1.postman_collection.json + postman/corelink-public-v1.postman_environment.json +].each do |path| + JSON.parse(File.read(path)) +rescue JSON::ParserError => error + errors << "#{path}: invalid JSON (#{error.message})" +end + +if errors.empty? + puts "Contract lint passed." +else + warn errors.join("\n") + exit 1 +end +