@comapeo/core 1.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 (186) hide show
  1. package/LICENSE.md +9 -0
  2. package/README.md +31 -0
  3. package/dist/blob-api.d.ts +92 -0
  4. package/dist/blob-api.d.ts.map +1 -0
  5. package/dist/blob-store/index.d.ts +163 -0
  6. package/dist/blob-store/index.d.ts.map +1 -0
  7. package/dist/blob-store/live-download.d.ts +107 -0
  8. package/dist/blob-store/live-download.d.ts.map +1 -0
  9. package/dist/config-import.d.ts +74 -0
  10. package/dist/config-import.d.ts.map +1 -0
  11. package/dist/constants.d.ts +14 -0
  12. package/dist/constants.d.ts.map +1 -0
  13. package/dist/core-manager/bitfield-rle.d.ts +25 -0
  14. package/dist/core-manager/bitfield-rle.d.ts.map +1 -0
  15. package/dist/core-manager/core-index.d.ts +56 -0
  16. package/dist/core-manager/core-index.d.ts.map +1 -0
  17. package/dist/core-manager/index.d.ts +125 -0
  18. package/dist/core-manager/index.d.ts.map +1 -0
  19. package/dist/core-manager/random-access-file-pool.d.ts +17 -0
  20. package/dist/core-manager/random-access-file-pool.d.ts.map +1 -0
  21. package/dist/core-manager/remote-bitfield.d.ts +146 -0
  22. package/dist/core-manager/remote-bitfield.d.ts.map +1 -0
  23. package/dist/core-ownership.d.ts +112 -0
  24. package/dist/core-ownership.d.ts.map +1 -0
  25. package/dist/datastore/index.d.ts +91 -0
  26. package/dist/datastore/index.d.ts.map +1 -0
  27. package/dist/datatype/index.d.ts +108 -0
  28. package/dist/discovery/local-discovery.d.ts +64 -0
  29. package/dist/discovery/local-discovery.d.ts.map +1 -0
  30. package/dist/errors.d.ts +4 -0
  31. package/dist/errors.d.ts.map +1 -0
  32. package/dist/fastify-controller.d.ts +27 -0
  33. package/dist/fastify-controller.d.ts.map +1 -0
  34. package/dist/fastify-plugins/blobs.d.ts +6 -0
  35. package/dist/fastify-plugins/blobs.d.ts.map +1 -0
  36. package/dist/fastify-plugins/constants.d.ts +3 -0
  37. package/dist/fastify-plugins/constants.d.ts.map +1 -0
  38. package/dist/fastify-plugins/icons.d.ts +6 -0
  39. package/dist/fastify-plugins/icons.d.ts.map +1 -0
  40. package/dist/fastify-plugins/maps/index.d.ts +11 -0
  41. package/dist/fastify-plugins/maps/index.d.ts.map +1 -0
  42. package/dist/fastify-plugins/maps/offline-fallback-map.d.ts +12 -0
  43. package/dist/fastify-plugins/maps/offline-fallback-map.d.ts.map +1 -0
  44. package/dist/fastify-plugins/maps/static-maps.d.ts +11 -0
  45. package/dist/fastify-plugins/maps/static-maps.d.ts.map +1 -0
  46. package/dist/fastify-plugins/utils.d.ts +23 -0
  47. package/dist/fastify-plugins/utils.d.ts.map +1 -0
  48. package/dist/generated/extensions.d.ts +44 -0
  49. package/dist/generated/extensions.d.ts.map +1 -0
  50. package/dist/generated/keys.d.ts +36 -0
  51. package/dist/generated/keys.d.ts.map +1 -0
  52. package/dist/generated/rpc.d.ts +87 -0
  53. package/dist/generated/rpc.d.ts.map +1 -0
  54. package/dist/icon-api.d.ts +109 -0
  55. package/dist/icon-api.d.ts.map +1 -0
  56. package/dist/index-writer/index.d.ts +51 -0
  57. package/dist/index-writer/index.d.ts.map +1 -0
  58. package/dist/index.d.ts +14 -0
  59. package/dist/index.d.ts.map +1 -0
  60. package/dist/invite-api.d.ts +70 -0
  61. package/dist/invite-api.d.ts.map +1 -0
  62. package/dist/lib/hashmap.d.ts +62 -0
  63. package/dist/lib/hashmap.d.ts.map +1 -0
  64. package/dist/lib/hypercore-helpers.d.ts +6 -0
  65. package/dist/lib/hypercore-helpers.d.ts.map +1 -0
  66. package/dist/lib/noise-secret-stream-helpers.d.ts +45 -0
  67. package/dist/lib/noise-secret-stream-helpers.d.ts.map +1 -0
  68. package/dist/lib/ponyfills.d.ts +10 -0
  69. package/dist/lib/ponyfills.d.ts.map +1 -0
  70. package/dist/lib/string.d.ts +2 -0
  71. package/dist/lib/string.d.ts.map +1 -0
  72. package/dist/lib/timing-safe-equal.d.ts +15 -0
  73. package/dist/lib/timing-safe-equal.d.ts.map +1 -0
  74. package/dist/local-peers.d.ts +151 -0
  75. package/dist/local-peers.d.ts.map +1 -0
  76. package/dist/logger.d.ts +32 -0
  77. package/dist/logger.d.ts.map +1 -0
  78. package/dist/mapeo-manager.d.ts +178 -0
  79. package/dist/mapeo-manager.d.ts.map +1 -0
  80. package/dist/mapeo-project.d.ts +3233 -0
  81. package/dist/mapeo-project.d.ts.map +1 -0
  82. package/dist/member-api.d.ts +114 -0
  83. package/dist/member-api.d.ts.map +1 -0
  84. package/dist/roles.d.ts +157 -0
  85. package/dist/roles.d.ts.map +1 -0
  86. package/dist/schema/client.d.ts +284 -0
  87. package/dist/schema/client.d.ts.map +1 -0
  88. package/dist/schema/project.d.ts +1812 -0
  89. package/dist/schema/project.d.ts.map +1 -0
  90. package/dist/schema/schema-to-drizzle.d.ts +20 -0
  91. package/dist/schema/schema-to-drizzle.d.ts.map +1 -0
  92. package/dist/schema/types.d.ts +98 -0
  93. package/dist/schema/types.d.ts.map +1 -0
  94. package/dist/schema/utils.d.ts +55 -0
  95. package/dist/schema/utils.d.ts.map +1 -0
  96. package/dist/sync/core-sync-state.d.ts +252 -0
  97. package/dist/sync/core-sync-state.d.ts.map +1 -0
  98. package/dist/sync/namespace-sync-state.d.ts +47 -0
  99. package/dist/sync/namespace-sync-state.d.ts.map +1 -0
  100. package/dist/sync/peer-sync-controller.d.ts +44 -0
  101. package/dist/sync/peer-sync-controller.d.ts.map +1 -0
  102. package/dist/sync/sync-api.d.ts +158 -0
  103. package/dist/sync/sync-api.d.ts.map +1 -0
  104. package/dist/sync/sync-state.d.ts +40 -0
  105. package/dist/sync/sync-state.d.ts.map +1 -0
  106. package/dist/translation-api.d.ts +288 -0
  107. package/dist/translation-api.d.ts.map +1 -0
  108. package/dist/types.d.ts +115 -0
  109. package/dist/types.d.ts.map +1 -0
  110. package/dist/utils.d.ts +115 -0
  111. package/dist/utils.d.ts.map +1 -0
  112. package/dist/utils_types.d.ts +14 -0
  113. package/drizzle/client/0000_bumpy_carnage.sql +33 -0
  114. package/drizzle/client/meta/0000_snapshot.json +199 -0
  115. package/drizzle/client/meta/_journal.json +13 -0
  116. package/drizzle/project/0000_spooky_lady_ursula.sql +192 -0
  117. package/drizzle/project/meta/0000_snapshot.json +1137 -0
  118. package/drizzle/project/meta/_journal.json +13 -0
  119. package/package.json +202 -0
  120. package/src/blob-api.js +139 -0
  121. package/src/blob-store/index.js +325 -0
  122. package/src/blob-store/live-download.js +373 -0
  123. package/src/config-import.js +604 -0
  124. package/src/constants.js +34 -0
  125. package/src/core-manager/bitfield-rle.js +235 -0
  126. package/src/core-manager/core-index.js +87 -0
  127. package/src/core-manager/index.js +504 -0
  128. package/src/core-manager/random-access-file-pool.js +30 -0
  129. package/src/core-manager/remote-bitfield.js +416 -0
  130. package/src/core-ownership.js +235 -0
  131. package/src/datastore/README.md +46 -0
  132. package/src/datastore/index.js +234 -0
  133. package/src/datatype/README.md +33 -0
  134. package/src/datatype/index.d.ts +108 -0
  135. package/src/datatype/index.js +358 -0
  136. package/src/discovery/local-discovery.js +303 -0
  137. package/src/errors.js +5 -0
  138. package/src/fastify-controller.js +84 -0
  139. package/src/fastify-plugins/blobs.js +139 -0
  140. package/src/fastify-plugins/constants.js +5 -0
  141. package/src/fastify-plugins/icons.js +158 -0
  142. package/src/fastify-plugins/maps/index.js +173 -0
  143. package/src/fastify-plugins/maps/offline-fallback-map.js +114 -0
  144. package/src/fastify-plugins/maps/static-maps.js +271 -0
  145. package/src/fastify-plugins/utils.js +52 -0
  146. package/src/generated/README.md +3 -0
  147. package/src/generated/extensions.d.ts +44 -0
  148. package/src/generated/extensions.js +196 -0
  149. package/src/generated/extensions.ts +237 -0
  150. package/src/generated/keys.d.ts +36 -0
  151. package/src/generated/keys.js +148 -0
  152. package/src/generated/keys.ts +185 -0
  153. package/src/generated/rpc.d.ts +87 -0
  154. package/src/generated/rpc.js +389 -0
  155. package/src/generated/rpc.ts +463 -0
  156. package/src/icon-api.js +282 -0
  157. package/src/index-writer/README.md +38 -0
  158. package/src/index-writer/index.js +124 -0
  159. package/src/index.js +16 -0
  160. package/src/invite-api.js +450 -0
  161. package/src/lib/hashmap.js +91 -0
  162. package/src/lib/hypercore-helpers.js +18 -0
  163. package/src/lib/noise-secret-stream-helpers.js +37 -0
  164. package/src/lib/ponyfills.js +25 -0
  165. package/src/lib/string.js +7 -0
  166. package/src/lib/timing-safe-equal.js +34 -0
  167. package/src/local-peers.js +737 -0
  168. package/src/logger.js +99 -0
  169. package/src/mapeo-manager.js +914 -0
  170. package/src/mapeo-project.js +980 -0
  171. package/src/member-api.js +319 -0
  172. package/src/roles.js +412 -0
  173. package/src/schema/client.js +55 -0
  174. package/src/schema/project.js +44 -0
  175. package/src/schema/schema-to-drizzle.js +118 -0
  176. package/src/schema/types.ts +153 -0
  177. package/src/schema/utils.js +51 -0
  178. package/src/sync/core-sync-state.js +440 -0
  179. package/src/sync/namespace-sync-state.js +193 -0
  180. package/src/sync/peer-sync-controller.js +332 -0
  181. package/src/sync/sync-api.js +588 -0
  182. package/src/sync/sync-state.js +63 -0
  183. package/src/translation-api.js +141 -0
  184. package/src/types.ts +149 -0
  185. package/src/utils.js +210 -0
  186. package/src/utils_types.d.ts +14 -0
@@ -0,0 +1,319 @@
1
+ import * as crypto from 'node:crypto'
2
+ import { TypedEmitter } from 'tiny-typed-emitter'
3
+ import { pEvent } from 'p-event'
4
+ import { InviteResponse_Decision } from './generated/rpc.js'
5
+ import {
6
+ assert,
7
+ noop,
8
+ ExhaustivenessError,
9
+ projectKeyToId,
10
+ projectKeyToProjectInviteId,
11
+ } from './utils.js'
12
+ import { abortSignalAny } from './lib/ponyfills.js'
13
+ import timingSafeEqual from './lib/timing-safe-equal.js'
14
+ import { ROLES, isRoleIdForNewInvite } from './roles.js'
15
+ /**
16
+ * @import {
17
+ * DeviceInfo,
18
+ * DeviceInfoValue,
19
+ * ProjectSettings,
20
+ * ProjectSettingsValue
21
+ * } from '@comapeo/schema'
22
+ */
23
+ /** @import { Invite, InviteResponse } from './generated/rpc.js' */
24
+ /** @import { DataType } from './datatype/index.js' */
25
+ /** @import { DataStore } from './datastore/index.js' */
26
+ /** @import { deviceInfoTable } from './schema/project.js' */
27
+ /** @import { projectSettingsTable } from './schema/client.js' */
28
+
29
+ /** @typedef {DataType<DataStore<'config'>, typeof deviceInfoTable, "deviceInfo", DeviceInfo, DeviceInfoValue>} DeviceInfoDataType */
30
+ /** @typedef {DataType<DataStore<'config'>, typeof projectSettingsTable, "projectSettings", ProjectSettings, ProjectSettingsValue>} ProjectDataType */
31
+ /**
32
+ * @typedef {object} MemberInfo
33
+ * @prop {string} deviceId
34
+ * @prop {import('./roles.js').Role} role
35
+ * @prop {DeviceInfo['name']} [name]
36
+ * @prop {DeviceInfo['deviceType']} [deviceType]
37
+ * @prop {DeviceInfo['createdAt']} [joinedAt]
38
+ */
39
+
40
+ export class MemberApi extends TypedEmitter {
41
+ #ownDeviceId
42
+ #roles
43
+ #coreOwnership
44
+ #encryptionKeys
45
+ #projectKey
46
+ #rpc
47
+ #dataTypes
48
+
49
+ /** @type {Map<string, { abortController: AbortController }>} */
50
+ #outboundInvitesByDevice = new Map()
51
+
52
+ /**
53
+ * @param {Object} opts
54
+ * @param {string} opts.deviceId public key of this device as hex string
55
+ * @param {import('./roles.js').Roles} opts.roles
56
+ * @param {import('./core-ownership.js').CoreOwnership} opts.coreOwnership
57
+ * @param {import('./generated/keys.js').EncryptionKeys} opts.encryptionKeys
58
+ * @param {Buffer} opts.projectKey
59
+ * @param {import('./local-peers.js').LocalPeers} opts.rpc
60
+ * @param {Object} opts.dataTypes
61
+ * @param {Pick<DeviceInfoDataType, 'getByDocId' | 'getMany'>} opts.dataTypes.deviceInfo
62
+ * @param {Pick<ProjectDataType, 'getByDocId'>} opts.dataTypes.project
63
+ */
64
+ constructor({
65
+ deviceId,
66
+ roles,
67
+ coreOwnership,
68
+ encryptionKeys,
69
+ projectKey,
70
+ rpc,
71
+ dataTypes,
72
+ }) {
73
+ super()
74
+ this.#ownDeviceId = deviceId
75
+ this.#roles = roles
76
+ this.#coreOwnership = coreOwnership
77
+ this.#encryptionKeys = encryptionKeys
78
+ this.#projectKey = projectKey
79
+ this.#rpc = rpc
80
+ this.#dataTypes = dataTypes
81
+ }
82
+
83
+ /**
84
+ * Send an invite. Resolves when receiving a response. Rejects if the invite
85
+ * is canceled, or if something else goes wrong.
86
+ *
87
+ * @param {string} deviceId
88
+ * @param {Object} opts
89
+ * @param {import('./roles.js').RoleIdForNewInvite} opts.roleId
90
+ * @param {string} [opts.roleName]
91
+ * @param {string} [opts.roleDescription]
92
+ * @returns {Promise<(
93
+ * typeof InviteResponse_Decision.ACCEPT |
94
+ * typeof InviteResponse_Decision.REJECT |
95
+ * typeof InviteResponse_Decision.ALREADY
96
+ * )>}
97
+ */
98
+ async invite(
99
+ deviceId,
100
+ { roleId, roleName = ROLES[roleId]?.name, roleDescription }
101
+ ) {
102
+ assert(isRoleIdForNewInvite(roleId), 'Invalid role ID for new invite')
103
+ assert(
104
+ !this.#outboundInvitesByDevice.has(deviceId),
105
+ 'Already inviting this device ID'
106
+ )
107
+
108
+ const abortController = new AbortController()
109
+ const abortSignal = abortController.signal
110
+ this.#outboundInvitesByDevice.set(deviceId, { abortController })
111
+
112
+ try {
113
+ const { name: invitorName } = await this.getById(this.#ownDeviceId)
114
+ // since we are always getting #ownDeviceId,
115
+ // this should never throw (see comment on getById), but it pleases ts
116
+ assert(
117
+ invitorName,
118
+ 'Internal error trying to read own device name for this invite'
119
+ )
120
+
121
+ abortSignal.throwIfAborted()
122
+
123
+ const inviteId = crypto.randomBytes(32)
124
+ const projectId = projectKeyToId(this.#projectKey)
125
+ const projectInviteId = projectKeyToProjectInviteId(this.#projectKey)
126
+ const project = await this.#dataTypes.project.getByDocId(projectId)
127
+ const projectName = project.name
128
+ assert(projectName, 'Project must have a name to invite people')
129
+
130
+ abortSignal.throwIfAborted()
131
+
132
+ const invite = {
133
+ inviteId,
134
+ projectInviteId,
135
+ projectName,
136
+ roleName,
137
+ roleDescription,
138
+ invitorName,
139
+ }
140
+
141
+ const inviteResponse = await this.#sendInviteAndGetResponse(
142
+ deviceId,
143
+ invite,
144
+ abortSignal
145
+ )
146
+
147
+ // Though the invite is still arguably outgoing here, it can no longer
148
+ // be canceled.
149
+ this.#outboundInvitesByDevice.delete(deviceId)
150
+
151
+ switch (inviteResponse.decision) {
152
+ case InviteResponse_Decision.ALREADY:
153
+ case InviteResponse_Decision.REJECT:
154
+ return inviteResponse.decision
155
+ case InviteResponse_Decision.UNRECOGNIZED:
156
+ case InviteResponse_Decision.DECISION_UNSPECIFIED:
157
+ return InviteResponse_Decision.REJECT
158
+ case InviteResponse_Decision.ACCEPT:
159
+ // We should assign the role locally *before* sharing the project details
160
+ // so that they're part of the project even if they don't receive the
161
+ // project details message.
162
+
163
+ await this.#roles.assignRole(deviceId, roleId)
164
+
165
+ await this.#rpc.sendProjectJoinDetails(deviceId, {
166
+ inviteId,
167
+ projectKey: this.#projectKey,
168
+ encryptionKeys: this.#encryptionKeys,
169
+ })
170
+
171
+ return inviteResponse.decision
172
+ default:
173
+ throw new ExhaustivenessError(inviteResponse.decision)
174
+ }
175
+ } finally {
176
+ this.#outboundInvitesByDevice.delete(deviceId)
177
+ }
178
+ }
179
+
180
+ /**
181
+ * @param {string} deviceId
182
+ * @param {Invite} invite
183
+ * @param {AbortSignal} signal
184
+ */
185
+ async #sendInviteAndGetResponse(deviceId, invite, signal) {
186
+ const inviteAbortedError = new Error('Invite aborted')
187
+
188
+ if (signal.aborted) throw inviteAbortedError
189
+
190
+ const abortController = new AbortController()
191
+
192
+ const responsePromise =
193
+ /** @type {typeof pEvent<'invite-response', [string, InviteResponse]>} */ (
194
+ pEvent
195
+ )(this.#rpc, 'invite-response', {
196
+ multiArgs: true,
197
+ filter: ([peerId, inviteResponse]) =>
198
+ timingSafeEqual(peerId, deviceId) &&
199
+ timingSafeEqual(invite.inviteId, inviteResponse.inviteId),
200
+ signal: abortSignalAny([abortController.signal, signal]),
201
+ }).then((args) => args?.[1])
202
+
203
+ responsePromise.catch(noop)
204
+
205
+ signal.addEventListener(
206
+ 'abort',
207
+ () => {
208
+ this.#rpc
209
+ .sendInviteCancel(deviceId, { inviteId: invite.inviteId })
210
+ .catch(noop)
211
+ },
212
+ { once: true }
213
+ )
214
+
215
+ try {
216
+ await this.#rpc.sendInvite(deviceId, invite)
217
+ return await responsePromise
218
+ } catch (err) {
219
+ if (err instanceof Error && err.name === 'AbortError') {
220
+ throw inviteAbortedError
221
+ } else {
222
+ throw err
223
+ }
224
+ } finally {
225
+ abortController.abort()
226
+ }
227
+ }
228
+
229
+ /**
230
+ * Attempt to cancel an outbound invite, if it exists.
231
+ *
232
+ * No-op if we weren't inviting this device.
233
+ *
234
+ * @param {string} deviceId
235
+ * @returns {void}
236
+ */
237
+ requestCancelInvite(deviceId) {
238
+ this.#outboundInvitesByDevice.get(deviceId)?.abortController.abort()
239
+ }
240
+
241
+ /**
242
+ * @param {string} deviceId
243
+ * @returns {Promise<MemberInfo>}
244
+ */
245
+ async getById(deviceId) {
246
+ const role = await this.#roles.getRole(deviceId)
247
+
248
+ /** @type {MemberInfo} */
249
+ const result = { deviceId, role }
250
+
251
+ try {
252
+ const configCoreId = await this.#coreOwnership.getCoreId(
253
+ deviceId,
254
+ 'config'
255
+ )
256
+
257
+ const deviceInfo = await this.#dataTypes.deviceInfo.getByDocId(
258
+ configCoreId
259
+ )
260
+
261
+ result.name = deviceInfo.name
262
+ result.deviceType = deviceInfo.deviceType
263
+ result.joinedAt = deviceInfo.createdAt
264
+ } catch (err) {
265
+ // Attempting to get someone else may throw because sync hasn't occurred or completed
266
+ // Only throw if attempting to get themself since the relevant information should be available
267
+ if (deviceId === this.#ownDeviceId) throw err
268
+ }
269
+
270
+ return result
271
+ }
272
+
273
+ /**
274
+ * @returns {Promise<Array<MemberInfo>>}
275
+ */
276
+ async getMany() {
277
+ const [allRoles, allDeviceInfo] = await Promise.all([
278
+ this.#roles.getAll(),
279
+ this.#dataTypes.deviceInfo.getMany(),
280
+ ])
281
+
282
+ return Promise.all(
283
+ [...allRoles.entries()].map(async ([deviceId, role]) => {
284
+ /** @type {MemberInfo} */
285
+ const memberInfo = { deviceId, role }
286
+
287
+ try {
288
+ const configCoreId = await this.#coreOwnership.getCoreId(
289
+ deviceId,
290
+ 'config'
291
+ )
292
+
293
+ const deviceInfo = allDeviceInfo.find(
294
+ ({ docId }) => docId === configCoreId
295
+ )
296
+
297
+ memberInfo.name = deviceInfo?.name
298
+ memberInfo.deviceType = deviceInfo?.deviceType
299
+ memberInfo.joinedAt = deviceInfo?.createdAt
300
+ } catch (err) {
301
+ // Attempting to get someone else may throw because sync hasn't occurred or completed
302
+ // Only throw if attempting to get themself since the relevant information should be available
303
+ if (deviceId === this.#ownDeviceId) throw err
304
+ }
305
+
306
+ return memberInfo
307
+ })
308
+ )
309
+ }
310
+
311
+ /**
312
+ * @param {string} deviceId
313
+ * @param {import('./roles.js').RoleIdAssignableToOthers} roleId
314
+ * @returns {Promise<void>}
315
+ */
316
+ async assignRole(deviceId, roleId) {
317
+ return this.#roles.assignRole(deviceId, roleId)
318
+ }
319
+ }