@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
package/src/roles.js ADDED
@@ -0,0 +1,412 @@
1
+ import { currentSchemaVersions } from '@comapeo/schema'
2
+ import mapObject from 'map-obj'
3
+ import { kCreateWithDocId, kDataStore } from './datatype/index.js'
4
+ import { assert, setHas } from './utils.js'
5
+ import { TypedEmitter } from 'tiny-typed-emitter'
6
+ /** @import { Namespace } from './types.js' */
7
+
8
+ // Randomly generated 8-byte encoded as hex
9
+ export const CREATOR_ROLE_ID = 'a12a6702b93bd7ff'
10
+ export const COORDINATOR_ROLE_ID = 'f7c150f5a3a9a855'
11
+ export const MEMBER_ROLE_ID = '012fd2d431c0bf60'
12
+ export const BLOCKED_ROLE_ID = '9e6d29263cba36c9'
13
+ export const LEFT_ROLE_ID = '8ced989b1904606b'
14
+ export const NO_ROLE_ID = '08e4251e36f6e7ed'
15
+
16
+ /**
17
+ * @typedef {T extends Iterable<infer U> ? U : never} ElementOf
18
+ * @template T
19
+ */
20
+
21
+ /** @typedef {ElementOf<typeof ROLE_IDS>} RoleId */
22
+ const ROLE_IDS = new Set(
23
+ /** @type {const} */ ([
24
+ CREATOR_ROLE_ID,
25
+ COORDINATOR_ROLE_ID,
26
+ MEMBER_ROLE_ID,
27
+ BLOCKED_ROLE_ID,
28
+ LEFT_ROLE_ID,
29
+ NO_ROLE_ID,
30
+ ])
31
+ )
32
+ const isRoleId = setHas(ROLE_IDS)
33
+
34
+ /** @typedef {ElementOf<typeof ROLE_IDS_FOR_NEW_INVITE>} RoleIdForNewInvite */
35
+ const ROLE_IDS_FOR_NEW_INVITE = new Set(
36
+ /** @type {const} */ ([COORDINATOR_ROLE_ID, MEMBER_ROLE_ID, BLOCKED_ROLE_ID])
37
+ )
38
+ export const isRoleIdForNewInvite = setHas(ROLE_IDS_FOR_NEW_INVITE)
39
+
40
+ /** @typedef {ElementOf<typeof ROLE_IDS_ASSIGNABLE_TO_OTHERS>} RoleIdAssignableToOthers */
41
+ const ROLE_IDS_ASSIGNABLE_TO_OTHERS = new Set(
42
+ /** @type {const} */ ([COORDINATOR_ROLE_ID, MEMBER_ROLE_ID, BLOCKED_ROLE_ID])
43
+ )
44
+ export const isRoleIdAssignableToOthers = setHas(ROLE_IDS_ASSIGNABLE_TO_OTHERS)
45
+
46
+ /** @typedef {ElementOf<typeof ROLE_IDS_ASSIGNABLE_TO_ANYONE>} RoleIdAssignableToAnyone */
47
+ const ROLE_IDS_ASSIGNABLE_TO_ANYONE = new Set(
48
+ /** @type {const} */ ([
49
+ COORDINATOR_ROLE_ID,
50
+ MEMBER_ROLE_ID,
51
+ BLOCKED_ROLE_ID,
52
+ LEFT_ROLE_ID,
53
+ ])
54
+ )
55
+ const isRoleIdAssignableToAnyone = setHas(ROLE_IDS_ASSIGNABLE_TO_ANYONE)
56
+
57
+ /**
58
+ * @typedef {object} DocCapability
59
+ * @property {boolean} readOwn - can read own data
60
+ * @property {boolean} writeOwn - can write own data
61
+ * @property {boolean} readOthers - can read other's data
62
+ * @property {boolean} writeOthers - can edit or delete other's data
63
+ */
64
+
65
+ /**
66
+ * @template {RoleId} [T=RoleId]
67
+ * @typedef {object} Role
68
+ * @property {T} roleId
69
+ * @property {string} name
70
+ * @property {Record<import('@comapeo/schema').MapeoDoc['schemaName'], DocCapability>} docs
71
+ * @property {RoleIdAssignableToOthers[]} roleAssignment
72
+ * @property {Record<Namespace, 'allowed' | 'blocked'>} sync
73
+ */
74
+
75
+ /**
76
+ * This is currently the same as 'Coordinator' role, but defined separately
77
+ * because the creator should always have ALL powers, but we could edit the
78
+ * 'Coordinator' powers in the future.
79
+ *
80
+ * @type {Role<typeof CREATOR_ROLE_ID>}
81
+ */
82
+ export const CREATOR_ROLE = {
83
+ roleId: CREATOR_ROLE_ID,
84
+ name: 'Project Creator',
85
+ docs: mapObject(currentSchemaVersions, (key) => {
86
+ return [
87
+ key,
88
+ { readOwn: true, writeOwn: true, readOthers: true, writeOthers: true },
89
+ ]
90
+ }),
91
+ roleAssignment: [COORDINATOR_ROLE_ID, MEMBER_ROLE_ID, BLOCKED_ROLE_ID],
92
+ sync: {
93
+ auth: 'allowed',
94
+ config: 'allowed',
95
+ data: 'allowed',
96
+ blobIndex: 'allowed',
97
+ blob: 'allowed',
98
+ },
99
+ }
100
+
101
+ /**
102
+ * This is the role assumed for a device when no role record can be found. This
103
+ * can happen when an invited device did not manage to sync with the device that
104
+ * invited them, and they then try to sync with someone else. We want them to be
105
+ * able to sync the auth and config store, because that way they may be able to
106
+ * receive their role record, and they can get the project config so that they
107
+ * can start collecting data.
108
+ *
109
+ * @type {Role<typeof NO_ROLE_ID>}
110
+ */
111
+ export const NO_ROLE = {
112
+ roleId: NO_ROLE_ID,
113
+ name: 'No Role',
114
+ docs: mapObject(currentSchemaVersions, (key) => {
115
+ return [
116
+ key,
117
+ { readOwn: true, writeOwn: true, readOthers: false, writeOthers: false },
118
+ ]
119
+ }),
120
+ roleAssignment: [],
121
+ sync: {
122
+ auth: 'allowed',
123
+ config: 'allowed',
124
+ data: 'blocked',
125
+ blobIndex: 'blocked',
126
+ blob: 'blocked',
127
+ },
128
+ }
129
+
130
+ /** @type {{ [K in RoleId]: Role<K> }} */
131
+ export const ROLES = {
132
+ [CREATOR_ROLE_ID]: CREATOR_ROLE,
133
+ [MEMBER_ROLE_ID]: {
134
+ roleId: MEMBER_ROLE_ID,
135
+ name: 'Member',
136
+ docs: mapObject(currentSchemaVersions, (key) => {
137
+ return [
138
+ key,
139
+ { readOwn: true, writeOwn: true, readOthers: true, writeOthers: false },
140
+ ]
141
+ }),
142
+ roleAssignment: [],
143
+ sync: {
144
+ auth: 'allowed',
145
+ config: 'allowed',
146
+ data: 'allowed',
147
+ blobIndex: 'allowed',
148
+ blob: 'allowed',
149
+ },
150
+ },
151
+ [COORDINATOR_ROLE_ID]: {
152
+ roleId: COORDINATOR_ROLE_ID,
153
+ name: 'Coordinator',
154
+ docs: mapObject(currentSchemaVersions, (key) => {
155
+ return [
156
+ key,
157
+ { readOwn: true, writeOwn: true, readOthers: true, writeOthers: true },
158
+ ]
159
+ }),
160
+ roleAssignment: [COORDINATOR_ROLE_ID, MEMBER_ROLE_ID, BLOCKED_ROLE_ID],
161
+ sync: {
162
+ auth: 'allowed',
163
+ config: 'allowed',
164
+ data: 'allowed',
165
+ blobIndex: 'allowed',
166
+ blob: 'allowed',
167
+ },
168
+ },
169
+ [BLOCKED_ROLE_ID]: {
170
+ roleId: BLOCKED_ROLE_ID,
171
+ name: 'Blocked',
172
+ docs: mapObject(currentSchemaVersions, (key) => {
173
+ return [
174
+ key,
175
+ {
176
+ readOwn: false,
177
+ writeOwn: false,
178
+ readOthers: false,
179
+ writeOthers: false,
180
+ },
181
+ ]
182
+ }),
183
+ roleAssignment: [],
184
+ sync: {
185
+ auth: 'blocked',
186
+ config: 'blocked',
187
+ data: 'blocked',
188
+ blobIndex: 'blocked',
189
+ blob: 'blocked',
190
+ },
191
+ },
192
+ [LEFT_ROLE_ID]: {
193
+ roleId: LEFT_ROLE_ID,
194
+ name: 'Left',
195
+ docs: mapObject(currentSchemaVersions, (key) => {
196
+ return [
197
+ key,
198
+ {
199
+ readOwn: false,
200
+ writeOwn: false,
201
+ readOthers: false,
202
+ writeOthers: false,
203
+ },
204
+ ]
205
+ }),
206
+ roleAssignment: [],
207
+ sync: {
208
+ auth: 'allowed',
209
+ config: 'blocked',
210
+ data: 'blocked',
211
+ blobIndex: 'blocked',
212
+ blob: 'blocked',
213
+ },
214
+ },
215
+ [NO_ROLE_ID]: NO_ROLE,
216
+ }
217
+
218
+ /**
219
+ * @typedef {object} RolesEvents
220
+ * @property {(docIds: Set<string>) => void} update Emitted when new role records are indexed
221
+ */
222
+
223
+ /**
224
+ * @extends {TypedEmitter<RolesEvents>}
225
+ */
226
+ export class Roles extends TypedEmitter {
227
+ #dataType
228
+ #coreOwnership
229
+ #coreManager
230
+ #projectCreatorAuthCoreId
231
+ #ownDeviceId
232
+
233
+ static NO_ROLE = NO_ROLE
234
+
235
+ /**
236
+ *
237
+ * @param {object} opts
238
+ * @param {import('./datatype/index.js').DataType<
239
+ * import('./datastore/index.js').DataStore<'auth'>,
240
+ * typeof import('./schema/project.js').roleTable,
241
+ * 'role',
242
+ * import('@comapeo/schema').Role,
243
+ * import('@comapeo/schema').RoleValue
244
+ * >} opts.dataType
245
+ * @param {import('./core-ownership.js').CoreOwnership} opts.coreOwnership
246
+ * @param {import('./core-manager/index.js').CoreManager} opts.coreManager
247
+ * @param {Buffer} opts.projectKey
248
+ * @param {Buffer} opts.deviceKey public key of this device
249
+ */
250
+ constructor({ dataType, coreOwnership, coreManager, projectKey, deviceKey }) {
251
+ super()
252
+ this.#dataType = dataType
253
+ this.#coreOwnership = coreOwnership
254
+ this.#coreManager = coreManager
255
+ this.#projectCreatorAuthCoreId = projectKey.toString('hex')
256
+ this.#ownDeviceId = deviceKey.toString('hex')
257
+ dataType[kDataStore].on('role', this.emit.bind(this, 'update'))
258
+ }
259
+
260
+ /**
261
+ * Get the role for device `deviceId`.
262
+ *
263
+ * @param {string} deviceId
264
+ * @returns {Promise<Role>}
265
+ */
266
+ async getRole(deviceId) {
267
+ /** @type {string} */
268
+ let roleId
269
+ try {
270
+ const roleAssignment = await this.#dataType.getByDocId(deviceId)
271
+ roleId = roleAssignment.roleId
272
+ } catch (e) {
273
+ // The project creator will have the creator role
274
+ const authCoreId = await this.#coreOwnership.getCoreId(deviceId, 'auth')
275
+ if (authCoreId === this.#projectCreatorAuthCoreId) {
276
+ return CREATOR_ROLE
277
+ } else {
278
+ // When no role assignment exists, e.g. a newly added device which has
279
+ // not yet synced role records.
280
+ return NO_ROLE
281
+ }
282
+ }
283
+ if (!isRoleId(roleId)) {
284
+ return ROLES[BLOCKED_ROLE_ID]
285
+ }
286
+ return ROLES[roleId]
287
+ }
288
+
289
+ /**
290
+ * Get roles of all devices in the project. For your own device, if you have
291
+ * not yet synced your own role record, the "no role" capabilties is
292
+ * returned. The project creator will have the creator role unless a
293
+ * different one has been assigned.
294
+ *
295
+ * @returns {Promise<Map<string, Role>>} Map of deviceId to Role
296
+ */
297
+ async getAll() {
298
+ const roles = await this.#dataType.getMany()
299
+ /** @type {Map<string, Role>} */
300
+ const result = new Map()
301
+ /** @type {undefined | string} */
302
+ let projectCreatorDeviceId
303
+ try {
304
+ projectCreatorDeviceId = await this.#coreOwnership.getOwner(
305
+ this.#projectCreatorAuthCoreId
306
+ )
307
+ // Default to creator role, but can be overwritten if a different role is
308
+ // set below
309
+ result.set(projectCreatorDeviceId, CREATOR_ROLE)
310
+ } catch (e) {
311
+ // Not found, we don't know who the project creator is so we can't include
312
+ // them in the returned map
313
+ }
314
+
315
+ for (const role of roles) {
316
+ if (!isRoleId(role.roleId)) {
317
+ console.error("Found a value that wasn't a role ID")
318
+ continue
319
+ }
320
+ if (role.roleId === CREATOR_ROLE_ID) {
321
+ console.error('Unexpected creator role')
322
+ continue
323
+ }
324
+ const deviceId = role.docId
325
+ result.set(deviceId, ROLES[role.roleId])
326
+ }
327
+ const includesSelf = result.has(this.#ownDeviceId)
328
+ if (!includesSelf) {
329
+ const isProjectCreator = this.#ownDeviceId === projectCreatorDeviceId
330
+ result.set(this.#ownDeviceId, isProjectCreator ? CREATOR_ROLE : NO_ROLE)
331
+ }
332
+ return result
333
+ }
334
+
335
+ /**
336
+ * Assign a role to the specified `deviceId`. Devices without an assigned role
337
+ * are unable to sync, except the project creator who can do anything. Only
338
+ * the project creator can assign their own role. Will throw if the device's
339
+ * role cannot assign the role by consulting `roleAssignment`.
340
+ *
341
+ * @param {string} deviceId
342
+ * @param {RoleIdAssignableToAnyone} roleId
343
+ */
344
+ async assignRole(deviceId, roleId) {
345
+ assert(
346
+ isRoleIdAssignableToAnyone(roleId),
347
+ `Role ID should be assignable to anyone but got ${roleId}`
348
+ )
349
+
350
+ let fromIndex = 0
351
+ let authCoreId
352
+ try {
353
+ authCoreId = await this.#coreOwnership.getCoreId(deviceId, 'auth')
354
+ const authCoreKey = Buffer.from(authCoreId, 'hex')
355
+ const authCore = this.#coreManager.getCoreByKey(authCoreKey)
356
+ if (authCore) {
357
+ await authCore.ready()
358
+ fromIndex = authCore.length
359
+ }
360
+ } catch {
361
+ // This will usually happen when assigning a role to a newly invited
362
+ // device that has not yet synced (so we do not yet have a replica of
363
+ // their authCore). In this case we want fromIndex to be 0
364
+ }
365
+ const isAssigningProjectCreatorRole =
366
+ authCoreId === this.#projectCreatorAuthCoreId
367
+ if (isAssigningProjectCreatorRole && !this.#isProjectCreator()) {
368
+ throw new Error(
369
+ "Only the project creator can assign the project creator's role"
370
+ )
371
+ }
372
+
373
+ if (roleId === LEFT_ROLE_ID) {
374
+ if (deviceId !== this.#ownDeviceId) {
375
+ throw new Error('Cannot assign LEFT role to another device')
376
+ }
377
+ } else {
378
+ const ownRole = await this.getRole(this.#ownDeviceId)
379
+ if (!ownRole.roleAssignment.includes(roleId)) {
380
+ throw new Error('Lacks permission to assign role ' + roleId)
381
+ }
382
+ }
383
+
384
+ const existingRoleDoc = await this.#dataType
385
+ .getByDocId(deviceId)
386
+ .catch(() => null)
387
+
388
+ if (existingRoleDoc) {
389
+ await this.#dataType.update(
390
+ [existingRoleDoc.versionId, ...existingRoleDoc.forks],
391
+ {
392
+ schemaName: 'role',
393
+ roleId,
394
+ fromIndex,
395
+ }
396
+ )
397
+ } else {
398
+ await this.#dataType[kCreateWithDocId](deviceId, {
399
+ schemaName: 'role',
400
+ roleId,
401
+ fromIndex,
402
+ })
403
+ }
404
+ }
405
+
406
+ async #isProjectCreator() {
407
+ const ownAuthCoreId = this.#coreManager
408
+ .getWriterCore('auth')
409
+ .key.toString('hex')
410
+ return ownAuthCoreId === this.#projectCreatorAuthCoreId
411
+ }
412
+ }
@@ -0,0 +1,55 @@
1
+ // These schemas are all in a "client" database. There is only one client
2
+ // database and it contains information that is shared across all projects on a
3
+ // device
4
+ import { blob, sqliteTable, text } from 'drizzle-orm/sqlite-core'
5
+ import { dereferencedDocSchemas as schemas } from '@comapeo/schema'
6
+ import { jsonSchemaToDrizzleColumns as toColumns } from './schema-to-drizzle.js'
7
+ import { backlinkTable, customJson } from './utils.js'
8
+
9
+ /**
10
+ * @internal
11
+ * @typedef {object} ProjectInfo
12
+ * @prop {string} [name]
13
+ */
14
+
15
+ const projectInfoColumn =
16
+ /** @type {ReturnType<typeof import('drizzle-orm/sqlite-core').customType<{data: ProjectInfo}>>} */ (
17
+ customJson
18
+ )
19
+
20
+ /** @type {ProjectInfo} */
21
+ const PROJECT_INFO_DEFAULT_VALUE = {}
22
+
23
+ export const projectSettingsTable = sqliteTable(
24
+ 'projectSettings',
25
+ toColumns(schemas.projectSettings)
26
+ )
27
+ export const projectBacklinkTable = backlinkTable(projectSettingsTable)
28
+ export const projectKeysTable = sqliteTable('projectKeys', {
29
+ projectId: text('projectId').notNull().primaryKey(),
30
+ projectPublicId: text('projectPublicId').notNull(),
31
+ projectInviteId: blob('projectInviteId').notNull(),
32
+ keysCipher: blob('keysCipher', { mode: 'buffer' }).notNull(),
33
+ projectInfo: projectInfoColumn('projectInfo')
34
+ .default(
35
+ // TODO: There's a bug in Drizzle where the default value does not get transformed by the custom type
36
+ // @ts-expect-error
37
+ JSON.stringify(PROJECT_INFO_DEFAULT_VALUE)
38
+ )
39
+ .notNull(),
40
+ })
41
+
42
+ /**
43
+ * @typedef {Omit<import('@comapeo/schema').DeviceInfoValue, 'schemaName'>} DeviceInfoParam
44
+ */
45
+
46
+ const deviceInfoColumn =
47
+ /** @type {ReturnType<typeof import('drizzle-orm/sqlite-core').customType<{data: DeviceInfoParam }>>} */ (
48
+ customJson
49
+ )
50
+
51
+ // This table only ever has one row in it.
52
+ export const localDeviceInfoTable = sqliteTable('localDeviceInfo', {
53
+ deviceId: text('deviceId').notNull().unique(),
54
+ deviceInfo: deviceInfoColumn('deviceInfo').notNull(),
55
+ })
@@ -0,0 +1,44 @@
1
+ // These schemas are all in a "project" database. Each project in Mapeo has an
2
+ // independent "project" database.
3
+ import { blob, sqliteTable, text } from 'drizzle-orm/sqlite-core'
4
+ import { dereferencedDocSchemas as schemas } from '@comapeo/schema'
5
+ import { NAMESPACES } from '../constants.js'
6
+ import { jsonSchemaToDrizzleColumns as toColumns } from './schema-to-drizzle.js'
7
+ import { backlinkTable } from './utils.js'
8
+
9
+ export const translationTable = sqliteTable(
10
+ 'translation',
11
+ toColumns(schemas.translation)
12
+ )
13
+ export const observationTable = sqliteTable(
14
+ 'observation',
15
+ toColumns(schemas.observation)
16
+ )
17
+ export const trackTable = sqliteTable('track', toColumns(schemas.track))
18
+ export const presetTable = sqliteTable('preset', toColumns(schemas.preset))
19
+ export const fieldTable = sqliteTable('field', toColumns(schemas.field))
20
+ export const coreOwnershipTable = sqliteTable(
21
+ 'coreOwnership',
22
+ toColumns(schemas.coreOwnership)
23
+ )
24
+ export const roleTable = sqliteTable('role', toColumns(schemas.role))
25
+ export const deviceInfoTable = sqliteTable(
26
+ 'deviceInfo',
27
+ toColumns(schemas.deviceInfo)
28
+ )
29
+ export const iconTable = sqliteTable('icon', toColumns(schemas.icon))
30
+
31
+ export const translationBacklinkTable = backlinkTable(translationTable)
32
+ export const observationBacklinkTable = backlinkTable(observationTable)
33
+ export const trackBacklinkTable = backlinkTable(trackTable)
34
+ export const presetBacklinkTable = backlinkTable(presetTable)
35
+ export const fieldBacklinkTable = backlinkTable(fieldTable)
36
+ export const coreOwnershipBacklinkTable = backlinkTable(coreOwnershipTable)
37
+ export const roleBacklinkTable = backlinkTable(roleTable)
38
+ export const deviceInfoBacklinkTable = backlinkTable(deviceInfoTable)
39
+ export const iconBacklinkTable = backlinkTable(iconTable)
40
+
41
+ export const coresTable = sqliteTable('cores', {
42
+ publicKey: blob('publicKey', { mode: 'buffer' }).notNull(),
43
+ namespace: text('namespace', { enum: NAMESPACES }).notNull(),
44
+ })
@@ -0,0 +1,118 @@
1
+ import { text, integer, real } from 'drizzle-orm/sqlite-core'
2
+ import { ExhaustivenessError } from '../utils.js'
3
+ import { customJson } from './utils.js'
4
+ /** @import { MapeoDoc } from '@comapeo/schema' */
5
+ /** @import { MapeoDocMap } from '../types.js' */
6
+
7
+ /**
8
+ Convert a JSONSchema definition to a Drizzle Columns Map (the parameter for
9
+ `sqliteTable()`).
10
+
11
+ **NOTE**: The return of this function is _not_ type-checked (it is coerced with
12
+ `as`, because it's not possible to type-check what this function is doing), but
13
+ the return type _should_ be correct when using this function.
14
+ @template {import('./types.js').JSONSchema7WithProps} TSchema
15
+ NB: The inline typescript checker often marks this next line as an error, but this seems to be a bug with JSDoc parsing - running `tsc` does not show this as an error.
16
+ @template {import('type-fest').Get<TSchema, 'properties.schemaName.const'>} TSchemaName
17
+ @template {TSchemaName extends MapeoDoc['schemaName'] ? MapeoDocMap[TSchemaName] : any} TObjectType
18
+ @param {TSchema} schema
19
+ @returns {import('./types.js').SchemaToDrizzleColumns<TSchema, TObjectType>}
20
+ */
21
+ export function jsonSchemaToDrizzleColumns(schema) {
22
+ if (schema.type !== 'object' || !schema.properties) {
23
+ throw new Error('Cannot process JSONSchema as SQL table')
24
+ }
25
+ /** @type {Record<string, any>} */
26
+ const columns = {}
27
+ for (const [key, value] of Object.entries(schema.properties)) {
28
+ if (typeof value !== 'object') continue
29
+ if (isArray(value.type) || typeof value.type === 'undefined') {
30
+ throw new Error('Cannot process JSONSchema as SQL table')
31
+ }
32
+ switch (value.type) {
33
+ case 'boolean':
34
+ columns[key] = integer(key, { mode: 'boolean' })
35
+ break
36
+ case 'number':
37
+ columns[key] = real(key)
38
+ break
39
+ case 'integer':
40
+ columns[key] = integer(key)
41
+ break
42
+ case 'string': {
43
+ const enumValue = isStringArray(value.enum)
44
+ ? value.enum
45
+ : typeof value.const === 'string'
46
+ ? /** @type {[typeof value.const]} */ ([value.const])
47
+ : undefined
48
+ columns[key] = text(key, { enum: enumValue })
49
+ if (key === 'docId') {
50
+ columns[key] = columns[key].primaryKey()
51
+ }
52
+ break
53
+ }
54
+ case 'array':
55
+ case 'object':
56
+ columns[key] = customJson(key)
57
+ break
58
+ case 'null':
59
+ // Skip handling this right now
60
+ continue
61
+ default:
62
+ throw new ExhaustivenessError(value.type)
63
+ }
64
+ if (isRequired(schema, key)) {
65
+ columns[key] = columns[key].notNull()
66
+ // Only set defaults for required fields
67
+ const defaultValue = getDefault(value)
68
+ if (typeof defaultValue !== 'undefined') {
69
+ columns[key] = columns[key].default(defaultValue)
70
+ }
71
+ }
72
+ }
73
+ // Not yet in @comapeo/schema
74
+ columns.forks = customJson('forks').notNull()
75
+ return /** @type {any} */ (columns)
76
+ }
77
+
78
+ /**
79
+ * @template {import('./types.js').JSONSchema7} T
80
+ * @param {T} value
81
+ * @returns {T['default']}
82
+ */
83
+ function getDefault(value) {
84
+ return value.default
85
+ }
86
+
87
+ /**
88
+ * @param {import('./types.js').JSONSchema7WithProps} schema
89
+ * @param {string} key
90
+ * @returns {boolean}
91
+ */
92
+ function isRequired(schema, key) {
93
+ if (!isArray(schema.required)) return false
94
+ return schema.required.includes(key)
95
+ }
96
+
97
+ /**
98
+ * Tests whether a value is an array.
99
+ * @param {any} value
100
+ * @returns {value is readonly unknown[]}
101
+ */
102
+ function isArray(value) {
103
+ // See: https://github.com/microsoft/TypeScript/issues/17002
104
+ return Array.isArray(value)
105
+ }
106
+
107
+ /**
108
+ * Check if a value is an array of strings of length at least one
109
+ * @param {any} value
110
+ * @returns {value is [string, ...string[]]}
111
+ */
112
+ function isStringArray(value) {
113
+ return (
114
+ isArray(value) &&
115
+ value.every((v) => typeof v === 'string') &&
116
+ value.length > 0
117
+ )
118
+ }