@openlfcp/client 0.1.5 → 0.2.0-beta.10

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.
@@ -0,0 +1,94 @@
1
+ import type { DataUnitId, ResourceId } from "@openlfcp/core";
2
+ import type { LfcpStorage } from "@openlfcp/storage";
3
+ import type { BatchStatus } from "./batch-status.js";
4
+ import type { AccessState } from "./write-access.js";
5
+ /**
6
+ * The status stream of a Resource (SDK-SECTIONS-INTEGRATION-01 §4, §5,
7
+ * LFCP-02-026): what an editor adapter shows as saved, waiting, accepted
8
+ * or not, and what it knows about units received from others. Every fact
9
+ * comes from evidence the SDK holds; none is derived from a sent frame.
10
+ */
11
+ /** §4.2: the signal that showed the server lost units it had acknowledged. */
12
+ export type ReofferReason = "unknown-previous" | "have-gap" | "rehost";
13
+ /** §4.3: what became of a unit received from others. */
14
+ export type ReceivedFact = "held" | "waiting" | "refused";
15
+ /**
16
+ * LFCP-02-028: how far this session caught up with the Resource.
17
+ * "current-at-checkpoint" only once the Resource was live (Control, keys
18
+ * and every offered unit received and applied), at `checkedAt`; it stays
19
+ * so offline, as of that check. "unknown" while a Control conflict or a
20
+ * missing key blocks the catch-up.
21
+ */
22
+ export type CatchUp = "not-started" | "receiving" | "current-at-checkpoint" | "unknown";
23
+ /** §4.3: a section without `ready` is importing. */
24
+ export type SectionState = "ready" | "importing";
25
+ /**
26
+ * §5: one event of a Resource's status stream. `revision` grows by one with
27
+ * each event of that Resource in this SDK session; a consumer that sees a
28
+ * revision other than the next one asks for statusSnapshot.
29
+ */
30
+ export type StatusEvent = {
31
+ readonly revision: number;
32
+ } & (({
33
+ readonly kind: "batch";
34
+ } & BatchStatus) | {
35
+ readonly kind: "reoffered";
36
+ readonly unitIds: readonly DataUnitId[];
37
+ readonly reason: ReofferReason;
38
+ } | {
39
+ readonly kind: "received";
40
+ readonly fact: ReceivedFact;
41
+ readonly unitIds: readonly DataUnitId[];
42
+ /** The admission or merge diagnostic, when refused. */
43
+ readonly diagnostic?: string;
44
+ } | {
45
+ readonly kind: "section-state";
46
+ readonly state: SectionState;
47
+ } | {
48
+ readonly kind: "access";
49
+ readonly access: AccessState;
50
+ } | {
51
+ readonly kind: "catch-up";
52
+ readonly state: CatchUp;
53
+ /** The local time the Resource was last live; null before. */
54
+ readonly checkedAt: number | null;
55
+ }
56
+ /** The Resource was hosted again on this route (§41.1). */
57
+ | {
58
+ readonly kind: "rehost";
59
+ readonly route: string;
60
+ });
61
+ /** The body of a status event, before its revision is assigned. */
62
+ export type StatusEventBody = StatusEvent extends infer E ? E extends {
63
+ readonly revision: number;
64
+ } ? Omit<E, "revision"> : never : never;
65
+ /** §4.3: the received units that are not merged, as stored. */
66
+ export interface ReceivedState {
67
+ readonly held: readonly DataUnitId[];
68
+ readonly waiting: readonly DataUnitId[];
69
+ readonly refused: readonly {
70
+ readonly unitId: DataUnitId;
71
+ readonly diagnostic: string;
72
+ }[];
73
+ }
74
+ /** §5: the complete state of a Resource's status, at `revision`. */
75
+ export interface StatusSnapshot {
76
+ readonly revision: number;
77
+ readonly batches: readonly BatchStatus[];
78
+ readonly received: ReceivedState;
79
+ /** Unknown without a section binding, or before the section exists. */
80
+ readonly section: SectionState | "unknown";
81
+ readonly access: AccessState;
82
+ readonly catchUp: {
83
+ readonly state: CatchUp;
84
+ readonly checkedAt: number | null;
85
+ };
86
+ }
87
+ /**
88
+ * The received units of `resource` that wait or were refused: held behind
89
+ * a change with their actor and sequence (SOP §14.1), waiting for a
90
+ * dependency (the profile's, or the previous unit of their actor, §26.2),
91
+ * refused when merging. Units of this client are never among them.
92
+ */
93
+ export declare function receivedState(storage: Pick<LfcpStorage, "dataUnits">, resource: ResourceId): Promise<ReceivedState>;
94
+ //# sourceMappingURL=section-status.d.ts.map
@@ -0,0 +1,16 @@
1
+ /**
2
+ * The received units of `resource` that wait or were refused: held behind
3
+ * a change with their actor and sequence (SOP §14.1), waiting for a
4
+ * dependency (the profile's, or the previous unit of their actor, §26.2),
5
+ * refused when merging. Units of this client are never among them.
6
+ */
7
+ export async function receivedState(storage, resource) {
8
+ const ids = async (status) => (await storage.dataUnits.withStatus(resource, status)).map((u) => u.unitId);
9
+ const refused = (await storage.dataUnits.withStatus(resource, "profile-rejected")).map((u) => Object.freeze({ unitId: u.unitId, diagnostic: u.detail ?? "" }));
10
+ return Object.freeze({
11
+ held: Object.freeze(await ids("profile-held")),
12
+ waiting: Object.freeze([...(await ids("profile-pending")), ...(await ids("held"))]),
13
+ refused: Object.freeze(refused),
14
+ });
15
+ }
16
+ //# sourceMappingURL=section-status.js.map
package/dist/storage.d.ts CHANGED
@@ -3,6 +3,7 @@ import { type ResourceDEK } from "@openlfcp/crypto";
3
3
  import { type CommitResult, type DataUnitRow, type LfcpStorage, type SecretStore, type StorageWrite } from "@openlfcp/storage";
4
4
  import { type ChainResult, type SeenRecord, type SeenUnits } from "@openlfcp/wire";
5
5
  import { type CreateDataUnitOptions, type CreatedDataUnit } from "./data-unit.js";
6
+ import { type Receipt } from "./receipts.js";
6
7
  /**
7
8
  * The client code on top of the storage interfaces (LFCP-034), so that a
8
9
  * durable adapter (LFCP-035) is all a platform has to write.
@@ -33,6 +34,23 @@ export declare class StoredSeenUnits implements SeenUnits {
33
34
  * stored head back to Genesis. Undefined when nothing is stored.
34
35
  */
35
36
  export declare function loadControlChain(storage: Pick<LfcpStorage, "control">, resource: ResourceId): Promise<ChainResult | undefined>;
37
+ /**
38
+ * The client's LOCAL Control Chain: the committed chain extended by this
39
+ * client's own queued Control Records that are still live (not yet
40
+ * acknowledged, and not blocked). A writer chains a new record on this
41
+ * local head, not the committed head, so records authored back to back
42
+ * (e.g. two invitations) carry consecutive sequence numbers and a linear
43
+ * `previous` instead of colliding on the committed head (§47,
44
+ * CONTROL_HEAD_MISMATCH). Blocked items are excluded: they will not land as
45
+ * they are, so nothing is chained on them (the re-proposal re-authors them).
46
+ *
47
+ * Returns the committed chain unchanged when there are no live pending
48
+ * records, `undefined` when the Resource has no stored chain, and the
49
+ * committed chain when the pending records do not extend it linearly (they
50
+ * are then left for the re-proposal to sort out, and the caller sees the
51
+ * committed head).
52
+ */
53
+ export declare function localControlChain(storage: Pick<LfcpStorage, "control" | "outbound">, resource: ResourceId): Promise<ChainResult | undefined>;
36
54
  /**
37
55
  * Persists a validated linear chain in one atomic batch: its exact records,
38
56
  * the head (compare-and-set against `expected`), the epoch history (keeping
@@ -93,4 +111,32 @@ export declare function createQueuedDataUnit<T>(storage: Pick<LfcpStorage, "acto
93
111
  */
94
112
  readonly onCreated?: (created: CreatedDataUnit, value: T) => void;
95
113
  }, also?: readonly StorageWrite[] | ((created: CreatedDataUnit) => readonly StorageWrite[])): Promise<CreatedDataUnit>;
114
+ /**
115
+ * SDK-SECTIONS-INTEGRATION-01 §3.1 (LFCP-02-025): commits one operation, a
116
+ * batch of a profile's changes, as Data Units with their outbound entries,
117
+ * the caller's writes (`also`: its profile checkpoint) and the receipt, in
118
+ * ONE storage transaction: after a crash either all of it exists or none.
119
+ * The units chain their previous units in order (§26.2).
120
+ *
121
+ * Idempotent per (Resource, operationId) (§3.3): an operation that already
122
+ * has a receipt for the same intents returns it and writes nothing; for
123
+ * different intents it throws OperationIdReusedError. The caller stages
124
+ * its changes first and adopts them only once this resolves.
125
+ */
126
+ export interface CommitOperationOptions<T> extends Omit<CreateDataUnitOptions<T>, "sequences" | "previousUnitId" | "value"> {
127
+ readonly operationId: string;
128
+ /** The batch's intents, as the caller submitted them (their canonical form is hashed, §3.3). */
129
+ readonly intents: readonly unknown[];
130
+ /** The batch's changes in order; each becomes one Data Unit. */
131
+ readonly values: readonly T[];
132
+ readonly affectedNodeIds: readonly string[];
133
+ /** The document's heads after the batch, sorted, as one string. */
134
+ readonly modelRevision: string;
135
+ /** Called for each sealed unit before the commit (e.g. the profile's recordLocal). */
136
+ readonly onCreated?: (created: CreatedDataUnit, value: T, index: number) => void;
137
+ }
138
+ export declare function commitOperation<T>(storage: Pick<LfcpStorage, "actorSequences" | "commit" | "dataUnits" | "localMarks">, options: CommitOperationOptions<T>, also?: readonly StorageWrite[] | ((units: readonly CreatedDataUnit[]) => readonly StorageWrite[])): Promise<{
139
+ readonly receipt: Receipt;
140
+ readonly committed: boolean;
141
+ }>;
96
142
  //# sourceMappingURL=storage.d.ts.map
package/dist/storage.js CHANGED
@@ -1,8 +1,10 @@
1
1
  import { actorSequence, bytesEqual, hash32, LfcpError, toHex, } from "@openlfcp/core";
2
2
  import { dekCommitment, importResourceDEK } from "@openlfcp/crypto";
3
3
  import { dekSecretRef, } from "@openlfcp/storage";
4
- import { parseDataUnit, validateControlChain, } from "@openlfcp/wire";
4
+ import { parseControlRecord, parseDataUnit, validateControlChain, } from "@openlfcp/wire";
5
+ import { receiptIndexWrites, replaceReleasedWrites } from "./batch-status.js";
5
6
  import { createDataUnit } from "./data-unit.js";
7
+ import { intentsHash, OperationIdReusedError, receiptOf, receiptWrite, } from "./receipts.js";
6
8
  /**
7
9
  * The client code on top of the storage interfaces (LFCP-034), so that a
8
10
  * durable adapter (LFCP-035) is all a platform has to write.
@@ -85,6 +87,38 @@ export async function loadControlChain(storage, resource) {
85
87
  throw new LfcpError("INVALID_CONTROL_CHAIN", `the stored Control Records validate to sequence ${result.state.seq}, not the stored head at ${head.controlSeq} (local corruption)`);
86
88
  return result;
87
89
  }
90
+ /**
91
+ * The client's LOCAL Control Chain: the committed chain extended by this
92
+ * client's own queued Control Records that are still live (not yet
93
+ * acknowledged, and not blocked). A writer chains a new record on this
94
+ * local head, not the committed head, so records authored back to back
95
+ * (e.g. two invitations) carry consecutive sequence numbers and a linear
96
+ * `previous` instead of colliding on the committed head (§47,
97
+ * CONTROL_HEAD_MISMATCH). Blocked items are excluded: they will not land as
98
+ * they are, so nothing is chained on them (the re-proposal re-authors them).
99
+ *
100
+ * Returns the committed chain unchanged when there are no live pending
101
+ * records, `undefined` when the Resource has no stored chain, and the
102
+ * committed chain when the pending records do not extend it linearly (they
103
+ * are then left for the re-proposal to sort out, and the caller sees the
104
+ * committed head).
105
+ */
106
+ export async function localControlChain(storage, resource) {
107
+ const committed = await loadControlChain(storage, resource);
108
+ if (committed === undefined || committed.kind !== "linear")
109
+ return committed;
110
+ const pending = (await storage.outbound.list(resource))
111
+ .filter((i) => i.kind === "control-record" && i.blocked === null)
112
+ .map((i) => parseControlRecord(i.bytes))
113
+ .sort((a, b) => (a.payload.controlSeq < b.payload.controlSeq ? -1 : 1));
114
+ if (pending.length === 0)
115
+ return committed;
116
+ const extended = validateControlChain([
117
+ ...committed.records.map((r) => r.signed.bytes),
118
+ ...pending.map((p) => p.signed.bytes),
119
+ ]);
120
+ return extended.kind === "linear" ? extended : committed;
121
+ }
88
122
  /**
89
123
  * Persists a validated linear chain in one atomic batch: its exact records,
90
124
  * the head (compare-and-set against `expected`), the epoch history (keeping
@@ -249,9 +283,13 @@ export async function createQueuedDataUnit(storage, options, also = []) {
249
283
  }
250
284
  async function createQueuedDataUnitNow(storage, options, also, resource, actor) {
251
285
  const { onCreated, previousUnitId, ...create } = options;
252
- const previous = previousUnitId !== undefined
253
- ? previousUnitId
254
- : await latestAcceptedOwnUnit(storage, resource, actor);
286
+ const latest = await latestAcceptedOwnUnit(storage, resource, actor);
287
+ // §26.2: a null previous only when the writer holds no own unit as
288
+ // accepted (before its first, or after a first that went stale); while it
289
+ // holds one, a receiver that accepted it would hold this unit for good.
290
+ if (previousUnitId === null && latest !== null)
291
+ throw new LfcpError("INVALID_STRUCTURE", "the writer still holds an accepted own unit: its next unit names it as previous (§26.2)");
292
+ const previous = previousUnitId !== undefined ? previousUnitId : latest;
255
293
  const created = await createDataUnit({
256
294
  ...create,
257
295
  previousUnitId: previous,
@@ -286,4 +324,67 @@ async function createQueuedDataUnitNow(storage, options, also, resource, actor)
286
324
  throw new Error(`the local unit was not stored: ${result.reason}`);
287
325
  return created;
288
326
  }
327
+ export async function commitOperation(storage, options, also = []) {
328
+ const resource = options.view.state.resourceId;
329
+ const actor = options.actor.descriptor.principalId;
330
+ return serialized(storage, `${toHex(resource)}:${toHex(actor)}`, async () => {
331
+ const hash = intentsHash(options.intents);
332
+ const existing = await receiptOf(storage, resource, options.operationId);
333
+ if (existing !== undefined) {
334
+ if (existing.intentsHash !== hash)
335
+ throw new OperationIdReusedError(options.operationId);
336
+ return { receipt: existing, committed: false };
337
+ }
338
+ const { onCreated, operationId, intents, values, affectedNodeIds, modelRevision, ...create } = options;
339
+ void intents;
340
+ const first = await latestAcceptedOwnUnit(storage, resource, actor);
341
+ const created = [];
342
+ let previous = first;
343
+ for (const [i, value] of values.entries()) {
344
+ const unit = await createDataUnit({
345
+ ...create,
346
+ value,
347
+ previousUnitId: previous,
348
+ sequences: storage.actorSequences,
349
+ });
350
+ onCreated?.(unit, value, i);
351
+ created.push(unit);
352
+ previous = unit.unitId;
353
+ }
354
+ const receipt = Object.freeze({
355
+ operationId,
356
+ unitIds: Object.freeze(created.map((u) => u.unitId)),
357
+ affectedNodeIds: Object.freeze([...affectedNodeIds]),
358
+ modelRevision,
359
+ intentsHash: hash,
360
+ durable: true,
361
+ });
362
+ const writes = [
363
+ { op: "expect-previous-unit", resourceId: resource, actor, previous: first },
364
+ ];
365
+ for (const unit of created) {
366
+ const row = dataUnitRow(unit.bytes);
367
+ writes.push({ op: "put-data-unit", unit: row, status: "merged", detail: "local", accepted: true }, {
368
+ op: "enqueue",
369
+ item: {
370
+ itemId: hash32(unit.unitId),
371
+ resourceId: row.resourceId,
372
+ kind: "data-unit",
373
+ bytes: unit.bytes,
374
+ attempts: 0,
375
+ lastAttempt: null,
376
+ nextAttempt: null,
377
+ blocked: null,
378
+ },
379
+ });
380
+ }
381
+ writes.push(
382
+ // A released batch kept for its status under this operation ID is replaced.
383
+ ...(await replaceReleasedWrites(storage, resource, options.operationId)), receiptWrite(resource, receipt), ...receiptIndexWrites(resource, receipt), ...(typeof also === "function" ? also(created) : also));
384
+ const result = await storage.commit(writes);
385
+ if (!result.ok)
386
+ throw new Error(`the operation was not stored: ${result.reason}`);
387
+ return { receipt, committed: true };
388
+ });
389
+ }
289
390
  //# sourceMappingURL=storage.js.map
@@ -1,12 +1,52 @@
1
- import { type ControlRecordId, type DataEpoch, type DataUnitId, type Hash32, type ResourceId } from "@openlfcp/core";
1
+ import { type ControlRecordId, type DataEpoch, type DataUnitId, type Hash32, type PrincipalId, type ResourceId } from "@openlfcp/core";
2
2
  import { type AgreementKeyPair } from "@openlfcp/crypto";
3
- import { type LfcpStorage, type SecretStore } from "@openlfcp/storage";
3
+ import { type LfcpStorage, type SecretStore, type StorageWrite } from "@openlfcp/storage";
4
4
  import { type ClientConnectionState, type DataProfileCodec, type HaveVector, type Signer } from "@openlfcp/wire";
5
5
  import type { ApplyOutcome, DataUnitApplier, EpochReconciliation } from "./apply.js";
6
6
  import type { ProfileCheckpointer } from "./checkpoint.js";
7
7
  import { type WebSocketFactory } from "./connection.js";
8
8
  import type { AckOutcome, NackOutcome, OutboundQueue, StaleOutboundUnit } from "./outbound.js";
9
+ import { type Receipt } from "./receipts.js";
10
+ import { type ReproposeOutcome } from "./repropose.js";
9
11
  import { type ResourcePhase } from "./resource-state.js";
12
+ import { type RemainingPath, type RevocationRefusal } from "./revoke.js";
13
+ import { type SectionState, type StatusEvent, type StatusSnapshot } from "./section-status.js";
14
+ import { type AccessState, type WriteAccess } from "./write-access.js";
15
+ /** LFCP-02-060: what revokeAccess did. */
16
+ export type RevokeAccessResult = {
17
+ readonly kind: "queued";
18
+ /** The grants revoked, and those delegated from them that this deactivates (§17.2). */
19
+ readonly revoked: readonly ControlRecordId[];
20
+ readonly deactivated: readonly ControlRecordId[];
21
+ /** The queued Control Records: the revocations, then the Key Epoch if any. */
22
+ readonly recordIds: readonly ControlRecordId[];
23
+ /** The new Data Epoch, or null when it was not rotated. */
24
+ readonly epoch: DataEpoch | null;
25
+ /** The remaining readers who get the new DEK in Key Packages. */
26
+ readonly recipients: readonly PrincipalId[];
27
+ /** Grants this revoker may not revoke: the member keeps access through them. */
28
+ readonly remainingPaths: readonly RemainingPath[];
29
+ } | {
30
+ readonly kind: "refused";
31
+ /**
32
+ * offline: the Resource is not live in this session; stale: the
33
+ * validated chain is behind what the server reported; control-pending:
34
+ * Control Records of this client are not committed yet. Nothing is
35
+ * queued: a removal never waits in an offline queue.
36
+ */
37
+ readonly reason: RevocationRefusal | "offline" | "stale" | "control-pending";
38
+ readonly message: string;
39
+ };
40
+ /**
41
+ * SDK-SECTIONS-INTEGRATION-01 §3.6: a batch the SDK refuses before the
42
+ * profile sees it, with the same codes and fields as the profile's
43
+ * refusals. Nothing is written.
44
+ */
45
+ export declare class CommitRefusedError extends Error {
46
+ readonly code: string;
47
+ readonly intentIndex: number;
48
+ constructor(code: string, message: string, intentIndex: number);
49
+ }
10
50
  /**
11
51
  * The client sync session (LFCP-039a): one LFCP connection to a server and
12
52
  * the §65 machine of every Resource opened on it, orchestrating the parts
@@ -41,6 +81,37 @@ export interface SnapshotBinding<T> {
41
81
  load(value: T): void;
42
82
  current(): T;
43
83
  }
84
+ /**
85
+ * SDK-SECTIONS-INTEGRATION-01 §3.1: a batch of intents a Data Profile has
86
+ * validated and changed on a copy of its state (for Shared Sections:
87
+ * SharedSectionsDataProfile.commitBinding().stage).
88
+ */
89
+ export interface StagedOperation<T> {
90
+ /** The batch's changes in order: one Data Unit each. */
91
+ readonly values: readonly T[];
92
+ readonly affectedNodeIds: readonly string[];
93
+ /** The profile state's revision after the batch. */
94
+ readonly modelRevision: string;
95
+ /** Adopts the batch in the profile state, before the commit is stored. */
96
+ apply(): void;
97
+ /** Undoes apply(): the commit did not happen, or had happened before. */
98
+ revert(): void;
99
+ /** Writes stored with the units: the profile checkpoint that includes them. */
100
+ writes(units: readonly {
101
+ readonly unitId: DataUnitId;
102
+ }[]): readonly StorageWrite[];
103
+ /** Called once the commit is durable, with the units in order. */
104
+ committed(unitIds: readonly DataUnitId[]): void;
105
+ }
106
+ /** How a Resource's Data Profile commits local batches (§3.1). */
107
+ export interface CommitBinding<T> {
108
+ /** The codec of this client's own units. */
109
+ readonly codec: DataProfileCodec<T>;
110
+ /** Validates and changes the batch on a copy; null when it writes nothing. Throws to refuse it. */
111
+ stage(intents: readonly unknown[]): StagedOperation<T> | null;
112
+ /** The section's state (SDK-SECTIONS-INTEGRATION-01 §4.3); undefined before there is a section. */
113
+ section?(): SectionState | undefined;
114
+ }
44
115
  /** A Resource to synchronize: its Data Profile applier (and optional checkpoints and Snapshots). */
45
116
  export interface ResourceBinding {
46
117
  readonly resourceId: ResourceId;
@@ -48,6 +119,8 @@ export interface ResourceBinding {
48
119
  readonly checkpointer?: ProfileCheckpointer;
49
120
  /** Load an offered Snapshot instead of replaying every unit, and allow publishing. */
50
121
  readonly snapshot?: SnapshotBinding<unknown>;
122
+ /** Commit local batches with receipts (commit, receiptOf). */
123
+ readonly commit?: CommitBinding<unknown>;
51
124
  }
52
125
  /** When to try connecting again after the `attempt`-th failure (1, 2, …): a delay in ms, or null to stop. */
53
126
  export type ReconnectPolicy = (attempt: number) => number | null;
@@ -80,7 +153,8 @@ export interface SyncClientOptions {
80
153
  readonly dataProfiles?: readonly string[];
81
154
  /**
82
155
  * Whether to publish a Snapshot of a LIVE Resource now, asked on every
83
- * tick with the number of units merged since the last one; no automatic
156
+ * tick with the number of units merged since the last one, received or
157
+ * committed here (a section's only writer reaches it too); no automatic
84
158
  * schedule otherwise (publishSnapshot can also be called directly).
85
159
  */
86
160
  readonly snapshotPolicy?: (resourceId: ResourceId, unitsSinceLast: number) => boolean;
@@ -142,6 +216,12 @@ export type SyncEvent = {
142
216
  readonly type: "ack";
143
217
  readonly outcome: AckOutcome;
144
218
  }
219
+ /** SDK-SECTIONS-INTEGRATION-01 §5: the Resource's status stream (statusSnapshot after a gap). */
220
+ | {
221
+ readonly type: "status";
222
+ readonly resourceId: ResourceId;
223
+ readonly event: StatusEvent;
224
+ }
145
225
  /** A NACK of an outbound object: stale, equivocation alarm, rejected, repropose, … */
146
226
  | {
147
227
  readonly type: "nack";
@@ -168,6 +248,34 @@ export type SyncEvent = {
168
248
  readonly outcome: "hosted" | "refused";
169
249
  readonly code?: string;
170
250
  }
251
+ /**
252
+ * RESOURCE_OPEN was refused with AUTHORIZATION_FAILED although this
253
+ * client's validated chain grants it data/read: the server may have lost
254
+ * Control Records in a restore (LFCP-02-106). "started": the client
255
+ * re-supplies its chain with CONTROL_PUT; "recovered": the server opened
256
+ * the Resource again after it; "ended": no recovery, with the reason (see
257
+ * RecoveryEnd, or "still-refused" when the server holds our chain and
258
+ * still refuses), and the refusal stands.
259
+ */
260
+ | {
261
+ readonly type: "access-recovery";
262
+ readonly resourceId: ResourceId;
263
+ readonly url: string;
264
+ readonly outcome: "started" | "recovered" | "ended";
265
+ readonly reason?: string;
266
+ }
267
+ /**
268
+ * This client's own Control Records that a moved head refused
269
+ * (CONTROL_HEAD_MISMATCH) were re-proposed on the current head, or dropped
270
+ * when no longer authorized or meaningful (repropose.ts). One outcome per
271
+ * record; a copied invitation link keeps working, as a re-proposed grant
272
+ * keeps the same Invitation Principal and claim parameters.
273
+ */
274
+ | {
275
+ readonly type: "repropose";
276
+ readonly resourceId: ResourceId;
277
+ readonly outcomes: readonly ReproposeOutcome[];
278
+ }
171
279
  /**
172
280
  * The server refused the Resource for good (see ResourceRefusal): it is
173
281
  * CLOSED and is not opened again until open() is called for it.
@@ -212,6 +320,13 @@ export declare class SyncClient {
212
320
  constructor(options: SyncClientOptions);
213
321
  /** Subscribes to session events; returns the unsubscribe function. */
214
322
  on(listener: (event: SyncEvent) => void): () => void;
323
+ /**
324
+ * §5: the complete status of `resource` at the stream's current revision:
325
+ * every unreleased batch, the received units that wait or were refused,
326
+ * the section state and write access. Events at or below its revision
327
+ * are already in it.
328
+ */
329
+ statusSnapshot(resource: ResourceId): Promise<StatusSnapshot>;
215
330
  /** Resolves once every message received so far has been handled (tests and shutdown). */
216
331
  idle(): Promise<void>;
217
332
  get connectionState(): ClientConnectionState;
@@ -242,6 +357,49 @@ export declare class SyncClient {
242
357
  * that loads it never skips content the state does not hold. Queued and
243
358
  * sent like any object; returns its ID.
244
359
  */
360
+ /**
361
+ * LFCP-02-060: removes `subject`'s access to `resource`. Every active
362
+ * grant naming it that this client may revoke is revoked (§17.3); when it
363
+ * has no read access left, the Data Epoch is rotated with a fresh DEK
364
+ * (§19, member revoked) and the DEK goes in Key Packages to every other
365
+ * remaining reader (§25), so it cannot read new data. Built on the
366
+ * validated head and checked before anything is queued; the records, the
367
+ * Key Packages and (first) the new DEK are written at once, then sent.
368
+ * Refused, with nothing queued, unless the Resource is live and current
369
+ * here and no Control Record of this client is pending.
370
+ */
371
+ revokeAccess(resource: ResourceId, subject: PrincipalId, options?: {
372
+ readonly rotate?: boolean;
373
+ }): Promise<RevokeAccessResult>;
374
+ /**
375
+ * SDK-SECTIONS-INTEGRATION-01 §3.1: commits a batch of intents on an open
376
+ * Resource and returns its receipt once the batch is durable: its Data
377
+ * Units, outbound entries, the profile checkpoint and the receipt are
378
+ * stored in one transaction, then the batch is sent. An operation that
379
+ * has a receipt returns it for the same intents and writes nothing, and
380
+ * throws OperationIdReusedError for different ones (§3.3). Runs between
381
+ * received units, never during their merge.
382
+ */
383
+ commit(resource: ResourceId, intents: readonly unknown[], options: {
384
+ readonly operationId: string;
385
+ }): Promise<Receipt>;
386
+ /**
387
+ * SDK-SECTIONS-INTEGRATION-01 §6: whether this client may commit to
388
+ * `resource`, from the validated Control state only: the chain of the
389
+ * last Control sync, or the stored chain validated now. `verifiedAt` is
390
+ * the local time of that validation (the last sync's when the stored
391
+ * chain is still at its head). Unknown without a validated chain.
392
+ */
393
+ canWrite(resource: ResourceId): Promise<WriteAccess>;
394
+ /**
395
+ * LFCP-02-027: write access with its evidence: the Control sequence it
396
+ * was validated at against the highest one a server reported in this
397
+ * session (a stale view is not current), the effective abilities and
398
+ * the active grant paths giving them, and apart from them the
399
+ * invitations we issued, our Control Records not yet committed (SI14)
400
+ * and an unsettled invitation claim of ours.
401
+ */
402
+ accessState(resource: ResourceId): Promise<AccessState>;
245
403
  publishSnapshot(resource: ResourceId): Promise<Hash32>;
246
404
  }
247
405
  /**