@freqhole/haruspex 0.0.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.
Files changed (250) hide show
  1. package/dist/flows/add-peer/machine.d.ts +36 -0
  2. package/dist/flows/add-peer/machine.d.ts.map +1 -0
  3. package/dist/flows/add-peer/machine.js +500 -0
  4. package/dist/flows/add-peer/machine.test.d.ts +2 -0
  5. package/dist/flows/add-peer/machine.test.d.ts.map +1 -0
  6. package/dist/flows/add-peer/machine.test.js +421 -0
  7. package/dist/flows/add-peer/runner.d.ts +16 -0
  8. package/dist/flows/add-peer/runner.d.ts.map +1 -0
  9. package/dist/flows/add-peer/runner.js +349 -0
  10. package/dist/flows/add-peer/runner.test.d.ts +2 -0
  11. package/dist/flows/add-peer/runner.test.d.ts.map +1 -0
  12. package/dist/flows/add-peer/runner.test.js +205 -0
  13. package/dist/flows/add-peer/types.d.ts +271 -0
  14. package/dist/flows/add-peer/types.d.ts.map +1 -0
  15. package/dist/flows/add-peer/types.js +10 -0
  16. package/dist/flows/index.d.ts +6 -0
  17. package/dist/flows/index.d.ts.map +1 -0
  18. package/dist/flows/index.js +6 -0
  19. package/dist/identity/idbStore.d.ts +51 -0
  20. package/dist/identity/idbStore.d.ts.map +1 -0
  21. package/dist/identity/idbStore.js +183 -0
  22. package/dist/identity/idbStore.test.d.ts +2 -0
  23. package/dist/identity/idbStore.test.d.ts.map +1 -0
  24. package/dist/identity/idbStore.test.js +103 -0
  25. package/dist/identity/index.d.ts +8 -0
  26. package/dist/identity/index.d.ts.map +1 -0
  27. package/dist/identity/index.js +3 -0
  28. package/dist/identity/resolve.d.ts +31 -0
  29. package/dist/identity/resolve.d.ts.map +1 -0
  30. package/dist/identity/resolve.js +37 -0
  31. package/dist/identity/resolve.test.d.ts +2 -0
  32. package/dist/identity/resolve.test.d.ts.map +1 -0
  33. package/dist/identity/resolve.test.js +105 -0
  34. package/dist/identity/types.d.ts +21 -0
  35. package/dist/identity/types.d.ts.map +1 -0
  36. package/dist/identity/types.js +1 -0
  37. package/dist/identity/webLocks.d.ts +22 -0
  38. package/dist/identity/webLocks.d.ts.map +1 -0
  39. package/dist/identity/webLocks.js +67 -0
  40. package/dist/identity/webLocks.test.d.ts +2 -0
  41. package/dist/identity/webLocks.test.d.ts.map +1 -0
  42. package/dist/identity/webLocks.test.js +125 -0
  43. package/dist/index.d.ts +2 -0
  44. package/dist/index.d.ts.map +1 -0
  45. package/dist/index.js +1 -0
  46. package/dist/index.test.d.ts +2 -0
  47. package/dist/index.test.d.ts.map +1 -0
  48. package/dist/index.test.js +7 -0
  49. package/dist/knock/index.d.ts +10 -0
  50. package/dist/knock/index.d.ts.map +1 -0
  51. package/dist/knock/index.js +5 -0
  52. package/dist/knock/pending-checker.d.ts +19 -0
  53. package/dist/knock/pending-checker.d.ts.map +1 -0
  54. package/dist/knock/pending-checker.js +27 -0
  55. package/dist/knock/pending-checker.test.d.ts +2 -0
  56. package/dist/knock/pending-checker.test.d.ts.map +1 -0
  57. package/dist/knock/pending-checker.test.js +93 -0
  58. package/dist/knock/requester.d.ts +26 -0
  59. package/dist/knock/requester.d.ts.map +1 -0
  60. package/dist/knock/requester.js +72 -0
  61. package/dist/knock/requester.test.d.ts +2 -0
  62. package/dist/knock/requester.test.d.ts.map +1 -0
  63. package/dist/knock/requester.test.js +108 -0
  64. package/dist/knock/responder.d.ts +13 -0
  65. package/dist/knock/responder.d.ts.map +1 -0
  66. package/dist/knock/responder.js +40 -0
  67. package/dist/knock/responder.test.d.ts +2 -0
  68. package/dist/knock/responder.test.d.ts.map +1 -0
  69. package/dist/knock/responder.test.js +89 -0
  70. package/dist/knock/store.d.ts +11 -0
  71. package/dist/knock/store.d.ts.map +1 -0
  72. package/dist/knock/store.js +211 -0
  73. package/dist/knock/store.test.d.ts +2 -0
  74. package/dist/knock/store.test.d.ts.map +1 -0
  75. package/dist/knock/store.test.js +195 -0
  76. package/dist/knock/types.d.ts +144 -0
  77. package/dist/knock/types.d.ts.map +1 -0
  78. package/dist/knock/types.js +26 -0
  79. package/dist/permissions/access.d.ts +54 -0
  80. package/dist/permissions/access.d.ts.map +1 -0
  81. package/dist/permissions/access.js +56 -0
  82. package/dist/permissions/access.test.d.ts +2 -0
  83. package/dist/permissions/access.test.d.ts.map +1 -0
  84. package/dist/permissions/access.test.js +72 -0
  85. package/dist/permissions/index.d.ts +5 -0
  86. package/dist/permissions/index.d.ts.map +1 -0
  87. package/dist/permissions/index.js +2 -0
  88. package/dist/permissions/role-table.d.ts +11 -0
  89. package/dist/permissions/role-table.d.ts.map +1 -0
  90. package/dist/permissions/role-table.js +16 -0
  91. package/dist/protocol/client.d.ts +67 -0
  92. package/dist/protocol/client.d.ts.map +1 -0
  93. package/dist/protocol/client.js +195 -0
  94. package/dist/protocol/client.test.d.ts +2 -0
  95. package/dist/protocol/client.test.d.ts.map +1 -0
  96. package/dist/protocol/client.test.js +282 -0
  97. package/dist/protocol/codec.d.ts +45 -0
  98. package/dist/protocol/codec.d.ts.map +1 -0
  99. package/dist/protocol/codec.js +111 -0
  100. package/dist/protocol/codec.test.d.ts +2 -0
  101. package/dist/protocol/codec.test.d.ts.map +1 -0
  102. package/dist/protocol/codec.test.js +93 -0
  103. package/dist/protocol/extensions.d.ts +33 -0
  104. package/dist/protocol/extensions.d.ts.map +1 -0
  105. package/dist/protocol/extensions.js +36 -0
  106. package/dist/protocol/extensions.test.d.ts +2 -0
  107. package/dist/protocol/extensions.test.d.ts.map +1 -0
  108. package/dist/protocol/extensions.test.js +65 -0
  109. package/dist/protocol/fixtures.test.d.ts +2 -0
  110. package/dist/protocol/fixtures.test.d.ts.map +1 -0
  111. package/dist/protocol/fixtures.test.js +51 -0
  112. package/dist/protocol/index.d.ts +9 -0
  113. package/dist/protocol/index.d.ts.map +1 -0
  114. package/dist/protocol/index.js +4 -0
  115. package/dist/protocol/messages.d.ts +480 -0
  116. package/dist/protocol/messages.d.ts.map +1 -0
  117. package/dist/protocol/messages.js +255 -0
  118. package/dist/protocol/messages.test.d.ts +2 -0
  119. package/dist/protocol/messages.test.d.ts.map +1 -0
  120. package/dist/protocol/messages.test.js +75 -0
  121. package/dist/share/codec.d.ts +58 -0
  122. package/dist/share/codec.d.ts.map +1 -0
  123. package/dist/share/codec.js +222 -0
  124. package/dist/share/codec.test.d.ts +2 -0
  125. package/dist/share/codec.test.d.ts.map +1 -0
  126. package/dist/share/codec.test.js +150 -0
  127. package/dist/share/detect.d.ts +24 -0
  128. package/dist/share/detect.d.ts.map +1 -0
  129. package/dist/share/detect.js +25 -0
  130. package/dist/share/detect.test.d.ts +2 -0
  131. package/dist/share/detect.test.d.ts.map +1 -0
  132. package/dist/share/detect.test.js +31 -0
  133. package/dist/share/index.d.ts +7 -0
  134. package/dist/share/index.d.ts.map +1 -0
  135. package/dist/share/index.js +3 -0
  136. package/dist/share/peer-addr.d.ts +32 -0
  137. package/dist/share/peer-addr.d.ts.map +1 -0
  138. package/dist/share/peer-addr.js +72 -0
  139. package/dist/share/peer-addr.test.d.ts +2 -0
  140. package/dist/share/peer-addr.test.d.ts.map +1 -0
  141. package/dist/share/peer-addr.test.js +58 -0
  142. package/dist/solid/auth-form.d.ts +43 -0
  143. package/dist/solid/auth-form.d.ts.map +1 -0
  144. package/dist/solid/auth-form.js +55 -0
  145. package/dist/solid/auth-form.test.d.ts +2 -0
  146. package/dist/solid/auth-form.test.d.ts.map +1 -0
  147. package/dist/solid/auth-form.test.js +131 -0
  148. package/dist/solid/auth-status.d.ts +21 -0
  149. package/dist/solid/auth-status.d.ts.map +1 -0
  150. package/dist/solid/auth-status.js +58 -0
  151. package/dist/solid/auth-status.test.d.ts +2 -0
  152. package/dist/solid/auth-status.test.d.ts.map +1 -0
  153. package/dist/solid/auth-status.test.js +60 -0
  154. package/dist/solid/index.d.ts +5 -0
  155. package/dist/solid/index.d.ts.map +1 -0
  156. package/dist/solid/index.js +10 -0
  157. package/dist/solid/knock-inbox.d.ts +61 -0
  158. package/dist/solid/knock-inbox.d.ts.map +1 -0
  159. package/dist/solid/knock-inbox.js +123 -0
  160. package/dist/solid/knock-inbox.test.d.ts +2 -0
  161. package/dist/solid/knock-inbox.test.d.ts.map +1 -0
  162. package/dist/solid/knock-inbox.test.js +153 -0
  163. package/dist/solid/reauth-flow.d.ts +26 -0
  164. package/dist/solid/reauth-flow.d.ts.map +1 -0
  165. package/dist/solid/reauth-flow.js +34 -0
  166. package/dist/solid/reauth-flow.test.d.ts +2 -0
  167. package/dist/solid/reauth-flow.test.d.ts.map +1 -0
  168. package/dist/solid/reauth-flow.test.js +91 -0
  169. package/dist/state/endpoint-control.d.ts +22 -0
  170. package/dist/state/endpoint-control.d.ts.map +1 -0
  171. package/dist/state/endpoint-control.js +34 -0
  172. package/dist/state/endpoint-control.test.d.ts +2 -0
  173. package/dist/state/endpoint-control.test.d.ts.map +1 -0
  174. package/dist/state/endpoint-control.test.js +63 -0
  175. package/dist/state/friend-directory.d.ts +37 -0
  176. package/dist/state/friend-directory.d.ts.map +1 -0
  177. package/dist/state/friend-directory.js +44 -0
  178. package/dist/state/friend-directory.test.d.ts +2 -0
  179. package/dist/state/friend-directory.test.d.ts.map +1 -0
  180. package/dist/state/friend-directory.test.js +63 -0
  181. package/dist/state/index.d.ts +6 -0
  182. package/dist/state/index.d.ts.map +1 -0
  183. package/dist/state/index.js +3 -0
  184. package/dist/state/peer-names.d.ts +7 -0
  185. package/dist/state/peer-names.d.ts.map +1 -0
  186. package/dist/state/peer-names.js +21 -0
  187. package/dist/state/peer-names.test.d.ts +2 -0
  188. package/dist/state/peer-names.test.d.ts.map +1 -0
  189. package/dist/state/peer-names.test.js +29 -0
  190. package/dist/testing/idb-harness.d.ts +9 -0
  191. package/dist/testing/idb-harness.d.ts.map +1 -0
  192. package/dist/testing/idb-harness.js +19 -0
  193. package/dist/testing/idb-harness.test.d.ts +2 -0
  194. package/dist/testing/idb-harness.test.d.ts.map +1 -0
  195. package/dist/testing/idb-harness.test.js +18 -0
  196. package/dist/testing/identity-fixtures.d.ts +10 -0
  197. package/dist/testing/identity-fixtures.d.ts.map +1 -0
  198. package/dist/testing/identity-fixtures.js +21 -0
  199. package/dist/testing/identity-fixtures.test.d.ts +2 -0
  200. package/dist/testing/identity-fixtures.test.d.ts.map +1 -0
  201. package/dist/testing/identity-fixtures.test.js +28 -0
  202. package/dist/testing/index.d.ts +7 -0
  203. package/dist/testing/index.d.ts.map +1 -0
  204. package/dist/testing/index.js +11 -0
  205. package/dist/testing/knock-fixtures.d.ts +13 -0
  206. package/dist/testing/knock-fixtures.d.ts.map +1 -0
  207. package/dist/testing/knock-fixtures.js +45 -0
  208. package/dist/testing/knock-fixtures.test.d.ts +2 -0
  209. package/dist/testing/knock-fixtures.test.d.ts.map +1 -0
  210. package/dist/testing/knock-fixtures.test.js +65 -0
  211. package/dist/testing/protocol-fixtures.d.ts +7 -0
  212. package/dist/testing/protocol-fixtures.d.ts.map +1 -0
  213. package/dist/testing/protocol-fixtures.js +28 -0
  214. package/dist/testing/protocol-fixtures.test.d.ts +2 -0
  215. package/dist/testing/protocol-fixtures.test.d.ts.map +1 -0
  216. package/dist/testing/protocol-fixtures.test.js +19 -0
  217. package/dist/testing/state-fixtures.d.ts +33 -0
  218. package/dist/testing/state-fixtures.d.ts.map +1 -0
  219. package/dist/testing/state-fixtures.js +67 -0
  220. package/dist/testing/state-fixtures.test.d.ts +2 -0
  221. package/dist/testing/state-fixtures.test.d.ts.map +1 -0
  222. package/dist/testing/state-fixtures.test.js +68 -0
  223. package/dist/testing/webauthn-mocks.d.ts +27 -0
  224. package/dist/testing/webauthn-mocks.d.ts.map +1 -0
  225. package/dist/testing/webauthn-mocks.js +78 -0
  226. package/dist/testing/webauthn-mocks.test.d.ts +2 -0
  227. package/dist/testing/webauthn-mocks.test.d.ts.map +1 -0
  228. package/dist/testing/webauthn-mocks.test.js +40 -0
  229. package/dist/webauthn/ceremony.d.ts +76 -0
  230. package/dist/webauthn/ceremony.d.ts.map +1 -0
  231. package/dist/webauthn/ceremony.js +136 -0
  232. package/dist/webauthn/ceremony.test.d.ts +2 -0
  233. package/dist/webauthn/ceremony.test.d.ts.map +1 -0
  234. package/dist/webauthn/ceremony.test.js +245 -0
  235. package/dist/webauthn/codec.d.ts +39 -0
  236. package/dist/webauthn/codec.d.ts.map +1 -0
  237. package/dist/webauthn/codec.js +118 -0
  238. package/dist/webauthn/codec.test.d.ts +2 -0
  239. package/dist/webauthn/codec.test.d.ts.map +1 -0
  240. package/dist/webauthn/codec.test.js +126 -0
  241. package/dist/webauthn/index.d.ts +7 -0
  242. package/dist/webauthn/index.d.ts.map +1 -0
  243. package/dist/webauthn/index.js +3 -0
  244. package/dist/webauthn/transport.d.ts +58 -0
  245. package/dist/webauthn/transport.d.ts.map +1 -0
  246. package/dist/webauthn/transport.js +6 -0
  247. package/dist/webauthn/types.d.ts +78 -0
  248. package/dist/webauthn/types.d.ts.map +1 -0
  249. package/dist/webauthn/types.js +29 -0
  250. package/package.json +83 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/identity/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAE7D,YAAY,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AACpD,OAAO,EACL,sBAAsB,EACtB,cAAc,EACd,uBAAuB,EACvB,oBAAoB,EACpB,gBAAgB,EAChB,eAAe,GAChB,MAAM,eAAe,CAAC;AAEvB,YAAY,EAAE,sBAAsB,EAAE,MAAM,cAAc,CAAC;AAC3D,OAAO,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAEhE,YAAY,EAAE,yBAAyB,EAAE,MAAM,eAAe,CAAC;AAC/D,OAAO,EAAE,qBAAqB,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC"}
@@ -0,0 +1,3 @@
1
+ export { createIdbIdentityStore, databaseExists, identitySourceAvailable, openExistingDatabase, readIdentityFrom, writeIdentityTo, } from "./idbStore.js";
2
+ export { persistIdentity, resolveIdentity } from "./resolve.js";
3
+ export { acquireNodeLeadership, LOCK_NAME } from "./webLocks.js";
@@ -0,0 +1,31 @@
1
+ import type { IdentitySource } from "./idbStore.js";
2
+ import type { IdentityStore, P2PIdentity } from "./types.js";
3
+ export interface ResolveIdentityOptions {
4
+ /**
5
+ * other apps' databases to check first, read-only - never created or
6
+ * upgraded. checked in order; the first source with a stored identity
7
+ * wins over the local store. this is how, e.g., a newly-installed app can
8
+ * pick up an identity an already-installed sibling app created for this
9
+ * origin, without either app depending on the other's schema beyond the
10
+ * shared identity record shape.
11
+ */
12
+ fallbackSources?: IdentitySource[];
13
+ }
14
+ /**
15
+ * resolve the p2p identity for this origin.
16
+ *
17
+ * checks each fallback source in order (without creating any of them), then
18
+ * falls back to the local store. the first fallback source that already has
19
+ * an identity wins over the local store, even if the local store also has
20
+ * one - this lets a newly-added app adopt an existing sibling app's identity
21
+ * on first run.
22
+ */
23
+ export declare function resolveIdentity(local: IdentityStore, options?: ResolveIdentityOptions): Promise<P2PIdentity | null>;
24
+ /**
25
+ * persist a p2p identity.
26
+ *
27
+ * writes to the first available fallback source (matching resolveIdentity's
28
+ * preference order); otherwise writes to the local store.
29
+ */
30
+ export declare function persistIdentity(identity: P2PIdentity, local: IdentityStore, options?: ResolveIdentityOptions): Promise<void>;
31
+ //# sourceMappingURL=resolve.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resolve.d.ts","sourceRoot":"","sources":["../../src/identity/resolve.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AACpD,OAAO,KAAK,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAE7D,MAAM,WAAW,sBAAsB;IACrC;;;;;;;OAOG;IACH,eAAe,CAAC,EAAE,cAAc,EAAE,CAAC;CACpC;AAED;;;;;;;;GAQG;AACH,wBAAsB,eAAe,CACnC,KAAK,EAAE,aAAa,EACpB,OAAO,GAAE,sBAA2B,GACnC,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,CAU7B;AAED;;;;;GAKG;AACH,wBAAsB,eAAe,CACnC,QAAQ,EAAE,WAAW,EACrB,KAAK,EAAE,aAAa,EACpB,OAAO,GAAE,sBAA2B,GACnC,OAAO,CAAC,IAAI,CAAC,CASf"}
@@ -0,0 +1,37 @@
1
+ import { identitySourceAvailable, readIdentityFrom, writeIdentityTo } from "./idbStore.js";
2
+ /**
3
+ * resolve the p2p identity for this origin.
4
+ *
5
+ * checks each fallback source in order (without creating any of them), then
6
+ * falls back to the local store. the first fallback source that already has
7
+ * an identity wins over the local store, even if the local store also has
8
+ * one - this lets a newly-added app adopt an existing sibling app's identity
9
+ * on first run.
10
+ */
11
+ export async function resolveIdentity(local, options = {}) {
12
+ for (const source of options.fallbackSources ?? []) {
13
+ if (!(await identitySourceAvailable(source)))
14
+ continue;
15
+ const identity = await readIdentityFrom(source);
16
+ if (identity)
17
+ return identity;
18
+ // source exists but has no identity yet - keep checking the remaining
19
+ // sources, then fall through to local
20
+ }
21
+ return local.get();
22
+ }
23
+ /**
24
+ * persist a p2p identity.
25
+ *
26
+ * writes to the first available fallback source (matching resolveIdentity's
27
+ * preference order); otherwise writes to the local store.
28
+ */
29
+ export async function persistIdentity(identity, local, options = {}) {
30
+ for (const source of options.fallbackSources ?? []) {
31
+ if (await identitySourceAvailable(source)) {
32
+ await writeIdentityTo(source, identity);
33
+ return;
34
+ }
35
+ }
36
+ await local.set(identity);
37
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=resolve.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resolve.test.d.ts","sourceRoot":"","sources":["../../src/identity/resolve.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,105 @@
1
+ import { IDBFactory } from "fake-indexeddb";
2
+ import { beforeEach, describe, expect, it } from "vitest";
3
+ import { createIdbIdentityStore } from "./idbStore.js";
4
+ import { persistIdentity, resolveIdentity } from "./resolve.js";
5
+ function makeIdentity(nodeId = "node-abc") {
6
+ return {
7
+ secret_key: new Uint8Array([1, 2, 3]),
8
+ node_id: nodeId,
9
+ created_at: 1000,
10
+ };
11
+ }
12
+ function makeLocalStore(initial = null) {
13
+ let stored = initial;
14
+ return {
15
+ get stored() {
16
+ return stored;
17
+ },
18
+ async get() {
19
+ return stored;
20
+ },
21
+ async set(identity) {
22
+ stored = identity;
23
+ },
24
+ };
25
+ }
26
+ beforeEach(() => {
27
+ globalThis.indexedDB = new IDBFactory();
28
+ });
29
+ describe("resolveIdentity", () => {
30
+ it("returns the local identity when there are no fallback sources", async () => {
31
+ const local = makeLocalStore(makeIdentity("node-local"));
32
+ expect((await resolveIdentity(local))?.node_id).toBe("node-local");
33
+ });
34
+ it("returns the local identity when configured fallback sources don't exist", async () => {
35
+ const local = makeLocalStore(makeIdentity("node-local"));
36
+ const result = await resolveIdentity(local, {
37
+ fallbackSources: [{ databaseName: "sibling-app", storeName: "app_state" }],
38
+ });
39
+ expect(result?.node_id).toBe("node-local");
40
+ });
41
+ it("prefers a fallback source's identity over the local one", async () => {
42
+ const siblingStore = createIdbIdentityStore({
43
+ databaseName: "sibling-app",
44
+ storeName: "app_state",
45
+ });
46
+ await siblingStore.set(makeIdentity("node-sibling"));
47
+ const local = makeLocalStore(makeIdentity("node-local"));
48
+ const result = await resolveIdentity(local, {
49
+ fallbackSources: [{ databaseName: "sibling-app", storeName: "app_state" }],
50
+ });
51
+ expect(result?.node_id).toBe("node-sibling");
52
+ });
53
+ it("falls back to local when the fallback source exists but has no identity yet", async () => {
54
+ // create the sibling db/store without writing an identity into it
55
+ const db = await new Promise((resolve, reject) => {
56
+ const req = indexedDB.open("sibling-app", 1);
57
+ req.onupgradeneeded = () => req.result.createObjectStore("app_state");
58
+ req.onsuccess = () => resolve(req.result);
59
+ req.onerror = () => reject(req.error);
60
+ });
61
+ db.close();
62
+ const local = makeLocalStore(makeIdentity("node-local"));
63
+ const result = await resolveIdentity(local, {
64
+ fallbackSources: [{ databaseName: "sibling-app", storeName: "app_state" }],
65
+ });
66
+ expect(result?.node_id).toBe("node-local");
67
+ });
68
+ it("checks fallback sources in order, taking the first hit", async () => {
69
+ const second = createIdbIdentityStore({ databaseName: "second-app", storeName: "app_state" });
70
+ await second.set(makeIdentity("node-second"));
71
+ const local = makeLocalStore(makeIdentity("node-local"));
72
+ const result = await resolveIdentity(local, {
73
+ fallbackSources: [
74
+ { databaseName: "first-app", storeName: "app_state" },
75
+ { databaseName: "second-app", storeName: "app_state" },
76
+ ],
77
+ });
78
+ expect(result?.node_id).toBe("node-second");
79
+ });
80
+ });
81
+ describe("persistIdentity", () => {
82
+ it("writes to the local store when there are no fallback sources", async () => {
83
+ const local = makeLocalStore(null);
84
+ await persistIdentity(makeIdentity("node-new"), local);
85
+ expect(local.stored?.node_id).toBe("node-new");
86
+ });
87
+ it("writes to the local store when configured fallback sources don't exist", async () => {
88
+ const local = makeLocalStore(null);
89
+ await persistIdentity(makeIdentity("node-new"), local, {
90
+ fallbackSources: [{ databaseName: "sibling-app", storeName: "app_state" }],
91
+ });
92
+ expect(local.stored?.node_id).toBe("node-new");
93
+ });
94
+ it("writes to an existing fallback source instead of local", async () => {
95
+ const sibling = createIdbIdentityStore({ databaseName: "sibling-app", storeName: "app_state" });
96
+ // seed the sibling db/store so it "exists" from persistIdentity's perspective
97
+ await sibling.set(makeIdentity("node-placeholder"));
98
+ const local = makeLocalStore(null);
99
+ await persistIdentity(makeIdentity("node-new"), local, {
100
+ fallbackSources: [{ databaseName: "sibling-app", storeName: "app_state" }],
101
+ });
102
+ expect(local.stored).toBeNull();
103
+ expect((await sibling.get())?.node_id).toBe("node-new");
104
+ });
105
+ });
@@ -0,0 +1,21 @@
1
+ export interface P2PIdentity {
2
+ /** 32-byte iroh secret key. */
3
+ secret_key: Uint8Array;
4
+ /** public node id (iroh public key, hex-encoded). */
5
+ node_id: string;
6
+ /** unix epoch millis when the identity was first created. */
7
+ created_at: number;
8
+ }
9
+ /**
10
+ * per-app identity persistence.
11
+ *
12
+ * apps supply their own storage behind this narrow interface - idb, a
13
+ * generic key/value store, secure platform storage, whatever fits. this
14
+ * package's idb-backed implementation (see idbStore.ts) is one convenient
15
+ * option, not a requirement.
16
+ */
17
+ export interface IdentityStore {
18
+ get(): Promise<P2PIdentity | null>;
19
+ set(identity: P2PIdentity): Promise<void>;
20
+ }
21
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/identity/types.ts"],"names":[],"mappings":"AAKA,MAAM,WAAW,WAAW;IAC1B,+BAA+B;IAC/B,UAAU,EAAE,UAAU,CAAC;IACvB,qDAAqD;IACrD,OAAO,EAAE,MAAM,CAAC;IAChB,6DAA6D;IAC7D,UAAU,EAAE,MAAM,CAAC;CACpB;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,aAAa;IAC5B,GAAG,IAAI,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,CAAC;IACnC,GAAG,CAAC,QAAQ,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC3C"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,22 @@
1
+ /** lock name used for node leader election. */
2
+ export declare const LOCK_NAME = "freqhole-iroh-node";
3
+ export interface AcquireNodeLeadershipOpts {
4
+ /** called when this tab becomes the node leader. */
5
+ onAcquired: () => void | Promise<void>;
6
+ /** called with state transitions; optional. */
7
+ onStateChange?: (state: "leader" | "waiting" | "unsupported") => void;
8
+ }
9
+ /**
10
+ * attempt to acquire exclusive leadership of the p2p node.
11
+ *
12
+ * behavior:
13
+ * - tries ifAvailable first; if the lock is free, holds it and calls onAcquired
14
+ * - if already held, reports "waiting" and queues a normal request with an AbortController
15
+ * so leadership transfers when the current leader tab closes
16
+ * - if navigator.locks is absent (e.g. non-secure context), reports "unsupported"
17
+ * and calls onAcquired immediately (single-tab fallback)
18
+ *
19
+ * returns a cancel/release function; call it to give up leadership or cancel the wait.
20
+ */
21
+ export declare function acquireNodeLeadership(opts: AcquireNodeLeadershipOpts): () => void;
22
+ //# sourceMappingURL=webLocks.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"webLocks.d.ts","sourceRoot":"","sources":["../../src/identity/webLocks.ts"],"names":[],"mappings":"AAGA,+CAA+C;AAC/C,eAAO,MAAM,SAAS,uBAAuB,CAAC;AAE9C,MAAM,WAAW,yBAAyB;IACxC,oDAAoD;IACpD,UAAU,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACvC,+CAA+C;IAC/C,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,QAAQ,GAAG,SAAS,GAAG,aAAa,KAAK,IAAI,CAAC;CACvE;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,yBAAyB,GAAG,MAAM,IAAI,CAqDjF"}
@@ -0,0 +1,67 @@
1
+ // web locks leader election for a p2p node.
2
+ // only one tab per origin should run the node at a time.
3
+ /** lock name used for node leader election. */
4
+ export const LOCK_NAME = "freqhole-iroh-node";
5
+ /**
6
+ * attempt to acquire exclusive leadership of the p2p node.
7
+ *
8
+ * behavior:
9
+ * - tries ifAvailable first; if the lock is free, holds it and calls onAcquired
10
+ * - if already held, reports "waiting" and queues a normal request with an AbortController
11
+ * so leadership transfers when the current leader tab closes
12
+ * - if navigator.locks is absent (e.g. non-secure context), reports "unsupported"
13
+ * and calls onAcquired immediately (single-tab fallback)
14
+ *
15
+ * returns a cancel/release function; call it to give up leadership or cancel the wait.
16
+ */
17
+ export function acquireNodeLeadership(opts) {
18
+ const { onAcquired, onStateChange } = opts;
19
+ // single-tab fallback for environments without web locks
20
+ if (typeof navigator === "undefined" || !navigator.locks) {
21
+ onStateChange?.("unsupported");
22
+ void Promise.resolve().then(() => onAcquired());
23
+ return () => { };
24
+ }
25
+ let released = false;
26
+ // resolving this promise releases the held lock
27
+ let releaseLock = null;
28
+ // used to cancel the waiting queue request
29
+ const waitAbort = new AbortController();
30
+ function cancel() {
31
+ released = true;
32
+ waitAbort.abort();
33
+ releaseLock?.();
34
+ }
35
+ // try to grab the lock immediately without queuing
36
+ navigator.locks.request(LOCK_NAME, { ifAvailable: true }, async (lock) => {
37
+ if (released)
38
+ return;
39
+ if (lock !== null) {
40
+ // we are now the leader - call onAcquired, then hold the lock until released
41
+ onStateChange?.("leader");
42
+ await onAcquired();
43
+ await new Promise((resolve) => {
44
+ releaseLock = resolve;
45
+ });
46
+ }
47
+ else {
48
+ // another tab holds the lock - queue up for leadership
49
+ onStateChange?.("waiting");
50
+ try {
51
+ await navigator.locks.request(LOCK_NAME, { signal: waitAbort.signal }, async () => {
52
+ if (released)
53
+ return;
54
+ onStateChange?.("leader");
55
+ await onAcquired();
56
+ await new Promise((resolve) => {
57
+ releaseLock = resolve;
58
+ });
59
+ });
60
+ }
61
+ catch {
62
+ // aborted by cancel() or context destroyed - not an error
63
+ }
64
+ }
65
+ });
66
+ return cancel;
67
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=webLocks.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"webLocks.test.d.ts","sourceRoot":"","sources":["../../src/identity/webLocks.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,125 @@
1
+ import { afterEach, describe, expect, it } from "vitest";
2
+ import { acquireNodeLeadership, LOCK_NAME } from "./webLocks.js";
3
+ /** navigator is read-only on globalThis in node; use defineProperty to stub it. */
4
+ function stubNavigator(value) {
5
+ const descriptor = Object.getOwnPropertyDescriptor(globalThis, "navigator");
6
+ Object.defineProperty(globalThis, "navigator", {
7
+ configurable: true,
8
+ writable: true,
9
+ value,
10
+ });
11
+ return () => {
12
+ if (descriptor) {
13
+ Object.defineProperty(globalThis, "navigator", descriptor);
14
+ }
15
+ else {
16
+ Object.defineProperty(globalThis, "navigator", {
17
+ configurable: true,
18
+ writable: true,
19
+ value: undefined,
20
+ });
21
+ }
22
+ };
23
+ }
24
+ const restores = [];
25
+ afterEach(() => {
26
+ while (restores.length)
27
+ restores.pop()?.();
28
+ });
29
+ describe("acquireNodeLeadership", () => {
30
+ it("exports the expected lock name", () => {
31
+ expect(LOCK_NAME).toBe("freqhole-iroh-node");
32
+ });
33
+ it("calls onAcquired immediately when navigator.locks is absent", async () => {
34
+ restores.push(stubNavigator(undefined));
35
+ const states = [];
36
+ let acquired = false;
37
+ const cancel = acquireNodeLeadership({
38
+ onAcquired: () => {
39
+ acquired = true;
40
+ },
41
+ onStateChange: (s) => states.push(s),
42
+ });
43
+ await Promise.resolve();
44
+ expect(states[0]).toBe("unsupported");
45
+ expect(acquired).toBe(true);
46
+ cancel();
47
+ });
48
+ it("calls onAcquired when the lock is granted immediately", async () => {
49
+ restores.push(stubNavigator({
50
+ locks: {
51
+ request: async (_name, opts, cb) => {
52
+ if (opts.ifAvailable) {
53
+ await cb({ name: _name, mode: "exclusive" });
54
+ }
55
+ },
56
+ },
57
+ }));
58
+ const states = [];
59
+ let acquired = false;
60
+ const cancel = acquireNodeLeadership({
61
+ onAcquired: () => {
62
+ acquired = true;
63
+ },
64
+ onStateChange: (s) => states.push(s),
65
+ });
66
+ await new Promise((r) => setTimeout(r, 10));
67
+ cancel();
68
+ expect(states).toContain("leader");
69
+ expect(acquired).toBe(true);
70
+ });
71
+ it("reports waiting when the lock is held elsewhere, then transfers leadership", async () => {
72
+ restores.push(stubNavigator({
73
+ locks: {
74
+ request: async (_name, opts, cb) => {
75
+ if (opts.ifAvailable) {
76
+ // lock held elsewhere
77
+ await cb(null);
78
+ }
79
+ else {
80
+ // queued request: resolve once the current leader "closes"
81
+ await new Promise((resolve, reject) => {
82
+ opts.signal?.addEventListener("abort", () => reject(new DOMException("aborted", "AbortError")));
83
+ setTimeout(() => resolve(cb(null)), 5);
84
+ });
85
+ }
86
+ },
87
+ },
88
+ }));
89
+ const states = [];
90
+ const cancel = acquireNodeLeadership({
91
+ onAcquired: () => { },
92
+ onStateChange: (s) => states.push(s),
93
+ });
94
+ await new Promise((r) => setTimeout(r, 10));
95
+ expect(states).toContain("waiting");
96
+ cancel();
97
+ await new Promise((r) => setTimeout(r, 10));
98
+ });
99
+ it("cancel() aborts a pending wait without throwing", async () => {
100
+ restores.push(stubNavigator({
101
+ locks: {
102
+ request: async (_name, opts, cb) => {
103
+ if (opts.ifAvailable) {
104
+ await cb(null);
105
+ }
106
+ else {
107
+ await new Promise((_, reject) => {
108
+ opts.signal?.addEventListener("abort", () => reject(new DOMException("aborted", "AbortError")));
109
+ });
110
+ }
111
+ },
112
+ },
113
+ }));
114
+ const states = [];
115
+ const cancel = acquireNodeLeadership({
116
+ onAcquired: () => { },
117
+ onStateChange: (s) => states.push(s),
118
+ });
119
+ await new Promise((r) => setTimeout(r, 10));
120
+ expect(states).toContain("waiting");
121
+ cancel();
122
+ await new Promise((r) => setTimeout(r, 10));
123
+ // no unhandled rejection - the AbortError is swallowed internally
124
+ });
125
+ });
@@ -0,0 +1,2 @@
1
+ export declare const HARUSPEX_VERSION = "0.1.0";
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,gBAAgB,UAAU,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1 @@
1
+ export const HARUSPEX_VERSION = "0.1.0";
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=index.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.test.d.ts","sourceRoot":"","sources":["../src/index.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,7 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { HARUSPEX_VERSION } from "./index.js";
3
+ describe("haruspex skeleton", () => {
4
+ it("exports something so vitest + tsc have a real target", () => {
5
+ expect(HARUSPEX_VERSION).toBe("0.1.0");
6
+ });
7
+ });
@@ -0,0 +1,10 @@
1
+ export type { CreateKnockInput, KnockDecision, KnockDirection, KnockPolicy, KnockPolicyResult, KnockRecord, KnockRequest, KnockScope, KnockStatus, KnockStatusReply, KnockStore, KnockTransport, } from "./types.js";
2
+ export { KnockConflictError } from "./types.js";
3
+ export type { KnockStoreOptions } from "./store.js";
4
+ export { createIdbKnockStore } from "./store.js";
5
+ export type { CheckKnockStatusOptions } from "./requester.js";
6
+ export { checkKnockStatus, sendKnock } from "./requester.js";
7
+ export { acceptKnock, denyKnock } from "./responder.js";
8
+ export type { PendingKnockCheckerDeps } from "./pending-checker.js";
9
+ export { checkPendingKnocks } from "./pending-checker.js";
10
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/knock/index.ts"],"names":[],"mappings":"AAAA,YAAY,EACV,gBAAgB,EAChB,aAAa,EACb,cAAc,EACd,WAAW,EACX,iBAAiB,EACjB,WAAW,EACX,YAAY,EACZ,UAAU,EACV,WAAW,EACX,gBAAgB,EAChB,UAAU,EACV,cAAc,GACf,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAEhD,YAAY,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AACpD,OAAO,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC;AAEjD,YAAY,EAAE,uBAAuB,EAAE,MAAM,gBAAgB,CAAC;AAC9D,OAAO,EAAE,gBAAgB,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAE7D,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAExD,YAAY,EAAE,uBAAuB,EAAE,MAAM,sBAAsB,CAAC;AACpE,OAAO,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC"}
@@ -0,0 +1,5 @@
1
+ export { KnockConflictError } from "./types.js";
2
+ export { createIdbKnockStore } from "./store.js";
3
+ export { checkKnockStatus, sendKnock } from "./requester.js";
4
+ export { acceptKnock, denyKnock } from "./responder.js";
5
+ export { checkPendingKnocks } from "./pending-checker.js";
@@ -0,0 +1,19 @@
1
+ import type { KnockRecord, KnockStore, KnockTransport } from "./types.js";
2
+ export interface PendingKnockCheckerDeps {
3
+ store: KnockStore;
4
+ transport: KnockTransport;
5
+ /**
6
+ * fired once per outbound knock that has just resolved to accepted - a
7
+ * toast/notification hook is the typical use. this sweep never acts on
8
+ * the result itself beyond persisting the decision.
9
+ */
10
+ onAccepted: (record: KnockRecord) => void | Promise<void>;
11
+ onDenied?: (record: KnockRecord) => void | Promise<void>;
12
+ }
13
+ /**
14
+ * re-checks every pending outbound knock and fires the injected callback
15
+ * for any that has resolved since the app was last open. peers that are
16
+ * unreachable are left pending, retried on the next sweep.
17
+ */
18
+ export declare function checkPendingKnocks(deps: PendingKnockCheckerDeps): Promise<void>;
19
+ //# sourceMappingURL=pending-checker.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pending-checker.d.ts","sourceRoot":"","sources":["../../src/knock/pending-checker.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,WAAW,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAE1E,MAAM,WAAW,uBAAuB;IACtC,KAAK,EAAE,UAAU,CAAC;IAClB,SAAS,EAAE,cAAc,CAAC;IAC1B;;;;OAIG;IACH,UAAU,EAAE,CAAC,MAAM,EAAE,WAAW,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1D,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,WAAW,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC1D;AAED;;;;GAIG;AACH,wBAAsB,kBAAkB,CAAC,IAAI,EAAE,uBAAuB,GAAG,OAAO,CAAC,IAAI,CAAC,CAmBrF"}
@@ -0,0 +1,27 @@
1
+ // boot-time sweep that promotes any outbound knock that has been accepted
2
+ // (or denied) while the app was closed.
3
+ import { checkKnockStatus } from "./requester.js";
4
+ /**
5
+ * re-checks every pending outbound knock and fires the injected callback
6
+ * for any that has resolved since the app was last open. peers that are
7
+ * unreachable are left pending, retried on the next sweep.
8
+ */
9
+ export async function checkPendingKnocks(deps) {
10
+ const pending = await deps.store.listPending();
11
+ const outbound = pending.filter((record) => record.direction === "outbound");
12
+ await Promise.all(outbound.map(async (record) => {
13
+ let updated;
14
+ try {
15
+ updated = await checkKnockStatus(deps.store, deps.transport, record.id);
16
+ }
17
+ catch {
18
+ return;
19
+ }
20
+ if (updated.status === "accepted") {
21
+ await deps.onAccepted(updated);
22
+ }
23
+ else if (updated.status === "denied") {
24
+ await deps.onDenied?.(updated);
25
+ }
26
+ }));
27
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=pending-checker.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pending-checker.test.d.ts","sourceRoot":"","sources":["../../src/knock/pending-checker.test.ts"],"names":[],"mappings":""}