@onlyne/lib 0.0.0-stage → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/types.ts ADDED
@@ -0,0 +1,383 @@
1
+ export type Json = null | boolean | number | string | Json[] | { [key: string]: Json };
2
+ export type NodeId = string;
3
+ export type DomainId = string;
4
+ export interface DomainMembership {
5
+ domain_id: DomainId; node_id: NodeId; state: 'active' | 'left'; revision: string;
6
+ }
7
+ export interface DomainMutation extends Mutation { expected_revision?: string }
8
+ export interface DomainDiscovery {
9
+ domain_id: DomainId; members: DomainMembership[]; unavailable: NodeId[];
10
+ }
11
+ export interface DiscoveryCandidate {
12
+ node_id: NodeId; instance_id: string; base_url: string; last_seen: string;
13
+ }
14
+ export interface DiscoveryRecord extends DiscoveryCandidate {
15
+ source: string; received_at: string; expires_at: string;
16
+ verified_at?: string; membership?: DomainMembership | null; error?: Failure;
17
+ }
18
+ export interface ServiceDescription {
19
+ protocol: 'onlyne/3'; node_id: NodeId; instance_id: string; base_url: string;
20
+ features: string[]; authentication: string[];
21
+ domain: DomainId | null; membership: DomainMembership | null;
22
+ }
23
+ export interface ThreadRef { owner: NodeId; id: string }
24
+ export interface MessageRef { sender: NodeId; id: string }
25
+ export interface ResourceRef { owner: NodeId; id: string }
26
+ export type ErrorCode = 'invalid_request' | 'unauthenticated' | 'forbidden' | 'not_a_member' | 'not_found' | 'id_conflict' | 'revision_conflict' | 'request_in_progress' | 'conflict' | 'thread_closed' | 'context_lost' | 'retry_window_expired' | 'cursor_expired' | 'payload_too_large' | 'unsupported_media_type' | 'unsupported_delivery' | 'ambiguous_target' | 'ambiguous_resource' | 'capacity_exceeded' | 'internal' | 'unavailable' | 'decision_unavailable';
27
+ export interface Failure { code: ErrorCode; message: string; retryable: boolean; details?: Json }
28
+ export interface Auth { actor: NodeId; sender?: NodeId; reply_authority?: NodeId }
29
+ export interface Transaction {
30
+ get<T>(key: string): Promise<T | undefined>;
31
+ put<T>(key: string, value: T): Promise<void>;
32
+ delete(key: string): Promise<void>;
33
+ scan<T>(prefix: string): Promise<Array<{ key: string; value: T }>>;
34
+ }
35
+ export interface Storage {
36
+ readonly guarantee: 'durable' | 'binding-defined';
37
+ /** Serializable, atomic commit. Rejecting the callback rolls back every write. */
38
+ transaction<T>(work: (tx: Transaction) => Promise<T>): Promise<T>;
39
+ }
40
+ export interface Page<T> { items: T[]; next_cursor: string | null }
41
+ export interface Mutation { idempotency_key: string; retry_until: string }
42
+ export interface DeliveryPlan { response: 'none' | 'async' | 'await'; execution: 'existing' | 'new'; context?: string }
43
+ export interface Message {
44
+ id: string;
45
+ sender: NodeId;
46
+ delivery_kind: string;
47
+ msg: string;
48
+ meta?: {
49
+ thread?: ThreadRef;
50
+ in_reply_to?: MessageRef;
51
+ relation?: 'reply' | 'followup';
52
+ mentions?: string[];
53
+ parts?: Json[];
54
+ data?: { [key: string]: Json };
55
+ };
56
+ }
57
+ export interface SendRequest extends Mutation { message: Message; target_endpoint: NodeId; plan: DeliveryPlan }
58
+ export interface Receipt {
59
+ message_ref: MessageRef; receipt_id: string; guarantee: Storage['guarantee'];
60
+ thread_ref: ThreadRef; seq: string; plan: DeliveryPlan;
61
+ thread_created?: boolean; previous_seq?: string;
62
+ /**
63
+ * The message this one answers, in the identity the owner stored: the sender's
64
+ * `{thread,seq}` spelling is resolved before delivery, so this is the pair every
65
+ * later read agrees on. Absent when the message answers nothing.
66
+ */
67
+ in_reply_to?: MessageRef;
68
+ }
69
+ export interface Entry {
70
+ thread_ref: ThreadRef; seq: string; message: Message; authenticated_author: NodeId;
71
+ reply_authority: NodeId; at: string;
72
+ }
73
+ export interface ReadScope { peers?: NodeId[]; since_seq?: string; related_to?: MessageRef }
74
+ export interface Member { peer: NodeId; state: 'requested' | 'active' | 'revoked' | 'denied'; access: Array<'read' | 'post'>; read_scope: ReadScope; revision: string }
75
+ export interface Thread {
76
+ thread_ref: ThreadRef; title: string; state: 'open' | 'closed'; next_seq: string;
77
+ event_seq: string; created_at: string; closed_at: string | null;
78
+ messages_until?: string;
79
+ }
80
+ export interface Attention {
81
+ obligation_id: string; recipient: NodeId; thread_ref: ThreadRef; seq: string;
82
+ message_ref: MessageRef; authenticated_author: NodeId; acked: boolean;
83
+ disposition?: 'expired';
84
+ plan?: DeliveryPlan;
85
+ entry?: Entry;
86
+ }
87
+ export interface Grant {
88
+ id: string; recipient: NodeId; thread_ref: ThreadRef; revision: string; acked: boolean;
89
+ }
90
+ export interface Event {
91
+ id: string; type: 'message' | 'membership' | 'thread_closed' | 'resource_event' | 'operation';
92
+ thread_ref?: ThreadRef; resource_ref?: ResourceRef; message_ref?: MessageRef; data: Json;
93
+ }
94
+ export interface PageOptions { cursor?: string; page_size?: number }
95
+ export interface MessageQuery extends PageOptions { attention?: 'inbox' | 'thread'; since_seq?: string; peer?: NodeId; related_to?: MessageRef }
96
+ export interface ReplyPermit {
97
+ message_ref: MessageRef; grantee: NodeId; landing: ThreadRef; expires_at: string;
98
+ remaining: number; revoked: boolean; followup: boolean;
99
+ }
100
+ /**
101
+ * RFC 0006 §3: the resource's runtime state. `binding` is the one pointer events route
102
+ * through; `log_readers` is a separate authorization for the append-only log, and a
103
+ * handover never touches it (§6).
104
+ */
105
+ export interface Resource {
106
+ resource_ref: ResourceRef; kind: string; operations: string[];
107
+ binding: { handler: NodeId; cursor: string };
108
+ log_readers: NodeId[]; revision: string;
109
+ event_seq: string; retention_ms: number; data?: Json;
110
+ }
111
+ /** RFC 0006 §5: an event belongs to the resource, never to a recipient list. */
112
+ export interface ResourceEvent {
113
+ resource_ref: ResourceRef; event_id: string; source_id: string;
114
+ data: Json; at: string; expires_at: string; expired: boolean;
115
+ }
116
+ /** RFC 0006 §6: the log query. `since` is exclusive, the same side of the cursor an ack names. */
117
+ export interface LogQuery extends PageOptions { since?: string }
118
+ /** What an ack answers: the handler's confirmed position, not the event it named. */
119
+ export interface AckReceipt { resource_ref: ResourceRef; cursor: string }
120
+ /** RFC 0006 §4.3: the probe an endpoint sends to the node it wants to become handler. */
121
+ export interface HandoverProbe {
122
+ operation_id: string; resource: ResourceRef; kind: string; operations: string[];
123
+ metadata?: Json; initiator: NodeId; cursor: string; deadline: string;
124
+ }
125
+ export type ProbeAnswer =
126
+ | { answer: 'accept'; cursor: string }
127
+ | { answer: 'reject'; reason: string };
128
+ export interface OperationRequest extends Mutation {
129
+ operation_id: string; operation: string; expected_revision?: string; data?: Json;
130
+ }
131
+ export interface OperationResult {
132
+ operation_id: string; state: 'committed' | 'pending' | 'aborted' | 'unknown';
133
+ data?: Json; error?: Failure;
134
+ }
135
+ export interface OperationRecord {
136
+ actor: NodeId; request: OperationRequest; digest: string; result: OperationResult;
137
+ resource_ref: ResourceRef; accepted_at: string; authorized_revision: string;
138
+ }
139
+ export interface Outbound {
140
+ request: SendRequest; expected_author: NodeId; state: 'pending' | 'accepted' | 'refused' | 'unknown';
141
+ receipt?: Receipt; error?: Failure; created_at?: string;
142
+ }
143
+ export interface HistoryMessage {
144
+ message_ref: MessageRef; message: Message; direction: 'in' | 'out';
145
+ thread_ref?: ThreadRef; seq?: string; at: string | null;
146
+ delivery: Outbound['state'] | 'received'; consumption?: 'pending' | 'consumed';
147
+ guarantee?: Storage['guarantee']; error?: Failure;
148
+ }
149
+ export interface Conversation {
150
+ id: MessageRef; title: string; thread_refs: ThreadRef[]; messages: HistoryMessage[];
151
+ last_at: string | null;
152
+ }
153
+ export interface HistoryQuery extends PageOptions { thread?: ThreadRef; message?: MessageRef }
154
+ /**
155
+ * One queued obligation and what became of it. `context` is where the delivery was sent;
156
+ * `injected` is **observed**, not submitted: it flips only in `confirmConsumption`, once the
157
+ * host has seen the entry in a provider request. A message submitted into a session that never
158
+ * reads it therefore stays `injected: false` and keeps its place in unread and alerts
159
+ * (caerulea report 3, problem 1). The name predates that rule and is kept because renaming it
160
+ * would read every stored row as unobserved and re-inject what was already read.
161
+ */
162
+ export interface LocalAttention { attention: Attention; context?: string; injected: boolean }
163
+ export interface Target { node_id: NodeId; base_url: string; headers?: Record<string, string> }
164
+ export interface Transport {
165
+ request<T>(target: NodeId, method: string, path: string, body?: unknown, signal?: AbortSignal): Promise<T>;
166
+ }
167
+ export interface MsgInput { delivery_kind: string; msg: string; meta?: Record<string, Json> }
168
+ export interface PreparedSend { to: NodeId; message: Message; plan: DeliveryPlan }
169
+ export type ResourceReadQuery =
170
+ | ({ op: 'list'; owners?: NodeId[] } & PageOptions)
171
+ | { op: 'describe'; resource: ResourceRef }
172
+ | ({ op: 'log'; resource: ResourceRef } & LogQuery)
173
+ | { op: 'event'; resource: ResourceRef; event_id: string }
174
+ | ({ op: 'operations'; resource: ResourceRef } & PageOptions)
175
+ | { op: 'status'; resource: ResourceRef; operation_id: string };
176
+ export interface ResourceInventory {
177
+ owners: Array<{ owner: NodeId; page: Page<Resource> } | { owner: NodeId; error: Failure }>;
178
+ }
179
+ export type ResourceReadResult = ResourceInventory | Resource | Page<ResourceEvent> | ResourceEvent | Page<OperationRecord> | OperationRecord;
180
+ export type Action =
181
+ | { kind: 'domain'; op: 'create' | 'join' | 'leave' | 'list' | 'describe' | 'discover'; domain_id?: DomainId; expected_revision?: string }
182
+ | { kind: 'send'; to: NodeId; thread?: ThreadRef; context?: string; in_reply_to?: MessageRef; relation?: 'reply' | 'followup'; mentions?: NodeId[]; response: DeliveryPlan['response']; execution: DeliveryPlan['execution'] }
183
+ | { kind: 'read'; scope: 'directory' | 'neighbors' | 'threads' | 'inbox' | 'messages' | 'message' | 'evidence'; thread?: ThreadRef; message?: MessageRef; seq?: string; cursor?: string; page_size?: number; attention?: 'inbox' | 'thread' }
184
+ | { kind: 'read'; scope: 'resources'; query: ResourceReadQuery }
185
+ | { kind: 'thread'; op: 'create' | 'close' | 'list' | 'describe' | 'member_add' | 'member_remove'; thread?: ThreadRef; to?: NodeId; title?: string }
186
+ | { kind: 'resource'; resource: ResourceRef; operation: string; data?: Json; expected_revision?: string };
187
+ /**
188
+ * One lifecycle transition of the local node, emitted for offline diagnosis. Every field is an
189
+ * identifier, a state name or a count: message bodies and credentials never appear here.
190
+ */
191
+ export type TraceEvent =
192
+ | { event: 'inbound.received'; obligation: string; thread: ThreadRef; seq: string; sender: NodeId; relation?: 'reply' | 'followup'; valid: boolean; context?: string; injected: boolean }
193
+ | { event: 'inbound.duplicate'; sender: NodeId; message: string }
194
+ | { event: 'inbound.acked'; obligation: string; thread: ThreadRef; seq: string }
195
+ | { event: 'inbound.inject'; thread: ThreadRef; seq: string; context: string; outcome: InjectOutcome; reason?: string }
196
+ | { event: 'consumption.confirmed'; thread: ThreadRef; seq: string; context: string; via: 'fragment' | 'tool_result' }
197
+ | { event: 'outbound.admitted'; sender: NodeId; message: string; target: string; state: string; guarantee?: string; code?: string }
198
+ | { event: 'outbound.redriven'; sender: NodeId; message: string; target: string }
199
+ | { event: 'outbound.expired'; sender: NodeId; message: string; code: string }
200
+ | { event: 'await.settled'; thread: ThreadRef; seq: string; waiters: number };
201
+ /** What the host did with one inbound entry, reported back with the injection result. */
202
+ export type InjectOutcome = 'delivered' | 'queued' | 'waiting' | 'skipped' | 'consumed';
203
+ export interface Runtime {
204
+ supports(plan: DeliveryPlan, message?: Message): Promise<boolean>;
205
+ /** Submission returns consumed:false; an observed request boundary may confirm it later. */
206
+ inject(entry: Entry, context?: string, plan?: DeliveryPlan): Promise<{ context: string; consumed: boolean; outcome?: InjectOutcome }>;
207
+ todo?(entry: Entry, context: string): Promise<void>;
208
+ /**
209
+ * RFC 0017 §5: the one notification channel. The node calls it for every committed
210
+ * trust transition, every inbound entry, and after each maintenance pass that changed
211
+ * the pending-work answer. A host that renders from these events never polls storage
212
+ * on its own; a host that misses one still converges on the next maintenance pass.
213
+ */
214
+ notify(event: NotifyEvent): void;
215
+ /**
216
+ * Optional structured trace. The node calls it on every lifecycle transition; the host maps
217
+ * it onto its own log, so a delivery can be followed from admission to consumption offline.
218
+ */
219
+ trace?(event: TraceEvent): void;
220
+ /**
221
+ * Called by the host once it observes that the model consumed an `await` tool result
222
+ * (RFC 0008 §5). The library records that as the shared consumption for the entry's
223
+ * `(context, ThreadRef, seq)`, so the automatic fragment does not present it again.
224
+ */
225
+ confirmToolResult?(entry: Entry, context: string): Promise<void>;
226
+ }
227
+ export interface ResourceDriver {
228
+ validate(resource: Resource, operation: string, data: Json | undefined): Promise<void>;
229
+ execute(resource: Resource, request: OperationRequest, signal?: AbortSignal): Promise<Omit<OperationResult, 'operation_id'>>;
230
+ query?(resource: Resource, operation_id: string): Promise<Omit<OperationResult, 'operation_id'>>;
231
+ }
232
+ /** RFC 0017 key material the host hands the library: the signing key and the public key it proves. */
233
+ export interface KeyMaterial {
234
+ readonly private_key: Uint8Array;
235
+ readonly public_key: Uint8Array;
236
+ }
237
+
238
+ /** Load-or-create the node identity key; storage is the host's choice (RFC 0017 §2). */
239
+ export type KeyHook = (() => Promise<KeyMaterial>) & { replace?(material: KeyMaterial): Promise<void> };
240
+
241
+ export interface KeyIdentity {
242
+ node_id: NodeId;
243
+ public_key: string;
244
+ fingerprint: string;
245
+ }
246
+
247
+ export interface TrustSeed { node_id: NodeId; fingerprint: string }
248
+
249
+ export interface TrustStatement {
250
+ node_id: NodeId;
251
+ target: NodeId;
252
+ public_key: string;
253
+ target_fingerprint: string;
254
+ base_url: string;
255
+ approved: boolean;
256
+ nonce: string;
257
+ timestamp: number;
258
+ signature: string;
259
+ previous_signature?: string;
260
+ /**
261
+ * Set on a reply: the nonce of the request this statement answers. A sender settles its
262
+ * delivery obligation only on a reply that echoes the nonce it sent (RFC 0017 §3.5).
263
+ */
264
+ reply_nonce?: string;
265
+ }
266
+
267
+ /**
268
+ * The observable stage of one trust relation, derived only from the approval halves
269
+ * (RFC 0017 §3). A host renders and decides from this value; it never re-derives
270
+ * the stage from the raw halves.
271
+ */
272
+ export type TrustPhase = 'invited' | 'requested' | 'exchanged' | 'revoked';
273
+
274
+ /** RFC 0017 §5: what changed when the node calls `runtime.notify`. */
275
+ export type NotifyEvent =
276
+ | { event: 'work-pending'; pending: boolean }
277
+ | { event: 'trust'; cause: 'local-request' | 'local-accept' | 'local-revoke' | 'peer-statement' | 'rotation'; phase: TrustPhase; entry: TrustEntry; previous?: TrustEntry }
278
+ | { event: 'inbound'; thread: ThreadRef; seq: string; sender: NodeId };
279
+
280
+ /** RFC 0017 §3: one exchange, as one object. `begin` hands this to the host. */
281
+ export interface KeyExchange {
282
+ /** The peer the host named; the statement proves this identity, never a guess from discovery. */
283
+ readonly node_id: NodeId;
284
+ /** The fingerprint the peer's live key lookup proved at `begin` time. */
285
+ readonly fingerprint: string;
286
+ readonly public_key: string;
287
+ readonly base_url: string;
288
+ /**
289
+ * Commits local approval and sends the signed introduction. `fingerprint` must repeat
290
+ * the value shown to the user: a changed key between display and commit fails here.
291
+ */
292
+ confirm(fingerprint: string): Promise<TrustEntry>;
293
+ }
294
+
295
+ /** One trust-table row: a peer's verified public key and the exchange state. */
296
+ export interface TrustEntry {
297
+ readonly node_id: NodeId;
298
+ readonly public_key: string;
299
+ readonly fingerprint: string;
300
+ readonly state: 'pending' | 'trusted' | 'revoked';
301
+ readonly first_seen: string;
302
+ readonly last_checked: string;
303
+ readonly local_approved: boolean;
304
+ readonly remote_approved: boolean;
305
+ readonly base_url: string;
306
+ readonly history?: string[];
307
+ }
308
+
309
+ /** The stage derived from the approval halves; the single source hosts read (RFC 0017 §3). */
310
+ export function trustPhase(entry: Pick<TrustEntry, 'local_approved' | 'remote_approved' | 'state'>): TrustPhase {
311
+ if (entry.state === 'revoked') return 'revoked';
312
+ if (entry.local_approved && entry.remote_approved) return 'exchanged';
313
+ return entry.local_approved ? 'requested' : 'invited';
314
+ }
315
+
316
+ /**
317
+ * What an admission policy may read. The policy runs inside the admission transaction, so it
318
+ * MUST read through this view and MUST NOT open a storage transaction of its own: the outer
319
+ * transaction still holds the store, and a nested one waits on it forever.
320
+ */
321
+ export interface AdmissionContext {
322
+ /** The peer's exchange stage, or undefined when the two nodes never exchanged keys. */
323
+ trust(node: NodeId): Promise<TrustPhase | undefined>;
324
+ }
325
+
326
+ export interface Plugins {
327
+ storage: Storage;
328
+ authenticate(request: Request): Promise<Auth | null>;
329
+ keys?: KeyHook;
330
+ trust_seeds?: TrustSeed[];
331
+ resolve(node: NodeId): Promise<Target>;
332
+ decide(input: MsgInput): Promise<Action>;
333
+ runtime?: Runtime;
334
+ /** Supplied when the host wants the library to own the maintenance loop (RFC 0017 §6). */
335
+ maintenanceClock?: MaintenanceClock;
336
+ resources?: ResourceDriver;
337
+ /** The host may adopt a dynamic resource after HTTP authentication and request parsing. */
338
+ prepareResourceOperation?(auth: Auth, ref: ResourceRef, request: OperationRequest): Promise<void>;
339
+ transport?: Transport;
340
+ mayAdmit?(actor: NodeId, thread: ThreadRef, context: AdmissionContext): Promise<boolean>;
341
+ /** Owner policy permitting a first-contact thread and membership for this actor. */
342
+ inboundThread?(actor: NodeId, context: AdmissionContext): Promise<boolean>;
343
+ /**
344
+ * Target-side handover policy (RFC 0006 §4.3): whether this node takes over the resource
345
+ * a probe names. The probe's `metadata` is what it judges from; `sender` is the endpoint
346
+ * whose signature the probe carried. A node without this hook accepts every well-formed
347
+ * probe from an authenticated peer.
348
+ */
349
+ handoverProbe?(probe: HandoverProbe, sender: NodeId): Promise<ProbeAnswer>;
350
+ directory?(): Promise<Json>;
351
+ executionContexts?(): Promise<Json>;
352
+ domainCandidates?(domain_id: DomainId): Promise<NodeId[]>;
353
+ /** Resolves a thread-local mention alias to the member node it names. */
354
+ mention?(alias: string, thread: ThreadRef): Promise<NodeId>;
355
+ /** Observed nodes with trust state; the same rows onlyne://neighbors renders. */
356
+ neighbors?(): Promise<unknown>;
357
+ }
358
+ export interface NodeOptions {
359
+ node_id: NodeId; base_url: string; plugins: Plugins;
360
+ instance_id?: string; authentication?: string[];
361
+ request_bytes?: number; retry_max_ms?: number; wait_ms?: number;
362
+ /**
363
+ * The trusted tick source, required at construction (RFC 0017 §6). Every protocol
364
+ * clock read — transcript windows, nonce expiry, retry gates — goes through `now`,
365
+ * and every nonce through `id`. A host that cannot supply an unguessable `id` must
366
+ * refuse to run a keyed node instead of falling back to a default.
367
+ */
368
+ now: () => number;
369
+ id: () => string;
370
+ }
371
+
372
+ /**
373
+ * RFC 0017 §6: the timer primitives the library's maintenance loop needs. Supplying
374
+ * them opts into `client.runMaintenance`, the loop the library owns: cadence, wake
375
+ * handling and the pending-work re-arm all live there, so a host cannot poll at a
376
+ * rate the protocol does not sanction. `setTimer` is one-shot: it calls `run` once and
377
+ * the loop re-arms, so a host that repeats the callback breaks the cadence contract.
378
+ * Every clock read goes through `NodeOptions.now`; the clock carries no second time source.
379
+ */
380
+ export interface MaintenanceClock {
381
+ setTimer(ms: number, run: () => void): unknown;
382
+ clearTimer(handle: unknown): void;
383
+ }