Implement replay-safe browser synchronization
This commit is contained in:
parent
06a7ac56b7
commit
459ffa04c6
3 changed files with 2734 additions and 25 deletions
|
|
@ -13,13 +13,17 @@ path or media type.
|
|||
|
||||
## Idempotency
|
||||
|
||||
Every public mutation requires `Idempotency-Key`. The identity is caller
|
||||
Every public domain mutation requires `Idempotency-Key`. The identity is caller
|
||||
subject, operation, and key. The server canonicalizes the semantic request,
|
||||
including the authorization target, with RFC 8785 and stores its SHA-256
|
||||
fingerprint and complete response atomically with the mutation. The default
|
||||
retention is 24 hours and is never shorter than the advertised
|
||||
`Idempotency-Expires-At`.
|
||||
|
||||
Event-ticket issuance is an ephemeral authentication exchange rather than a
|
||||
domain mutation. Each retry independently mints a short-lived credential; it
|
||||
does not refine a `JobProtocol` effect or enter the public mutation ledger.
|
||||
|
||||
The same identity and fingerprint replays the original status, resource, and
|
||||
body after duplicate or ambiguous delivery. A different fingerprint returns
|
||||
`idempotency_conflict` and cannot affect either request. Naturally idempotent
|
||||
|
|
@ -118,7 +122,15 @@ An HTTP job or list snapshot carries `event_cursor` at its consistency
|
|||
watermark. The client connects to `/v1/events` with that cursor; there is no
|
||||
subscription frame. Server frames are ordered by increasing `sequence` for a
|
||||
connection and carry an opaque next cursor. Delivery is at least once, and
|
||||
clients deduplicate event IDs.
|
||||
clients deduplicate event IDs. Each connection starts at sequence one and
|
||||
increments it by one for every frame, including heartbeats and replayed event
|
||||
IDs; a client resnapshots when it observes a gap.
|
||||
|
||||
Browser clients first create a short-lived, single-use event ticket through an
|
||||
OIDC bearer-authenticated HTTP request. The server stores only its digest and
|
||||
binds it to the authenticated principal and exact allowed origin. Redemption
|
||||
atomically consumes it during the WebSocket handshake. Native clients may
|
||||
authenticate that handshake directly with the OIDC bearer header.
|
||||
|
||||
`job_changed` is an invalidation with an attempt-aware display hint, not a
|
||||
resource replacement or patch. A client compares `job_revision` with the last
|
||||
|
|
|
|||
|
|
@ -384,10 +384,35 @@ paths:
|
|||
$ref: '#/components/responses/NotFound'
|
||||
default:
|
||||
$ref: '#/components/responses/Error'
|
||||
/v1/event-tickets:
|
||||
post:
|
||||
operationId: createEventTicket
|
||||
description: >-
|
||||
Creates an independent short-lived, single-use browser WebSocket
|
||||
credential bound to the authenticated principal and request origin.
|
||||
This ephemeral authentication exchange does not enter the public
|
||||
domain mutation idempotency ledger.
|
||||
responses:
|
||||
'201':
|
||||
description: Event ticket created. The response is not cacheable.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/EventTicket'
|
||||
default:
|
||||
$ref: '#/components/responses/Error'
|
||||
/v1/events:
|
||||
get:
|
||||
operationId: connectEvents
|
||||
security:
|
||||
- oidc: []
|
||||
- eventTicket: []
|
||||
parameters:
|
||||
- name: ticket
|
||||
in: query
|
||||
description: Short-lived single-use browser credential created by createEventTicket. Native clients may use OIDC bearer authentication instead.
|
||||
schema:
|
||||
$ref: '#/components/schemas/OpaqueId'
|
||||
- name: cursor
|
||||
in: query
|
||||
description: Opaque authorization-bound replay cursor from an HTTP snapshot or prior frame.
|
||||
|
|
@ -643,6 +668,10 @@ components:
|
|||
openIdConnectUrl: https://identity.invalid/.well-known/openid-configuration
|
||||
serviceAuth:
|
||||
type: mutualTLS
|
||||
eventTicket:
|
||||
type: apiKey
|
||||
in: query
|
||||
name: ticket
|
||||
parameters:
|
||||
IdempotencyKey:
|
||||
name: Idempotency-Key
|
||||
|
|
@ -1619,6 +1648,16 @@ components:
|
|||
updated_at:
|
||||
type: string
|
||||
format: date-time
|
||||
EventTicket:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
required: [ticket, expires_at]
|
||||
properties:
|
||||
ticket:
|
||||
$ref: '#/components/schemas/OpaqueId'
|
||||
expires_at:
|
||||
type: string
|
||||
format: date-time
|
||||
ApiError:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
|
|
|
|||
|
|
@ -204,7 +204,7 @@ flowchart TD
|
|||
## API and Live Updates
|
||||
|
||||
- Mutable HTTP and WebSocket implementation contracts live under [`docs/api`](../api/). They are non-authoritative projections of `JobProtocol` and this document and must be corrected when they conflict with either source.
|
||||
- Public mutation idempotency refines `RemoraApiMutation`: operation scope is the caller subject and operation, the request fingerprint is RFC 8785 canonical JSON hashed with SHA-256, and records are retained for at least the advertised 24-hour default window.
|
||||
- Public domain mutation idempotency refines `RemoraApiMutation`: operation scope is the caller subject and operation, the request fingerprint is RFC 8785 canonical JSON hashed with SHA-256, and records are retained for at least the advertised 24-hour default window. Ephemeral authentication exchanges that do not refine a `JobProtocol` effect, including browser event-ticket issuance, independently mint short-lived credentials on retry and do not enter that ledger.
|
||||
- Public job status projects `JobProtocol` state and adds deletion lifecycle, concrete progress, checkpoint, result, and failure fields.
|
||||
- Job listing applies current authorization before stable keyset pagination. Cursors are opaque, integrity-protected, and bound to the subject, filters, and membership snapshot.
|
||||
- HTTP snapshots carry a live-event cursor. WebSocket replay is ordered and at least once; invalid, expired, authorization-incompatible, or unavailable history requires a new HTTP snapshot. Event emission exposes progress only when it matches the protocol projection being published.
|
||||
|
|
|
|||
Loading…
Reference in a new issue