@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,255 @@
1
+ // the unified friendz protocol message set - the normative wire-message
2
+ // shapes for peer-to-peer presence, friend requests, knocks, and identity
3
+ // updates. every message is validated against the same 20 fixture files
4
+ // the rust crate's message enum was generated from
5
+ // (haruspex/rust/fixtures/protocol/), so a message accepted here parses
6
+ // identically on both sides of the wire.
7
+ //
8
+ // normalization:
9
+ // - the wire discriminant is a `type` field, kebab-case values.
10
+ // - every other field is camelCase.
11
+ // - every message carries `v` (defaulting to 1 when the sender omits it,
12
+ // for a transition window where not every peer sends it yet).
13
+ // - identity naming: `nodeId` everywhere; `username` for handle-like
14
+ // names; avatars use a blob-id reference (`avatarBlobId`) on the
15
+ // hello/hello-ok handshake, and an inline data url (`avatarDataUrl`)
16
+ // on profile-response/identity-update.
17
+ // - knock outcome status is `pending | accepted | denied`.
18
+ import { z } from "zod";
19
+ const wireVersion = z.number().int().nonnegative().default(1);
20
+ // ---------------------------------------------------------------------------
21
+ // shared enums
22
+ // ---------------------------------------------------------------------------
23
+ /** the grantable role vocabulary carried on the wire (knock-outcome, acl-change). */
24
+ export const WireRoleSchema = z.enum(["viewer", "member", "admin", "root"]);
25
+ /** resolution of a knock request. */
26
+ export const WireKnockStatusSchema = z.enum(["pending", "accepted", "denied"]);
27
+ /** whether a peer may browse without knocking first, or must knock. */
28
+ export const BrowseCapabilitySchema = z.enum(["public", "knock"]);
29
+ /** capability bag carried on the hello/hello-ok handshake. */
30
+ export const CapabilitiesSchema = z.object({
31
+ browse: BrowseCapabilitySchema,
32
+ });
33
+ // ---------------------------------------------------------------------------
34
+ // knock scope
35
+ // ---------------------------------------------------------------------------
36
+ /**
37
+ * the wire shape of a knock's scope: what access is being requested.
38
+ * - account: acceptance creates or links a user identity
39
+ * - browse: list access, no specific resource named
40
+ * - resource: access to one named resource, with an optional requested role
41
+ */
42
+ export const WireKnockScopeSchema = z.discriminatedUnion("kind", [
43
+ z.object({
44
+ kind: z.literal("account"),
45
+ requestedUsername: z.string().optional(),
46
+ }),
47
+ z.object({
48
+ kind: z.literal("browse"),
49
+ }),
50
+ z.object({
51
+ kind: z.literal("resource"),
52
+ resourceId: z.string(),
53
+ requestedRole: WireRoleSchema.optional(),
54
+ }),
55
+ ]);
56
+ // ---------------------------------------------------------------------------
57
+ // gossip digest sub-shapes
58
+ // ---------------------------------------------------------------------------
59
+ /** a pending-knock entry gossiped in a gossip-digest message. */
60
+ export const GossipDigestPendingKnockSchema = z.object({
61
+ knockId: z.string(),
62
+ nodeId: z.string(),
63
+ username: z.string().optional(),
64
+ message: z.string(),
65
+ scope: WireKnockScopeSchema,
66
+ knockedAt: z.string(),
67
+ });
68
+ /** a profile-doc pointer entry in a gossip digest. */
69
+ export const GossipDigestProfileEntrySchema = z.object({
70
+ peerNodeId: z.string(),
71
+ profileDocId: z.string(),
72
+ updatedAt: z.string(),
73
+ });
74
+ // ---------------------------------------------------------------------------
75
+ // core message variants
76
+ // ---------------------------------------------------------------------------
77
+ const ProfileRequestSchema = z.object({
78
+ type: z.literal("profile-request"),
79
+ v: wireVersion,
80
+ });
81
+ const ProfileResponseSchema = z.object({
82
+ type: z.literal("profile-response"),
83
+ v: wireVersion,
84
+ username: z.string(),
85
+ bio: z.string(),
86
+ avatarDataUrl: z.string(),
87
+ accentColor: z.number().optional(),
88
+ profileDocId: z.string().optional(),
89
+ profileUpdatedAt: z.string().optional(),
90
+ });
91
+ const FriendRequestSchema = z.object({
92
+ type: z.literal("friend-request"),
93
+ v: wireVersion,
94
+ fromNodeId: z.string(),
95
+ fromUsername: z.string(),
96
+ isHub: z.boolean().optional(),
97
+ });
98
+ const FriendAcceptSchema = z.object({
99
+ type: z.literal("friend-accept"),
100
+ v: wireVersion,
101
+ fromNodeId: z.string(),
102
+ fromUsername: z.string(),
103
+ isHub: z.boolean().optional(),
104
+ });
105
+ const FriendAcceptAckSchema = z.object({
106
+ type: z.literal("friend-accept-ack"),
107
+ v: wireVersion,
108
+ fromNodeId: z.string(),
109
+ });
110
+ const FriendRejectSchema = z.object({
111
+ type: z.literal("friend-reject"),
112
+ v: wireVersion,
113
+ fromNodeId: z.string(),
114
+ });
115
+ const HeartbeatSchema = z.object({
116
+ type: z.literal("heartbeat"),
117
+ v: wireVersion,
118
+ nodeId: z.string(),
119
+ username: z.string(),
120
+ appPayload: z.unknown().optional(),
121
+ });
122
+ const OfflineAnnouncementSchema = z.object({
123
+ type: z.literal("offline-announcement"),
124
+ v: wireVersion,
125
+ nodeId: z.string(),
126
+ });
127
+ const HelloSchema = z.object({
128
+ type: z.literal("hello"),
129
+ v: wireVersion,
130
+ nodeId: z.string(),
131
+ username: z.string().optional(),
132
+ avatarBlobId: z.string().optional(),
133
+ capabilities: CapabilitiesSchema,
134
+ });
135
+ const HelloOkSchema = z.object({
136
+ type: z.literal("hello-ok"),
137
+ v: wireVersion,
138
+ nodeId: z.string(),
139
+ username: z.string().optional(),
140
+ avatarBlobId: z.string().optional(),
141
+ capabilities: CapabilitiesSchema,
142
+ });
143
+ const KnockRequestSchema = z.object({
144
+ type: z.literal("knock-request"),
145
+ v: wireVersion,
146
+ knockId: z.string(),
147
+ nodeId: z.string(),
148
+ username: z.string().optional(),
149
+ message: z.string(),
150
+ scope: WireKnockScopeSchema,
151
+ });
152
+ const KnockAckSchema = z.object({
153
+ type: z.literal("knock-ack"),
154
+ v: wireVersion,
155
+ knockId: z.string(),
156
+ ackerNodeId: z.string(),
157
+ resourceId: z.string().optional(),
158
+ });
159
+ const KnockOutcomeSchema = z.object({
160
+ type: z.literal("knock-outcome"),
161
+ v: wireVersion,
162
+ knockId: z.string().optional(),
163
+ status: WireKnockStatusSchema,
164
+ grantedRole: WireRoleSchema.optional(),
165
+ grantedResourceIds: z.array(z.string()).default([]),
166
+ byNodeId: z.string().optional(),
167
+ });
168
+ const IdentityUpdateSchema = z.object({
169
+ type: z.literal("identity-update"),
170
+ v: wireVersion,
171
+ nodeId: z.string(),
172
+ username: z.string().optional(),
173
+ avatarDataUrl: z.string().optional(),
174
+ });
175
+ const AclChangeSchema = z.object({
176
+ type: z.literal("acl-change"),
177
+ v: wireVersion,
178
+ resourceId: z.string(),
179
+ resourceTitle: z.string().optional(),
180
+ targetNodeId: z.string(),
181
+ newRole: WireRoleSchema.optional(),
182
+ changedBy: z.string(),
183
+ changedByUsername: z.string(),
184
+ });
185
+ const GossipDigestSchema = z.object({
186
+ type: z.literal("gossip-digest"),
187
+ v: wireVersion,
188
+ pendingKnocks: z.array(GossipDigestPendingKnockSchema).default([]),
189
+ profiles: z.array(GossipDigestProfileEntrySchema).default([]),
190
+ appPayload: z.unknown().optional(),
191
+ });
192
+ const BlobSeekSchema = z.object({
193
+ type: z.literal("blob-seek"),
194
+ v: wireVersion,
195
+ needed: z.array(z.string()).default([]),
196
+ });
197
+ const BlobOfferSchema = z.object({
198
+ type: z.literal("blob-offer"),
199
+ v: wireVersion,
200
+ available: z.array(z.string()).default([]),
201
+ });
202
+ const ErrorMessageSchema = z.object({
203
+ type: z.literal("error"),
204
+ v: wireVersion,
205
+ code: z.string(),
206
+ message: z.string(),
207
+ });
208
+ /** the discriminated union of every core (non-namespaced) friendz message. */
209
+ export const CoreMessageSchema = z.discriminatedUnion("type", [
210
+ ProfileRequestSchema,
211
+ ProfileResponseSchema,
212
+ FriendRequestSchema,
213
+ FriendAcceptSchema,
214
+ FriendAcceptAckSchema,
215
+ FriendRejectSchema,
216
+ HeartbeatSchema,
217
+ OfflineAnnouncementSchema,
218
+ HelloSchema,
219
+ HelloOkSchema,
220
+ KnockRequestSchema,
221
+ KnockAckSchema,
222
+ KnockOutcomeSchema,
223
+ IdentityUpdateSchema,
224
+ AclChangeSchema,
225
+ GossipDigestSchema,
226
+ BlobSeekSchema,
227
+ BlobOfferSchema,
228
+ ErrorMessageSchema,
229
+ ]);
230
+ /** every core message's wire `type` discriminant, in enum-declaration order. */
231
+ export const CORE_MESSAGE_TYPES = [
232
+ "profile-request",
233
+ "profile-response",
234
+ "friend-request",
235
+ "friend-accept",
236
+ "friend-accept-ack",
237
+ "friend-reject",
238
+ "heartbeat",
239
+ "offline-announcement",
240
+ "hello",
241
+ "hello-ok",
242
+ "knock-request",
243
+ "knock-ack",
244
+ "knock-outcome",
245
+ "identity-update",
246
+ "acl-change",
247
+ "gossip-digest",
248
+ "blob-seek",
249
+ "blob-offer",
250
+ "error",
251
+ ];
252
+ /** is `type` one of the core (non-namespaced) message types above? */
253
+ export function isCoreMessageType(type) {
254
+ return CORE_MESSAGE_TYPES.includes(type);
255
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=messages.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"messages.test.d.ts","sourceRoot":"","sources":["../../src/protocol/messages.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,75 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { CoreMessageSchema, WireKnockScopeSchema, isCoreMessageType, } from "./messages.js";
3
+ describe("CoreMessageSchema", () => {
4
+ it("defaults a missing v to 1", () => {
5
+ const parsed = CoreMessageSchema.parse({ type: "profile-request" });
6
+ expect(parsed).toEqual({ type: "profile-request", v: 1 });
7
+ });
8
+ it("accepts an explicit v", () => {
9
+ const parsed = CoreMessageSchema.parse({ type: "profile-request", v: 3 });
10
+ expect(parsed.v).toBe(3);
11
+ });
12
+ it("rejects a type with no matching variant", () => {
13
+ expect(() => CoreMessageSchema.parse({ type: "not-a-real-type" })).toThrow();
14
+ });
15
+ it("defaults missing array fields to []", () => {
16
+ const parsed = CoreMessageSchema.parse({ type: "blob-seek" });
17
+ expect(parsed).toMatchObject({ type: "blob-seek", needed: [] });
18
+ });
19
+ it("parses a full hello handshake with capabilities", () => {
20
+ const parsed = CoreMessageSchema.parse({
21
+ type: "hello",
22
+ v: 1,
23
+ nodeId: "node-abc123",
24
+ username: "alice",
25
+ capabilities: { browse: "knock" },
26
+ });
27
+ expect(parsed).toMatchObject({
28
+ type: "hello",
29
+ nodeId: "node-abc123",
30
+ capabilities: { browse: "knock" },
31
+ });
32
+ });
33
+ it("rejects hello with an invalid browse capability", () => {
34
+ expect(() => CoreMessageSchema.parse({
35
+ type: "hello",
36
+ nodeId: "node-abc123",
37
+ capabilities: { browse: "sometimes" },
38
+ })).toThrow();
39
+ });
40
+ });
41
+ describe("WireKnockScopeSchema", () => {
42
+ it("parses an account scope with an optional requestedUsername", () => {
43
+ expect(WireKnockScopeSchema.parse({ kind: "account" })).toEqual({ kind: "account" });
44
+ expect(WireKnockScopeSchema.parse({ kind: "account", requestedUsername: "bob" })).toEqual({ kind: "account", requestedUsername: "bob" });
45
+ });
46
+ it("parses a bare browse scope", () => {
47
+ expect(WireKnockScopeSchema.parse({ kind: "browse" })).toEqual({ kind: "browse" });
48
+ });
49
+ it("parses a resource scope with a resourceId and optional requestedRole", () => {
50
+ const parsed = WireKnockScopeSchema.parse({
51
+ kind: "resource",
52
+ resourceId: "doc-1",
53
+ requestedRole: "member",
54
+ });
55
+ expect(parsed).toEqual({ kind: "resource", resourceId: "doc-1", requestedRole: "member" });
56
+ });
57
+ it("rejects an unknown scope kind", () => {
58
+ expect(() => WireKnockScopeSchema.parse({ kind: "everyone" })).toThrow();
59
+ });
60
+ it("rejects an invalid role value", () => {
61
+ expect(() => WireKnockScopeSchema.parse({ kind: "resource", resourceId: "doc-1", requestedRole: "owner" })).toThrow();
62
+ });
63
+ });
64
+ describe("isCoreMessageType", () => {
65
+ it("is true for every core type", () => {
66
+ expect(isCoreMessageType("hello")).toBe(true);
67
+ expect(isCoreMessageType("knock-outcome")).toBe(true);
68
+ });
69
+ it("is false for a namespaced app-extension type", () => {
70
+ expect(isCoreMessageType("skein:canvas-invite")).toBe(false);
71
+ });
72
+ it("is false for an unknown type", () => {
73
+ expect(isCoreMessageType("not-a-real-type")).toBe(false);
74
+ });
75
+ });
@@ -0,0 +1,58 @@
1
+ /** a bare node reference: "come dial this peer", nothing else. */
2
+ export interface NodeSharePayload {
3
+ kind: "node";
4
+ nodeId: string;
5
+ title?: string;
6
+ }
7
+ /** a node id + automerge doc id, with an optional sharing-mode hint. */
8
+ export interface DocSharePayload {
9
+ kind: "doc";
10
+ nodeId: string;
11
+ docId: string;
12
+ title?: string;
13
+ mode?: "public" | "knock";
14
+ }
15
+ /**
16
+ * an entity hosted on a source (a p2p node id and/or an http origin - at
17
+ * least one should be set so a recipient can resolve it). `entityKind` is
18
+ * app-defined (e.g. "album", "song", "canvas") - this package does not own
19
+ * a fixed vocabulary for it.
20
+ */
21
+ export interface EntitySharePayload {
22
+ kind: "entity";
23
+ source: {
24
+ nodeId?: string;
25
+ httpOrigin?: string;
26
+ };
27
+ entityKind: string;
28
+ entityId: string;
29
+ parentId?: string;
30
+ title?: string;
31
+ artist?: string;
32
+ album?: string;
33
+ }
34
+ export type ShareTokenPayload = NodeSharePayload | DocSharePayload | EntitySharePayload;
35
+ /** encode a share payload as a base64url token (no padding). */
36
+ export declare function encodeShareToken(payload: ShareTokenPayload): string;
37
+ /** build a url fragment for embedding in window.location.hash. */
38
+ export declare function shareFragment(payload: ShareTokenPayload): string;
39
+ /**
40
+ * strip a share url/fragment down to the bare token: handles a full url
41
+ * ending in a `#share/<token>` fragment, a bare `share/<token>` prefix, and
42
+ * a trailing `&`-joined query fragment appended after the token. returns
43
+ * the input unchanged (trimmed) when none of those prefixes are present,
44
+ * so a bare token passes through untouched.
45
+ */
46
+ export declare function extractShareToken(input: string): string;
47
+ /**
48
+ * decode a share token back to a payload. accepts a raw token, a
49
+ * `#share/<token>` fragment, or a full url ending in one. also decodes the
50
+ * legacy doc-share shapes (versioned `{v:1,n,d,t,m}` and the bare,
51
+ * unversioned `{n,d}` form) and the legacy entity-share shape
52
+ * (`{v:1,s:{n,h},k,i,...}`), mapping every one of them onto the current
53
+ * payload types. returns null on anything invalid rather than throwing, so
54
+ * callers can treat "not a share token" as just another input
55
+ * classification.
56
+ */
57
+ export declare function decodeShareToken(input: string): ShareTokenPayload | null;
58
+ //# sourceMappingURL=codec.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"codec.d.ts","sourceRoot":"","sources":["../../src/share/codec.ts"],"names":[],"mappings":"AAiBA,kEAAkE;AAClE,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,wEAAwE;AACxE,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,KAAK,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,QAAQ,GAAG,OAAO,CAAC;CAC3B;AAED;;;;;GAKG;AACH,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,QAAQ,CAAC;IACf,MAAM,EAAE;QAAE,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,UAAU,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IACjD,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,MAAM,iBAAiB,GAAG,gBAAgB,GAAG,eAAe,GAAG,kBAAkB,CAAC;AA2IxF,gEAAgE;AAChE,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,iBAAiB,GAAG,MAAM,CAInE;AAED,kEAAkE;AAClE,wBAAgB,aAAa,CAAC,OAAO,EAAE,iBAAiB,GAAG,MAAM,CAEhE;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAcvD;AAED;;;;;;;;;GASG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,iBAAiB,GAAG,IAAI,CA0CxE"}
@@ -0,0 +1,222 @@
1
+ // unified share-link token codec.
2
+ //
3
+ // three token shapes exist in the wild for sharing a p2p entity via a
4
+ // pasted string or url fragment:
5
+ // - a node id + an automerge doc id, with optional title/mode hints
6
+ // - a node id alone (a plain peer reference, no doc)
7
+ // - a source (p2p node id and/or http origin) plus an app-defined entity
8
+ // kind + id, with optional parent/title/artist/album display hints
9
+ //
10
+ // this codec's own wire format is versioned explicitly (`v`) so a future
11
+ // payload addition never breaks a link already sitting in someone's
12
+ // clipboard or browser history. decoding also recognizes the legacy token
13
+ // shapes below, none of which carry a wire version of their own that lines
14
+ // up with this codec's, so they are told apart by their field shape.
15
+ import { z } from "zod";
16
+ const CURRENT_WIRE_VERSION = 2;
17
+ // wire schema (short keys - these end up in url bars).
18
+ const NodeWireSchema = z.object({
19
+ v: z.literal(CURRENT_WIRE_VERSION),
20
+ k: z.literal("node"),
21
+ n: z.string().min(1),
22
+ t: z.string().optional(),
23
+ });
24
+ const DocWireSchema = z.object({
25
+ v: z.literal(CURRENT_WIRE_VERSION),
26
+ k: z.literal("doc"),
27
+ n: z.string().min(1),
28
+ d: z.string().min(1),
29
+ t: z.string().optional(),
30
+ m: z.enum(["public", "knock"]).optional(),
31
+ });
32
+ const EntityWireSchema = z.object({
33
+ v: z.literal(CURRENT_WIRE_VERSION),
34
+ k: z.literal("entity"),
35
+ sn: z.string().optional(),
36
+ sh: z.string().optional(),
37
+ ek: z.string().min(1),
38
+ i: z.string().min(1),
39
+ p: z.string().optional(),
40
+ t: z.string().optional(),
41
+ a: z.string().optional(),
42
+ al: z.string().optional(),
43
+ });
44
+ const WireSchema = z.discriminatedUnion("k", [NodeWireSchema, DocWireSchema, EntityWireSchema]);
45
+ // legacy shapes this codec still decodes, so links handed out before the
46
+ // unified codec existed keep working.
47
+ const DocLegacyV1Schema = z.object({
48
+ v: z.literal(1),
49
+ n: z.string().min(1),
50
+ d: z.string().min(1),
51
+ t: z.string().optional(),
52
+ m: z.enum(["public", "knock"]).optional(),
53
+ });
54
+ const EntityLegacyV1Schema = z.object({
55
+ v: z.literal(1),
56
+ s: z.object({ n: z.string().optional(), h: z.string().optional() }),
57
+ k: z.string().min(1),
58
+ i: z.string().min(1),
59
+ p: z.string().optional(),
60
+ t: z.string().optional(),
61
+ a: z.string().optional(),
62
+ al: z.string().optional(),
63
+ });
64
+ // bare node id + doc id, no version marker at all.
65
+ const DocBareLegacySchema = z.object({
66
+ n: z.string().min(1),
67
+ d: z.string().min(1),
68
+ });
69
+ function toWire(payload) {
70
+ switch (payload.kind) {
71
+ case "node":
72
+ return { v: CURRENT_WIRE_VERSION, k: "node", n: payload.nodeId, t: payload.title };
73
+ case "doc":
74
+ return {
75
+ v: CURRENT_WIRE_VERSION,
76
+ k: "doc",
77
+ n: payload.nodeId,
78
+ d: payload.docId,
79
+ t: payload.title,
80
+ m: payload.mode,
81
+ };
82
+ case "entity":
83
+ return {
84
+ v: CURRENT_WIRE_VERSION,
85
+ k: "entity",
86
+ sn: payload.source.nodeId,
87
+ sh: payload.source.httpOrigin,
88
+ ek: payload.entityKind,
89
+ i: payload.entityId,
90
+ p: payload.parentId,
91
+ t: payload.title,
92
+ a: payload.artist,
93
+ al: payload.album,
94
+ };
95
+ }
96
+ }
97
+ function fromWire(wire) {
98
+ switch (wire.k) {
99
+ case "node":
100
+ return { kind: "node", nodeId: wire.n, title: wire.t };
101
+ case "doc":
102
+ return { kind: "doc", nodeId: wire.n, docId: wire.d, title: wire.t, mode: wire.m };
103
+ case "entity":
104
+ return {
105
+ kind: "entity",
106
+ source: { nodeId: wire.sn, httpOrigin: wire.sh },
107
+ entityKind: wire.ek,
108
+ entityId: wire.i,
109
+ parentId: wire.p,
110
+ title: wire.t,
111
+ artist: wire.a,
112
+ album: wire.al,
113
+ };
114
+ }
115
+ }
116
+ // base64url helpers built on TextEncoder/TextDecoder rather than raw
117
+ // btoa/atob, so a title containing non-latin1 characters round-trips
118
+ // instead of throwing.
119
+ function base64UrlEncode(bytes) {
120
+ let bin = "";
121
+ for (let i = 0; i < bytes.length; i++)
122
+ bin += String.fromCharCode(bytes[i]);
123
+ return btoa(bin).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
124
+ }
125
+ function base64UrlDecode(token) {
126
+ const pad = token.length % 4 === 0 ? "" : "=".repeat(4 - (token.length % 4));
127
+ const b64 = token.replace(/-/g, "+").replace(/_/g, "/") + pad;
128
+ const bin = atob(b64);
129
+ const out = new Uint8Array(bin.length);
130
+ for (let i = 0; i < bin.length; i++)
131
+ out[i] = bin.charCodeAt(i);
132
+ return out;
133
+ }
134
+ function stripUndefined(value) {
135
+ const out = {};
136
+ for (const [key, v] of Object.entries(value)) {
137
+ if (v !== undefined)
138
+ out[key] = v;
139
+ }
140
+ return out;
141
+ }
142
+ /** encode a share payload as a base64url token (no padding). */
143
+ export function encodeShareToken(payload) {
144
+ const wire = stripUndefined(toWire(payload));
145
+ const json = JSON.stringify(wire);
146
+ return base64UrlEncode(new TextEncoder().encode(json));
147
+ }
148
+ /** build a url fragment for embedding in window.location.hash. */
149
+ export function shareFragment(payload) {
150
+ return `#share/${encodeShareToken(payload)}`;
151
+ }
152
+ /**
153
+ * strip a share url/fragment down to the bare token: handles a full url
154
+ * ending in a `#share/<token>` fragment, a bare `share/<token>` prefix, and
155
+ * a trailing `&`-joined query fragment appended after the token. returns
156
+ * the input unchanged (trimmed) when none of those prefixes are present,
157
+ * so a bare token passes through untouched.
158
+ */
159
+ export function extractShareToken(input) {
160
+ let raw = input.trim();
161
+ const hashIdx = raw.indexOf("#share/");
162
+ if (hashIdx !== -1) {
163
+ raw = raw.slice(hashIdx + "#share/".length);
164
+ }
165
+ else if (raw.startsWith("share/")) {
166
+ raw = raw.slice("share/".length);
167
+ }
168
+ const ampIdx = raw.indexOf("&");
169
+ if (ampIdx !== -1)
170
+ raw = raw.slice(0, ampIdx);
171
+ return raw;
172
+ }
173
+ /**
174
+ * decode a share token back to a payload. accepts a raw token, a
175
+ * `#share/<token>` fragment, or a full url ending in one. also decodes the
176
+ * legacy doc-share shapes (versioned `{v:1,n,d,t,m}` and the bare,
177
+ * unversioned `{n,d}` form) and the legacy entity-share shape
178
+ * (`{v:1,s:{n,h},k,i,...}`), mapping every one of them onto the current
179
+ * payload types. returns null on anything invalid rather than throwing, so
180
+ * callers can treat "not a share token" as just another input
181
+ * classification.
182
+ */
183
+ export function decodeShareToken(input) {
184
+ const raw = extractShareToken(input);
185
+ if (!raw)
186
+ return null;
187
+ let json;
188
+ try {
189
+ json = JSON.parse(new TextDecoder().decode(base64UrlDecode(raw)));
190
+ }
191
+ catch {
192
+ return null;
193
+ }
194
+ const wire = WireSchema.safeParse(json);
195
+ if (wire.success)
196
+ return fromWire(wire.data);
197
+ const docLegacy = DocLegacyV1Schema.safeParse(json);
198
+ if (docLegacy.success) {
199
+ const p = docLegacy.data;
200
+ return { kind: "doc", nodeId: p.n, docId: p.d, title: p.t, mode: p.m };
201
+ }
202
+ const entityLegacy = EntityLegacyV1Schema.safeParse(json);
203
+ if (entityLegacy.success) {
204
+ const p = entityLegacy.data;
205
+ return {
206
+ kind: "entity",
207
+ source: { nodeId: p.s.n, httpOrigin: p.s.h },
208
+ entityKind: p.k,
209
+ entityId: p.i,
210
+ parentId: p.p,
211
+ title: p.t,
212
+ artist: p.a,
213
+ album: p.al,
214
+ };
215
+ }
216
+ const docBareLegacy = DocBareLegacySchema.safeParse(json);
217
+ if (docBareLegacy.success) {
218
+ const p = docBareLegacy.data;
219
+ return { kind: "doc", nodeId: p.n, docId: p.d };
220
+ }
221
+ return null;
222
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=codec.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"codec.test.d.ts","sourceRoot":"","sources":["../../src/share/codec.test.ts"],"names":[],"mappings":""}