@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.
- package/LICENSE.md +9 -0
- package/README.md +31 -0
- package/dist/blob-api.d.ts +92 -0
- package/dist/blob-api.d.ts.map +1 -0
- package/dist/blob-store/index.d.ts +163 -0
- package/dist/blob-store/index.d.ts.map +1 -0
- package/dist/blob-store/live-download.d.ts +107 -0
- package/dist/blob-store/live-download.d.ts.map +1 -0
- package/dist/config-import.d.ts +74 -0
- package/dist/config-import.d.ts.map +1 -0
- package/dist/constants.d.ts +14 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/core-manager/bitfield-rle.d.ts +25 -0
- package/dist/core-manager/bitfield-rle.d.ts.map +1 -0
- package/dist/core-manager/core-index.d.ts +56 -0
- package/dist/core-manager/core-index.d.ts.map +1 -0
- package/dist/core-manager/index.d.ts +125 -0
- package/dist/core-manager/index.d.ts.map +1 -0
- package/dist/core-manager/random-access-file-pool.d.ts +17 -0
- package/dist/core-manager/random-access-file-pool.d.ts.map +1 -0
- package/dist/core-manager/remote-bitfield.d.ts +146 -0
- package/dist/core-manager/remote-bitfield.d.ts.map +1 -0
- package/dist/core-ownership.d.ts +112 -0
- package/dist/core-ownership.d.ts.map +1 -0
- package/dist/datastore/index.d.ts +91 -0
- package/dist/datastore/index.d.ts.map +1 -0
- package/dist/datatype/index.d.ts +108 -0
- package/dist/discovery/local-discovery.d.ts +64 -0
- package/dist/discovery/local-discovery.d.ts.map +1 -0
- package/dist/errors.d.ts +4 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/fastify-controller.d.ts +27 -0
- package/dist/fastify-controller.d.ts.map +1 -0
- package/dist/fastify-plugins/blobs.d.ts +6 -0
- package/dist/fastify-plugins/blobs.d.ts.map +1 -0
- package/dist/fastify-plugins/constants.d.ts +3 -0
- package/dist/fastify-plugins/constants.d.ts.map +1 -0
- package/dist/fastify-plugins/icons.d.ts +6 -0
- package/dist/fastify-plugins/icons.d.ts.map +1 -0
- package/dist/fastify-plugins/maps/index.d.ts +11 -0
- package/dist/fastify-plugins/maps/index.d.ts.map +1 -0
- package/dist/fastify-plugins/maps/offline-fallback-map.d.ts +12 -0
- package/dist/fastify-plugins/maps/offline-fallback-map.d.ts.map +1 -0
- package/dist/fastify-plugins/maps/static-maps.d.ts +11 -0
- package/dist/fastify-plugins/maps/static-maps.d.ts.map +1 -0
- package/dist/fastify-plugins/utils.d.ts +23 -0
- package/dist/fastify-plugins/utils.d.ts.map +1 -0
- package/dist/generated/extensions.d.ts +44 -0
- package/dist/generated/extensions.d.ts.map +1 -0
- package/dist/generated/keys.d.ts +36 -0
- package/dist/generated/keys.d.ts.map +1 -0
- package/dist/generated/rpc.d.ts +87 -0
- package/dist/generated/rpc.d.ts.map +1 -0
- package/dist/icon-api.d.ts +109 -0
- package/dist/icon-api.d.ts.map +1 -0
- package/dist/index-writer/index.d.ts +51 -0
- package/dist/index-writer/index.d.ts.map +1 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/invite-api.d.ts +70 -0
- package/dist/invite-api.d.ts.map +1 -0
- package/dist/lib/hashmap.d.ts +62 -0
- package/dist/lib/hashmap.d.ts.map +1 -0
- package/dist/lib/hypercore-helpers.d.ts +6 -0
- package/dist/lib/hypercore-helpers.d.ts.map +1 -0
- package/dist/lib/noise-secret-stream-helpers.d.ts +45 -0
- package/dist/lib/noise-secret-stream-helpers.d.ts.map +1 -0
- package/dist/lib/ponyfills.d.ts +10 -0
- package/dist/lib/ponyfills.d.ts.map +1 -0
- package/dist/lib/string.d.ts +2 -0
- package/dist/lib/string.d.ts.map +1 -0
- package/dist/lib/timing-safe-equal.d.ts +15 -0
- package/dist/lib/timing-safe-equal.d.ts.map +1 -0
- package/dist/local-peers.d.ts +151 -0
- package/dist/local-peers.d.ts.map +1 -0
- package/dist/logger.d.ts +32 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/mapeo-manager.d.ts +178 -0
- package/dist/mapeo-manager.d.ts.map +1 -0
- package/dist/mapeo-project.d.ts +3233 -0
- package/dist/mapeo-project.d.ts.map +1 -0
- package/dist/member-api.d.ts +114 -0
- package/dist/member-api.d.ts.map +1 -0
- package/dist/roles.d.ts +157 -0
- package/dist/roles.d.ts.map +1 -0
- package/dist/schema/client.d.ts +284 -0
- package/dist/schema/client.d.ts.map +1 -0
- package/dist/schema/project.d.ts +1812 -0
- package/dist/schema/project.d.ts.map +1 -0
- package/dist/schema/schema-to-drizzle.d.ts +20 -0
- package/dist/schema/schema-to-drizzle.d.ts.map +1 -0
- package/dist/schema/types.d.ts +98 -0
- package/dist/schema/types.d.ts.map +1 -0
- package/dist/schema/utils.d.ts +55 -0
- package/dist/schema/utils.d.ts.map +1 -0
- package/dist/sync/core-sync-state.d.ts +252 -0
- package/dist/sync/core-sync-state.d.ts.map +1 -0
- package/dist/sync/namespace-sync-state.d.ts +47 -0
- package/dist/sync/namespace-sync-state.d.ts.map +1 -0
- package/dist/sync/peer-sync-controller.d.ts +44 -0
- package/dist/sync/peer-sync-controller.d.ts.map +1 -0
- package/dist/sync/sync-api.d.ts +158 -0
- package/dist/sync/sync-api.d.ts.map +1 -0
- package/dist/sync/sync-state.d.ts +40 -0
- package/dist/sync/sync-state.d.ts.map +1 -0
- package/dist/translation-api.d.ts +288 -0
- package/dist/translation-api.d.ts.map +1 -0
- package/dist/types.d.ts +115 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/utils.d.ts +115 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils_types.d.ts +14 -0
- package/drizzle/client/0000_bumpy_carnage.sql +33 -0
- package/drizzle/client/meta/0000_snapshot.json +199 -0
- package/drizzle/client/meta/_journal.json +13 -0
- package/drizzle/project/0000_spooky_lady_ursula.sql +192 -0
- package/drizzle/project/meta/0000_snapshot.json +1137 -0
- package/drizzle/project/meta/_journal.json +13 -0
- package/package.json +202 -0
- package/src/blob-api.js +139 -0
- package/src/blob-store/index.js +325 -0
- package/src/blob-store/live-download.js +373 -0
- package/src/config-import.js +604 -0
- package/src/constants.js +34 -0
- package/src/core-manager/bitfield-rle.js +235 -0
- package/src/core-manager/core-index.js +87 -0
- package/src/core-manager/index.js +504 -0
- package/src/core-manager/random-access-file-pool.js +30 -0
- package/src/core-manager/remote-bitfield.js +416 -0
- package/src/core-ownership.js +235 -0
- package/src/datastore/README.md +46 -0
- package/src/datastore/index.js +234 -0
- package/src/datatype/README.md +33 -0
- package/src/datatype/index.d.ts +108 -0
- package/src/datatype/index.js +358 -0
- package/src/discovery/local-discovery.js +303 -0
- package/src/errors.js +5 -0
- package/src/fastify-controller.js +84 -0
- package/src/fastify-plugins/blobs.js +139 -0
- package/src/fastify-plugins/constants.js +5 -0
- package/src/fastify-plugins/icons.js +158 -0
- package/src/fastify-plugins/maps/index.js +173 -0
- package/src/fastify-plugins/maps/offline-fallback-map.js +114 -0
- package/src/fastify-plugins/maps/static-maps.js +271 -0
- package/src/fastify-plugins/utils.js +52 -0
- package/src/generated/README.md +3 -0
- package/src/generated/extensions.d.ts +44 -0
- package/src/generated/extensions.js +196 -0
- package/src/generated/extensions.ts +237 -0
- package/src/generated/keys.d.ts +36 -0
- package/src/generated/keys.js +148 -0
- package/src/generated/keys.ts +185 -0
- package/src/generated/rpc.d.ts +87 -0
- package/src/generated/rpc.js +389 -0
- package/src/generated/rpc.ts +463 -0
- package/src/icon-api.js +282 -0
- package/src/index-writer/README.md +38 -0
- package/src/index-writer/index.js +124 -0
- package/src/index.js +16 -0
- package/src/invite-api.js +450 -0
- package/src/lib/hashmap.js +91 -0
- package/src/lib/hypercore-helpers.js +18 -0
- package/src/lib/noise-secret-stream-helpers.js +37 -0
- package/src/lib/ponyfills.js +25 -0
- package/src/lib/string.js +7 -0
- package/src/lib/timing-safe-equal.js +34 -0
- package/src/local-peers.js +737 -0
- package/src/logger.js +99 -0
- package/src/mapeo-manager.js +914 -0
- package/src/mapeo-project.js +980 -0
- package/src/member-api.js +319 -0
- package/src/roles.js +412 -0
- package/src/schema/client.js +55 -0
- package/src/schema/project.js +44 -0
- package/src/schema/schema-to-drizzle.js +118 -0
- package/src/schema/types.ts +153 -0
- package/src/schema/utils.js +51 -0
- package/src/sync/core-sync-state.js +440 -0
- package/src/sync/namespace-sync-state.js +193 -0
- package/src/sync/peer-sync-controller.js +332 -0
- package/src/sync/sync-api.js +588 -0
- package/src/sync/sync-state.js +63 -0
- package/src/translation-api.js +141 -0
- package/src/types.ts +149 -0
- package/src/utils.js +210 -0
- 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
|
+
}
|