@calimero-network/mero-js 26.11.0 → 27.0.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 +73 -288
- package/dist/cloud-v2/attestation.d.ts +91 -0
- package/dist/cloud-v2/attestation.js +135 -0
- package/dist/cloud-v2/capabilities.d.ts +30 -0
- package/dist/cloud-v2/capabilities.js +31 -0
- package/dist/cloud-v2/client.d.ts +426 -0
- package/dist/cloud-v2/client.js +646 -0
- package/dist/cloud-v2/errors.d.ts +98 -0
- package/dist/cloud-v2/errors.js +165 -0
- package/dist/cloud-v2/index.d.ts +10 -0
- package/dist/cloud-v2/index.js +5 -0
- package/dist/cloud-v2/passkey.d.ts +27 -0
- package/dist/cloud-v2/passkey.js +73 -0
- package/dist/crypto/internal.d.ts +3 -37
- package/dist/crypto/internal.js +3 -72
- package/dist/index.browser.mjs +4 -2
- package/dist/index.browser.mjs.map +4 -4
- package/dist/index.cjs +1750 -12772
- package/dist/index.cjs.map +4 -4
- package/dist/index.d.ts +3 -56
- package/dist/index.js +14 -75
- package/dist/index.mjs +1750 -12772
- package/dist/index.mjs.map +4 -4
- package/dist/sealed/index.d.ts +4 -4
- package/dist/sealed/index.js +2 -2
- package/dist/sealed/release.d.ts +21 -76
- package/dist/sealed/release.js +24 -108
- package/dist/sealed/sealed.d.ts +8 -56
- package/dist/sealed/sealed.js +11 -89
- package/dist/sealed/verify.d.ts +7 -13
- package/dist/sealed/verify.js +3 -7
- package/package.json +8 -13
- package/dist/account/account-admin.d.ts +0 -80
- package/dist/account/account-admin.js +0 -346
- package/dist/account/account.d.ts +0 -196
- package/dist/account/account.js +0 -269
- package/dist/account/application-id.d.ts +0 -50
- package/dist/account/application-id.js +0 -83
- package/dist/account/bootstrap-from-invitation.d.ts +0 -110
- package/dist/account/bootstrap-from-invitation.js +0 -80
- package/dist/account/create-context.d.ts +0 -103
- package/dist/account/create-context.js +0 -267
- package/dist/account/govern.d.ts +0 -24
- package/dist/account/govern.js +0 -50
- package/dist/account/index.d.ts +0 -26
- package/dist/account/index.js +0 -15
- package/dist/account/join-as-account.d.ts +0 -23
- package/dist/account/join-as-account.js +0 -34
- package/dist/account/join-with-node.d.ts +0 -115
- package/dist/account/join-with-node.js +0 -161
- package/dist/account/node-admin.d.ts +0 -20
- package/dist/account/node-admin.js +0 -33
- package/dist/account/relay-from-invitation.d.ts +0 -137
- package/dist/account/relay-from-invitation.js +0 -180
- package/dist/account/relay-login-error.d.ts +0 -32
- package/dist/account/relay-login-error.js +0 -34
- package/dist/account/session.d.ts +0 -266
- package/dist/account/session.js +0 -915
- package/dist/account/wordlist.d.ts +0 -16
- package/dist/account/wordlist.js +0 -273
- package/dist/admin-api/admin-client.d.ts +0 -688
- package/dist/admin-api/admin-client.js +0 -1230
- package/dist/admin-api/admin-factory.d.ts +0 -14
- package/dist/admin-api/admin-factory.js +0 -22
- package/dist/admin-api/admin-types.d.ts +0 -2046
- package/dist/admin-api/admin-types.js +0 -12
- package/dist/admin-api/index.d.ts +0 -4
- package/dist/admin-api/index.js +0 -5
- package/dist/admin-api/redeem-invitation.d.ts +0 -101
- package/dist/admin-api/redeem-invitation.js +0 -200
- package/dist/auth/index.d.ts +0 -34
- package/dist/auth/index.js +0 -67
- package/dist/auth-api/auth-client.d.ts +0 -31
- package/dist/auth-api/auth-client.js +0 -117
- package/dist/auth-api/auth-factory.d.ts +0 -7
- package/dist/auth-api/auth-factory.js +0 -41
- package/dist/auth-api/auth-types.d.ts +0 -130
- package/dist/auth-api/auth-types.js +0 -2
- package/dist/auth-api/index.d.ts +0 -3
- package/dist/auth-api/index.js +0 -4
- package/dist/capabilities.d.ts +0 -84
- package/dist/capabilities.js +0 -117
- package/dist/cloud/account-ownership.d.ts +0 -63
- package/dist/cloud/account-ownership.js +0 -87
- package/dist/cloud/cloud-client.d.ts +0 -949
- package/dist/cloud/cloud-client.js +0 -1068
- package/dist/cloud/connect.d.ts +0 -213
- package/dist/cloud/connect.js +0 -260
- package/dist/cloud/enrol-redirect.d.ts +0 -181
- package/dist/cloud/enrol-redirect.js +0 -244
- package/dist/cloud/index.d.ts +0 -12
- package/dist/cloud/index.js +0 -10
- package/dist/cloud/link-redirect.d.ts +0 -95
- package/dist/cloud/link-redirect.js +0 -121
- package/dist/cloud/link-state.d.ts +0 -36
- package/dist/cloud/link-state.js +0 -104
- package/dist/cloud/routing-proof.d.ts +0 -113
- package/dist/cloud/routing-proof.js +0 -96
- package/dist/device-cert/device-cert.d.ts +0 -189
- package/dist/device-cert/device-cert.js +0 -239
- package/dist/device-cert/device-scope.d.ts +0 -58
- package/dist/device-cert/device-scope.js +0 -76
- package/dist/device-cert/index.d.ts +0 -4
- package/dist/device-cert/index.js +0 -3
- package/dist/ephemeral/codec.d.ts +0 -4
- package/dist/ephemeral/codec.js +0 -12
- package/dist/ephemeral/index.d.ts +0 -57
- package/dist/ephemeral/index.js +0 -101
- package/dist/ephemeral/types.d.ts +0 -27
- package/dist/ephemeral/types.js +0 -1
- package/dist/errors/classify.d.ts +0 -65
- package/dist/errors/classify.js +0 -117
- package/dist/errors/index.d.ts +0 -2
- package/dist/errors/index.js +0 -1
- package/dist/events/group.d.ts +0 -95
- package/dist/events/group.js +0 -10
- package/dist/events/index.d.ts +0 -5
- package/dist/events/index.js +0 -2
- package/dist/events/sse.d.ts +0 -127
- package/dist/events/sse.js +0 -469
- package/dist/events/ws.d.ts +0 -57
- package/dist/events/ws.js +0 -212
- package/dist/http-client/api-response.d.ts +0 -15
- package/dist/http-client/api-response.js +0 -1
- package/dist/http-client/http-factory.d.ts +0 -71
- package/dist/http-client/http-factory.js +0 -68
- package/dist/http-client/http-types.d.ts +0 -87
- package/dist/http-client/http-types.js +0 -1
- package/dist/http-client/index.d.ts +0 -7
- package/dist/http-client/index.js +0 -11
- package/dist/http-client/path-segment.d.ts +0 -1
- package/dist/http-client/path-segment.js +0 -8
- package/dist/http-client/retry.d.ts +0 -5
- package/dist/http-client/retry.js +0 -82
- package/dist/http-client/signal-utils.d.ts +0 -2
- package/dist/http-client/signal-utils.js +0 -41
- package/dist/http-client/web-client.d.ts +0 -80
- package/dist/http-client/web-client.js +0 -557
- package/dist/invitation/index.d.ts +0 -2
- package/dist/invitation/index.js +0 -2
- package/dist/invitation/invitation.d.ts +0 -106
- package/dist/invitation/invitation.js +0 -104
- package/dist/login/index.d.ts +0 -4
- package/dist/login/index.js +0 -3
- package/dist/login/login.d.ts +0 -111
- package/dist/login/login.js +0 -110
- package/dist/login/session.d.ts +0 -124
- package/dist/login/session.js +0 -141
- package/dist/mero-js.d.ts +0 -231
- package/dist/mero-js.js +0 -469
- package/dist/namespace-op/index.d.ts +0 -2
- package/dist/namespace-op/index.js +0 -3
- package/dist/namespace-op/namespace-op.d.ts +0 -150
- package/dist/namespace-op/namespace-op.js +0 -191
- package/dist/nodeDiscovery.d.ts +0 -47
- package/dist/nodeDiscovery.js +0 -101
- package/dist/owner-op/index.d.ts +0 -2
- package/dist/owner-op/index.js +0 -3
- package/dist/owner-op/owner-op.d.ts +0 -119
- package/dist/owner-op/owner-op.js +0 -214
- package/dist/presence/relay-presence.d.ts +0 -56
- package/dist/presence/relay-presence.js +0 -107
- package/dist/presence/statement.d.ts +0 -12
- package/dist/presence/statement.js +0 -17
- package/dist/relay/index.d.ts +0 -4
- package/dist/relay/index.js +0 -2
- package/dist/relay/nonce-source.d.ts +0 -72
- package/dist/relay/nonce-source.js +0 -111
- package/dist/relay/relay-client.d.ts +0 -472
- package/dist/relay/relay-client.js +0 -518
- package/dist/relay-attestation/attest-node-key.d.ts +0 -66
- package/dist/relay-attestation/attest-node-key.js +0 -110
- package/dist/relay-attestation/index.d.ts +0 -1
- package/dist/relay-attestation/index.js +0 -1
- package/dist/request/caller-proof.d.ts +0 -43
- package/dist/request/caller-proof.js +0 -36
- package/dist/request/index.d.ts +0 -3
- package/dist/request/index.js +0 -3
- package/dist/request/proof-signer.d.ts +0 -49
- package/dist/request/proof-signer.js +0 -59
- package/dist/request/request-sig.d.ts +0 -99
- package/dist/request/request-sig.js +0 -95
- package/dist/request-proof/authorizer.d.ts +0 -41
- package/dist/request-proof/authorizer.js +0 -60
- package/dist/request-proof/index.d.ts +0 -4
- package/dist/request-proof/index.js +0 -2
- package/dist/request-proof/request-proof.d.ts +0 -53
- package/dist/request-proof/request-proof.js +0 -95
- package/dist/rpc/index.d.ts +0 -64
- package/dist/rpc/index.js +0 -78
- package/dist/signer/index.d.ts +0 -2
- package/dist/signer/index.js +0 -3
- package/dist/signer/signer.d.ts +0 -92
- package/dist/signer/signer.js +0 -147
- package/dist/token-store/index.d.ts +0 -27
- package/dist/token-store/index.js +0 -62
- package/dist/transport/index.d.ts +0 -6
- package/dist/transport/index.js +0 -3
- package/dist/transport/mero-client.d.ts +0 -294
- package/dist/transport/mero-client.js +0 -386
- package/dist/transport/relay-observer.d.ts +0 -152
- package/dist/transport/relay-observer.js +0 -173
- package/dist/transport/relay-transport.d.ts +0 -73
- package/dist/transport/relay-transport.js +0 -94
- package/dist/transport/types.d.ts +0 -100
- package/dist/transport/types.js +0 -32
- package/dist/types/api-response.d.ts +0 -15
- package/dist/types/api-response.js +0 -1
- package/dist/types/index.d.ts +0 -1
- package/dist/types/index.js +0 -1
- package/dist/warrant/creation-warrant.d.ts +0 -163
- package/dist/warrant/creation-warrant.js +0 -194
- package/dist/warrant/governance-op.d.ts +0 -202
- package/dist/warrant/governance-op.js +0 -355
- package/dist/warrant/governance-warrant.d.ts +0 -124
- package/dist/warrant/governance-warrant.js +0 -164
- package/dist/warrant/index.d.ts +0 -8
- package/dist/warrant/index.js +0 -5
- package/dist/warrant/warrant.d.ts +0 -188
- package/dist/warrant/warrant.js +0 -202
|
@@ -1,214 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Root proofs for owner-level governance ops (core schema 18).
|
|
3
|
-
*
|
|
4
|
-
* A device key speaks for its account everywhere in governance, so a stolen
|
|
5
|
-
* device used to be enough to take a group from its owner: promote an
|
|
6
|
-
* accomplice, transfer ownership, remove the owner. Core now refuses the ops
|
|
7
|
-
* that can do that unless they carry an authorisation signed by the account's
|
|
8
|
-
* **root** key, which a device does not hold:
|
|
9
|
-
*
|
|
10
|
-
* - `GroupOp::TransferOwnership`, `GroupOp::GroupDelete` and
|
|
11
|
-
* `RootOp::AdminChanged`, which are owner-only;
|
|
12
|
-
* - the TEE admission, release-admission and authoring policies, which stay
|
|
13
|
-
* admin-level but need the signing admin's own root.
|
|
14
|
-
*
|
|
15
|
-
* A node that holds the account root (as `merod init` provisions) signs the
|
|
16
|
-
* proof itself, so a caller on such a node passes nothing. This module is for
|
|
17
|
-
* the other case: a nodeless account, or a root kept cold, signing offline and
|
|
18
|
-
* passing the hex proof as `rootProof`.
|
|
19
|
-
*
|
|
20
|
-
* What the root signs binds the account, the namespace, the group, the kind of
|
|
21
|
-
* op, a digest of the op's own borsh bytes, the root epoch, and the group's
|
|
22
|
-
* guarded-op counter. Read that counter (`ownerOpCounter`, with `namespaceId`)
|
|
23
|
-
* from `getGroupInfo` first: the node applies a proof only while the group is
|
|
24
|
-
* still at that count, and advances it once the op applies, so each proof is
|
|
25
|
-
* single-use. For `AdminChanged` and the TEE policies the group is the
|
|
26
|
-
* namespace root.
|
|
27
|
-
*
|
|
28
|
-
* Byte layouts follow core's `calimero_account::OwnerOpAuthorization` and
|
|
29
|
-
* `calimero_governance_types::{GroupOp, RootOp}`. They are pinned against core's
|
|
30
|
-
* own vectors (`crates/account/src/tests/owner_op.rs`,
|
|
31
|
-
* `root_guarded_vectors_are_stable` in `crates/governance-types/src/tests.rs`)
|
|
32
|
-
* in `owner-op.test.ts`.
|
|
33
|
-
*/
|
|
34
|
-
import { concat, derivePublicKey, domainHash, fromHex, fromHexUnsized, hex, importSigningKey, u32le, u64le, } from '../crypto/internal.js';
|
|
35
|
-
/** core's `OWNER_OP_SIGN_DOMAIN`: what the root signs under. */
|
|
36
|
-
const OWNER_OP_SIGN_DOMAIN = new TextEncoder().encode('calimero.account.owner-op.v1');
|
|
37
|
-
/** core's `OWNER_OP_BODY_DOMAIN`: what the op's bytes are hashed under. */
|
|
38
|
-
const OWNER_OP_BODY_DOMAIN = new TextEncoder().encode('calimero.account.owner-op.body.v1');
|
|
39
|
-
/** core's `ACCOUNT_ID_DOMAIN`. */
|
|
40
|
-
const ACCOUNT_ID_DOMAIN = new TextEncoder().encode('calimero.account.genesis.v1');
|
|
41
|
-
/** core's `ACCOUNT_GENESIS_VERSION`. */
|
|
42
|
-
const ACCOUNT_GENESIS_VERSION = 2;
|
|
43
|
-
/**
|
|
44
|
-
* Which owner-level op a proof authorises. The value is the byte core signs,
|
|
45
|
-
* its `OwnerOpKind` discriminant.
|
|
46
|
-
*/
|
|
47
|
-
export const OWNER_OP_KIND = {
|
|
48
|
-
transferOwnership: 0,
|
|
49
|
-
adminChanged: 1,
|
|
50
|
-
groupDelete: 2,
|
|
51
|
-
teeAdmissionPolicy: 3,
|
|
52
|
-
teeAuthoringPolicy: 4,
|
|
53
|
-
teeReleaseAdmissionPolicy: 5,
|
|
54
|
-
};
|
|
55
|
-
/** `GroupOp` discriminants, by position in core's enum. */
|
|
56
|
-
const GROUP_OP = {
|
|
57
|
-
GroupDelete: 14,
|
|
58
|
-
TransferOwnership: 21,
|
|
59
|
-
TeeAuthoringPolicySet: 33,
|
|
60
|
-
TeeAdmissionPolicySetV2: 37,
|
|
61
|
-
TeeReleaseAdmissionPolicySetV2: 38,
|
|
62
|
-
RootGuarded: 42,
|
|
63
|
-
};
|
|
64
|
-
/** `RootOp` discriminants, by position in core's enum. */
|
|
65
|
-
const ROOT_OP = {
|
|
66
|
-
AdminChanged: 3,
|
|
67
|
-
RootGuarded: 12,
|
|
68
|
-
};
|
|
69
|
-
/** core's `TeeAdmissionMode` discriminants. */
|
|
70
|
-
const TEE_MODE = { replica: 0, relay: 1 };
|
|
71
|
-
const te = new TextEncoder();
|
|
72
|
-
function tag(byte) {
|
|
73
|
-
return new Uint8Array([byte]);
|
|
74
|
-
}
|
|
75
|
-
function borshString(value) {
|
|
76
|
-
const bytes = te.encode(value);
|
|
77
|
-
return concat(u32le(bytes.length), bytes);
|
|
78
|
-
}
|
|
79
|
-
function borshStrings(values) {
|
|
80
|
-
return concat(u32le(values.length), ...values.map(borshString));
|
|
81
|
-
}
|
|
82
|
-
function borshBool(value) {
|
|
83
|
-
return tag(value ? 1 : 0);
|
|
84
|
-
}
|
|
85
|
-
function teeMode(mode) {
|
|
86
|
-
const byte = TEE_MODE[mode ?? 'replica'];
|
|
87
|
-
if (byte === undefined) {
|
|
88
|
-
throw new Error(`mode must be replica or relay, got ${String(mode)}`);
|
|
89
|
-
}
|
|
90
|
-
return tag(byte);
|
|
91
|
-
}
|
|
92
|
-
/** Hand the group to `newOwner` (an account, hex), who must already be an admin. */
|
|
93
|
-
export function transferOwnershipOp(newOwner) {
|
|
94
|
-
return {
|
|
95
|
-
kind: 'transferOwnership',
|
|
96
|
-
plane: 'group',
|
|
97
|
-
bytes: concat(tag(GROUP_OP.TransferOwnership), fromHex(newOwner, 'newOwner', 32)),
|
|
98
|
-
};
|
|
99
|
-
}
|
|
100
|
-
/** The owner-only deletion of a group that holds no contexts. */
|
|
101
|
-
export function groupDeleteOp() {
|
|
102
|
-
return { kind: 'groupDelete', plane: 'group', bytes: tag(GROUP_OP.GroupDelete) };
|
|
103
|
-
}
|
|
104
|
-
/** Repoint the namespace's admin pin at `newAdmin` (an account, hex). A root op. */
|
|
105
|
-
export function adminChangedOp(newAdmin) {
|
|
106
|
-
return {
|
|
107
|
-
kind: 'adminChanged',
|
|
108
|
-
plane: 'root',
|
|
109
|
-
bytes: concat(tag(ROOT_OP.AdminChanged), fromHex(newAdmin, 'newAdmin', 32)),
|
|
110
|
-
};
|
|
111
|
-
}
|
|
112
|
-
/** The TEE authoring policy `PUT`/`DELETE …/settings/tee-authoring-policy` publishes. */
|
|
113
|
-
export function teeAuthoringPolicyOp(allowedMrtd) {
|
|
114
|
-
return {
|
|
115
|
-
kind: 'teeAuthoringPolicy',
|
|
116
|
-
plane: 'group',
|
|
117
|
-
bytes: concat(tag(GROUP_OP.TeeAuthoringPolicySet), borshStrings(allowedMrtd)),
|
|
118
|
-
};
|
|
119
|
-
}
|
|
120
|
-
/**
|
|
121
|
-
* The measurement-list admission policy `PUT …/settings/tee-admission-policy`
|
|
122
|
-
* publishes. Pass the fields exactly as you send them: the node signs over the
|
|
123
|
-
* op it builds from them, and any difference is a different op.
|
|
124
|
-
*/
|
|
125
|
-
export function teeAdmissionPolicyOp(input) {
|
|
126
|
-
return {
|
|
127
|
-
kind: 'teeAdmissionPolicy',
|
|
128
|
-
plane: 'group',
|
|
129
|
-
bytes: concat(tag(GROUP_OP.TeeAdmissionPolicySetV2), borshStrings(input.allowedMrtd), borshStrings(input.allowedRtmr0), borshStrings(input.allowedRtmr1), borshStrings(input.allowedRtmr2), borshStrings(input.allowedRtmr3), borshStrings(input.allowedTcbStatuses), borshBool(input.acceptMock), teeMode(input.mode)),
|
|
130
|
-
};
|
|
131
|
-
}
|
|
132
|
-
/** The signed-release admission policy `PUT …/settings/tee-admission-policy` publishes. */
|
|
133
|
-
export function teeReleaseAdmissionPolicyOp(input) {
|
|
134
|
-
const minRelease = input.minReleaseVersion === undefined
|
|
135
|
-
? tag(0)
|
|
136
|
-
: concat(tag(1), borshString(input.minReleaseVersion));
|
|
137
|
-
return {
|
|
138
|
-
kind: 'teeReleaseAdmissionPolicy',
|
|
139
|
-
plane: 'group',
|
|
140
|
-
bytes: concat(tag(GROUP_OP.TeeReleaseAdmissionPolicySetV2), borshStrings(input.allowedProfiles), minRelease, borshStrings(input.allowedTcbStatuses), borshBool(input.acceptMock), teeMode(input.mode)),
|
|
141
|
-
};
|
|
142
|
-
}
|
|
143
|
-
/** core's `OwnerOpAuthorization::op_digest`: the op's bytes, hashed under the body domain. */
|
|
144
|
-
export async function ownerOpDigest(opBytes) {
|
|
145
|
-
return domainHash(OWNER_OP_BODY_DOMAIN, [opBytes]);
|
|
146
|
-
}
|
|
147
|
-
/** core's `OwnerOpTerms::signing_payload`. */
|
|
148
|
-
export async function ownerOpSigningPayload(input) {
|
|
149
|
-
return domainHash(OWNER_OP_SIGN_DOMAIN, [
|
|
150
|
-
fromHex(input.account, 'account', 32),
|
|
151
|
-
fromHex(input.namespaceId, 'namespaceId', 32),
|
|
152
|
-
fromHex(input.groupId, 'groupId', 32),
|
|
153
|
-
tag(kindByte(input.kind)),
|
|
154
|
-
input.opDigest,
|
|
155
|
-
u64le(input.counter),
|
|
156
|
-
u32le(input.keyEpoch),
|
|
157
|
-
]);
|
|
158
|
-
}
|
|
159
|
-
function kindByte(kind) {
|
|
160
|
-
const byte = OWNER_OP_KIND[kind];
|
|
161
|
-
if (byte === undefined) {
|
|
162
|
-
throw new Error(`unknown owner-op kind ${String(kind)}`);
|
|
163
|
-
}
|
|
164
|
-
return byte;
|
|
165
|
-
}
|
|
166
|
-
const HANDOFF_BYTES = 32 + 4 + 32 + 64;
|
|
167
|
-
/**
|
|
168
|
-
* Sign a root proof for `op` and return it as core's `rootProof`: the hex
|
|
169
|
-
* borsh of an `AccountProof<OwnerOpAuthorization>`.
|
|
170
|
-
*
|
|
171
|
-
* Layout: `AccountGenesis` (version u8 + root pk 32), the handoff chain (u32
|
|
172
|
-
* count + 132 bytes each), then `OwnerOpAuthorization` (account 32, namespace
|
|
173
|
-
* 32, group 32, kind u8, op digest 32, counter u64, key epoch u32, signature
|
|
174
|
-
* 64).
|
|
175
|
-
*/
|
|
176
|
-
export async function signOwnerOpProof(input) {
|
|
177
|
-
const rootPublicKey = await derivePublicKey(input.rootSecret);
|
|
178
|
-
const genesis = concat(tag(ACCOUNT_GENESIS_VERSION), rootPublicKey);
|
|
179
|
-
const account = hex(await domainHash(ACCOUNT_ID_DOMAIN, [genesis]));
|
|
180
|
-
const keyEpoch = input.keyEpoch ?? 0;
|
|
181
|
-
const chain = (input.chain ?? []).map((handoff, index) => {
|
|
182
|
-
const bytes = fromHexUnsized(handoff, `chain[${index}]`);
|
|
183
|
-
if (bytes.length !== HANDOFF_BYTES) {
|
|
184
|
-
throw new Error(`chain[${index}] must be a ${HANDOFF_BYTES}-byte RootKeyHandoff, got ${bytes.length}`);
|
|
185
|
-
}
|
|
186
|
-
return bytes;
|
|
187
|
-
});
|
|
188
|
-
if (keyEpoch > chain.length) {
|
|
189
|
-
throw new Error(`keyEpoch ${keyEpoch} needs ${keyEpoch} handoffs in chain, got ${chain.length}`);
|
|
190
|
-
}
|
|
191
|
-
const opDigest = await ownerOpDigest(input.op.bytes);
|
|
192
|
-
const payload = await ownerOpSigningPayload({
|
|
193
|
-
account,
|
|
194
|
-
namespaceId: input.namespaceId,
|
|
195
|
-
groupId: input.groupId,
|
|
196
|
-
kind: input.op.kind,
|
|
197
|
-
opDigest,
|
|
198
|
-
counter: input.counter,
|
|
199
|
-
keyEpoch,
|
|
200
|
-
});
|
|
201
|
-
const key = await importSigningKey(input.rootSecret);
|
|
202
|
-
const signature = new Uint8Array(await crypto.subtle.sign('Ed25519', key, payload));
|
|
203
|
-
return hex(concat(genesis, u32le(chain.length), ...chain, fromHex(account, 'account', 32), fromHex(input.namespaceId, 'namespaceId', 32), fromHex(input.groupId, 'groupId', 32), tag(kindByte(input.op.kind)), opDigest, u64le(input.counter), u32le(keyEpoch), signature));
|
|
204
|
-
}
|
|
205
|
-
/**
|
|
206
|
-
* The `RootGuarded` wrapper core publishes: the op and its proof, as one
|
|
207
|
-
* `GroupOp` or `RootOp`. A client never sends this to a node (the endpoints
|
|
208
|
-
* take the proof and build the wrapper); it is here so the whole encoding is
|
|
209
|
-
* checkable against core's vector.
|
|
210
|
-
*/
|
|
211
|
-
export function rootGuardedOpBytes(op, rootProof) {
|
|
212
|
-
const wrapper = op.plane === 'group' ? GROUP_OP.RootGuarded : ROOT_OP.RootGuarded;
|
|
213
|
-
return concat(tag(wrapper), op.bytes, fromHexUnsized(rootProof, 'rootProof'));
|
|
214
|
-
}
|
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* An account's ephemeral presence, through its relay.
|
|
3
|
-
*
|
|
4
|
-
* A node publishes and heartbeats its own presence; an account has no node, so
|
|
5
|
-
* this client signs each update with the device key, posts it to the relay's
|
|
6
|
-
* `presence-intents`, and resends the last state every 2.5 s until
|
|
7
|
-
* `set(contextId, null)` or `close()`. The relay checks the update and seals
|
|
8
|
-
* and gossips it; other members verify the device's signature and certificate
|
|
9
|
-
* themselves, so the relay cannot invent or alter an account's presence.
|
|
10
|
-
*/
|
|
11
|
-
import type { SseClient } from '../events/index.js';
|
|
12
|
-
import type { Codec, EphemeralEntry } from '../ephemeral/types.js';
|
|
13
|
-
import type { RelayClient } from '../relay/relay-client.js';
|
|
14
|
-
type PresenceRelay = Pick<RelayClient, 'authorProof' | 'authorSigner' | 'presenceIntent'>;
|
|
15
|
-
export declare class RelayPresenceClient {
|
|
16
|
-
private readonly relay;
|
|
17
|
-
private readonly events;
|
|
18
|
-
private readonly heartbeatMs;
|
|
19
|
-
private readonly now;
|
|
20
|
-
private readonly timers;
|
|
21
|
-
private readonly lastSeq;
|
|
22
|
-
private readonly refused;
|
|
23
|
-
/**
|
|
24
|
-
* Bumped by every `set` and `close`, per context. A `set` installs its
|
|
25
|
-
* resends only if nothing newer happened while its first send was in flight,
|
|
26
|
-
* so a retract (or a newer state) can never be overtaken by an older one.
|
|
27
|
-
*/
|
|
28
|
-
private readonly generation;
|
|
29
|
-
/**
|
|
30
|
-
* @param opts.events the relay's event stream, asked for only on `subscribe`:
|
|
31
|
-
* publishing needs none, so a client with no node key can still publish.
|
|
32
|
-
*/
|
|
33
|
-
constructor(opts: {
|
|
34
|
-
relay: PresenceRelay;
|
|
35
|
-
events: () => SseClient;
|
|
36
|
-
heartbeatMs?: number;
|
|
37
|
-
now?: () => number;
|
|
38
|
-
});
|
|
39
|
-
/**
|
|
40
|
-
* Set this account's presence in `contextId`, or retract it with `null`.
|
|
41
|
-
* Throws when the relay refuses this first send for good (a 4xx other than
|
|
42
|
-
* 429), which also stops the resends. A rate limit, a network failure or a
|
|
43
|
-
* 5xx is not thrown: the next resend covers it.
|
|
44
|
-
*/
|
|
45
|
-
set<T>(contextId: string, state: T | null, codec?: Codec<T>): Promise<void>;
|
|
46
|
-
subscribe<T>(contextId: string, handler: (entry: EphemeralEntry<T>) => void, codec?: Codec<T>): () => void;
|
|
47
|
-
/** Stop every resend. Entries expire on the relay and its peers within 7 s. */
|
|
48
|
-
close(): void;
|
|
49
|
-
/** Stop the context's resends and invalidate any `set` still in flight. */
|
|
50
|
-
private bump;
|
|
51
|
-
private stop;
|
|
52
|
-
/** Wall-clock seeded and strictly rising, so a relay restart never sees an old seq. */
|
|
53
|
-
private nextSeq;
|
|
54
|
-
private send;
|
|
55
|
-
}
|
|
56
|
-
export {};
|
|
@@ -1,107 +0,0 @@
|
|
|
1
|
-
import { hex } from '../crypto/internal.js';
|
|
2
|
-
import { jsonCodec, subscribePresence } from '../ephemeral/index.js';
|
|
3
|
-
import { presenceStatementBytes } from './statement.js';
|
|
4
|
-
/** The node's own heartbeat interval, so a relayed entry never outlives the 7 s TTL. */
|
|
5
|
-
const HEARTBEAT_MS = 2500;
|
|
6
|
-
export class RelayPresenceClient {
|
|
7
|
-
/**
|
|
8
|
-
* @param opts.events the relay's event stream, asked for only on `subscribe`:
|
|
9
|
-
* publishing needs none, so a client with no node key can still publish.
|
|
10
|
-
*/
|
|
11
|
-
constructor(opts) {
|
|
12
|
-
this.timers = new Map();
|
|
13
|
-
this.lastSeq = new Map();
|
|
14
|
-
this.refused = new Set();
|
|
15
|
-
/**
|
|
16
|
-
* Bumped by every `set` and `close`, per context. A `set` installs its
|
|
17
|
-
* resends only if nothing newer happened while its first send was in flight,
|
|
18
|
-
* so a retract (or a newer state) can never be overtaken by an older one.
|
|
19
|
-
*/
|
|
20
|
-
this.generation = new Map();
|
|
21
|
-
this.relay = opts.relay;
|
|
22
|
-
this.events = opts.events;
|
|
23
|
-
this.heartbeatMs = opts.heartbeatMs ?? HEARTBEAT_MS;
|
|
24
|
-
this.now = opts.now ?? Date.now;
|
|
25
|
-
}
|
|
26
|
-
/**
|
|
27
|
-
* Set this account's presence in `contextId`, or retract it with `null`.
|
|
28
|
-
* Throws when the relay refuses this first send for good (a 4xx other than
|
|
29
|
-
* 429), which also stops the resends. A rate limit, a network failure or a
|
|
30
|
-
* 5xx is not thrown: the next resend covers it.
|
|
31
|
-
*/
|
|
32
|
-
async set(contextId, state, codec = jsonCodec()) {
|
|
33
|
-
const generation = this.bump(contextId);
|
|
34
|
-
this.refused.delete(contextId);
|
|
35
|
-
const bytes = state === null ? null : new Uint8Array(codec.encode(state));
|
|
36
|
-
await this.send(contextId, bytes);
|
|
37
|
-
if (bytes === null || this.refused.has(contextId))
|
|
38
|
-
return;
|
|
39
|
-
if (this.generation.get(contextId) !== generation)
|
|
40
|
-
return; // a newer set or close won
|
|
41
|
-
this.timers.set(contextId, setInterval(() => void this.send(contextId, bytes).catch(() => undefined), this.heartbeatMs));
|
|
42
|
-
}
|
|
43
|
-
subscribe(contextId, handler, codec = jsonCodec()) {
|
|
44
|
-
return subscribePresence(this.events(), contextId, handler, codec);
|
|
45
|
-
}
|
|
46
|
-
/** Stop every resend. Entries expire on the relay and its peers within 7 s. */
|
|
47
|
-
close() {
|
|
48
|
-
for (const contextId of new Set([...this.timers.keys(), ...this.generation.keys()])) {
|
|
49
|
-
this.bump(contextId);
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
/** Stop the context's resends and invalidate any `set` still in flight. */
|
|
53
|
-
bump(contextId) {
|
|
54
|
-
this.stop(contextId);
|
|
55
|
-
const next = (this.generation.get(contextId) ?? 0) + 1;
|
|
56
|
-
this.generation.set(contextId, next);
|
|
57
|
-
return next;
|
|
58
|
-
}
|
|
59
|
-
stop(contextId) {
|
|
60
|
-
const timer = this.timers.get(contextId);
|
|
61
|
-
if (timer !== undefined)
|
|
62
|
-
clearInterval(timer);
|
|
63
|
-
this.timers.delete(contextId);
|
|
64
|
-
}
|
|
65
|
-
/** Wall-clock seeded and strictly rising, so a relay restart never sees an old seq. */
|
|
66
|
-
nextSeq(contextId) {
|
|
67
|
-
const seq = Math.max(this.now(), (this.lastSeq.get(contextId) ?? 0) + 1);
|
|
68
|
-
this.lastSeq.set(contextId, seq);
|
|
69
|
-
return seq;
|
|
70
|
-
}
|
|
71
|
-
async send(contextId, state) {
|
|
72
|
-
if (this.refused.has(contextId))
|
|
73
|
-
return;
|
|
74
|
-
const signer = await this.relay.authorSigner();
|
|
75
|
-
const seq = this.nextSeq(contextId);
|
|
76
|
-
const sentAtMs = this.now();
|
|
77
|
-
const statement = await presenceStatementBytes({
|
|
78
|
-
contextId,
|
|
79
|
-
author: signer.publicKey,
|
|
80
|
-
seq: BigInt(seq),
|
|
81
|
-
sentAtMs: BigInt(sentAtMs),
|
|
82
|
-
state,
|
|
83
|
-
});
|
|
84
|
-
const signature = hex(await signer.sign(statement));
|
|
85
|
-
try {
|
|
86
|
-
await this.relay.presenceIntent(contextId, {
|
|
87
|
-
state: state === null ? null : hex(state),
|
|
88
|
-
seq,
|
|
89
|
-
sentAtMs,
|
|
90
|
-
signature,
|
|
91
|
-
authorProof: this.relay.authorProof,
|
|
92
|
-
});
|
|
93
|
-
}
|
|
94
|
-
catch (err) {
|
|
95
|
-
const status = err.status;
|
|
96
|
-
// Rate limited, a network failure, or the relay unable right now (5xx,
|
|
97
|
-
// e.g. no current key mid-rotation): the next resend covers it.
|
|
98
|
-
if (status === undefined || status === 0 || status === 429 || status >= 500)
|
|
99
|
-
return;
|
|
100
|
-
// Any other refusal (403: not a member or revoked; 400: malformed or a
|
|
101
|
-
// clock outside the relay's window) cannot be fixed by resending.
|
|
102
|
-
this.refused.add(contextId);
|
|
103
|
-
this.stop(contextId);
|
|
104
|
-
throw err;
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
}
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
/** core's `PRESENCE_DOMAIN`: part of the signed bytes. */
|
|
2
|
-
export declare const PRESENCE_DOMAIN: Uint8Array;
|
|
3
|
-
/** `sha256(borsh(Option<Vec<u8>>))`: `None` is `[0]`, `Some(v)` is `[1] ‖ u32le(len) ‖ v`. */
|
|
4
|
-
export declare function stateHash(state: Uint8Array | null): Promise<Uint8Array>;
|
|
5
|
-
/** What the device key signs, raw: domain ‖ context ‖ author ‖ seq ‖ sentAtMs ‖ stateHash. */
|
|
6
|
-
export declare function presenceStatementBytes(input: {
|
|
7
|
-
contextId: string;
|
|
8
|
-
author: string;
|
|
9
|
-
seq: bigint;
|
|
10
|
-
sentAtMs: bigint;
|
|
11
|
-
state: Uint8Array | null;
|
|
12
|
-
}): Promise<Uint8Array>;
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The bytes a device signs for one presence update: the borsh of core's
|
|
3
|
-
* `PresenceStatement` (`calimero_node_primitives::presence`). Pinned to core's
|
|
4
|
-
* vector in `statement.test.ts`, so the two encoders cannot drift.
|
|
5
|
-
*/
|
|
6
|
-
import { concat, fromHex, u32le, u64le } from '../crypto/internal.js';
|
|
7
|
-
/** core's `PRESENCE_DOMAIN`: part of the signed bytes. */
|
|
8
|
-
export const PRESENCE_DOMAIN = new TextEncoder().encode('calimero/presence/1');
|
|
9
|
-
/** `sha256(borsh(Option<Vec<u8>>))`: `None` is `[0]`, `Some(v)` is `[1] ‖ u32le(len) ‖ v`. */
|
|
10
|
-
export async function stateHash(state) {
|
|
11
|
-
const borsh = state === null ? new Uint8Array([0]) : concat(new Uint8Array([1]), u32le(state.length), state);
|
|
12
|
-
return new Uint8Array(await crypto.subtle.digest('SHA-256', borsh));
|
|
13
|
-
}
|
|
14
|
-
/** What the device key signs, raw: domain ‖ context ‖ author ‖ seq ‖ sentAtMs ‖ stateHash. */
|
|
15
|
-
export async function presenceStatementBytes(input) {
|
|
16
|
-
return concat(PRESENCE_DOMAIN, fromHex(input.contextId, 'contextId', 32), fromHex(input.author, 'author', 32), u64le(input.seq), u64le(input.sentAtMs), await stateHash(input.state));
|
|
17
|
-
}
|
package/dist/relay/index.d.ts
DELETED
|
@@ -1,4 +0,0 @@
|
|
|
1
|
-
export { RelayClient, IntentRefusedError } from './relay-client.js';
|
|
2
|
-
export type { RelayClientConfig, RelayExecutor, RelayDescription, IntentResult, CreationDescription, CreateContextInput, CreatedContext, GovernanceDescription, GovernInput, GovernResult, FoundNamespaceInput, FoundedNamespace, } from './relay-client.js';
|
|
3
|
-
export { createMemoryNonceSource, createLocalStorageNonceSource, } from './nonce-source.js';
|
|
4
|
-
export type { NonceSource } from './nonce-source.js';
|
package/dist/relay/index.js
DELETED
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Where a warrant's nonce comes from.
|
|
3
|
-
*
|
|
4
|
-
* # Why this is a named concept and not a counter in the relay client
|
|
5
|
-
*
|
|
6
|
-
* A warrant's nonce is spent, once, per `(context, author device)` on every
|
|
7
|
-
* node that applies the delta. So the sequence a client emits is not a local
|
|
8
|
-
* detail — it is state the network remembers, and the client is the only party
|
|
9
|
-
* that can keep it.
|
|
10
|
-
*
|
|
11
|
-
* The receiving ledger is a **sliding window** 64 wide, not a high-water mark,
|
|
12
|
-
* which is what makes the two failure modes so different:
|
|
13
|
-
*
|
|
14
|
-
* - **Skipping** numbers is free. Gossip gives no ordering, so the window
|
|
15
|
-
* exists precisely to accept whatever unseen nonce arrives.
|
|
16
|
-
* - **Restarting** the sequence is fatal. A tab that reloads and resumes at `1`
|
|
17
|
-
* re-presents nonces the network has already spent, and every one of those
|
|
18
|
-
* writes is refused as a replay — indistinguishable, to the user, from the
|
|
19
|
-
* app silently not saving.
|
|
20
|
-
*
|
|
21
|
-
* That asymmetry is why persistence is the default here and an in-memory source
|
|
22
|
-
* is the one you have to ask for. It is also why a source may jump: reserving a
|
|
23
|
-
* block and losing the tail is the correct trade.
|
|
24
|
-
*/
|
|
25
|
-
/** Produces the next nonce for one author device. Must never go backwards. */
|
|
26
|
-
export interface NonceSource {
|
|
27
|
-
next(): Promise<bigint>;
|
|
28
|
-
/**
|
|
29
|
-
* Raise the floor of the sequence to `floor`, if this source can record it.
|
|
30
|
-
*
|
|
31
|
-
* Optional, and additive: a source that does not implement it still works
|
|
32
|
-
* everywhere a `NonceSource` is taken. Implementing it is what lets a caller
|
|
33
|
-
* that learns a higher spent nonce out of band — a refusal naming the nonce,
|
|
34
|
-
* say — skip ahead, and lets a *persistent* source remember that jump across
|
|
35
|
-
* a cold start.
|
|
36
|
-
*
|
|
37
|
-
* Never lowers: a smaller `floor` than the source's own position is ignored.
|
|
38
|
-
*/
|
|
39
|
-
advanceTo?(floor: bigint): Promise<void>;
|
|
40
|
-
}
|
|
41
|
-
/**
|
|
42
|
-
* A counter in memory, starting at `start`.
|
|
43
|
-
*
|
|
44
|
-
* Correct only for a process whose lifetime is the whole sequence — a script, a
|
|
45
|
-
* server-side worker that owns its own device key, a test. In a browser tab it
|
|
46
|
-
* resets on reload and replays; use {@link createLocalStorageNonceSource}
|
|
47
|
-
* there.
|
|
48
|
-
*/
|
|
49
|
-
export declare function createMemoryNonceSource(start?: number | bigint): NonceSource;
|
|
50
|
-
/**
|
|
51
|
-
* A counter persisted in `localStorage`, so a reload continues the sequence.
|
|
52
|
-
*
|
|
53
|
-
* Key it per author **device**, not per account: two devices of one account are
|
|
54
|
-
* independent replicas with independent sequences, and sharing a counter would
|
|
55
|
-
* have them refusing each other's warrants.
|
|
56
|
-
*
|
|
57
|
-
* **Reserve-then-hand-out.** The stored value is advanced *before* the nonce is
|
|
58
|
-
* returned, so a crash between the two loses a number rather than reusing one.
|
|
59
|
-
* Losing a number costs nothing (the window accepts gaps); reusing one costs a
|
|
60
|
-
* write.
|
|
61
|
-
*
|
|
62
|
-
* Concurrent tabs are the case this cannot fully solve, because `localStorage`
|
|
63
|
-
* has no atomic read-modify-write. Two tabs racing can hand out the same nonce,
|
|
64
|
-
* and the loser's write is refused as a replay rather than silently
|
|
65
|
-
* misattributed — a visible error, not corruption. An app that genuinely writes
|
|
66
|
-
* from several tabs at once should hold a Web Lock around `next()` or give each
|
|
67
|
-
* tab its own device.
|
|
68
|
-
*/
|
|
69
|
-
export declare function createLocalStorageNonceSource(key: string, storage?: {
|
|
70
|
-
getItem(k: string): string | null;
|
|
71
|
-
setItem(k: string, v: string): void;
|
|
72
|
-
}): NonceSource;
|
|
@@ -1,111 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Where a warrant's nonce comes from.
|
|
3
|
-
*
|
|
4
|
-
* # Why this is a named concept and not a counter in the relay client
|
|
5
|
-
*
|
|
6
|
-
* A warrant's nonce is spent, once, per `(context, author device)` on every
|
|
7
|
-
* node that applies the delta. So the sequence a client emits is not a local
|
|
8
|
-
* detail — it is state the network remembers, and the client is the only party
|
|
9
|
-
* that can keep it.
|
|
10
|
-
*
|
|
11
|
-
* The receiving ledger is a **sliding window** 64 wide, not a high-water mark,
|
|
12
|
-
* which is what makes the two failure modes so different:
|
|
13
|
-
*
|
|
14
|
-
* - **Skipping** numbers is free. Gossip gives no ordering, so the window
|
|
15
|
-
* exists precisely to accept whatever unseen nonce arrives.
|
|
16
|
-
* - **Restarting** the sequence is fatal. A tab that reloads and resumes at `1`
|
|
17
|
-
* re-presents nonces the network has already spent, and every one of those
|
|
18
|
-
* writes is refused as a replay — indistinguishable, to the user, from the
|
|
19
|
-
* app silently not saving.
|
|
20
|
-
*
|
|
21
|
-
* That asymmetry is why persistence is the default here and an in-memory source
|
|
22
|
-
* is the one you have to ask for. It is also why a source may jump: reserving a
|
|
23
|
-
* block and losing the tail is the correct trade.
|
|
24
|
-
*/
|
|
25
|
-
/**
|
|
26
|
-
* A counter in memory, starting at `start`.
|
|
27
|
-
*
|
|
28
|
-
* Correct only for a process whose lifetime is the whole sequence — a script, a
|
|
29
|
-
* server-side worker that owns its own device key, a test. In a browser tab it
|
|
30
|
-
* resets on reload and replays; use {@link createLocalStorageNonceSource}
|
|
31
|
-
* there.
|
|
32
|
-
*/
|
|
33
|
-
export function createMemoryNonceSource(start = 1) {
|
|
34
|
-
let next = BigInt(start);
|
|
35
|
-
if (next < 1n) {
|
|
36
|
-
throw new Error('nonce sequence starts at 1');
|
|
37
|
-
}
|
|
38
|
-
return {
|
|
39
|
-
next() {
|
|
40
|
-
const value = next;
|
|
41
|
-
next += 1n;
|
|
42
|
-
return Promise.resolve(value);
|
|
43
|
-
},
|
|
44
|
-
advanceTo(floor) {
|
|
45
|
-
if (floor > next)
|
|
46
|
-
next = floor;
|
|
47
|
-
return Promise.resolve();
|
|
48
|
-
},
|
|
49
|
-
};
|
|
50
|
-
}
|
|
51
|
-
/**
|
|
52
|
-
* A counter persisted in `localStorage`, so a reload continues the sequence.
|
|
53
|
-
*
|
|
54
|
-
* Key it per author **device**, not per account: two devices of one account are
|
|
55
|
-
* independent replicas with independent sequences, and sharing a counter would
|
|
56
|
-
* have them refusing each other's warrants.
|
|
57
|
-
*
|
|
58
|
-
* **Reserve-then-hand-out.** The stored value is advanced *before* the nonce is
|
|
59
|
-
* returned, so a crash between the two loses a number rather than reusing one.
|
|
60
|
-
* Losing a number costs nothing (the window accepts gaps); reusing one costs a
|
|
61
|
-
* write.
|
|
62
|
-
*
|
|
63
|
-
* Concurrent tabs are the case this cannot fully solve, because `localStorage`
|
|
64
|
-
* has no atomic read-modify-write. Two tabs racing can hand out the same nonce,
|
|
65
|
-
* and the loser's write is refused as a replay rather than silently
|
|
66
|
-
* misattributed — a visible error, not corruption. An app that genuinely writes
|
|
67
|
-
* from several tabs at once should hold a Web Lock around `next()` or give each
|
|
68
|
-
* tab its own device.
|
|
69
|
-
*/
|
|
70
|
-
export function createLocalStorageNonceSource(key, storage) {
|
|
71
|
-
const store = storage ?? globalThis.localStorage;
|
|
72
|
-
if (!store) {
|
|
73
|
-
throw new Error('no localStorage in this runtime: pass a storage implementation, or use createMemoryNonceSource for a process that owns its whole sequence');
|
|
74
|
-
}
|
|
75
|
-
// A missing, empty, or corrupted value means "no sequence recorded", and the
|
|
76
|
-
// safe reading of that is not 1 — a cleared storage on a device that has
|
|
77
|
-
// already written would replay. But the client cannot know, and 1 is the only
|
|
78
|
-
// defensible start, so it is what a fresh key gets. This is why the doc above
|
|
79
|
-
// tells apps to key per device: a device whose storage was cleared should be
|
|
80
|
-
// re-paired rather than resumed. No node serves a "where does my sequence
|
|
81
|
-
// stand" lookup, so the local counter is the only record there is.
|
|
82
|
-
const lastHandedOut = () => {
|
|
83
|
-
const raw = store.getItem(key);
|
|
84
|
-
if (raw === null || raw === '')
|
|
85
|
-
return 0n;
|
|
86
|
-
try {
|
|
87
|
-
const parsed = BigInt(raw);
|
|
88
|
-
return parsed > 0n ? parsed : 0n;
|
|
89
|
-
}
|
|
90
|
-
catch {
|
|
91
|
-
return 0n;
|
|
92
|
-
}
|
|
93
|
-
};
|
|
94
|
-
return {
|
|
95
|
-
next() {
|
|
96
|
-
const value = lastHandedOut() + 1n;
|
|
97
|
-
store.setItem(key, value.toString());
|
|
98
|
-
return Promise.resolve(value);
|
|
99
|
-
},
|
|
100
|
-
advanceTo(floor) {
|
|
101
|
-
// Stored is the last number handed out, so the floor is recorded as the
|
|
102
|
-
// one below it — persisted rather than held in memory, which is the whole
|
|
103
|
-
// point: the next cold start resumes above the jump even if the node
|
|
104
|
-
// cannot be reached then.
|
|
105
|
-
const wanted = floor > 0n ? floor - 1n : 0n;
|
|
106
|
-
if (wanted > lastHandedOut())
|
|
107
|
-
store.setItem(key, wanted.toString());
|
|
108
|
-
return Promise.resolve();
|
|
109
|
-
},
|
|
110
|
-
};
|
|
111
|
-
}
|