@plane31/blind-relay 0.1.1 → 0.1.3
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/dist/crypto/sodium.d.ts +5 -3
- package/dist/crypto/sodium.d.ts.map +1 -1
- package/dist/crypto/sodium.js +13 -1
- package/dist/crypto/sodium.js.map +1 -1
- package/package.json +3 -2
- package/src/adapters/index.ts +102 -0
- package/src/audience.ts +123 -0
- package/src/bytes.ts +209 -0
- package/src/circle.ts +127 -0
- package/src/client.ts +1442 -0
- package/src/codec.ts +642 -0
- package/src/crypto/backend.ts +48 -0
- package/src/crypto/noble.ts +104 -0
- package/src/crypto/node-module.d.ts +11 -0
- package/src/crypto/sodium.ts +53 -0
- package/src/crypto.ts +266 -0
- package/src/epoch.ts +201 -0
- package/src/errors.ts +78 -0
- package/src/historyIndex.ts +61 -0
- package/src/identity.ts +158 -0
- package/src/inbox.ts +560 -0
- package/src/index.ts +59 -0
- package/src/outbox.ts +385 -0
- package/src/policy.ts +88 -0
- package/src/record.ts +106 -0
- package/src/reset.ts +176 -0
- package/src/rows.ts +116 -0
- package/src/slots.ts +55 -0
- package/src/store.ts +557 -0
- package/src/testing/clock.ts +29 -0
- package/src/testing/fakeHost.ts +482 -0
- package/src/testing/faults.ts +321 -0
- package/src/testing/fixtures.ts +35 -0
- package/src/testing/index.ts +6 -0
- package/src/testing/providers.ts +39 -0
- package/src/testing/sweep.ts +407 -0
- package/src/transactions.ts +231 -0
- package/src/transport.ts +106 -0
- package/src/vault.ts +126 -0
package/src/reset.ts
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Last-resort account reset and the reset keys (protocol section 12).
|
|
3
|
+
*
|
|
4
|
+
* `ResetTrust` is the acceptance model for management-signed key lists (type 8) and reset
|
|
5
|
+
* receipts (type 7): version rules, the 30-day lifetime, the trusted-clock rules with a durable
|
|
6
|
+
* time high-water mark, revocation by list, and immutable reset bindings. It is pure state with
|
|
7
|
+
* row conversions; the client persists it in T9. Nothing here pauses sharing.
|
|
8
|
+
*/
|
|
9
|
+
import { equalBytes, fromHex, toHex } from "./bytes.js";
|
|
10
|
+
import {
|
|
11
|
+
DocumentType,
|
|
12
|
+
expectType,
|
|
13
|
+
parseDocument,
|
|
14
|
+
verifyDocument,
|
|
15
|
+
type AccountDevice,
|
|
16
|
+
type AccountResetReceipt,
|
|
17
|
+
type Parsed,
|
|
18
|
+
type ResetKeyList
|
|
19
|
+
} from "./codec.js";
|
|
20
|
+
import { H, identityId } from "./crypto.js";
|
|
21
|
+
import { KavachError } from "./errors.js";
|
|
22
|
+
import { big, unbig } from "./rows.js";
|
|
23
|
+
|
|
24
|
+
export interface ResetTrustConfig {
|
|
25
|
+
/** The deployment's pinned management public key. */
|
|
26
|
+
readonly managementKey: Uint8Array;
|
|
27
|
+
/** The packaged minimum list version. */
|
|
28
|
+
readonly minimumVersion: bigint;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface ResetBinding {
|
|
32
|
+
readonly accountName: string;
|
|
33
|
+
readonly oldIdentityId: string;
|
|
34
|
+
readonly newIdentityId: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface ResetTrustState {
|
|
38
|
+
readonly current: { wire: Uint8Array; list: Parsed<ResetKeyList> } | undefined;
|
|
39
|
+
/** The greatest trusted time seen: a durable high-water mark. */
|
|
40
|
+
readonly timeHighWater: bigint;
|
|
41
|
+
readonly acceptedReceipts: ReadonlySet<string>;
|
|
42
|
+
readonly events: ReadonlyMap<string, ResetBinding>;
|
|
43
|
+
readonly retired: ReadonlySet<string>;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export const emptyResetTrust: ResetTrustState = { current: undefined, timeHighWater: 0n, acceptedReceipts: new Set(), events: new Map(), retired: new Set() };
|
|
47
|
+
|
|
48
|
+
export type InstallOutcome = "INSTALLED" | "DUPLICATE";
|
|
49
|
+
export type ApplyOutcome = "ACCOUNT_KEY_CHANGED" | "DUPLICATE";
|
|
50
|
+
|
|
51
|
+
/** A peer's view of an accepted reset: who to stop sharing with, and whose circles close. */
|
|
52
|
+
export interface ResetEffect {
|
|
53
|
+
readonly outcome: ApplyOutcome;
|
|
54
|
+
readonly oldIdentityId: Uint8Array;
|
|
55
|
+
readonly newIdentityId: Uint8Array;
|
|
56
|
+
readonly receipt: Parsed<AccountResetReceipt>;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export class ResetTrust {
|
|
60
|
+
constructor(
|
|
61
|
+
readonly config: ResetTrustConfig,
|
|
62
|
+
public state: ResetTrustState = emptyResetTrust
|
|
63
|
+
) {}
|
|
64
|
+
|
|
65
|
+
/** The platform clock under the accurate-clock assumption, checked against the high-water mark. */
|
|
66
|
+
private clock(now: bigint | undefined, stage: string): bigint {
|
|
67
|
+
if (now === undefined || now < this.state.timeHighWater) throw new KavachError("RESET_CLOCK_UNTRUSTED", stage);
|
|
68
|
+
return now;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Is the current list usable at `now`? */
|
|
72
|
+
fresh(now: bigint): boolean {
|
|
73
|
+
const c = this.state.current;
|
|
74
|
+
return !!c && c.list.issuedAt <= now && now < c.list.expiresAt;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Install a key list: verified under the pinned management key, version at least the packaged
|
|
79
|
+
* minimum and the durable highest, equal version only with an equal body, fresh at `now`.
|
|
80
|
+
*/
|
|
81
|
+
install(wire: Uint8Array, now: bigint | undefined): InstallOutcome {
|
|
82
|
+
const stage = "resetKeyList";
|
|
83
|
+
const list = expectType<ResetKeyList>(verifyDocument(wire, this.config.managementKey), DocumentType.ResetKeyList, stage);
|
|
84
|
+
if (list.keys.some((k) => equalBytes(k.publicKey, this.config.managementKey))) {
|
|
85
|
+
throw new KavachError("INVALID_KEY", stage, "the management key cannot be an operational key");
|
|
86
|
+
}
|
|
87
|
+
const current = this.state.current;
|
|
88
|
+
if (list.version < this.config.minimumVersion || (current && list.version < current.list.version)) throw new KavachError("ROLLBACK", stage);
|
|
89
|
+
if (current && list.version === current.list.version && !equalBytes(list.bodyHash, current.list.bodyHash)) {
|
|
90
|
+
throw new KavachError("EQUIVOCATION", stage, "two key lists at one version");
|
|
91
|
+
}
|
|
92
|
+
const t = this.clock(now, stage);
|
|
93
|
+
if (!(list.issuedAt <= t && t < list.expiresAt)) throw new KavachError("RESET_KEYS_REQUIRED", stage, "list not fresh");
|
|
94
|
+
const outcome: InstallOutcome = current && list.version === current.list.version ? "DUPLICATE" : "INSTALLED";
|
|
95
|
+
this.state = { ...this.state, current: { wire, list }, timeHighWater: t };
|
|
96
|
+
return outcome;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Apply a reset receipt for an account whose current pinned identity is `currentIdentityId`.
|
|
101
|
+
* Requires a fresh list, an allowed signer, a verified new certificate and consistent bindings.
|
|
102
|
+
*/
|
|
103
|
+
apply(receiptWire: Uint8Array, newCertificate: Uint8Array, accountName: string, currentIdentityId: Uint8Array, now: bigint | undefined): ResetEffect {
|
|
104
|
+
const stage = "resetReceipt";
|
|
105
|
+
const parsed = expectType<AccountResetReceipt>(parseDocument(receiptWire), DocumentType.AccountResetReceipt, stage);
|
|
106
|
+
const receiptHash = toHex(H(receiptWire));
|
|
107
|
+
if (this.state.acceptedReceipts.has(receiptHash)) {
|
|
108
|
+
return { outcome: "DUPLICATE", oldIdentityId: parsed.oldIdentityId, newIdentityId: parsed.newIdentityId, receipt: parsed };
|
|
109
|
+
}
|
|
110
|
+
const t = this.clock(now, stage);
|
|
111
|
+
if (!this.fresh(t)) throw new KavachError("RESET_KEYS_REQUIRED", stage, "no fresh key list");
|
|
112
|
+
const current = this.state.current;
|
|
113
|
+
if (!current) throw new KavachError("RESET_KEYS_REQUIRED", stage);
|
|
114
|
+
const signer = current.list.keys.find((k) => equalBytes(k.keyId, parsed.authorityKeyId));
|
|
115
|
+
if (!signer) throw new KavachError("RESET_KEY_REVOKED", stage, "the signing key is not in the current list");
|
|
116
|
+
const receipt = expectType<AccountResetReceipt>(verifyDocument(receiptWire, signer.publicKey), DocumentType.AccountResetReceipt, stage);
|
|
117
|
+
const cert = expectType<AccountDevice>(verifyDocument(newCertificate), DocumentType.AccountDevice, stage);
|
|
118
|
+
const binding: ResetBinding = { accountName: receipt.accountName, oldIdentityId: toHex(receipt.oldIdentityId), newIdentityId: toHex(receipt.newIdentityId) };
|
|
119
|
+
if (receipt.accountName !== accountName || cert.accountName !== accountName || !equalBytes(identityId(accountName, cert.accountKey), receipt.newIdentityId)) {
|
|
120
|
+
throw new KavachError("RESET_BINDING", stage, "receipt, account and certificate do not bind");
|
|
121
|
+
}
|
|
122
|
+
const resetId = toHex(receipt.resetId);
|
|
123
|
+
const known = this.state.events.get(resetId);
|
|
124
|
+
let outcome: ApplyOutcome;
|
|
125
|
+
if (known) {
|
|
126
|
+
if (known.accountName !== binding.accountName || known.oldIdentityId !== binding.oldIdentityId || known.newIdentityId !== binding.newIdentityId) {
|
|
127
|
+
throw new KavachError("RESET_BINDING", stage, "a conflicting binding for a known reset id");
|
|
128
|
+
}
|
|
129
|
+
outcome = "DUPLICATE";
|
|
130
|
+
} else {
|
|
131
|
+
if (!equalBytes(receipt.oldIdentityId, currentIdentityId) || this.state.retired.has(binding.newIdentityId)) {
|
|
132
|
+
throw new KavachError("RESET_BINDING", stage, "old identity is not the current pin, or the new one is retired");
|
|
133
|
+
}
|
|
134
|
+
outcome = "ACCOUNT_KEY_CHANGED";
|
|
135
|
+
}
|
|
136
|
+
const accepted = new Set(this.state.acceptedReceipts);
|
|
137
|
+
accepted.add(receiptHash);
|
|
138
|
+
const events = new Map(this.state.events);
|
|
139
|
+
events.set(resetId, binding);
|
|
140
|
+
const retired = new Set(this.state.retired);
|
|
141
|
+
retired.add(binding.oldIdentityId);
|
|
142
|
+
this.state = { ...this.state, acceptedReceipts: accepted, events, retired, timeHighWater: t };
|
|
143
|
+
return { outcome, oldIdentityId: receipt.oldIdentityId, newIdentityId: receipt.newIdentityId, receipt };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Persistence ----------------------------------------------------------------------------------
|
|
147
|
+
|
|
148
|
+
toRows(): ResetTrustRows {
|
|
149
|
+
return {
|
|
150
|
+
list: this.state.current ? { wire: toHex(this.state.current.wire), version: big(this.state.current.list.version), hash: toHex(this.state.current.list.bodyHash), checkedAt: big(this.state.timeHighWater) } : undefined,
|
|
151
|
+
clock: { highWater: big(this.state.timeHighWater) },
|
|
152
|
+
accepted: [...this.state.acceptedReceipts],
|
|
153
|
+
events: [...this.state.events.entries()].map(([resetId, b]) => ({ resetId, ...b })),
|
|
154
|
+
retired: [...this.state.retired]
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
static fromRows(config: ResetTrustConfig, rows: ResetTrustRows): ResetTrust {
|
|
159
|
+
const current = rows.list ? { wire: fromHex(rows.list.wire), list: expectType<ResetKeyList>(parseDocument(fromHex(rows.list.wire)), DocumentType.ResetKeyList) } : undefined;
|
|
160
|
+
return new ResetTrust(config, {
|
|
161
|
+
current,
|
|
162
|
+
timeHighWater: unbig(rows.clock.highWater),
|
|
163
|
+
acceptedReceipts: new Set(rows.accepted),
|
|
164
|
+
events: new Map(rows.events.map((e) => [e.resetId, { accountName: e.accountName, oldIdentityId: e.oldIdentityId, newIdentityId: e.newIdentityId }])),
|
|
165
|
+
retired: new Set(rows.retired)
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export interface ResetTrustRows {
|
|
171
|
+
list: { wire: string; version: string; hash: string; checkedAt: string } | undefined;
|
|
172
|
+
clock: { highWater: string };
|
|
173
|
+
accepted: string[];
|
|
174
|
+
events: { resetId: string; accountName: string; oldIdentityId: string; newIdentityId: string }[];
|
|
175
|
+
retired: string[];
|
|
176
|
+
}
|
package/src/rows.ts
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/** Conversions between domain objects and the JSON-safe rows of the store. */
|
|
2
|
+
import { fromHex, toHex } from "./bytes.js";
|
|
3
|
+
import type { CircleState } from "./circle.js";
|
|
4
|
+
import type { AccountPin } from "./identity.js";
|
|
5
|
+
import type { RevisionState } from "./audience.js";
|
|
6
|
+
import type { PreparedEpoch } from "./epoch.js";
|
|
7
|
+
import type { CircleRow, EpochRow, PinRow, RevisionRow } from "./store.js";
|
|
8
|
+
|
|
9
|
+
export const big = (v: bigint): string => v.toString();
|
|
10
|
+
export const unbig = (v: string): bigint => BigInt(v);
|
|
11
|
+
|
|
12
|
+
export function pinToRow(p: AccountPin): PinRow {
|
|
13
|
+
return {
|
|
14
|
+
accountName: p.accountName,
|
|
15
|
+
accountKey: toHex(p.accountKey),
|
|
16
|
+
identityId: toHex(p.identityId),
|
|
17
|
+
generation: big(p.generation),
|
|
18
|
+
certificateHash: toHex(p.certificateHash),
|
|
19
|
+
certificate: toHex(p.certificate),
|
|
20
|
+
deviceId: toHex(p.deviceId),
|
|
21
|
+
signingKey: toHex(p.signingKey),
|
|
22
|
+
boxKey: toHex(p.boxKey),
|
|
23
|
+
streamId: toHex(p.streamId),
|
|
24
|
+
deviceIds: p.deviceIds.map(toHex)
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function rowToPin(r: PinRow): AccountPin {
|
|
29
|
+
return {
|
|
30
|
+
accountName: r.accountName,
|
|
31
|
+
accountKey: fromHex(r.accountKey),
|
|
32
|
+
identityId: fromHex(r.identityId),
|
|
33
|
+
generation: unbig(r.generation),
|
|
34
|
+
certificateHash: fromHex(r.certificateHash),
|
|
35
|
+
certificate: fromHex(r.certificate),
|
|
36
|
+
deviceId: fromHex(r.deviceId),
|
|
37
|
+
signingKey: fromHex(r.signingKey),
|
|
38
|
+
boxKey: fromHex(r.boxKey),
|
|
39
|
+
streamId: fromHex(r.streamId),
|
|
40
|
+
deviceIds: r.deviceIds.map(fromHex)
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function circleToRow(c: CircleState): CircleRow {
|
|
45
|
+
return {
|
|
46
|
+
circleId: toHex(c.circleId),
|
|
47
|
+
ownerIdentityId: toHex(c.ownerIdentityId),
|
|
48
|
+
revision: big(c.revision),
|
|
49
|
+
bodyHash: c.bodyHash ? toHex(c.bodyHash) : undefined,
|
|
50
|
+
snapshot: c.snapshot ? toHex(c.snapshot) : undefined,
|
|
51
|
+
members: c.members.map(toHex),
|
|
52
|
+
enabled: c.enabled,
|
|
53
|
+
status: c.status,
|
|
54
|
+
verifiedAt: big(c.verifiedAt)
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function rowToCircle(r: CircleRow): CircleState {
|
|
59
|
+
return {
|
|
60
|
+
circleId: fromHex(r.circleId),
|
|
61
|
+
ownerIdentityId: fromHex(r.ownerIdentityId),
|
|
62
|
+
revision: unbig(r.revision),
|
|
63
|
+
bodyHash: r.bodyHash ? fromHex(r.bodyHash) : undefined,
|
|
64
|
+
snapshot: r.snapshot ? fromHex(r.snapshot) : undefined,
|
|
65
|
+
members: r.members.map(fromHex),
|
|
66
|
+
enabled: r.enabled,
|
|
67
|
+
status: r.status,
|
|
68
|
+
verifiedAt: unbig(r.verifiedAt)
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function revisionToRow(s: RevisionState, paused: RevisionRow["paused"]): RevisionRow {
|
|
73
|
+
return {
|
|
74
|
+
desiredRevision: big(s.desiredRevision),
|
|
75
|
+
seenSnapshots: Object.fromEntries([...s.seenSnapshots].map(([k, v]) => [k, big(v)])),
|
|
76
|
+
seenCertificates: Object.fromEntries([...s.seenCertificates]),
|
|
77
|
+
paused
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function rowToRevision(r: RevisionRow): RevisionState {
|
|
82
|
+
return {
|
|
83
|
+
desiredRevision: unbig(r.desiredRevision),
|
|
84
|
+
seenSnapshots: new Map(Object.entries(r.seenSnapshots).map(([k, v]) => [k, unbig(v)])),
|
|
85
|
+
seenCertificates: new Map(Object.entries(r.seenCertificates))
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function preparedToRow(p: PreparedEpoch): EpochRow {
|
|
90
|
+
return {
|
|
91
|
+
epoch: p.epoch,
|
|
92
|
+
status: "prepared",
|
|
93
|
+
manifest: toHex(p.manifest),
|
|
94
|
+
manifestHash: toHex(p.manifestHash),
|
|
95
|
+
wraps: Object.fromEntries([...p.wraps].map(([k, v]) => [k, toHex(v)])),
|
|
96
|
+
epochKey: toHex(p.epochKey),
|
|
97
|
+
capturedRevision: big(p.capturedRevision),
|
|
98
|
+
audienceKey: p.audienceKey,
|
|
99
|
+
createdAt: big(p.createdAt),
|
|
100
|
+
nextCounter: 0,
|
|
101
|
+
lastSentAt: undefined
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function rowToPrepared(r: EpochRow): PreparedEpoch {
|
|
106
|
+
return {
|
|
107
|
+
epoch: r.epoch,
|
|
108
|
+
manifest: fromHex(r.manifest),
|
|
109
|
+
manifestHash: fromHex(r.manifestHash),
|
|
110
|
+
wraps: new Map(Object.entries(r.wraps).map(([k, v]) => [k, fromHex(v)])),
|
|
111
|
+
epochKey: fromHex(r.epochKey),
|
|
112
|
+
capturedRevision: unbig(r.capturedRevision),
|
|
113
|
+
audienceKey: r.audienceKey,
|
|
114
|
+
createdAt: unbig(r.createdAt)
|
|
115
|
+
};
|
|
116
|
+
}
|
package/src/slots.ts
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The newest slot (protocol section 10): for every kind declared `newest`, one row per author
|
|
3
|
+
* holding the newest record from the author's current device, shared by the receiver (T7) and
|
|
4
|
+
* the sender's own row (T6). Order: a newer device generation first; within one generation the
|
|
5
|
+
* later event time, clamped to the record's `sentAt` so a clock set ahead cannot hold the slot;
|
|
6
|
+
* equal event times by (epoch, counter). The slot outlives its payload: expiry drops the payload
|
|
7
|
+
* and keeps the order, so an older record replayed later cannot become current.
|
|
8
|
+
*/
|
|
9
|
+
import { compareSlots } from "./record.js";
|
|
10
|
+
import { big, unbig } from "./rows.js";
|
|
11
|
+
import type { NewestRow, Tx } from "./store.js";
|
|
12
|
+
|
|
13
|
+
export interface SlotOrder {
|
|
14
|
+
readonly generation: bigint;
|
|
15
|
+
readonly eventTime: bigint;
|
|
16
|
+
readonly epoch: number;
|
|
17
|
+
readonly counter: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function compareSlotOrder(a: SlotOrder, b: SlotOrder): number {
|
|
21
|
+
if (a.generation !== b.generation) return a.generation < b.generation ? -1 : 1;
|
|
22
|
+
if (a.eventTime !== b.eventTime) return a.eventTime < b.eventTime ? -1 : 1;
|
|
23
|
+
return compareSlots(a, b);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** The event time a slot orders by: the envelope's, never later than `sentAt`; `sentAt` when the sender gave none. */
|
|
27
|
+
export function clampEventTime(eventTime: bigint | undefined, sentAt: bigint): bigint {
|
|
28
|
+
return eventTime === undefined || eventTime > sentAt ? sentAt : eventTime;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function slotKey(authorIdentityId: string, kind: number): string {
|
|
32
|
+
return `${authorIdentityId}:${kind}`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function slotOrderOf(row: NewestRow): SlotOrder {
|
|
36
|
+
return { generation: unbig(row.generation), eventTime: unbig(row.eventTime), epoch: row.epoch, counter: row.counter };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface SlotCandidate extends SlotOrder {
|
|
40
|
+
readonly recordId: string;
|
|
41
|
+
readonly bodyHash: string;
|
|
42
|
+
readonly sentAt: bigint;
|
|
43
|
+
readonly receivedAt: bigint;
|
|
44
|
+
readonly payload: Uint8Array;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Take the slot if the candidate orders after its current holder (or there is none); true when it did. */
|
|
48
|
+
export function takeSlot(tx: Tx, authorIdentityId: string, kind: number, c: SlotCandidate, toHex: (b: Uint8Array) => string): boolean {
|
|
49
|
+
const key = slotKey(authorIdentityId, kind);
|
|
50
|
+
const current = tx.get("newest", key);
|
|
51
|
+
if (current && compareSlotOrder(c, slotOrderOf(current)) <= 0) return false;
|
|
52
|
+
const next: NewestRow = { recordId: c.recordId, bodyHash: c.bodyHash, generation: big(c.generation), epoch: c.epoch, counter: c.counter, sentAt: big(c.sentAt), receivedAt: big(c.receivedAt), eventTime: big(c.eventTime), payload: toHex(c.payload) };
|
|
53
|
+
tx.put("newest", key, next);
|
|
54
|
+
return true;
|
|
55
|
+
}
|