@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,56 @@
1
+ // role-hierarchy privilege checks + a generic route-auth-style access rule
2
+ // evaluator, parameterized over an injected RoleHierarchy rather than any
3
+ // one app's fixed role enum. a route-auth binding over a specific app's
4
+ // codegen'd auth metadata (e.g. tomb's `RouteAuth`/`routes` types) stays
5
+ // app-side; this subpath only owns the privilege-comparison logic itself.
6
+ /**
7
+ * does `actualRole` have at least the privilege of `requiredRole`? lower
8
+ * hierarchy numbers are more privileged, so this holds when `actualRole`'s
9
+ * level is less than or equal to `requiredRole`'s.
10
+ */
11
+ export function hasAtLeastRole(hierarchy, actualRole, requiredRole) {
12
+ return hierarchy[actualRole] <= hierarchy[requiredRole];
13
+ }
14
+ /** is `userId` strictly the resource's owner? */
15
+ export function isOwner(userId, ownerId) {
16
+ return ownerId !== null && userId === ownerId;
17
+ }
18
+ /**
19
+ * is `userId` the resource's owner, or does `actualRole` have at least
20
+ * `requiredRole`'s privilege? owner always passes regardless of role.
21
+ */
22
+ export function canAccessOwnerOr(hierarchy, userId, ownerId, actualRole, requiredRole) {
23
+ if (isOwner(userId, ownerId))
24
+ return true;
25
+ return hasAtLeastRole(hierarchy, actualRole, requiredRole);
26
+ }
27
+ /** evaluate any `AccessRule` against a caller context. */
28
+ export function canAccess(hierarchy, rule, ctx) {
29
+ switch (rule.type) {
30
+ case "public":
31
+ return true;
32
+ case "authenticated":
33
+ return ctx.userRole !== null;
34
+ case "role":
35
+ return ctx.userRole !== null && hasAtLeastRole(hierarchy, ctx.userRole, rule.role);
36
+ case "owner":
37
+ return ctx.userId !== null && isOwner(ctx.userId, ctx.ownerId);
38
+ case "owner_or":
39
+ return (ctx.userId !== null &&
40
+ ctx.userRole !== null &&
41
+ canAccessOwnerOr(hierarchy, ctx.userId, ctx.ownerId, ctx.userRole, rule.role));
42
+ }
43
+ }
44
+ /**
45
+ * bind a role hierarchy once and get back plain functions that no longer
46
+ * need it threaded through every call - convenient for a caller that only
47
+ * ever checks against one hierarchy for its whole lifetime.
48
+ */
49
+ export function bindRoleHierarchy(hierarchy) {
50
+ return {
51
+ hasAtLeastRole: (actualRole, requiredRole) => hasAtLeastRole(hierarchy, actualRole, requiredRole),
52
+ canAccessOwnerOr: (userId, ownerId, actualRole, requiredRole) => canAccessOwnerOr(hierarchy, userId, ownerId, actualRole, requiredRole),
53
+ isOwner,
54
+ canAccess: (rule, ctx) => canAccess(hierarchy, rule, ctx),
55
+ };
56
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=access.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"access.test.d.ts","sourceRoot":"","sources":["../../src/permissions/access.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,72 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { FREQHOLE_ROLE_HIERARCHY } from "./role-table.js";
3
+ import { bindRoleHierarchy, canAccess, canAccessOwnerOr, hasAtLeastRole, isOwner, } from "./access.js";
4
+ describe("hasAtLeastRole", () => {
5
+ it("holds when the actual role is at least as privileged", () => {
6
+ expect(hasAtLeastRole(FREQHOLE_ROLE_HIERARCHY, "admin", "member")).toBe(true);
7
+ expect(hasAtLeastRole(FREQHOLE_ROLE_HIERARCHY, "root", "root")).toBe(true);
8
+ });
9
+ it("fails when the actual role is less privileged", () => {
10
+ expect(hasAtLeastRole(FREQHOLE_ROLE_HIERARCHY, "viewer", "member")).toBe(false);
11
+ });
12
+ it("works against a caller-supplied role table with its own vocabulary", () => {
13
+ const hierarchy = { owner: 0, editor: 10, reader: 20 };
14
+ expect(hasAtLeastRole(hierarchy, "editor", "reader")).toBe(true);
15
+ expect(hasAtLeastRole(hierarchy, "reader", "editor")).toBe(false);
16
+ });
17
+ });
18
+ describe("isOwner", () => {
19
+ it("is true only when the ids match and ownerId is set", () => {
20
+ expect(isOwner("u1", "u1")).toBe(true);
21
+ expect(isOwner("u1", "u2")).toBe(false);
22
+ expect(isOwner("u1", null)).toBe(false);
23
+ });
24
+ });
25
+ describe("canAccessOwnerOr", () => {
26
+ it("passes for the owner regardless of role", () => {
27
+ expect(canAccessOwnerOr(FREQHOLE_ROLE_HIERARCHY, "u1", "u1", "viewer", "admin")).toBe(true);
28
+ });
29
+ it("falls back to the role check for a non-owner", () => {
30
+ expect(canAccessOwnerOr(FREQHOLE_ROLE_HIERARCHY, "u1", "u2", "admin", "admin")).toBe(true);
31
+ expect(canAccessOwnerOr(FREQHOLE_ROLE_HIERARCHY, "u1", "u2", "viewer", "admin")).toBe(false);
32
+ });
33
+ });
34
+ describe("canAccess", () => {
35
+ const ctx = (userRole, userId, ownerId) => ({
36
+ userRole,
37
+ userId,
38
+ ownerId,
39
+ });
40
+ it("public always passes", () => {
41
+ expect(canAccess(FREQHOLE_ROLE_HIERARCHY, { type: "public" }, ctx(null, null, null))).toBe(true);
42
+ });
43
+ it("authenticated requires a non-null role", () => {
44
+ expect(canAccess(FREQHOLE_ROLE_HIERARCHY, { type: "authenticated" }, ctx("viewer", "u1", null))).toBe(true);
45
+ expect(canAccess(FREQHOLE_ROLE_HIERARCHY, { type: "authenticated" }, ctx(null, null, null))).toBe(false);
46
+ });
47
+ it("role requires at least the given privilege", () => {
48
+ const rule = { type: "role", role: "admin" };
49
+ expect(canAccess(FREQHOLE_ROLE_HIERARCHY, rule, ctx("root", "u1", null))).toBe(true);
50
+ expect(canAccess(FREQHOLE_ROLE_HIERARCHY, rule, ctx("member", "u1", null))).toBe(false);
51
+ });
52
+ it("owner requires the caller to be the resource owner", () => {
53
+ const rule = { type: "owner" };
54
+ expect(canAccess(FREQHOLE_ROLE_HIERARCHY, rule, ctx("viewer", "u1", "u1"))).toBe(true);
55
+ expect(canAccess(FREQHOLE_ROLE_HIERARCHY, rule, ctx("viewer", "u1", "u2"))).toBe(false);
56
+ });
57
+ it("owner_or passes for the owner or a sufficiently privileged role", () => {
58
+ const rule = { type: "owner_or", role: "admin" };
59
+ expect(canAccess(FREQHOLE_ROLE_HIERARCHY, rule, ctx("viewer", "u1", "u1"))).toBe(true);
60
+ expect(canAccess(FREQHOLE_ROLE_HIERARCHY, rule, ctx("admin", "u1", "u2"))).toBe(true);
61
+ expect(canAccess(FREQHOLE_ROLE_HIERARCHY, rule, ctx("member", "u1", "u2"))).toBe(false);
62
+ });
63
+ });
64
+ describe("bindRoleHierarchy", () => {
65
+ it("bundles the same behavior without re-threading the hierarchy", () => {
66
+ const checks = bindRoleHierarchy(FREQHOLE_ROLE_HIERARCHY);
67
+ expect(checks.hasAtLeastRole("admin", "member")).toBe(true);
68
+ expect(checks.isOwner("u1", "u1")).toBe(true);
69
+ expect(checks.canAccessOwnerOr("u1", "u2", "admin", "admin")).toBe(true);
70
+ expect(checks.canAccess({ type: "role", role: "member" }, { userRole: "admin", userId: "u1", ownerId: null })).toBe(true);
71
+ });
72
+ });
@@ -0,0 +1,5 @@
1
+ export type { FreqholeRole, RoleHierarchy } from "./role-table.js";
2
+ export { FREQHOLE_ROLE_HIERARCHY } from "./role-table.js";
3
+ export type { AccessContext, AccessRule, RoleChecks } from "./access.js";
4
+ export { bindRoleHierarchy, canAccess, canAccessOwnerOr, hasAtLeastRole, isOwner, } from "./access.js";
5
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/permissions/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AACnE,OAAO,EAAE,uBAAuB,EAAE,MAAM,iBAAiB,CAAC;AAE1D,YAAY,EAAE,aAAa,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzE,OAAO,EACL,iBAAiB,EACjB,SAAS,EACT,gBAAgB,EAChB,cAAc,EACd,OAAO,GACR,MAAM,aAAa,CAAC"}
@@ -0,0 +1,2 @@
1
+ export { FREQHOLE_ROLE_HIERARCHY } from "./role-table.js";
2
+ export { bindRoleHierarchy, canAccess, canAccessOwnerOr, hasAtLeastRole, isOwner, } from "./access.js";
@@ -0,0 +1,11 @@
1
+ /** a role hierarchy: role name -> privilege level (lower = more privileged). */
2
+ export type RoleHierarchy<TRole extends string> = Record<TRole, number>;
3
+ /** the role names haruspex's own grant model uses (see the rust `Role` enum). */
4
+ export type FreqholeRole = "root" | "admin" | "member" | "viewer";
5
+ /**
6
+ * the default freqhole role hierarchy - root above admin above member above
7
+ * viewer. a caller with its own role vocabulary supplies its own
8
+ * `RoleHierarchy` instead; nothing in this subpath requires this one.
9
+ */
10
+ export declare const FREQHOLE_ROLE_HIERARCHY: RoleHierarchy<FreqholeRole>;
11
+ //# sourceMappingURL=role-table.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"role-table.d.ts","sourceRoot":"","sources":["../../src/permissions/role-table.ts"],"names":[],"mappings":"AAMA,gFAAgF;AAChF,MAAM,MAAM,aAAa,CAAC,KAAK,SAAS,MAAM,IAAI,MAAM,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC;AAExE,iFAAiF;AACjF,MAAM,MAAM,YAAY,GAAG,MAAM,GAAG,OAAO,GAAG,QAAQ,GAAG,QAAQ,CAAC;AAElE;;;;GAIG;AACH,eAAO,MAAM,uBAAuB,EAAE,aAAa,CAAC,YAAY,CAK/D,CAAC"}
@@ -0,0 +1,16 @@
1
+ // role hierarchy tables: a plain mapping from role name to a privilege
2
+ // level, lower number = higher privilege. every function in ./permissions
3
+ // takes one of these as an explicit argument rather than assuming any
4
+ // fixed set of role names, so an app with its own role vocabulary can use
5
+ // the same privilege-comparison logic haruspex ships by default.
6
+ /**
7
+ * the default freqhole role hierarchy - root above admin above member above
8
+ * viewer. a caller with its own role vocabulary supplies its own
9
+ * `RoleHierarchy` instead; nothing in this subpath requires this one.
10
+ */
11
+ export const FREQHOLE_ROLE_HIERARCHY = {
12
+ root: 0,
13
+ admin: 10,
14
+ member: 20,
15
+ viewer: 30,
16
+ };
@@ -0,0 +1,67 @@
1
+ import { type FriendzMessage } from "./codec.js";
2
+ /** how often to send heartbeat pings to known peers (ms). */
3
+ export declare const DEFAULT_HEARTBEAT_INTERVAL_MS = 30000;
4
+ /** time after last heartbeat before marking a peer offline (ms). */
5
+ export declare const DEFAULT_HEARTBEAT_TIMEOUT_MS = 90000;
6
+ /**
7
+ * the structural surface this subpath needs from a bidirectional byte
8
+ * stream to one peer - matches midden's wasm-exported `BiStream`.
9
+ * `read_message`/`write_message` are already message-boundary-framed by
10
+ * the underlying transport, so the codec's unframed encodeMessage/
11
+ * decodeMessage are what this client uses over a BiStreamLike (the
12
+ * length-delimited FrameReader in codec.ts is for transports without that
13
+ * built-in framing).
14
+ */
15
+ export interface BiStreamLike {
16
+ peer_node_id(): string;
17
+ alpn(): string;
18
+ write_message(data: Uint8Array): Promise<void>;
19
+ read_message(): Promise<Uint8Array | null>;
20
+ close(): void;
21
+ }
22
+ /** the structural surface this subpath needs from a p2p node to open streams. */
23
+ export interface MiddenNodeLike {
24
+ node_id(): string;
25
+ open_bi(peerAddr: string, alpn: string): Promise<BiStreamLike>;
26
+ }
27
+ export interface FriendzClientOptions {
28
+ /** resolves to the node used to open outbound streams. */
29
+ getNode: () => Promise<MiddenNodeLike>;
30
+ /** the alpn to open outbound streams on. default "freqhole-friendz/1". */
31
+ alpn?: string;
32
+ localNodeId: string;
33
+ localUsername: string;
34
+ heartbeatIntervalMs?: number;
35
+ heartbeatTimeoutMs?: number;
36
+ /** called for every decoded message received from any peer. */
37
+ onMessage?: (message: FriendzMessage, fromNodeId: string) => void;
38
+ /** called when a peer transitions from offline/unknown to online. */
39
+ onPeerBecameOnline?: (nodeId: string) => void;
40
+ /** called when a peer times out or announces going offline. */
41
+ onPeerWentOffline?: (nodeId: string) => void;
42
+ /** called when a received message's bytes fail to decode. never throws. */
43
+ onDecodeError?: (error: unknown, fromNodeId: string) => void;
44
+ }
45
+ export interface FriendzClient {
46
+ /** send a message to a peer, opening a stream if one isn't already open. */
47
+ sendMessage(peerNodeId: string, message: FriendzMessage): Promise<void>;
48
+ /** hand an accepted inbound stream to the client - starts its read loop. */
49
+ handleIncomingStream(stream: BiStreamLike): void;
50
+ /** start the periodic heartbeat. getPeerNodeIds is re-read each tick. */
51
+ startHeartbeat(getPeerNodeIds: () => string[]): void;
52
+ /** stop the periodic heartbeat. */
53
+ stopHeartbeat(): void;
54
+ /** fire-and-forget an offline-announcement to every currently-online peer. */
55
+ announceOffline(): void;
56
+ /** is this peer considered online (a heartbeat arrived within the timeout)? */
57
+ isOnline(nodeId: string): boolean;
58
+ /** every peer node id currently considered online. */
59
+ getOnlinePeers(): string[];
60
+ /** subscribe to online/offline transitions. returns an unsubscribe function. */
61
+ onOnlineChange(handler: () => void): () => void;
62
+ /** close every open stream, stop timers, release listeners. */
63
+ destroy(): void;
64
+ }
65
+ /** create a FriendzClient bound to the given transport + identity. */
66
+ export declare function createFriendzClient(options: FriendzClientOptions): FriendzClient;
67
+ //# sourceMappingURL=client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/protocol/client.ts"],"names":[],"mappings":"AAMA,OAAO,EAAgC,KAAK,cAAc,EAAE,MAAM,YAAY,CAAC;AAG/E,6DAA6D;AAC7D,eAAO,MAAM,6BAA6B,QAAS,CAAC;AAEpD,oEAAoE;AACpE,eAAO,MAAM,4BAA4B,QAAS,CAAC;AAEnD;;;;;;;;GAQG;AACH,MAAM,WAAW,YAAY;IAC3B,YAAY,IAAI,MAAM,CAAC;IACvB,IAAI,IAAI,MAAM,CAAC;IACf,aAAa,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/C,YAAY,IAAI,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC,CAAC;IAC3C,KAAK,IAAI,IAAI,CAAC;CACf;AAED,iFAAiF;AACjF,MAAM,WAAW,cAAc;IAC7B,OAAO,IAAI,MAAM,CAAC;IAClB,OAAO,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC;CAChE;AAED,MAAM,WAAW,oBAAoB;IACnC,0DAA0D;IAC1D,OAAO,EAAE,MAAM,OAAO,CAAC,cAAc,CAAC,CAAC;IACvC,0EAA0E;IAC1E,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,aAAa,EAAE,MAAM,CAAC;IACtB,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,+DAA+D;IAC/D,SAAS,CAAC,EAAE,CAAC,OAAO,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,KAAK,IAAI,CAAC;IAClE,qEAAqE;IACrE,kBAAkB,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;IAC9C,+DAA+D;IAC/D,iBAAiB,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;IAC7C,2EAA2E;IAC3E,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,KAAK,IAAI,CAAC;CAC9D;AAED,MAAM,WAAW,aAAa;IAC5B,4EAA4E;IAC5E,WAAW,CAAC,UAAU,EAAE,MAAM,EAAE,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACxE,4EAA4E;IAC5E,oBAAoB,CAAC,MAAM,EAAE,YAAY,GAAG,IAAI,CAAC;IACjD,yEAAyE;IACzE,cAAc,CAAC,cAAc,EAAE,MAAM,MAAM,EAAE,GAAG,IAAI,CAAC;IACrD,mCAAmC;IACnC,aAAa,IAAI,IAAI,CAAC;IACtB,8EAA8E;IAC9E,eAAe,IAAI,IAAI,CAAC;IACxB,+EAA+E;IAC/E,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC;IAClC,sDAAsD;IACtD,cAAc,IAAI,MAAM,EAAE,CAAC;IAC3B,gFAAgF;IAChF,cAAc,CAAC,OAAO,EAAE,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC;IAChD,+DAA+D;IAC/D,OAAO,IAAI,IAAI,CAAC;CACjB;AAcD,sEAAsE;AACtE,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,oBAAoB,GAAG,aAAa,CAmLhF"}
@@ -0,0 +1,195 @@
1
+ // FriendzClient: the presence engine for the unified friendz protocol -
2
+ // per-peer send streams, heartbeats, and online/offline tracking. the
3
+ // transport is an injected structural type (BiStreamLike/MiddenNodeLike),
4
+ // matching the shape of midden's own wasm-exported bidirectional stream
5
+ // api - this subpath never imports midden itself.
6
+ import { encodeMessage, decodeMessage } from "./codec.js";
7
+ /** how often to send heartbeat pings to known peers (ms). */
8
+ export const DEFAULT_HEARTBEAT_INTERVAL_MS = 30_000;
9
+ /** time after last heartbeat before marking a peer offline (ms). */
10
+ export const DEFAULT_HEARTBEAT_TIMEOUT_MS = 90_000;
11
+ function coreMessage(message) {
12
+ return { kind: "core", message };
13
+ }
14
+ function heartbeatMessage(nodeId, username) {
15
+ return coreMessage({ type: "heartbeat", v: 1, nodeId, username });
16
+ }
17
+ function offlineAnnouncementMessage(nodeId) {
18
+ return coreMessage({ type: "offline-announcement", v: 1, nodeId });
19
+ }
20
+ /** create a FriendzClient bound to the given transport + identity. */
21
+ export function createFriendzClient(options) {
22
+ const alpn = options.alpn ?? "freqhole-friendz/1";
23
+ const heartbeatIntervalMs = options.heartbeatIntervalMs ?? DEFAULT_HEARTBEAT_INTERVAL_MS;
24
+ const heartbeatTimeoutMs = options.heartbeatTimeoutMs ?? DEFAULT_HEARTBEAT_TIMEOUT_MS;
25
+ const streams = new Map();
26
+ const pendingConnections = new Map();
27
+ const lastSeen = new Map();
28
+ const onlineChangeListeners = new Set();
29
+ let heartbeatTimer = null;
30
+ let destroyed = false;
31
+ function emitOnlineChange() {
32
+ for (const handler of onlineChangeListeners)
33
+ handler();
34
+ }
35
+ function markOnline(nodeId) {
36
+ const wasOnline = isOnline(nodeId);
37
+ lastSeen.set(nodeId, Date.now());
38
+ emitOnlineChange();
39
+ if (!wasOnline)
40
+ options.onPeerBecameOnline?.(nodeId);
41
+ }
42
+ function markOffline(nodeId) {
43
+ if (!lastSeen.has(nodeId))
44
+ return;
45
+ lastSeen.delete(nodeId);
46
+ emitOnlineChange();
47
+ options.onPeerWentOffline?.(nodeId);
48
+ }
49
+ function isOnline(nodeId) {
50
+ const seenAt = lastSeen.get(nodeId);
51
+ if (seenAt === undefined)
52
+ return false;
53
+ return Date.now() - seenAt < heartbeatTimeoutMs;
54
+ }
55
+ function getOnlinePeers() {
56
+ const now = Date.now();
57
+ const online = [];
58
+ for (const [nodeId, seenAt] of lastSeen) {
59
+ if (now - seenAt < heartbeatTimeoutMs)
60
+ online.push(nodeId);
61
+ }
62
+ return online;
63
+ }
64
+ async function readLoop(peerId, stream) {
65
+ try {
66
+ while (!destroyed) {
67
+ const data = await stream.read_message();
68
+ if (!data)
69
+ break;
70
+ try {
71
+ const message = decodeMessage(data);
72
+ if (message.kind === "core" && message.message.type === "heartbeat") {
73
+ markOnline(peerId);
74
+ }
75
+ if (message.kind === "core" && message.message.type === "offline-announcement") {
76
+ markOffline(message.message.nodeId);
77
+ }
78
+ options.onMessage?.(message, peerId);
79
+ }
80
+ catch (err) {
81
+ options.onDecodeError?.(err, peerId);
82
+ }
83
+ }
84
+ }
85
+ catch {
86
+ // the stream errored (peer gone, transport reset). if it was replaced
87
+ // by a newer stream already, that's expected - not this peer going
88
+ // offline, just a reconnect racing this loop's own close.
89
+ const wasReplaced = streams.get(peerId) !== stream;
90
+ if (!destroyed && !wasReplaced)
91
+ markOffline(peerId);
92
+ }
93
+ finally {
94
+ if (streams.get(peerId) === stream)
95
+ streams.delete(peerId);
96
+ }
97
+ }
98
+ async function openStream(peerNodeId) {
99
+ const node = await options.getNode();
100
+ const stream = await node.open_bi(peerNodeId, alpn);
101
+ streams.set(peerNodeId, stream);
102
+ void readLoop(peerNodeId, stream);
103
+ return stream;
104
+ }
105
+ async function getOrOpenStream(peerNodeId) {
106
+ const existing = streams.get(peerNodeId);
107
+ if (existing)
108
+ return existing;
109
+ let pending = pendingConnections.get(peerNodeId);
110
+ if (!pending) {
111
+ pending = openStream(peerNodeId);
112
+ pendingConnections.set(peerNodeId, pending);
113
+ }
114
+ try {
115
+ return await pending;
116
+ }
117
+ finally {
118
+ pendingConnections.delete(peerNodeId);
119
+ }
120
+ }
121
+ async function sendMessage(peerNodeId, message) {
122
+ const stream = await getOrOpenStream(peerNodeId);
123
+ await stream.write_message(encodeMessage(message));
124
+ }
125
+ function handleIncomingStream(stream) {
126
+ const peerId = stream.peer_node_id();
127
+ const existing = streams.get(peerId);
128
+ if (existing)
129
+ existing.close();
130
+ streams.set(peerId, stream);
131
+ void readLoop(peerId, stream);
132
+ }
133
+ function startHeartbeat(getPeerNodeIds) {
134
+ stopHeartbeat();
135
+ const tick = async (peerIds) => {
136
+ const msg = heartbeatMessage(options.localNodeId, options.localUsername);
137
+ for (const peerId of peerIds) {
138
+ try {
139
+ await sendMessage(peerId, msg);
140
+ }
141
+ catch {
142
+ // unreachable this tick - the next heartbeat (or an explicit
143
+ // reconnect) will retry; nothing to recover here.
144
+ }
145
+ const seenAt = lastSeen.get(peerId);
146
+ if (seenAt !== undefined && Date.now() - seenAt >= heartbeatTimeoutMs) {
147
+ markOffline(peerId);
148
+ }
149
+ }
150
+ };
151
+ void tick(getPeerNodeIds());
152
+ heartbeatTimer = setInterval(() => {
153
+ void tick(getPeerNodeIds());
154
+ }, heartbeatIntervalMs);
155
+ }
156
+ function stopHeartbeat() {
157
+ if (heartbeatTimer) {
158
+ clearInterval(heartbeatTimer);
159
+ heartbeatTimer = null;
160
+ }
161
+ }
162
+ function announceOffline() {
163
+ const msg = offlineAnnouncementMessage(options.localNodeId);
164
+ for (const peerId of getOnlinePeers()) {
165
+ sendMessage(peerId, msg).catch(() => {
166
+ // fire-and-forget - we're shutting down.
167
+ });
168
+ }
169
+ }
170
+ function onOnlineChange(handler) {
171
+ onlineChangeListeners.add(handler);
172
+ return () => onlineChangeListeners.delete(handler);
173
+ }
174
+ function destroy() {
175
+ destroyed = true;
176
+ stopHeartbeat();
177
+ for (const [, stream] of streams)
178
+ stream.close();
179
+ streams.clear();
180
+ pendingConnections.clear();
181
+ lastSeen.clear();
182
+ onlineChangeListeners.clear();
183
+ }
184
+ return {
185
+ sendMessage,
186
+ handleIncomingStream,
187
+ startHeartbeat,
188
+ stopHeartbeat,
189
+ announceOffline,
190
+ isOnline,
191
+ getOnlinePeers,
192
+ onOnlineChange,
193
+ destroy,
194
+ };
195
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=client.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.test.d.ts","sourceRoot":"","sources":["../../src/protocol/client.test.ts"],"names":[],"mappings":""}