@openlfcp/client 0.1.5 → 0.2.0-beta.1
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/README.md +2 -2
- package/dist/access-recovery.d.ts +69 -0
- package/dist/access-recovery.js +45 -0
- package/dist/batch-status.d.ts +104 -0
- package/dist/batch-status.js +171 -0
- package/dist/claim-journal.d.ts +54 -0
- package/dist/claim-journal.js +52 -0
- package/dist/index.d.ts +12 -5
- package/dist/index.js +11 -4
- package/dist/invite.d.ts +41 -2
- package/dist/invite.js +115 -2
- package/dist/outbound.d.ts +18 -3
- package/dist/outbound.js +7 -2
- package/dist/queue.d.ts +4 -0
- package/dist/queue.js +16 -10
- package/dist/receipts.d.ts +60 -0
- package/dist/receipts.js +118 -0
- package/dist/revoke.d.ts +71 -0
- package/dist/revoke.js +108 -0
- package/dist/section-status.d.ts +94 -0
- package/dist/section-status.js +16 -0
- package/dist/storage.d.ts +29 -0
- package/dist/storage.js +65 -0
- package/dist/sync-client.d.ts +148 -3
- package/dist/sync-client.js +736 -11
- package/dist/typing.d.ts +60 -0
- package/dist/typing.js +110 -0
- package/dist/write-access.d.ts +115 -0
- package/dist/write-access.js +87 -0
- package/package.json +6 -6
package/README.md
CHANGED
|
@@ -12,9 +12,9 @@ ESM only; Node.js 24 or later (browsers and Electron for every package except `@
|
|
|
12
12
|
|
|
13
13
|
Part of the OpenLFCP TypeScript SDK, which implements the OpenLFCP MVP 0.1
|
|
14
14
|
subset of [LFCP-WIRE-01](https://github.com/openlfcp/spec/blob/main/wire/LFCP-WIRE-01.md)
|
|
15
|
-
at `mvp-0.1-baseline.
|
|
15
|
+
at `mvp-0.1-baseline.9`, not every deferred WIRE-01 feature. It does not
|
|
16
16
|
claim full LFCP-WIRE-01 conformance. This is the MVP 0.1 release
|
|
17
|
-
(`0.1.
|
|
17
|
+
(`0.1.3`, npm dist-tag `latest`). Until 1.0, minor versions may change APIs.
|
|
18
18
|
|
|
19
19
|
## Links
|
|
20
20
|
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { type PrincipalId } from "@openlfcp/core";
|
|
2
|
+
import { type ControlRecord, type ControlState } from "@openlfcp/wire";
|
|
3
|
+
/**
|
|
4
|
+
* Recovering access after a server restore (LFCP-02-106; design:
|
|
5
|
+
* `.github: docs/devel/design/member-recovery-after-restore.md`).
|
|
6
|
+
*
|
|
7
|
+
* A restored server may have lost the Control Records that grant this
|
|
8
|
+
* session's Principal `data/read`, and then refuses RESOURCE_OPEN with
|
|
9
|
+
* AUTHORIZATION_FAILED. A member whose own validated chain grants it
|
|
10
|
+
* `data/read` re-supplies the records the server lacks with CONTROL_PUT,
|
|
11
|
+
* which needs no read access (§47, §84): the server checks each record by
|
|
12
|
+
* its issuer. It learns the server's head from CONTROL_HEAD_MISMATCH,
|
|
13
|
+
* pushes the records above it oldest first, and opens again once.
|
|
14
|
+
*
|
|
15
|
+
* Security: the member uploads only records it holds, each signed by its
|
|
16
|
+
* issuer, and learns only the server's head ID (any session learns it from
|
|
17
|
+
* a mismatch). A chain that does not grant the member access sends nothing;
|
|
18
|
+
* a server head the member does not know (a revocation it has not seen, a
|
|
19
|
+
* fork) stops the recovery. Attempts are bounded.
|
|
20
|
+
*
|
|
21
|
+
* These are the pure decisions; SyncClient sends the requests.
|
|
22
|
+
*/
|
|
23
|
+
/** What to do next. */
|
|
24
|
+
export type RecoveryStep =
|
|
25
|
+
/** CONTROL_PUT of `records[index]`, expecting `records[index - 1]`. */
|
|
26
|
+
{
|
|
27
|
+
readonly kind: "push";
|
|
28
|
+
readonly index: number;
|
|
29
|
+
}
|
|
30
|
+
/** The server holds the whole chain now: RESOURCE_OPEN again, once. */
|
|
31
|
+
| {
|
|
32
|
+
readonly kind: "reopen";
|
|
33
|
+
}
|
|
34
|
+
/** No recovery: the refusal stands. */
|
|
35
|
+
| {
|
|
36
|
+
readonly kind: "final";
|
|
37
|
+
readonly reason: RecoveryEnd;
|
|
38
|
+
};
|
|
39
|
+
export type RecoveryEnd =
|
|
40
|
+
/** The local chain does not grant this Principal data/read: never tried. */
|
|
41
|
+
"not-granted"
|
|
42
|
+
/** The server holds our head: the refusal is not caused by a loss. */
|
|
43
|
+
| "server-current"
|
|
44
|
+
/** The server's head is not in our chain: a record we have not seen, or a fork. */
|
|
45
|
+
| "unknown-head"
|
|
46
|
+
/** The server refused a record for good, or the transient bound was reached. */
|
|
47
|
+
| "refused";
|
|
48
|
+
/** Transient refusals of a recovery CONTROL_PUT, retried with backoff a bounded number of times. */
|
|
49
|
+
export declare const RECOVERY_TRANSIENT_CODES: ReadonlySet<string>;
|
|
50
|
+
/** How many transient refusals a recovery tolerates before it ends. */
|
|
51
|
+
export declare const RECOVERY_TRANSIENT_ATTEMPTS = 3;
|
|
52
|
+
/**
|
|
53
|
+
* The first step after RESOURCE_OPEN was refused with AUTHORIZATION_FAILED:
|
|
54
|
+
* push our head (the server's answer tells where its chain ends), or not
|
|
55
|
+
* at all when our chain does not grant `principal` data/read or has nothing
|
|
56
|
+
* above the Genesis to push.
|
|
57
|
+
*/
|
|
58
|
+
export declare function startRecovery(state: ControlState, records: readonly ControlRecord[], principal: PrincipalId): RecoveryStep;
|
|
59
|
+
/** The next step after the server committed `records[index]`. */
|
|
60
|
+
export declare function afterAccepted(records: readonly ControlRecord[], index: number): RecoveryStep;
|
|
61
|
+
/**
|
|
62
|
+
* The next step after CONTROL_HEAD_MISMATCH naming the server's head
|
|
63
|
+
* `head`: push the record above it; open again once when the server holds
|
|
64
|
+
* our head (another holder re-supplied it meanwhile; if the refusal is not
|
|
65
|
+
* caused by a loss, the second AUTHORIZATION_FAILED is final); stop at a
|
|
66
|
+
* head we do not know.
|
|
67
|
+
*/
|
|
68
|
+
export declare function afterMismatch(records: readonly ControlRecord[], head: unknown): RecoveryStep;
|
|
69
|
+
//# sourceMappingURL=access-recovery.d.ts.map
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { bytesEqual } from "@openlfcp/core";
|
|
2
|
+
import { ABILITY, hasAbility } from "@openlfcp/wire";
|
|
3
|
+
/** Transient refusals of a recovery CONTROL_PUT, retried with backoff a bounded number of times. */
|
|
4
|
+
export const RECOVERY_TRANSIENT_CODES = new Set([
|
|
5
|
+
"RATE_LIMITED",
|
|
6
|
+
"INTERNAL_ERROR",
|
|
7
|
+
]);
|
|
8
|
+
/** How many transient refusals a recovery tolerates before it ends. */
|
|
9
|
+
export const RECOVERY_TRANSIENT_ATTEMPTS = 3;
|
|
10
|
+
/**
|
|
11
|
+
* The first step after RESOURCE_OPEN was refused with AUTHORIZATION_FAILED:
|
|
12
|
+
* push our head (the server's answer tells where its chain ends), or not
|
|
13
|
+
* at all when our chain does not grant `principal` data/read or has nothing
|
|
14
|
+
* above the Genesis to push.
|
|
15
|
+
*/
|
|
16
|
+
export function startRecovery(state, records, principal) {
|
|
17
|
+
if (!hasAbility(state, principal, ABILITY.DATA_READ))
|
|
18
|
+
return final("not-granted");
|
|
19
|
+
if (records.length < 2)
|
|
20
|
+
return final("server-current");
|
|
21
|
+
return { kind: "push", index: records.length - 1 };
|
|
22
|
+
}
|
|
23
|
+
/** The next step after the server committed `records[index]`. */
|
|
24
|
+
export function afterAccepted(records, index) {
|
|
25
|
+
return index >= records.length - 1 ? { kind: "reopen" } : { kind: "push", index: index + 1 };
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* The next step after CONTROL_HEAD_MISMATCH naming the server's head
|
|
29
|
+
* `head`: push the record above it; open again once when the server holds
|
|
30
|
+
* our head (another holder re-supplied it meanwhile; if the refusal is not
|
|
31
|
+
* caused by a loss, the second AUTHORIZATION_FAILED is final); stop at a
|
|
32
|
+
* head we do not know.
|
|
33
|
+
*/
|
|
34
|
+
export function afterMismatch(records, head) {
|
|
35
|
+
if (!(head instanceof Uint8Array))
|
|
36
|
+
return final("refused");
|
|
37
|
+
const at = records.findIndex((r) => bytesEqual(r.signed.id, head));
|
|
38
|
+
if (at < 0)
|
|
39
|
+
return final("unknown-head");
|
|
40
|
+
if (at === records.length - 1)
|
|
41
|
+
return { kind: "reopen" };
|
|
42
|
+
return { kind: "push", index: at + 1 };
|
|
43
|
+
}
|
|
44
|
+
const final = (reason) => ({ kind: "final", reason });
|
|
45
|
+
//# sourceMappingURL=access-recovery.js.map
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { type DataUnitId, type ResourceId } from "@openlfcp/core";
|
|
2
|
+
import type { LfcpStorage, StorageWrite } from "@openlfcp/storage";
|
|
3
|
+
import { type Receipt } from "./receipts.js";
|
|
4
|
+
/**
|
|
5
|
+
* The status of a committed batch (SDK-SECTIONS-INTEGRATION-01 §4.1,
|
|
6
|
+
* LFCP-02-026): which of its units the server accepted, and whether it was
|
|
7
|
+
* rejected. Kept next to the batch's receipt as a local mark, and written
|
|
8
|
+
* in the same storage transaction as the evidence that changes it (the
|
|
9
|
+
* ACK's dequeue), so a restart never loses an acceptance nor invents one.
|
|
10
|
+
* An index maps each unit of a receipt to its operation. A batch whose
|
|
11
|
+
* receipt was released before it was final stays here until it is final
|
|
12
|
+
* (receipts.ts).
|
|
13
|
+
*/
|
|
14
|
+
/** §4.1. */
|
|
15
|
+
export type BatchStatusName = "saved" | "pending" | "accepted" | "evidence-unavailable" | "rejected"
|
|
16
|
+
/**
|
|
17
|
+
* LFCP-02-115: not accepted, and the server refuses the Resource in this
|
|
18
|
+
* session (e.g. AUTHORIZATION_FAILED on RESOURCE_OPEN): it cannot be
|
|
19
|
+
* accepted there now. Not a refusal of the content; the work is kept.
|
|
20
|
+
*/
|
|
21
|
+
| "blocked";
|
|
22
|
+
/** §4.1, §5 `batch`: a committed batch and the evidence about its units. */
|
|
23
|
+
export interface BatchStatus {
|
|
24
|
+
readonly operationId: string;
|
|
25
|
+
readonly status: BatchStatusName;
|
|
26
|
+
readonly unitIds: readonly DataUnitId[];
|
|
27
|
+
/** The units a correlated durable ACK from a durability-2 server on the route set named (§4.1). */
|
|
28
|
+
readonly acceptedUnitIds: readonly DataUnitId[];
|
|
29
|
+
readonly affectedNodeIds: readonly string[];
|
|
30
|
+
/** A terminal NACK of one of its units. */
|
|
31
|
+
readonly rejection?: {
|
|
32
|
+
readonly code: string;
|
|
33
|
+
readonly unitIds: readonly DataUnitId[];
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
/** The durability (§37) an ACK must establish to be acceptance evidence (§4.1). */
|
|
37
|
+
export declare const ACCEPTANCE_DURABILITY = 2n;
|
|
38
|
+
interface Stored {
|
|
39
|
+
/** Units with acceptance evidence (hex). */
|
|
40
|
+
readonly accepted: readonly string[];
|
|
41
|
+
/** Units acknowledged without it: not durable, below durability 2, or off the route set (hex). */
|
|
42
|
+
readonly unconfirmed: readonly string[];
|
|
43
|
+
readonly rejection?: {
|
|
44
|
+
readonly code: string;
|
|
45
|
+
readonly unitIds: readonly string[];
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
/** The index writes of a new receipt (in the commit's transaction). */
|
|
49
|
+
export declare function receiptIndexWrites(resource: ResourceId, receipt: Receipt): StorageWrite[];
|
|
50
|
+
/**
|
|
51
|
+
* The writes of releasing `receipt` (with its receipt mark's removal): a
|
|
52
|
+
* final batch's status and index go; a batch not final yet is kept,
|
|
53
|
+
* reported until it is final.
|
|
54
|
+
*/
|
|
55
|
+
export declare function releaseWrites(storage: Pick<LfcpStorage, "localMarks">, resource: ResourceId, receipt: Receipt): Promise<StorageWrite[]>;
|
|
56
|
+
/**
|
|
57
|
+
* The writes that forget a released batch still kept for its status, when
|
|
58
|
+
* a new commit takes its operation ID (the new batch replaces it).
|
|
59
|
+
*/
|
|
60
|
+
export declare function replaceReleasedWrites(storage: Pick<LfcpStorage, "localMarks">, resource: ResourceId, operationId: string): Promise<StorageWrite[]>;
|
|
61
|
+
/** The writes that forget a released receipt's status and index. */
|
|
62
|
+
export declare function releaseStatusWrites(resource: ResourceId, receipt: Receipt): StorageWrite[];
|
|
63
|
+
/**
|
|
64
|
+
* §4.1: the status of a receipt's batch. `readyDurability` is the current
|
|
65
|
+
* session's READY durability, or null without a session.
|
|
66
|
+
*/
|
|
67
|
+
export declare function deriveStatus(receipt: Receipt, s: Stored, readyDurability: bigint | null, refused?: boolean): BatchStatus;
|
|
68
|
+
/** The status of one operation's batch; undefined without a receipt. */
|
|
69
|
+
export declare function batchStatus(storage: Pick<LfcpStorage, "localMarks">, resource: ResourceId, operationId: string, readyDurability: bigint | null, refused?: boolean): Promise<BatchStatus | undefined>;
|
|
70
|
+
/** Every receipt's batch of a Resource, and every released batch not final yet, by operation ID. */
|
|
71
|
+
export declare function batchStatuses(storage: Pick<LfcpStorage, "localMarks">, resource: ResourceId, readyDurability: bigint | null, refused?: boolean): Promise<BatchStatus[]>;
|
|
72
|
+
/** What happened to units of receipts: how their batches' stored status changes. */
|
|
73
|
+
export type UnitEvidence =
|
|
74
|
+
/** Acceptance evidence (§4.1). */
|
|
75
|
+
{
|
|
76
|
+
readonly kind: "accepted";
|
|
77
|
+
}
|
|
78
|
+
/** Acknowledged without acceptance evidence. */
|
|
79
|
+
| {
|
|
80
|
+
readonly kind: "unconfirmed";
|
|
81
|
+
}
|
|
82
|
+
/** A terminal NACK. */
|
|
83
|
+
| {
|
|
84
|
+
readonly kind: "rejected";
|
|
85
|
+
readonly code: string;
|
|
86
|
+
}
|
|
87
|
+
/** Offered again after the server lost them (§4.2): no longer accepted. */
|
|
88
|
+
| {
|
|
89
|
+
readonly kind: "reoffered";
|
|
90
|
+
};
|
|
91
|
+
/**
|
|
92
|
+
* The writes recording `evidence` for `units` (hex), and the operations
|
|
93
|
+
* whose stored status changed with the units that changed in each. Units
|
|
94
|
+
* of no receipt are ignored. A released batch that becomes final is
|
|
95
|
+
* forgotten in the same writes; its final status is in `finals`, since it
|
|
96
|
+
* cannot be read back. Pure apart from reading the marks.
|
|
97
|
+
*/
|
|
98
|
+
export declare function recordEvidence(storage: Pick<LfcpStorage, "localMarks">, resource: ResourceId, units: readonly string[], evidence: UnitEvidence): Promise<{
|
|
99
|
+
readonly writes: StorageWrite[];
|
|
100
|
+
readonly changed: ReadonlyMap<string, readonly string[]>;
|
|
101
|
+
readonly finals: ReadonlyMap<string, BatchStatus>;
|
|
102
|
+
}>;
|
|
103
|
+
export {};
|
|
104
|
+
//# sourceMappingURL=batch-status.d.ts.map
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { dataUnitId, fromHex, toHex } from "@openlfcp/core";
|
|
2
|
+
import { receiptOf, receiptsOf, releasedReceiptOf, releasedReceiptsOf, releasedWrite, } from "./receipts.js";
|
|
3
|
+
/** The durability (§37) an ACK must establish to be acceptance evidence (§4.1). */
|
|
4
|
+
export const ACCEPTANCE_DURABILITY = 2n;
|
|
5
|
+
const EMPTY = { accepted: [], unconfirmed: [] };
|
|
6
|
+
const statusKey = (resource, operationId) => `batch-status:${toHex(resource)}:${operationId}`;
|
|
7
|
+
const unitKey = (resource, unit) => `receipt-unit:${toHex(resource)}:${unit}`;
|
|
8
|
+
/** The index writes of a new receipt (in the commit's transaction). */
|
|
9
|
+
export function receiptIndexWrites(resource, receipt) {
|
|
10
|
+
return receipt.unitIds.map((u) => ({
|
|
11
|
+
op: "put-local-mark",
|
|
12
|
+
key: unitKey(resource, toHex(u)),
|
|
13
|
+
value: receipt.operationId,
|
|
14
|
+
}));
|
|
15
|
+
}
|
|
16
|
+
/** Accepted or rejected: nothing more will happen to the batch (§4.1). */
|
|
17
|
+
function isFinal(receipt, s) {
|
|
18
|
+
return s.rejection !== undefined || receipt.unitIds.every((u) => s.accepted.includes(toHex(u)));
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* The writes of releasing `receipt` (with its receipt mark's removal): a
|
|
22
|
+
* final batch's status and index go; a batch not final yet is kept,
|
|
23
|
+
* reported until it is final.
|
|
24
|
+
*/
|
|
25
|
+
export async function releaseWrites(storage, resource, receipt) {
|
|
26
|
+
const s = await stored(storage, resource, receipt.operationId);
|
|
27
|
+
return isFinal(receipt, s)
|
|
28
|
+
? releaseStatusWrites(resource, receipt)
|
|
29
|
+
: [releasedWrite(resource, receipt.operationId, receipt)];
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* The writes that forget a released batch still kept for its status, when
|
|
33
|
+
* a new commit takes its operation ID (the new batch replaces it).
|
|
34
|
+
*/
|
|
35
|
+
export async function replaceReleasedWrites(storage, resource, operationId) {
|
|
36
|
+
const released = await releasedReceiptOf(storage, resource, operationId);
|
|
37
|
+
return released === undefined
|
|
38
|
+
? []
|
|
39
|
+
: [releasedWrite(resource, operationId, null), ...releaseStatusWrites(resource, released)];
|
|
40
|
+
}
|
|
41
|
+
/** The writes that forget a released receipt's status and index. */
|
|
42
|
+
export function releaseStatusWrites(resource, receipt) {
|
|
43
|
+
return [
|
|
44
|
+
{ op: "put-local-mark", key: statusKey(resource, receipt.operationId), value: null },
|
|
45
|
+
...receipt.unitIds.map((u) => ({
|
|
46
|
+
op: "put-local-mark",
|
|
47
|
+
key: unitKey(resource, toHex(u)),
|
|
48
|
+
value: null,
|
|
49
|
+
})),
|
|
50
|
+
];
|
|
51
|
+
}
|
|
52
|
+
async function stored(storage, resource, operationId) {
|
|
53
|
+
const raw = await storage.localMarks.get(statusKey(resource, operationId));
|
|
54
|
+
return raw === undefined ? EMPTY : JSON.parse(raw);
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* §4.1: the status of a receipt's batch. `readyDurability` is the current
|
|
58
|
+
* session's READY durability, or null without a session.
|
|
59
|
+
*/
|
|
60
|
+
export function deriveStatus(receipt, s, readyDurability, refused = false) {
|
|
61
|
+
const units = receipt.unitIds.map((u) => toHex(u));
|
|
62
|
+
const accepted = units.filter((u) => s.accepted.includes(u));
|
|
63
|
+
const status = s.rejection !== undefined
|
|
64
|
+
? "rejected"
|
|
65
|
+
: units.length === 0
|
|
66
|
+
? "saved"
|
|
67
|
+
: accepted.length === units.length
|
|
68
|
+
? "accepted"
|
|
69
|
+
: refused
|
|
70
|
+
? "blocked"
|
|
71
|
+
: units.some((u) => s.unconfirmed.includes(u) && !s.accepted.includes(u)) ||
|
|
72
|
+
(readyDurability !== null && readyDurability < ACCEPTANCE_DURABILITY)
|
|
73
|
+
? "evidence-unavailable"
|
|
74
|
+
: "pending";
|
|
75
|
+
const ids = (hex) => Object.freeze(hex.map((h) => dataUnitId(fromHex(h))));
|
|
76
|
+
return Object.freeze({
|
|
77
|
+
operationId: receipt.operationId,
|
|
78
|
+
status,
|
|
79
|
+
unitIds: receipt.unitIds,
|
|
80
|
+
acceptedUnitIds: ids(accepted),
|
|
81
|
+
affectedNodeIds: receipt.affectedNodeIds,
|
|
82
|
+
...(s.rejection === undefined
|
|
83
|
+
? {}
|
|
84
|
+
: {
|
|
85
|
+
rejection: Object.freeze({ code: s.rejection.code, unitIds: ids(s.rejection.unitIds) }),
|
|
86
|
+
}),
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
/** The status of one operation's batch; undefined without a receipt. */
|
|
90
|
+
export async function batchStatus(storage, resource, operationId, readyDurability, refused = false) {
|
|
91
|
+
const receipt = (await receiptOf(storage, resource, operationId)) ??
|
|
92
|
+
(await releasedReceiptOf(storage, resource, operationId));
|
|
93
|
+
if (receipt === undefined)
|
|
94
|
+
return undefined;
|
|
95
|
+
return deriveStatus(receipt, await stored(storage, resource, operationId), readyDurability, refused);
|
|
96
|
+
}
|
|
97
|
+
/** Every receipt's batch of a Resource, and every released batch not final yet, by operation ID. */
|
|
98
|
+
export async function batchStatuses(storage, resource, readyDurability, refused = false) {
|
|
99
|
+
const out = [];
|
|
100
|
+
const held = await receiptsOf(storage, resource);
|
|
101
|
+
const ids = new Set(held.map((r) => r.operationId));
|
|
102
|
+
const released = (await releasedReceiptsOf(storage, resource)).filter((r) => !ids.has(r.operationId));
|
|
103
|
+
for (const receipt of [...held, ...released])
|
|
104
|
+
out.push(deriveStatus(receipt, await stored(storage, resource, receipt.operationId), readyDurability, refused));
|
|
105
|
+
return out.sort((a, b) => (a.operationId < b.operationId ? -1 : 1));
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* The writes recording `evidence` for `units` (hex), and the operations
|
|
109
|
+
* whose stored status changed with the units that changed in each. Units
|
|
110
|
+
* of no receipt are ignored. A released batch that becomes final is
|
|
111
|
+
* forgotten in the same writes; its final status is in `finals`, since it
|
|
112
|
+
* cannot be read back. Pure apart from reading the marks.
|
|
113
|
+
*/
|
|
114
|
+
export async function recordEvidence(storage, resource, units, evidence) {
|
|
115
|
+
const byOperation = new Map();
|
|
116
|
+
for (const u of new Set(units)) {
|
|
117
|
+
const op = await storage.localMarks.get(unitKey(resource, u));
|
|
118
|
+
if (op !== undefined)
|
|
119
|
+
byOperation.set(op, [...(byOperation.get(op) ?? []), u]);
|
|
120
|
+
}
|
|
121
|
+
const writes = [];
|
|
122
|
+
const changed = new Map();
|
|
123
|
+
const finals = new Map();
|
|
124
|
+
for (const [op, mine] of byOperation) {
|
|
125
|
+
const s = await stored(storage, resource, op);
|
|
126
|
+
let next = s;
|
|
127
|
+
let touched = [];
|
|
128
|
+
switch (evidence.kind) {
|
|
129
|
+
case "accepted":
|
|
130
|
+
touched = mine.filter((u) => !s.accepted.includes(u));
|
|
131
|
+
next = { ...s, accepted: [...s.accepted, ...touched] };
|
|
132
|
+
break;
|
|
133
|
+
case "unconfirmed":
|
|
134
|
+
touched = mine.filter((u) => !s.accepted.includes(u) && !s.unconfirmed.includes(u));
|
|
135
|
+
next = { ...s, unconfirmed: [...s.unconfirmed, ...touched] };
|
|
136
|
+
break;
|
|
137
|
+
case "rejected":
|
|
138
|
+
if (s.rejection !== undefined)
|
|
139
|
+
break;
|
|
140
|
+
touched = mine;
|
|
141
|
+
next = { ...s, rejection: { code: evidence.code, unitIds: mine } };
|
|
142
|
+
break;
|
|
143
|
+
case "reoffered":
|
|
144
|
+
touched = mine.filter((u) => s.accepted.includes(u) || s.unconfirmed.includes(u));
|
|
145
|
+
next = {
|
|
146
|
+
...s,
|
|
147
|
+
accepted: s.accepted.filter((u) => !mine.includes(u)),
|
|
148
|
+
unconfirmed: s.unconfirmed.filter((u) => !mine.includes(u)),
|
|
149
|
+
};
|
|
150
|
+
break;
|
|
151
|
+
}
|
|
152
|
+
if (touched.length === 0)
|
|
153
|
+
continue;
|
|
154
|
+
changed.set(op, touched);
|
|
155
|
+
const released = (await receiptOf(storage, resource, op)) === undefined
|
|
156
|
+
? await releasedReceiptOf(storage, resource, op)
|
|
157
|
+
: undefined;
|
|
158
|
+
if (released !== undefined && isFinal(released, next)) {
|
|
159
|
+
finals.set(op, deriveStatus(released, next, null));
|
|
160
|
+
writes.push(releasedWrite(resource, op, null), ...releaseStatusWrites(resource, released));
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
writes.push({
|
|
164
|
+
op: "put-local-mark",
|
|
165
|
+
key: statusKey(resource, op),
|
|
166
|
+
value: JSON.stringify(next),
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
return { writes, changed, finals };
|
|
170
|
+
}
|
|
171
|
+
//# sourceMappingURL=batch-status.js.map
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { type ResourceId } from "@openlfcp/core";
|
|
2
|
+
import { type ResourceDEK } from "@openlfcp/crypto";
|
|
3
|
+
import { type LfcpStorage, type SecretStore } from "@openlfcp/storage";
|
|
4
|
+
/**
|
|
5
|
+
* The journal of a capability claim in flight (LFCP-02-110).
|
|
6
|
+
*
|
|
7
|
+
* A one-time invitation is spent when the coordinator commits the claim
|
|
8
|
+
* (LFCP-WIRE-01 §18.1). If the CONTROL_PUT's answer is lost, the claimant
|
|
9
|
+
* cannot tell whether it was: retrying with the link fails once the
|
|
10
|
+
* invitation is used up, and the claimant would be left without the chain
|
|
11
|
+
* and the DEK. So before every claim is sent, the claimant journals it:
|
|
12
|
+
* the claim record, the head it expects, the validated chain up to that
|
|
13
|
+
* head, and the epochs whose DEKs it holds (the DEKs themselves go to the
|
|
14
|
+
* SecretStore first, LFCP-034). After a lost answer, the same record is
|
|
15
|
+
* sent again: a coordinator answers a record it has already committed with
|
|
16
|
+
* the same ACK (§47, §70), so a claim that landed completes without
|
|
17
|
+
* spending the invitation twice, and one that did not is refused or meets
|
|
18
|
+
* a moved head.
|
|
19
|
+
*
|
|
20
|
+
* The journal is a local mark (`invitation-claim:<resource hex>`). It holds
|
|
21
|
+
* no secret: the claim and the chain are signed public records.
|
|
22
|
+
*/
|
|
23
|
+
/** A journaled claim, as JSON in a local mark. Byte strings are hex. */
|
|
24
|
+
export interface ClaimJournal {
|
|
25
|
+
readonly v: 1;
|
|
26
|
+
readonly resourceId: string;
|
|
27
|
+
/** The invitation grant the claim spends. */
|
|
28
|
+
readonly invitationGrantId: string;
|
|
29
|
+
/** The claimant's Principal ID. */
|
|
30
|
+
readonly claimant: string;
|
|
31
|
+
/** The signed CAPABILITY_CLAIM record. */
|
|
32
|
+
readonly claim: string;
|
|
33
|
+
/** The head the claim was sent against (its prev_control_id). */
|
|
34
|
+
readonly expectedHead: string;
|
|
35
|
+
/** The validated Control Records up to that head, in order. */
|
|
36
|
+
readonly chain: readonly string[];
|
|
37
|
+
/** The epochs whose DEK is in the SecretStore, as decimal strings. */
|
|
38
|
+
readonly epochs: readonly string[];
|
|
39
|
+
/** The claimed abilities, as decimal strings. */
|
|
40
|
+
readonly abilities: readonly string[];
|
|
41
|
+
}
|
|
42
|
+
type JournalStorage = Pick<LfcpStorage, "localMarks" | "commit">;
|
|
43
|
+
/** Writes the DEKs to the SecretStore, then the journal (secret first, LFCP-034). */
|
|
44
|
+
export declare function writeClaimJournal(storage: JournalStorage, secrets: SecretStore, resource: ResourceId, journal: ClaimJournal, deks: ReadonlyMap<string, ResourceDEK>, exportDek: (dek: ResourceDEK) => Uint8Array): Promise<void>;
|
|
45
|
+
/** The journaled claim of `resource`, if any. */
|
|
46
|
+
export declare function readClaimJournal(storage: Pick<LfcpStorage, "localMarks">, resource: ResourceId): Promise<ClaimJournal | undefined>;
|
|
47
|
+
/** Removes the journal of `resource` once its claim's outcome is known. */
|
|
48
|
+
export declare function clearClaimJournal(storage: JournalStorage, resource: ResourceId): Promise<void>;
|
|
49
|
+
/** Every journaled claim, e.g. to resume them after a restart (resumeInvitationClaim). */
|
|
50
|
+
export declare function pendingInvitationClaims(storage: Pick<LfcpStorage, "localMarks">): Promise<readonly ClaimJournal[]>;
|
|
51
|
+
/** The journaled epochs' DEKs, from the SecretStore. */
|
|
52
|
+
export declare function journaledDeks(secrets: SecretStore, resource: ResourceId, journal: ClaimJournal): Promise<Map<string, ResourceDEK>>;
|
|
53
|
+
export {};
|
|
54
|
+
//# sourceMappingURL=claim-journal.d.ts.map
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { dataEpoch, toHex } from "@openlfcp/core";
|
|
2
|
+
import { importResourceDEK } from "@openlfcp/crypto";
|
|
3
|
+
import { dekSecretRef } from "@openlfcp/storage";
|
|
4
|
+
const PREFIX = "invitation-claim:";
|
|
5
|
+
const key = (resource) => `${PREFIX}${toHex(resource)}`;
|
|
6
|
+
/** Writes the DEKs to the SecretStore, then the journal (secret first, LFCP-034). */
|
|
7
|
+
export async function writeClaimJournal(storage, secrets, resource, journal, deks, exportDek) {
|
|
8
|
+
for (const [epoch, dek] of deks) {
|
|
9
|
+
const bytes = exportDek(dek);
|
|
10
|
+
try {
|
|
11
|
+
await secrets.put(dekSecretRef(resource, dataEpoch(BigInt(epoch))), bytes);
|
|
12
|
+
}
|
|
13
|
+
finally {
|
|
14
|
+
bytes.fill(0);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
const result = await storage.commit([
|
|
18
|
+
{ op: "put-local-mark", key: key(resource), value: JSON.stringify(journal) },
|
|
19
|
+
]);
|
|
20
|
+
if (!result.ok)
|
|
21
|
+
throw new Error(`the claim journal was not stored: ${result.reason}`);
|
|
22
|
+
}
|
|
23
|
+
/** The journaled claim of `resource`, if any. */
|
|
24
|
+
export async function readClaimJournal(storage, resource) {
|
|
25
|
+
const value = await storage.localMarks.get(key(resource));
|
|
26
|
+
return value === undefined ? undefined : JSON.parse(value);
|
|
27
|
+
}
|
|
28
|
+
/** Removes the journal of `resource` once its claim's outcome is known. */
|
|
29
|
+
export async function clearClaimJournal(storage, resource) {
|
|
30
|
+
await storage.commit([{ op: "put-local-mark", key: key(resource), value: null }]);
|
|
31
|
+
}
|
|
32
|
+
/** Every journaled claim, e.g. to resume them after a restart (resumeInvitationClaim). */
|
|
33
|
+
export async function pendingInvitationClaims(storage) {
|
|
34
|
+
return (await storage.localMarks.list(PREFIX)).map((m) => JSON.parse(m.value));
|
|
35
|
+
}
|
|
36
|
+
/** The journaled epochs' DEKs, from the SecretStore. */
|
|
37
|
+
export async function journaledDeks(secrets, resource, journal) {
|
|
38
|
+
const deks = new Map();
|
|
39
|
+
for (const epoch of journal.epochs) {
|
|
40
|
+
const bytes = await secrets.get(dekSecretRef(resource, dataEpoch(BigInt(epoch))));
|
|
41
|
+
if (bytes === undefined)
|
|
42
|
+
continue;
|
|
43
|
+
try {
|
|
44
|
+
deks.set(epoch, importResourceDEK(bytes));
|
|
45
|
+
}
|
|
46
|
+
finally {
|
|
47
|
+
bytes.fill(0);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return deks;
|
|
51
|
+
}
|
|
52
|
+
//# sourceMappingURL=claim-journal.js.map
|
package/dist/index.d.ts
CHANGED
|
@@ -1,15 +1,22 @@
|
|
|
1
1
|
/** Name of this package. */
|
|
2
2
|
export declare const PACKAGE = "@openlfcp/client";
|
|
3
3
|
export { type ApplyOutcome, type DataProfileHandler, DataUnitApplier, type DataUnitApplierOptions, type EpochReconciliation, type EquivocationOutcome, type ExcludedUnit, type ProfileApplyResult, type ProfileDiagnostic, type ProfileExcludeResult, type ProfileUnit, } from "./apply.js";
|
|
4
|
+
export { ACCEPTANCE_DURABILITY, type BatchStatus, type BatchStatusName, batchStatus, batchStatuses, } from "./batch-status.js";
|
|
4
5
|
export { type CheckpointSource, ProfileCheckpointer } from "./checkpoint.js";
|
|
6
|
+
export { type ClaimJournal, pendingInvitationClaims } from "./claim-journal.js";
|
|
5
7
|
export { type ConnectionEvents, type ConnectionOptions, LFCP_SUBPROTOCOL, LfcpConnection, platformWebSocket, type WebSocketFactory, type WebSocketLike, } from "./connection.js";
|
|
6
8
|
export { type CreateDataUnitOptions, type CreatedDataUnit, createDataUnit } from "./data-unit.js";
|
|
7
9
|
export { EngineGuard, type EngineItem, isEngineTrap, type Suspicion, snapshotItem, unitItem, } from "./engine-guard.js";
|
|
8
|
-
export { type AcceptedInvitation, type AcceptInvitationOptions, type AcceptInvitationProgress, type AcceptInvitationStage, acceptInvitation, type CreatedInvitation, type CreateInvitationOptions, createInvitation, DEFAULT_INVITATION_ABILITIES, InvitationLink, } from "./invite.js";
|
|
9
|
-
export { type AckOutcome, type BlockedItem, exponentialBackoff, type NackOutcome, type OutboundMessage, OutboundQueue, type OutboundQueueOptions, type ResourceSyncState, type RetryPolicy, type RetryReason, resourceSyncState, type StaleOutboundUnit, snapshotFrontier, } from "./outbound.js";
|
|
10
|
-
export { createQueuedSnapshot, outboundItem, queueControlRecord, queueKeyEpoch, queueKeyPackage, queueSnapshot, } from "./queue.js";
|
|
10
|
+
export { type AcceptedInvitation, type AcceptInvitationOptions, type AcceptInvitationProgress, type AcceptInvitationStage, abandonInvitationClaim, acceptInvitation, type CreatedInvitation, type CreateInvitationOptions, createInvitation, DEFAULT_INVITATION_ABILITIES, InvitationLink, type ResumeInvitationClaimOptions, resumeInvitationClaim, } from "./invite.js";
|
|
11
|
+
export { type AckedItem, type AckOutcome, type AckWrites, type BlockedItem, exponentialBackoff, type NackOutcome, type OutboundMessage, OutboundQueue, type OutboundQueueOptions, type ResourceSyncState, type RetryPolicy, type RetryReason, resourceSyncState, type StaleOutboundUnit, snapshotFrontier, } from "./outbound.js";
|
|
12
|
+
export { controlRecordWrites, createQueuedSnapshot, keyPackageWrites, outboundItem, queueControlRecord, queueKeyEpoch, queueKeyPackage, queueSnapshot, } from "./queue.js";
|
|
13
|
+
export { intentsHash, OperationIdReusedError, type Receipt, receiptOf, receiptsOf, releaseReceipt, } from "./receipts.js";
|
|
11
14
|
export { type ResourcePhase, type ResourcePhaseEvent, resourcePhaseTransition, } from "./resource-state.js";
|
|
15
|
+
export { planRevocation, type RemainingPath, type RevocationPlan, type RevocationRefusal, RevocationRefused, } from "./revoke.js";
|
|
16
|
+
export { type CatchUp, type ReceivedFact, type ReceivedState, type ReofferReason, receivedState, type SectionState, type StatusEvent, type StatusSnapshot, } from "./section-status.js";
|
|
12
17
|
export { type CreatedSnapshot, type CreateSnapshotOptions, createSnapshot } from "./snapshot.js";
|
|
13
|
-
export { adoptStoredDeks, createQueuedDataUnit, dataUnitRow, dekResolver, latestAcceptedOwnUnit, loadControlChain, StoredSeenUnits, saveControlChain, saveControlConflict, } from "./storage.js";
|
|
14
|
-
export { defaultReconnect, type ReconnectPolicy, type ResourceBinding, type ResourceRefusal, type SnapshotBinding, SyncClient, type SyncClientOptions, type SyncEvent, startSyncDriver, TERMINAL_RESOURCE_CODES, } from "./sync-client.js";
|
|
18
|
+
export { adoptStoredDeks, type CommitOperationOptions, commitOperation, createQueuedDataUnit, dataUnitRow, dekResolver, latestAcceptedOwnUnit, loadControlChain, StoredSeenUnits, saveControlChain, saveControlConflict, } from "./storage.js";
|
|
19
|
+
export { type CommitBinding, CommitRefusedError, defaultReconnect, type ReconnectPolicy, type ResourceBinding, type ResourceRefusal, type RevokeAccessResult, type SnapshotBinding, type StagedOperation, SyncClient, type SyncClientOptions, type SyncEvent, startSyncDriver, TERMINAL_RESOURCE_CODES, } from "./sync-client.js";
|
|
20
|
+
export { type Flushed, type Submitted, TypingCoalescer, type TypingCoalescerOptions, } from "./typing.js";
|
|
21
|
+
export { type AccessContext, type AccessState, accessState, type GrantPath, type IssuedInvitation, NotWritableError, type PendingControl, type ServerRefusal, UNKNOWN_ACCESS, type WriteAccess, type WriteDeniedReason, writeAccess, } from "./write-access.js";
|
|
15
22
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
CHANGED
|
@@ -1,15 +1,22 @@
|
|
|
1
1
|
/** Name of this package. */
|
|
2
2
|
export const PACKAGE = "@openlfcp/client";
|
|
3
3
|
export { DataUnitApplier, } from "./apply.js";
|
|
4
|
+
export { ACCEPTANCE_DURABILITY, batchStatus, batchStatuses, } from "./batch-status.js";
|
|
4
5
|
export { ProfileCheckpointer } from "./checkpoint.js";
|
|
6
|
+
export { pendingInvitationClaims } from "./claim-journal.js";
|
|
5
7
|
export { LFCP_SUBPROTOCOL, LfcpConnection, platformWebSocket, } from "./connection.js";
|
|
6
8
|
export { createDataUnit } from "./data-unit.js";
|
|
7
9
|
export { EngineGuard, isEngineTrap, snapshotItem, unitItem, } from "./engine-guard.js";
|
|
8
|
-
export { acceptInvitation, createInvitation, DEFAULT_INVITATION_ABILITIES, InvitationLink, } from "./invite.js";
|
|
10
|
+
export { abandonInvitationClaim, acceptInvitation, createInvitation, DEFAULT_INVITATION_ABILITIES, InvitationLink, resumeInvitationClaim, } from "./invite.js";
|
|
9
11
|
export { exponentialBackoff, OutboundQueue, resourceSyncState, snapshotFrontier, } from "./outbound.js";
|
|
10
|
-
export { createQueuedSnapshot, outboundItem, queueControlRecord, queueKeyEpoch, queueKeyPackage, queueSnapshot, } from "./queue.js";
|
|
12
|
+
export { controlRecordWrites, createQueuedSnapshot, keyPackageWrites, outboundItem, queueControlRecord, queueKeyEpoch, queueKeyPackage, queueSnapshot, } from "./queue.js";
|
|
13
|
+
export { intentsHash, OperationIdReusedError, receiptOf, receiptsOf, releaseReceipt, } from "./receipts.js";
|
|
11
14
|
export { resourcePhaseTransition, } from "./resource-state.js";
|
|
15
|
+
export { planRevocation, RevocationRefused, } from "./revoke.js";
|
|
16
|
+
export { receivedState, } from "./section-status.js";
|
|
12
17
|
export { createSnapshot } from "./snapshot.js";
|
|
13
|
-
export { adoptStoredDeks, createQueuedDataUnit, dataUnitRow, dekResolver, latestAcceptedOwnUnit, loadControlChain, StoredSeenUnits, saveControlChain, saveControlConflict, } from "./storage.js";
|
|
14
|
-
export { defaultReconnect, SyncClient, startSyncDriver, TERMINAL_RESOURCE_CODES, } from "./sync-client.js";
|
|
18
|
+
export { adoptStoredDeks, commitOperation, createQueuedDataUnit, dataUnitRow, dekResolver, latestAcceptedOwnUnit, loadControlChain, StoredSeenUnits, saveControlChain, saveControlConflict, } from "./storage.js";
|
|
19
|
+
export { CommitRefusedError, defaultReconnect, SyncClient, startSyncDriver, TERMINAL_RESOURCE_CODES, } from "./sync-client.js";
|
|
20
|
+
export { TypingCoalescer, } from "./typing.js";
|
|
21
|
+
export { accessState, NotWritableError, UNKNOWN_ACCESS, writeAccess, } from "./write-access.js";
|
|
15
22
|
//# sourceMappingURL=index.js.map
|
package/dist/invite.d.ts
CHANGED
|
@@ -90,8 +90,13 @@ export interface AcceptInvitationOptions {
|
|
|
90
90
|
readonly signer: Signer;
|
|
91
91
|
readonly agreement: AgreementKeyPair;
|
|
92
92
|
};
|
|
93
|
-
/**
|
|
94
|
-
|
|
93
|
+
/**
|
|
94
|
+
* The claimant's storage and secrets: on "claimed" they hold the chain and
|
|
95
|
+
* the DEK. While a claim is in flight they also hold its journal
|
|
96
|
+
* (LFCP-02-110): a lost answer is then resolved by sending the same claim
|
|
97
|
+
* again, never by spending the invitation twice.
|
|
98
|
+
*/
|
|
99
|
+
readonly storage: Pick<LfcpStorage, "control" | "commit" | "localMarks">;
|
|
95
100
|
readonly secrets: SecretStore;
|
|
96
101
|
/** Abilities to claim; default the grant's, without invite/claim unless it is delegable (§18.1 rule 4). */
|
|
97
102
|
readonly abilities?: readonly bigint[];
|
|
@@ -132,6 +137,12 @@ export type AcceptedInvitation = {
|
|
|
132
137
|
readonly epochs: readonly DataEpoch[];
|
|
133
138
|
/** The CONTROL_PUT answers in order, e.g. ["ACK"] or ["CONTROL_HEAD_MISMATCH", "ACK"]. */
|
|
134
139
|
readonly attempts: readonly string[];
|
|
140
|
+
/**
|
|
141
|
+
* True when an earlier attempt's claim, whose answer was lost, turned
|
|
142
|
+
* out to be committed: it was sent again from its journal (§47) and
|
|
143
|
+
* acknowledged as already committed (LFCP-02-110).
|
|
144
|
+
*/
|
|
145
|
+
readonly resumed: boolean;
|
|
135
146
|
} | {
|
|
136
147
|
/** The coordinator refused the claim, e.g. AUTHORIZATION_FAILED: the invitation is used up. */
|
|
137
148
|
readonly kind: "refused";
|
|
@@ -171,4 +182,32 @@ export type AcceptedInvitation = {
|
|
|
171
182
|
* epoch's DEK; transport and server stops are "unavailable".
|
|
172
183
|
*/
|
|
173
184
|
export declare function acceptInvitation(options: AcceptInvitationOptions): Promise<AcceptedInvitation>;
|
|
185
|
+
export interface ResumeInvitationClaimOptions {
|
|
186
|
+
/** The Resource of a journaled claim (pendingInvitationClaims). */
|
|
187
|
+
readonly resourceId: ResourceId;
|
|
188
|
+
/** The claimant: the session authenticates as it (§84: any session may upload a record). */
|
|
189
|
+
readonly claimant: {
|
|
190
|
+
readonly signer: Signer;
|
|
191
|
+
};
|
|
192
|
+
readonly storage: Pick<LfcpStorage, "control" | "commit" | "localMarks">;
|
|
193
|
+
readonly secrets: SecretStore;
|
|
194
|
+
/** The coordinator's endpoint. */
|
|
195
|
+
readonly url: string;
|
|
196
|
+
readonly now: () => number;
|
|
197
|
+
readonly webSocket?: WebSocketFactory;
|
|
198
|
+
readonly timeout?: Promise<unknown>;
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Settles a journaled claim without its link, e.g. after a restart
|
|
202
|
+
* (LFCP-02-110): "claimed" when the claim had landed; "not-claimed" when it
|
|
203
|
+
* had not (the journal is removed, and the link, still unused, can be
|
|
204
|
+
* accepted again); "refused" for another refusal; "unavailable" keeps the
|
|
205
|
+
* journal for a later try; "none" when no claim is journaled.
|
|
206
|
+
*/
|
|
207
|
+
export declare function resumeInvitationClaim(options: ResumeInvitationClaimOptions): Promise<AcceptedInvitation | {
|
|
208
|
+
readonly kind: "not-claimed" | "none";
|
|
209
|
+
readonly resourceId: ResourceId;
|
|
210
|
+
}>;
|
|
211
|
+
/** Gives up a journaled claim (the user declined to retry it): its journal is removed. */
|
|
212
|
+
export declare function abandonInvitationClaim(storage: Pick<LfcpStorage, "commit">, resource: ResourceId): Promise<void>;
|
|
174
213
|
//# sourceMappingURL=invite.d.ts.map
|