ftown 0.21.0 → 0.24.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/dist/attention-events.d.ts +1 -1
- package/dist/attention-store.d.ts +3 -0
- package/dist/attention-store.js +25 -1
- package/dist/attention-store.js.map +1 -1
- package/dist/centrifugo-client.d.ts +14 -1
- package/dist/centrifugo-client.js +23 -2
- package/dist/centrifugo-client.js.map +1 -1
- package/dist/command-rpc.d.ts +2 -0
- package/dist/command-rpc.js +55 -9
- package/dist/command-rpc.js.map +1 -1
- package/dist/identity-reset.d.ts +3 -1
- package/dist/identity-reset.js +27 -5
- package/dist/identity-reset.js.map +1 -1
- package/dist/index.js +15 -5
- package/dist/index.js.map +1 -1
- package/dist/ledger-pairing-client.d.ts +1 -1
- package/dist/ledger-pairing-client.js +5 -3
- package/dist/ledger-pairing-client.js.map +1 -1
- package/dist/ledger-relay.d.ts +42 -7
- package/dist/ledger-relay.js +175 -33
- package/dist/ledger-relay.js.map +1 -1
- package/dist/ledger-reoffer.d.ts +56 -0
- package/dist/ledger-reoffer.js +210 -0
- package/dist/ledger-reoffer.js.map +1 -0
- package/dist/ledger-startup.d.ts +30 -5
- package/dist/ledger-startup.js +40 -10
- package/dist/ledger-startup.js.map +1 -1
- package/dist/local-api-server.d.ts +3 -0
- package/dist/local-api-server.js +68 -6
- package/dist/local-api-server.js.map +1 -1
- package/dist/mail-delivery.d.ts +26 -3
- package/dist/mail-delivery.js +65 -7
- package/dist/mail-delivery.js.map +1 -1
- package/dist/preview-protocol.d.ts +19 -3
- package/dist/preview-protocol.js +14 -0
- package/dist/preview-protocol.js.map +1 -1
- package/dist/relay-token.d.ts +6 -0
- package/dist/relay-token.js +9 -1
- package/dist/relay-token.js.map +1 -1
- package/dist/security/bridge/ledger-rpc.d.ts +34 -0
- package/dist/security/bridge/ledger-rpc.js +58 -0
- package/dist/security/bridge/ledger-rpc.js.map +1 -0
- package/dist/security/bridge/ledger-store.d.ts +82 -13
- package/dist/security/bridge/ledger-store.js +394 -54
- package/dist/security/bridge/ledger-store.js.map +1 -1
- package/dist/security/bridge/permissions.js +2 -1
- package/dist/security/bridge/permissions.js.map +1 -1
- package/dist/security/bridge/policy.d.ts +2 -1
- package/dist/security/bridge/policy.js.map +1 -1
- package/dist/security/bridge/security.d.ts +138 -10
- package/dist/security/bridge/security.js +252 -54
- package/dist/security/bridge/security.js.map +1 -1
- package/dist/security/ledger/announce.d.ts +32 -0
- package/dist/security/ledger/announce.js +101 -0
- package/dist/security/ledger/announce.js.map +1 -0
- package/dist/security/ledger/ledger.d.ts +107 -14
- package/dist/security/ledger/ledger.js +364 -83
- package/dist/security/ledger/ledger.js.map +1 -1
- package/dist/security/ledger/proof.d.ts +12 -0
- package/dist/security/ledger/proof.js +40 -8
- package/dist/security/ledger/proof.js.map +1 -1
- package/dist/security/ledger/protocol.d.ts +14 -1
- package/dist/security/ledger/protocol.js.map +1 -1
- package/dist/session-controller.d.ts +9 -3
- package/dist/session-controller.js +120 -20
- package/dist/session-controller.js.map +1 -1
- package/dist/session-inbox-protocol.d.ts +2 -0
- package/dist/session-preview.d.ts +20 -5
- package/dist/session-preview.js +185 -37
- package/dist/session-preview.js.map +1 -1
- package/dist/session-store.js +19 -1
- package/dist/session-store.js.map +1 -1
- package/dist/sessions-commands.js +41 -7
- package/dist/sessions-commands.js.map +1 -1
- package/dist/types.d.ts +1 -1
- package/opencode-plugin/ftown.js +15 -2
- package/package.json +2 -2
- package/skills/ftown/SKILL.md +18 -7
- package/skills/ftown/references/sessions.md +35 -10
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { LEDGER_LIMITS } from './protocol.js';
|
|
2
|
+
import { signDomain, verifyDomain } from './proof.js';
|
|
3
|
+
/** Signed bytes: UTF-8 of HEAD_ANNOUNCE_DOMAIN + JSON of { accountId, at, head, keyId, kind, revision } in that key order. */
|
|
4
|
+
export const HEAD_ANNOUNCE_DOMAIN = 'ftown-head-announce-v1\n';
|
|
5
|
+
/** Receivers accept |now - at| <= this. */
|
|
6
|
+
export const HEAD_ANNOUNCE_MAX_AGE_MS = 120_000;
|
|
7
|
+
const KEYS = ['type', 'version', 'accountId', 'kind', 'keyId', 'head', 'revision', 'at', 'signature'];
|
|
8
|
+
/** Canonical unpadded base64url of exactly 32 bytes: 43 chars, the last one carrying no stray bits. */
|
|
9
|
+
const B64URL_32 = /^[A-Za-z0-9_-]{42}[AEIMQUYcgkosw048]$/;
|
|
10
|
+
/** Canonical unpadded base64url of exactly 64 bytes. */
|
|
11
|
+
const B64URL_64 = /^[A-Za-z0-9_-]{85}[AQgw]$/;
|
|
12
|
+
/** A ledger bridgeId (protocol token). */
|
|
13
|
+
const BRIDGE_ID = /^[A-Za-z0-9_-]{1,200}$/;
|
|
14
|
+
function validFields(record) {
|
|
15
|
+
const { accountId, kind, keyId, head, revision, at } = record;
|
|
16
|
+
if (typeof accountId !== 'string' || !B64URL_32.test(accountId))
|
|
17
|
+
return false;
|
|
18
|
+
if (typeof head !== 'string' || !B64URL_32.test(head))
|
|
19
|
+
return false;
|
|
20
|
+
if (kind !== 'device' && kind !== 'bridge')
|
|
21
|
+
return false;
|
|
22
|
+
if (typeof keyId !== 'string' || !(kind === 'device' ? B64URL_32 : BRIDGE_ID).test(keyId))
|
|
23
|
+
return false;
|
|
24
|
+
if (typeof revision !== 'number' || !Number.isSafeInteger(revision) || revision < 0 || revision > LEDGER_LIMITS.maxTransitions)
|
|
25
|
+
return false;
|
|
26
|
+
return typeof at === 'number' && Number.isSafeInteger(at) && at >= 0;
|
|
27
|
+
}
|
|
28
|
+
function signedBytes(fields) {
|
|
29
|
+
const ordered = {
|
|
30
|
+
accountId: fields.accountId,
|
|
31
|
+
at: fields.at,
|
|
32
|
+
head: fields.head,
|
|
33
|
+
keyId: fields.keyId,
|
|
34
|
+
kind: fields.kind,
|
|
35
|
+
revision: fields.revision,
|
|
36
|
+
};
|
|
37
|
+
return new TextEncoder().encode(HEAD_ANNOUNCE_DOMAIN + JSON.stringify(ordered));
|
|
38
|
+
}
|
|
39
|
+
/** Strict shape: exactly the nine keys, literal type/version, canonical base64url ids, safe integers. Anything else → null; never throws. */
|
|
40
|
+
export function parseHeadAnnouncement(value) {
|
|
41
|
+
try {
|
|
42
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value))
|
|
43
|
+
return null;
|
|
44
|
+
const record = value;
|
|
45
|
+
const keys = Object.keys(record);
|
|
46
|
+
if (keys.length !== KEYS.length || !keys.every(key => KEYS.indexOf(key) !== -1))
|
|
47
|
+
return null;
|
|
48
|
+
if (record.type !== 'ledger_head' || record.version !== 1)
|
|
49
|
+
return null;
|
|
50
|
+
if (!validFields(record))
|
|
51
|
+
return null;
|
|
52
|
+
if (typeof record.signature !== 'string' || !B64URL_64.test(record.signature))
|
|
53
|
+
return null;
|
|
54
|
+
const { accountId, kind, keyId, head, revision, at, signature } = record;
|
|
55
|
+
return { type: 'ledger_head', version: 1, accountId, kind, keyId, head, revision, at, signature };
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/** Signs with the member's ECDSA P-256 auth private key. Extra properties of `fields` are ignored; invalid fields throw invalid_head_announcement. */
|
|
62
|
+
export async function signHeadAnnouncement(key, fields) {
|
|
63
|
+
const { accountId, kind, keyId, head, revision, at } = fields;
|
|
64
|
+
const copy = { accountId, kind, keyId, head, revision, at };
|
|
65
|
+
if (!validFields(copy))
|
|
66
|
+
throw new Error('invalid_head_announcement');
|
|
67
|
+
const signature = await signDomain(key, signedBytes(copy));
|
|
68
|
+
return { type: 'ledger_head', version: 1, accountId, kind, keyId, head, revision, at, signature };
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* True only when `announcement` is well formed, of `checkpoint`'s account, from a member of `checkpoint` (by kind + keyId) that is not
|
|
72
|
+
* revoked there, within HEAD_ANNOUNCE_MAX_AGE_MS of `now` either way, and signed by that member's authPublicKey. `checkpoint` must be the
|
|
73
|
+
* receiver's TRUSTED view of its own chain. Cheap checks run first: at most one signature verification per call. Never throws.
|
|
74
|
+
*/
|
|
75
|
+
export async function verifyHeadAnnouncement(checkpoint, announcement, now) {
|
|
76
|
+
try {
|
|
77
|
+
const parsed = parseHeadAnnouncement(announcement);
|
|
78
|
+
if (!parsed || parsed.accountId !== checkpoint.accountId)
|
|
79
|
+
return false;
|
|
80
|
+
if (typeof now !== 'number' || !Number.isFinite(now) || Math.abs(now - parsed.at) > HEAD_ANNOUNCE_MAX_AGE_MS)
|
|
81
|
+
return false;
|
|
82
|
+
let authPublicKey;
|
|
83
|
+
if (parsed.kind === 'device') {
|
|
84
|
+
if (checkpoint.revokedDeviceIds.indexOf(parsed.keyId) !== -1)
|
|
85
|
+
return false;
|
|
86
|
+
authPublicKey = checkpoint.devices.filter(entry => entry.endpoint.id === parsed.keyId)[0]?.authPublicKey;
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
if (checkpoint.revokedBridgeIds.indexOf(parsed.keyId) !== -1)
|
|
90
|
+
return false;
|
|
91
|
+
authPublicKey = checkpoint.bridges.filter(entry => entry.bridgeId === parsed.keyId)[0]?.authPublicKey;
|
|
92
|
+
}
|
|
93
|
+
if (!authPublicKey)
|
|
94
|
+
return false;
|
|
95
|
+
return await verifyDomain(authPublicKey, parsed.signature, signedBytes(parsed));
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
//# sourceMappingURL=announce.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"announce.js","sourceRoot":"","sources":["../../../src/security/ledger/announce.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,16 +1,63 @@
|
|
|
1
1
|
import type { Ledger, LedgerAuthority, LedgerCheckpoint, LedgerGenesis, LedgerOperation, LedgerTransition, Signed } from './protocol.js';
|
|
2
|
-
export type LedgerErrorCode = 'invalid_ledger' | 'ledger_other_account' | 'ledger_revoked' | 'ledger_limit';
|
|
3
|
-
/**
|
|
2
|
+
export type LedgerErrorCode = 'invalid_ledger' | 'ledger_other_account' | 'ledger_revoked' | 'ledger_limit' | 'resolution_incomplete';
|
|
3
|
+
/** How a candidate chain relates to the local one, from record heads alone (see relate). */
|
|
4
|
+
export type LedgerRelation = 'same' | 'ahead' | 'behind' | 'diverged';
|
|
5
|
+
/** relateHead: what a bare `{ revision, head }` of another replica says about it. 'unknown' (ahead, diverged or another account) needs its chain. */
|
|
6
|
+
export type LedgerHeadRelation = 'same' | 'behind' | 'unknown';
|
|
7
|
+
/** The last record two chains share. Revision 0 is the genesis, whose head is the accountId. */
|
|
8
|
+
export interface LedgerForkPoint {
|
|
9
|
+
revision: number;
|
|
10
|
+
head: string;
|
|
11
|
+
}
|
|
12
|
+
/** What an adopted resolve_fork discarded from the local chain: its records after the fork point, in chain order. `deviceIds`/`bridgeIds` list
|
|
13
|
+
* revocations among them that the adopted chain lacks; a resolution is only adopted when it carries every one of them, so both are always
|
|
14
|
+
* empty (kept so callers can go on merging them). Everything is empty unless a diverged chain was adopted. */
|
|
15
|
+
export interface LedgerDiscarded {
|
|
16
|
+
records: Signed<LedgerTransition>[];
|
|
17
|
+
deviceIds: string[];
|
|
18
|
+
bridgeIds: string[];
|
|
19
|
+
}
|
|
20
|
+
/** chooseLedger's verdict. `ledger` is the chain to STORE afterwards (never the candidate as passed in): local on 'keep'/'reject'; on 'adopt'
|
|
21
|
+
* local's own prefix records byte-for-byte followed by the candidate's verified records after the fork point; the parsed candidate on a first
|
|
22
|
+
* bind. `checkpoint` is the RAW checkpoint of that chain. `relation` is null only when there is none: no local chain (first bind), or a
|
|
23
|
+
* candidate that is malformed or of another account. 'keep' + 'diverged' = a VALID different history (alert the owner); 'reject' = an invalid
|
|
24
|
+
* candidate (ignore it). `seen`: see LedgerSeen. */
|
|
4
25
|
export interface LedgerChoice {
|
|
5
26
|
decision: 'adopt' | 'keep' | 'reject';
|
|
27
|
+
relation: LedgerRelation | null;
|
|
6
28
|
checkpoint: LedgerCheckpoint;
|
|
7
|
-
discarded:
|
|
29
|
+
discarded: LedgerDiscarded;
|
|
30
|
+
ledger: Ledger;
|
|
31
|
+
seen: LedgerSeen;
|
|
32
|
+
}
|
|
33
|
+
/** Removals found on a VALID diverged chain that chooseLedger kept out ('keep' + 'diverged'): the device endpoint ids / bridgeIds revoked by
|
|
34
|
+
* the candidate's records after the fork point whose signer was already an authority at the fork point, de-duplicated, in chain order.
|
|
35
|
+
* Empty for every other verdict. Callers add them to their STICKY set (local enforcement only); they never enter a chain. A bridge that
|
|
36
|
+
* finds its own bridgeId here must not treat it as its own removal (status only). No attack surface: chooseLedger just verified those
|
|
37
|
+
* records; authorities are add-only, so whoever signed such a removal could equally append it to our own chain; and a resolution must
|
|
38
|
+
* carry every removal of the branch it discards, so enforcing them early agrees with every state the two chains can converge to. Records
|
|
39
|
+
* signed by an authority added only on the candidate's branch contribute nothing. */
|
|
40
|
+
export interface LedgerSeen {
|
|
41
|
+
deviceIds: string[];
|
|
42
|
+
bridgeIds: string[];
|
|
43
|
+
}
|
|
44
|
+
/** resolutionPlan: the fork point, and the operations to append to the chosen chain, in order, before its resolve_fork. */
|
|
45
|
+
export interface LedgerResolutionPlan {
|
|
46
|
+
fork: LedgerForkPoint;
|
|
47
|
+
carry: LedgerOperation[];
|
|
8
48
|
}
|
|
9
49
|
/** A checkpoint that verifyLedger or chooseLedger returned for the local chain whose last head is `head`. */
|
|
10
50
|
export interface LedgerCache {
|
|
11
51
|
head: string;
|
|
12
52
|
checkpoint: LedgerCheckpoint;
|
|
13
53
|
}
|
|
54
|
+
/** describeFork: the last shared record and each side's payloads after it, in chain order (payloads only, no signatures). */
|
|
55
|
+
export interface LedgerFork {
|
|
56
|
+
relation: LedgerRelation;
|
|
57
|
+
fork: LedgerForkPoint;
|
|
58
|
+
mine: LedgerTransition[];
|
|
59
|
+
theirs: LedgerTransition[];
|
|
60
|
+
}
|
|
14
61
|
/** No untrusted text, key material or underlying crypto error enters this error. */
|
|
15
62
|
export declare class LedgerError extends Error {
|
|
16
63
|
readonly code: LedgerErrorCode;
|
|
@@ -28,8 +75,8 @@ export declare function signInChallenge(nonce: string): Promise<Uint8Array<Array
|
|
|
28
75
|
export declare function authorityConsentChallenge(accountId: string, authority: Pick<LedgerAuthority, 'credentialId' | 'publicKey'>): Promise<Uint8Array<ArrayBuffer>>;
|
|
29
76
|
/** The issuedAt of the last transition, or the genesis createdAt: the `lastIssuedAt` argument of nextTransition. */
|
|
30
77
|
export declare function ledgerLastIssuedAt(ledger: Ledger): number;
|
|
31
|
-
/** Every device endpoint id and bridgeId revoked anywhere in the chain, in chain order. Only for chains that verified
|
|
32
|
-
*
|
|
78
|
+
/** Every device endpoint id and bridgeId revoked anywhere in the chain, in chain order. Only for chains that verified: replicas keep the
|
|
79
|
+
* union over the chains they held (plus chooseLedger's `discarded` ids) and never trust those ids again. Throws on a malformed shape. */
|
|
33
80
|
export declare function ledgerRevocations(ledger: Ledger): {
|
|
34
81
|
deviceIds: string[];
|
|
35
82
|
bridgeIds: string[];
|
|
@@ -46,16 +93,62 @@ export declare function parseLedger(value: unknown): Ledger;
|
|
|
46
93
|
/** Verifies ONE chain from genesis and returns its last checkpoint. Every record's passkey signature is checked against an authority of the
|
|
47
94
|
* PREVIOUS state (genesis: its own authority) with genesis.rpId/origins. Throws LedgerError: invalid_ledger, or a more specific code. */
|
|
48
95
|
export declare function verifyLedger(ledger: Ledger, now?: number): Promise<LedgerCheckpoint>;
|
|
49
|
-
/**
|
|
50
|
-
*
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
96
|
+
/** The head of every record, index = revision: [accountId, head of revision 1, ..., head of the tip]. O(n) SHA-256 over the parsed chain and no
|
|
97
|
+
* signature work; compute it once per chain and keep it for relate/forkPoint/relateHead. Throws like parseLedger. */
|
|
98
|
+
export declare function ledgerHeads(ledger: Ledger): Promise<string[]>;
|
|
99
|
+
/** How `candidate` relates to `local` (both ledgerHeads lists of chains of ONE account): 'same' (identical heads), 'ahead' (candidate strictly
|
|
100
|
+
* extends local), 'behind' (local strictly extends candidate), 'diverged' (they differ at some revision both have). Pure, O(min(n, m)) string
|
|
101
|
+
* compares, no hashing. Another account is not a relation: throws ledger_other_account (genesis heads differ); a malformed list throws
|
|
102
|
+
* invalid_ledger. Every head of the shorter list is compared, so a list whose tip matches but whose middle differs is 'diverged'. */
|
|
103
|
+
export declare function relate(local: readonly string[], candidate: readonly string[]): LedgerRelation;
|
|
104
|
+
/** The last record `local` and `candidate` (ledgerHeads lists) share, or null when even the genesis differs. Same cost as relate. */
|
|
105
|
+
export declare function forkPoint(local: readonly string[], candidate: readonly string[]): LedgerForkPoint | null;
|
|
106
|
+
/** What another replica's bare `{ revision, head }` (an RPC answer or head announcement; other fields ignored) says relative to `local`
|
|
107
|
+
* (a ledgerHeads list): 'same', 'behind' (local holds that head at that revision and more), or 'unknown' (ahead, diverged or another
|
|
108
|
+
* account: fetch its chain and run chooseLedger). O(1). Throws invalid_ledger on a malformed head or revision. */
|
|
109
|
+
export declare function relateHead(local: readonly string[], other: {
|
|
110
|
+
revision: number;
|
|
111
|
+
head: string;
|
|
112
|
+
}): LedgerHeadRelation;
|
|
113
|
+
/**
|
|
114
|
+
* The append-only acceptance rule, the same on every replica (server, browser, bridge). The candidate's relation decides:
|
|
115
|
+
* - no local chain (first bind): verified from genesis and adopted, relation null; an invalid candidate throws.
|
|
116
|
+
* - malformed or another account: 'reject', relation null.
|
|
117
|
+
* - 'same' / 'behind': 'keep', decided from record heads alone (no candidate signature is checked).
|
|
118
|
+
* - 'ahead': only the records after local's head are verified, on top of local's state -> 'adopt', or 'reject' if any is invalid.
|
|
119
|
+
* - 'diverged': the candidate's records after the fork point are verified on top of the shared prefix -> 'reject' if invalid. It is
|
|
120
|
+
* 'adopt'ed only under the owner-signed resolve_fork rule of resolves(); `discarded.records` then lists local's records after the fork.
|
|
121
|
+
* Otherwise 'keep': a valid different history is never adopted, whatever its length or content; `seen` lists its removals to enforce locally.
|
|
122
|
+
* The candidate's copy of the shared prefix is never verified and never stored: its heads equal local's, so its payloads and state do, and
|
|
123
|
+
* only the candidate's records after the fork point (after local's tip when 'ahead') are checked. Store `choice.ledger`, which keeps local's
|
|
124
|
+
* prefix records byte-for-byte; re-encoded prefix signatures therefore cost nothing and are dropped. `cache` is the checkpoint of the
|
|
125
|
+
* already-verified local chain: when it vouches for `local` (same last head, hash-linked) local is not re-verified, else local is verified
|
|
126
|
+
* from genesis. Throws only when `local` is invalid (and not vouched for), or when `local` is null and the candidate is invalid.
|
|
127
|
+
*/
|
|
56
128
|
export declare function chooseLedger(local: Ledger | null, candidate: Ledger, now?: number, cache?: LedgerCache): Promise<LedgerChoice>;
|
|
129
|
+
/** describeFork(local, other): where the two chains part and each side's payloads after that point, for showing two histories side by side.
|
|
130
|
+
* Works for any relation of one account (for 'same' both lists are empty). Throws ledger_other_account for another account, else like parseLedger.
|
|
131
|
+
* Neither chain is verified: pass only a local chain and one that chooseLedger classified as 'keep' + 'diverged'. */
|
|
132
|
+
export declare function describeFork(local: Ledger, other: Ledger): Promise<LedgerFork>;
|
|
133
|
+
/** What keeping `chosen` over `other` requires first: their fork point, and `carry`, the operations to append to `chosen` (each signed as an
|
|
134
|
+
* ordinary record, in this order) before resolveForkTransition. In `other`'s chain order: one revoke_device / revoke_bridge per id revoked on
|
|
135
|
+
* other's branch after the fork and not revoked in `chosen` (an id that is not a member of `chosen` is tombstoned, so it can never be added
|
|
136
|
+
* there), and one add_authority, with the ORIGINAL authority and consent (a consent binds only accountId, credentialId and key), per authority
|
|
137
|
+
* added on other's branch and absent from `chosen` with the same credentialId and key. Building a carried record can still throw (for example
|
|
138
|
+
* ledger_limit, or invalid_ledger when `chosen` holds that credentialId or key under another authority): then `chosen` cannot be kept.
|
|
139
|
+
* Throws ledger_other_account for another account and invalid_ledger unless the chains diverge. Neither chain is verified: pass only a
|
|
140
|
+
* verified local chain and one that chooseLedger classified as 'keep' + 'diverged'. */
|
|
141
|
+
export declare function resolutionPlan(chosen: Ledger, other: Ledger): Promise<LedgerResolutionPlan>;
|
|
142
|
+
/** The unsigned resolve_fork that keeps `chosen` and discards `other` from their fork point: appended to `chosen`, forkHead = the last shared
|
|
143
|
+
* record, discardedFirst = other's first record after it, discardedHead/discardedRevision = other's tip. Verifies `chosen` (skipped when
|
|
144
|
+
* `cache` vouches for it, as in chooseLedger) and throws invalid_ledger when the chains do not diverge or `credentialId` was not an authority
|
|
145
|
+
* at the fork point (one added on chosen's own branch cannot discard the other); resolution_incomplete while resolutionPlan(chosen, other)
|
|
146
|
+
* still has operations to carry; ledger_other_account for another account; else nextTransition's errors. `other` is not verified: pass only
|
|
147
|
+
* a chain that chooseLedger classified as 'keep' + 'diverged'. */
|
|
148
|
+
export declare function resolveForkTransition(chosen: Ledger, other: Ledger, credentialId: string, now?: number, cache?: LedgerCache): Promise<LedgerTransition>;
|
|
57
149
|
/** Pure next-payload builder for the signer. Applies the operation to a copy of the checkpoint with the verifier's rules and throws the
|
|
58
|
-
* same errors. Not checked here (they need async crypto): endpoint id = SHA-256(point), points on the curve,
|
|
59
|
-
* consent signature (only its shape is checked)
|
|
150
|
+
* same errors. Not checked here (they need async crypto or the chain): endpoint id = SHA-256(point), points on the curve, the add_authority
|
|
151
|
+
* consent signature (only its shape is checked) and resolve_fork's forkHead and signer rules (build it with resolveForkTransition);
|
|
152
|
+
* verifyLedger enforces all of them.
|
|
60
153
|
* `lastIssuedAt` is the issuedAt of the ledger's last record (see ledgerLastIssuedAt); the result's issuedAt is max(now, lastIssuedAt). */
|
|
61
154
|
export declare function nextTransition(checkpoint: LedgerCheckpoint, credentialId: string, operation: LedgerOperation, now?: number, lastIssuedAt?: number): LedgerTransition;
|