@plane31/blind-relay 0.0.0-stage → 0.1.2

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.
Files changed (169) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +26 -2
  3. package/dist/adapters/index.d.ts +18 -0
  4. package/dist/adapters/index.d.ts.map +1 -0
  5. package/dist/adapters/index.js +22 -0
  6. package/dist/adapters/index.js.map +1 -0
  7. package/dist/audience.d.ts +77 -0
  8. package/dist/audience.d.ts.map +1 -0
  9. package/dist/audience.js +76 -0
  10. package/dist/audience.js.map +1 -0
  11. package/dist/bytes.d.ts +54 -0
  12. package/dist/bytes.d.ts.map +1 -0
  13. package/dist/bytes.js +200 -0
  14. package/dist/bytes.js.map +1 -0
  15. package/dist/circle.d.ts +59 -0
  16. package/dist/circle.d.ts.map +1 -0
  17. package/dist/circle.js +105 -0
  18. package/dist/circle.js.map +1 -0
  19. package/dist/client.d.ts +234 -0
  20. package/dist/client.d.ts.map +1 -0
  21. package/dist/client.js +1365 -0
  22. package/dist/client.js.map +1 -0
  23. package/dist/codec.d.ts +189 -0
  24. package/dist/codec.d.ts.map +1 -0
  25. package/dist/codec.js +512 -0
  26. package/dist/codec.js.map +1 -0
  27. package/dist/crypto/backend.d.ts +39 -0
  28. package/dist/crypto/backend.d.ts.map +1 -0
  29. package/dist/crypto/backend.js +8 -0
  30. package/dist/crypto/backend.js.map +1 -0
  31. package/dist/crypto/noble.d.ts +3 -0
  32. package/dist/crypto/noble.d.ts.map +1 -0
  33. package/dist/crypto/noble.js +107 -0
  34. package/dist/crypto/noble.js.map +1 -0
  35. package/dist/crypto/sodium.d.ts +9 -0
  36. package/dist/crypto/sodium.d.ts.map +1 -0
  37. package/dist/crypto/sodium.js +51 -0
  38. package/dist/crypto/sodium.js.map +1 -0
  39. package/dist/crypto.d.ts +66 -0
  40. package/dist/crypto.d.ts.map +1 -0
  41. package/dist/crypto.js +225 -0
  42. package/dist/crypto.js.map +1 -0
  43. package/dist/epoch.d.ts +93 -0
  44. package/dist/epoch.d.ts.map +1 -0
  45. package/dist/epoch.js +121 -0
  46. package/dist/epoch.js.map +1 -0
  47. package/dist/errors.d.ts +17 -0
  48. package/dist/errors.d.ts.map +1 -0
  49. package/dist/errors.js +30 -0
  50. package/dist/errors.js.map +1 -0
  51. package/dist/historyIndex.d.ts +15 -0
  52. package/dist/historyIndex.d.ts.map +1 -0
  53. package/dist/historyIndex.js +57 -0
  54. package/dist/historyIndex.js.map +1 -0
  55. package/dist/identity.d.ts +62 -0
  56. package/dist/identity.d.ts.map +1 -0
  57. package/dist/identity.js +99 -0
  58. package/dist/identity.js.map +1 -0
  59. package/dist/inbox.d.ts +153 -0
  60. package/dist/inbox.d.ts.map +1 -0
  61. package/dist/inbox.js +472 -0
  62. package/dist/inbox.js.map +1 -0
  63. package/dist/index.d.ts +19 -0
  64. package/dist/index.d.ts.map +1 -0
  65. package/dist/index.js +22 -0
  66. package/dist/index.js.map +1 -0
  67. package/dist/outbox.d.ts +96 -0
  68. package/dist/outbox.d.ts.map +1 -0
  69. package/dist/outbox.js +339 -0
  70. package/dist/outbox.js.map +1 -0
  71. package/dist/policy.d.ts +33 -0
  72. package/dist/policy.d.ts.map +1 -0
  73. package/dist/policy.js +71 -0
  74. package/dist/policy.js.map +1 -0
  75. package/dist/record.d.ts +49 -0
  76. package/dist/record.d.ts.map +1 -0
  77. package/dist/record.js +46 -0
  78. package/dist/record.js.map +1 -0
  79. package/dist/reset.d.ts +74 -0
  80. package/dist/reset.d.ts.map +1 -0
  81. package/dist/reset.js +127 -0
  82. package/dist/reset.js.map +1 -0
  83. package/dist/rows.d.ts +16 -0
  84. package/dist/rows.d.ts.map +1 -0
  85. package/dist/rows.js +103 -0
  86. package/dist/rows.js.map +1 -0
  87. package/dist/slots.d.ts +22 -0
  88. package/dist/slots.d.ts.map +1 -0
  89. package/dist/slots.js +38 -0
  90. package/dist/slots.js.map +1 -0
  91. package/dist/store.d.ts +357 -0
  92. package/dist/store.d.ts.map +1 -0
  93. package/dist/store.js +192 -0
  94. package/dist/store.js.map +1 -0
  95. package/dist/testing/clock.d.ts +14 -0
  96. package/dist/testing/clock.d.ts.map +1 -0
  97. package/dist/testing/clock.js +27 -0
  98. package/dist/testing/clock.js.map +1 -0
  99. package/dist/testing/fakeHost.d.ts +118 -0
  100. package/dist/testing/fakeHost.d.ts.map +1 -0
  101. package/dist/testing/fakeHost.js +462 -0
  102. package/dist/testing/fakeHost.js.map +1 -0
  103. package/dist/testing/faults.d.ts +97 -0
  104. package/dist/testing/faults.d.ts.map +1 -0
  105. package/dist/testing/faults.js +264 -0
  106. package/dist/testing/faults.js.map +1 -0
  107. package/dist/testing/fixtures.d.ts +15 -0
  108. package/dist/testing/fixtures.d.ts.map +1 -0
  109. package/dist/testing/fixtures.js +22 -0
  110. package/dist/testing/fixtures.js.map +1 -0
  111. package/dist/testing/index.d.ts +7 -0
  112. package/dist/testing/index.d.ts.map +1 -0
  113. package/dist/testing/index.js +7 -0
  114. package/dist/testing/index.js.map +1 -0
  115. package/dist/testing/providers.d.ts +16 -0
  116. package/dist/testing/providers.d.ts.map +1 -0
  117. package/dist/testing/providers.js +36 -0
  118. package/dist/testing/providers.js.map +1 -0
  119. package/dist/testing/sweep.d.ts +46 -0
  120. package/dist/testing/sweep.d.ts.map +1 -0
  121. package/dist/testing/sweep.js +366 -0
  122. package/dist/testing/sweep.js.map +1 -0
  123. package/dist/transactions.d.ts +75 -0
  124. package/dist/transactions.d.ts.map +1 -0
  125. package/dist/transactions.js +212 -0
  126. package/dist/transactions.js.map +1 -0
  127. package/dist/transport.d.ts +125 -0
  128. package/dist/transport.d.ts.map +1 -0
  129. package/dist/transport.js +6 -0
  130. package/dist/transport.js.map +1 -0
  131. package/dist/vault.d.ts +59 -0
  132. package/dist/vault.d.ts.map +1 -0
  133. package/dist/vault.js +69 -0
  134. package/dist/vault.js.map +1 -0
  135. package/package.json +67 -3
  136. package/src/adapters/index.ts +102 -0
  137. package/src/audience.ts +123 -0
  138. package/src/bytes.ts +209 -0
  139. package/src/circle.ts +127 -0
  140. package/src/client.ts +1442 -0
  141. package/src/codec.ts +642 -0
  142. package/src/crypto/backend.ts +48 -0
  143. package/src/crypto/noble.ts +104 -0
  144. package/src/crypto/node-module.d.ts +11 -0
  145. package/src/crypto/sodium.ts +53 -0
  146. package/src/crypto.ts +266 -0
  147. package/src/epoch.ts +201 -0
  148. package/src/errors.ts +78 -0
  149. package/src/historyIndex.ts +61 -0
  150. package/src/identity.ts +158 -0
  151. package/src/inbox.ts +560 -0
  152. package/src/index.ts +59 -0
  153. package/src/outbox.ts +385 -0
  154. package/src/policy.ts +88 -0
  155. package/src/record.ts +106 -0
  156. package/src/reset.ts +176 -0
  157. package/src/rows.ts +116 -0
  158. package/src/slots.ts +55 -0
  159. package/src/store.ts +557 -0
  160. package/src/testing/clock.ts +29 -0
  161. package/src/testing/fakeHost.ts +482 -0
  162. package/src/testing/faults.ts +321 -0
  163. package/src/testing/fixtures.ts +35 -0
  164. package/src/testing/index.ts +6 -0
  165. package/src/testing/providers.ts +39 -0
  166. package/src/testing/sweep.ts +407 -0
  167. package/src/transactions.ts +231 -0
  168. package/src/transport.ts +106 -0
  169. package/src/vault.ts +126 -0
package/src/errors.ts ADDED
@@ -0,0 +1,78 @@
1
+ /** Error codes of Kavach v1, protocol section 14. The set is closed: new codes are a protocol change. */
2
+ export type ErrorCode =
3
+ // Parsing
4
+ | "SIZE_LIMIT"
5
+ | "MALFORMED"
6
+ | "UNSUPPORTED_VERSION"
7
+ | "UNSUPPORTED_TYPE"
8
+ | "BAD_PAYLOAD"
9
+ // Authentication
10
+ | "INVALID_KEY"
11
+ | "BAD_SIGNATURE"
12
+ | "DECRYPT_FAILED"
13
+ | "WRAP_HASH_MISMATCH"
14
+ | "KEY_COMMITMENT_MISMATCH"
15
+ // Trust and state
16
+ | "ACCOUNT_KEY_CHANGED"
17
+ | "ACCOUNT_RECOVERY_REQUIRED"
18
+ | "DEVICE_ROLLBACK"
19
+ | "DEVICE_ID_REUSED"
20
+ | "SUPERSEDED_DEVICE"
21
+ | "VAULT_BINDING"
22
+ | "RESET_BINDING"
23
+ | "RESET_KEYS_REQUIRED"
24
+ | "RESET_KEY_REVOKED"
25
+ | "RESET_CLOCK_UNTRUSTED"
26
+ | "ROLLBACK"
27
+ | "EQUIVOCATION"
28
+ | "METADATA_REQUIRED"
29
+ | "KEY_PENDING"
30
+ | "NOT_AUTHORIZED"
31
+ | "ROTATION_REQUIRED"
32
+ | "COUNTER_EXHAUSTED"
33
+ | "RECIPIENT_LIMIT"
34
+ // Delivery
35
+ | "CLOCK_SKEW"
36
+ | "EXPIRED"
37
+ | "QUEUE_FULL"
38
+ | "CURSOR_OUT_OF_RANGE"
39
+ | "STALE_APPEND"
40
+ // Storage and transport
41
+ | "LOCKED"
42
+ | "RECOVERY_REQUIRED"
43
+ | "STORAGE_FAILURE"
44
+ /** The host could not be reached or did not answer in time; the same call may succeed later. A transport raises it, never a plain error. */
45
+ | "NETWORK";
46
+
47
+ const RETRYABLE: ReadonlySet<ErrorCode> = new Set<ErrorCode>([
48
+ "KEY_PENDING",
49
+ "METADATA_REQUIRED",
50
+ "RESET_KEYS_REQUIRED",
51
+ "ROTATION_REQUIRED",
52
+ "LOCKED",
53
+ "STORAGE_FAILURE",
54
+ "NETWORK"
55
+ ]);
56
+
57
+ /**
58
+ * Every failure the SDK reports. `code` is one of the protocol's codes, `stage` is a safe
59
+ * stage name for logs (never a secret, a payload or an identifier), `retryable` says whether
60
+ * the same call may succeed later without any change of state.
61
+ */
62
+ export class KavachError extends Error {
63
+ readonly code: ErrorCode;
64
+ readonly stage: string;
65
+ readonly retryable: boolean;
66
+
67
+ constructor(code: ErrorCode, stage = "unknown", message?: string) {
68
+ super(message ?? `${code} at ${stage}`);
69
+ this.name = "KavachError";
70
+ this.code = code;
71
+ this.stage = stage;
72
+ this.retryable = RETRYABLE.has(code);
73
+ }
74
+ }
75
+
76
+ export function isKavachError(value: unknown, code?: ErrorCode): value is KavachError {
77
+ return value instanceof KavachError && (code === undefined || value.code === code);
78
+ }
@@ -0,0 +1,61 @@
1
+ /**
2
+ * The history index: two time-ordered keys per history row, so a history query, a cursor and
3
+ * the retention purge read a bounded slice of the store instead of the whole history table.
4
+ *
5
+ * Keys sort by time and are unique: the tuple is the zero-padded `sentAt`, epoch and counter
6
+ * followed by the record id. `t:<kind>:<tuple>` is the timeline of one kind; `a:<author>:<kind>:
7
+ * <tuple>` is one author's. A query without a kind merges the two kinds that reach history
8
+ * declared with history; a slot-only kind is never history. The tuple doubles as the page cursor:
9
+ * it names a position that no later write moves.
10
+ */
11
+ import { unbig } from "./rows.js";
12
+ import type { HistoryRow, Store, Tx } from "./store.js";
13
+
14
+ const META = "meta";
15
+
16
+ /** A u64 as twenty decimal digits, so that string order is numeric order. */
17
+ export function padTime(ms: bigint): string {
18
+ return (ms < 0n ? 0n : ms).toString().padStart(20, "0");
19
+ }
20
+
21
+ export function historyTuple(r: Pick<HistoryRow, "sentAt" | "epoch" | "counter" | "recordId">): string {
22
+ return `${padTime(unbig(r.sentAt))}:${r.epoch.toString().padStart(10, "0")}:${r.counter.toString().padStart(10, "0")}:${r.recordId}`;
23
+ }
24
+
25
+ export function timelinePrefix(kind: number): string {
26
+ return `t:${kind}:`;
27
+ }
28
+
29
+ export function authorPrefix(authorIdentityId: string, kind: number): string {
30
+ return `a:${authorIdentityId}:${kind}:`;
31
+ }
32
+
33
+ function keysOf(r: HistoryRow): string[] {
34
+ const tuple = historyTuple(r);
35
+ return [timelinePrefix(r.kind) + tuple, authorPrefix(r.authorIdentityId, r.kind) + tuple];
36
+ }
37
+
38
+ /** Index a history row; written in the same transaction as the row (T6, T7). */
39
+ export function indexHistory(tx: Tx, r: HistoryRow): void {
40
+ for (const k of keysOf(r)) tx.put("historyIndex", k, { recordId: r.recordId });
41
+ }
42
+
43
+ export function unindexHistory(tx: Tx, r: HistoryRow): void {
44
+ for (const k of keysOf(r)) tx.delete("historyIndex", k);
45
+ }
46
+
47
+ /**
48
+ * Build the index once for a store written before it existed: every history row is indexed and
49
+ * the header records it. A fresh store is marked at T1 and never runs this. Idempotent.
50
+ */
51
+ export async function ensureHistoryIndex(store: Store): Promise<boolean> {
52
+ const built = await store.read((tx) => tx.get("meta", META)?.historyIndex === 1);
53
+ if (built) return false;
54
+ return store.transaction((tx) => {
55
+ const meta = tx.get("meta", META);
56
+ if (!meta || meta.historyIndex === 1) return false;
57
+ for (const [, r] of tx.list("history")) indexHistory(tx, r);
58
+ tx.put("meta", META, { ...meta, historyIndex: 1 });
59
+ return true;
60
+ });
61
+ }
@@ -0,0 +1,158 @@
1
+ /**
2
+ * Accounts, devices, certificates and generation rules (protocol section 4).
3
+ *
4
+ * The pin is the local trust state for one account: (`accountName`, `AK`) at first contact, then
5
+ * the greatest accepted device generation with its certificate hash and device keys. Everything
6
+ * here is a pure decision over a pin and a candidate certificate; persistence is the store's job.
7
+ */
8
+ import { concat, equalBytes, toHex } from "./bytes.js";
9
+ import {
10
+ DocumentType,
11
+ buildAccountDevice,
12
+ expectType,
13
+ verifyDocument,
14
+ type AccountDevice,
15
+ type Parsed
16
+ } from "./codec.js";
17
+ import {
18
+ DEVICE_ID_BYTES,
19
+ SEED_BYTES,
20
+ accountKeypair,
21
+ deviceBoxKeypair,
22
+ deviceSigningKeypair,
23
+ identityId,
24
+ randomBytes,
25
+ streamId,
26
+ type BoxKeypair,
27
+ type SigningKeypair
28
+ } from "./crypto.js";
29
+ import { KavachError } from "./errors.js";
30
+
31
+ /** What the SDK persists per known account. */
32
+ export interface AccountPin {
33
+ readonly accountName: string;
34
+ readonly accountKey: Uint8Array;
35
+ readonly identityId: Uint8Array;
36
+ readonly generation: bigint;
37
+ readonly certificateHash: Uint8Array;
38
+ readonly certificate: Uint8Array;
39
+ readonly deviceId: Uint8Array;
40
+ readonly signingKey: Uint8Array;
41
+ readonly boxKey: Uint8Array;
42
+ readonly streamId: Uint8Array;
43
+ /** Every device id this account has used, including the current one. */
44
+ readonly deviceIds: readonly Uint8Array[];
45
+ }
46
+
47
+ export type CertificateOutcome =
48
+ | { readonly outcome: "ACCEPTED"; readonly pin: AccountPin; readonly rotate: boolean; readonly previous: AccountPin | undefined }
49
+ | { readonly outcome: "DUPLICATE"; readonly pin: AccountPin };
50
+
51
+ export function pinFromCertificate(doc: Parsed<AccountDevice>, previous?: AccountPin): AccountPin {
52
+ const iid = identityId(doc.accountName, doc.accountKey);
53
+ const ids = previous ? [...previous.deviceIds] : [];
54
+ if (!ids.some((d) => equalBytes(d, doc.deviceId))) ids.push(doc.deviceId);
55
+ return {
56
+ accountName: doc.accountName,
57
+ accountKey: doc.accountKey,
58
+ identityId: iid,
59
+ generation: doc.generation,
60
+ certificateHash: doc.bodyHash,
61
+ certificate: concat(doc.body, doc.signature),
62
+ deviceId: doc.deviceId,
63
+ signingKey: doc.signingKey,
64
+ boxKey: doc.boxKey,
65
+ streamId: streamId(iid, doc.deviceId),
66
+ deviceIds: ids
67
+ };
68
+ }
69
+
70
+ /**
71
+ * Accept or reject a certificate against the local pin for its account.
72
+ *
73
+ * - No pin yet: first contact. If `expected` names the account the directory resolved, the
74
+ * certificate must carry that name and key; the pin is created and nothing rotates.
75
+ * - A pinned account: a different `AK` is `ACCOUNT_KEY_CHANGED` and the caller quarantines
76
+ * (never closing or re-admitting anything); a lower generation is `DEVICE_ROLLBACK`; the same
77
+ * generation is a duplicate or `EQUIVOCATION`; a higher one with a reused device id is
78
+ * `DEVICE_ID_REUSED`; otherwise it is accepted and the audience must rotate.
79
+ */
80
+ export function acceptCertificate(
81
+ current: AccountPin | undefined,
82
+ wire: Uint8Array,
83
+ expected?: { readonly accountName: string; readonly accountKey: Uint8Array }
84
+ ): CertificateOutcome {
85
+ const stage = "acceptCertificate";
86
+ const doc = expectType<AccountDevice>(verifyDocument(wire), DocumentType.AccountDevice, stage);
87
+ const anchor = current ?? expected;
88
+ if (anchor && (anchor.accountName !== doc.accountName || !equalBytes(anchor.accountKey, doc.accountKey))) {
89
+ throw new KavachError("ACCOUNT_KEY_CHANGED", stage, `certificate for ${doc.accountName} carries a different account key`);
90
+ }
91
+ if (!current) return { outcome: "ACCEPTED", pin: pinFromCertificate(doc), rotate: false, previous: undefined };
92
+ if (doc.generation < current.generation) throw new KavachError("DEVICE_ROLLBACK", stage);
93
+ if (doc.generation === current.generation) {
94
+ if (equalBytes(doc.bodyHash, current.certificateHash)) return { outcome: "DUPLICATE", pin: current };
95
+ throw new KavachError("EQUIVOCATION", stage, "two certificates at one generation");
96
+ }
97
+ if (current.deviceIds.some((d) => equalBytes(d, doc.deviceId))) throw new KavachError("DEVICE_ID_REUSED", stage);
98
+ return { outcome: "ACCEPTED", pin: pinFromCertificate(doc, current), rotate: true, previous: current };
99
+ }
100
+
101
+ // Own account and device ---------------------------------------------------------------------
102
+
103
+ export interface DeviceKeys {
104
+ readonly deviceSeed: Uint8Array;
105
+ readonly deviceId: Uint8Array;
106
+ readonly signing: SigningKeypair;
107
+ readonly box: BoxKeypair;
108
+ }
109
+
110
+ export interface OwnAccount {
111
+ readonly accountName: string;
112
+ readonly accountSeed: Uint8Array;
113
+ readonly account: SigningKeypair;
114
+ readonly identityId: Uint8Array;
115
+ }
116
+
117
+ export function openAccount(accountName: string, accountSeed: Uint8Array): OwnAccount {
118
+ if (accountSeed.length !== SEED_BYTES) throw new KavachError("INVALID_KEY", "openAccount");
119
+ const account = accountKeypair(accountSeed);
120
+ return { accountName, accountSeed, account, identityId: identityId(accountName, account.publicKey) };
121
+ }
122
+
123
+ export function newAccountSeed(): Uint8Array {
124
+ return randomBytes(SEED_BYTES);
125
+ }
126
+
127
+ /** A fresh installation: new device seed and a random nonzero device id. Never backed up. */
128
+ export function newDevice(): DeviceKeys {
129
+ const deviceSeed = randomBytes(SEED_BYTES);
130
+ let deviceId = randomBytes(DEVICE_ID_BYTES);
131
+ while (deviceId.every((b) => b === 0)) deviceId = randomBytes(DEVICE_ID_BYTES);
132
+ return deviceFromSeed(deviceSeed, deviceId);
133
+ }
134
+
135
+ export function deviceFromSeed(deviceSeed: Uint8Array, deviceId: Uint8Array): DeviceKeys {
136
+ if (deviceId.length !== DEVICE_ID_BYTES || deviceId.every((b) => b === 0)) throw new KavachError("MALFORMED", "device", "device id");
137
+ return { deviceSeed, deviceId, signing: deviceSigningKeypair(deviceSeed), box: deviceBoxKeypair(deviceSeed) };
138
+ }
139
+
140
+ /** Build and sign this account's certificate for a device at a generation. */
141
+ export function certify(account: OwnAccount, device: DeviceKeys, generation: bigint): Uint8Array {
142
+ if (generation < 1n) throw new KavachError("MALFORMED", "certify", "generation");
143
+ return buildAccountDevice(
144
+ {
145
+ accountName: account.accountName,
146
+ accountKey: account.account.publicKey,
147
+ generation,
148
+ deviceId: device.deviceId,
149
+ signingKey: device.signing.publicKey,
150
+ boxKey: device.box.publicKey
151
+ },
152
+ account.account.secretKey
153
+ );
154
+ }
155
+
156
+ export function pinKey(identity: Uint8Array): string {
157
+ return toHex(identity);
158
+ }