Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

update MapeoManager to return and handle project public IDs #247

Merged
merged 4 commits into from
Sep 6, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ CREATE TABLE `project_backlink` (
--> statement-breakpoint
CREATE TABLE `projectKeys` (
`projectId` text PRIMARY KEY NOT NULL,
`projectPublicId` text NOT NULL,
`keysCipher` blob NOT NULL,
`projectInfo` text DEFAULT '{}' NOT NULL
);
Expand Down
9 changes: 8 additions & 1 deletion drizzle/client/meta/0000_snapshot.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"version": "5",
"dialect": "sqlite",
"id": "a4afa0b6-5fd1-4c8d-bc09-7e3f3fc1928f",
"id": "e047380e-7d10-442c-b5e4-0791b8970eeb",
"prevId": "00000000-0000-0000-0000-000000000000",
"tables": {
"project_backlink": {
Expand Down Expand Up @@ -30,6 +30,13 @@
"notNull": true,
"autoincrement": false
},
"projectPublicId": {
"name": "projectPublicId",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"keysCipher": {
"name": "keysCipher",
"type": "blob",
Expand Down
4 changes: 2 additions & 2 deletions drizzle/client/meta/_journal.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
{
"idx": 0,
"version": "5",
"when": 1693337118674,
"tag": "0000_steady_jackpot",
"when": 1693938675535,
"tag": "0000_needy_hex",
"breakpoints": true
}
]
Expand Down
13 changes: 7 additions & 6 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@
"@digidem/types": "^2.0.0",
"@fastify/type-provider-typebox": "^3.3.0",
"@hyperswarm/secret-stream": "^6.1.2",
"@mapeo/crypto": "^1.0.0-alpha.7",
"@mapeo/crypto": "^1.0.0-alpha.8",
"@mapeo/schema": "^3.0.0-next.8",
"@mapeo/sqlite-indexer": "^1.0.0-alpha.6",
"@sinclair/typebox": "^0.29.6",
Expand Down
97 changes: 65 additions & 32 deletions src/mapeo-manager.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,17 @@ import { IndexWriter } from './index-writer/index.js'
import { MapeoProject } from './mapeo-project.js'
import { projectKeysTable, projectTable } from './schema/client.js'
import { ProjectKeys } from './generated/keys.js'
import { deNullify } from './utils.js'
import {
deNullify,
projectIdToNonce,
projectKeyToId,
projectKeyToPublicId,
} from './utils.js'
import { RandomAccessFilePool } from './core-manager/random-access-file-pool.js'

/** @typedef {import("@mapeo/schema").ProjectValue} ProjectValue */
/** @typedef {import('./types.js').ProjectId} ProjectId */
/** @typedef {import('./types.js').ProjectPublicId} ProjectPublicId */

const CLIENT_SQLITE_FILE_NAME = 'client.db'

Expand All @@ -27,7 +34,7 @@ export class MapeoManager {
#keyManager
#projectSettingsIndexWriter
#db
/** @type {Map<string, MapeoProject>} */
/** @type {Map<ProjectPublicId, MapeoProject>} */
#activeProjects
/** @type {import('./types.js').CoreStorage} */
#coreStorage
Expand Down Expand Up @@ -67,17 +74,18 @@ export class MapeoManager {

/**
* @param {Buffer} keysCipher
* @param {string} projectId
* @param {ProjectId} projectId
* @returns {ProjectKeys}
*/
#decodeProjectKeysCipher(keysCipher, projectId) {
const nonce = projectIdToNonce(projectId)
return ProjectKeys.decode(
this.#keyManager.decryptLocalMessage(keysCipher, projectId)
this.#keyManager.decryptLocalMessage(keysCipher, nonce)
)
}

/**
* @param {string} projectId
* @param {ProjectId} projectId
* @returns {Pick<ConstructorParameters<typeof MapeoProject>[0], 'dbPath' | 'coreStorage'>}
*/
#projectStorage(projectId) {
Expand All @@ -92,19 +100,28 @@ export class MapeoManager {

/**
* @param {Object} opts
* @param {string} opts.projectId
* @param {ProjectId} opts.projectId
* @param {ProjectPublicId} opts.projectPublicId
* @param {ProjectKeys} opts.projectKeys
* @param {import('./generated/rpc.js').Invite_ProjectInfo} [opts.projectInfo]
*/
#saveToProjectKeysTable({ projectId, projectKeys, projectInfo }) {
#saveToProjectKeysTable({
projectId,
projectPublicId,
projectKeys,
projectInfo,
}) {
const encoded = ProjectKeys.encode(projectKeys).finish()
const nonce = projectIdToNonce(projectId)

this.#db
.insert(projectKeysTable)
.values({
projectId,
projectPublicId,
keysCipher: this.#keyManager.encryptLocalMessage(
Buffer.from(encoded.buffer, encoded.byteOffset, encoded.byteLength),
projectId
nonce
),
projectInfo,
})
Expand All @@ -114,7 +131,7 @@ export class MapeoManager {
/**
* Create a new project.
* @param {import('type-fest').Simplify<Partial<Pick<ProjectValue, 'name'>>>} [settings]
* @returns {Promise<string>}
* @returns {Promise<ProjectPublicId>}
*/
async createProject(settings = {}) {
// 1. Create project keypair
Expand All @@ -138,10 +155,14 @@ export class MapeoManager {
encryptionKeys,
}

// TODO: Update to use @mapeo/crypto when ready (https://github.com/digidem/mapeo-core-next/issues/171)
const projectId = projectKeypair.publicKey.toString('hex')
const projectId = projectKeyToId(keys.projectKey)
const projectPublicId = projectKeyToPublicId(keys.projectKey)

this.#saveToProjectKeysTable({ projectId, projectKeys: keys })
this.#saveToProjectKeysTable({
projectId,
projectPublicId,
projectKeys: keys,
})

// 4. Create MapeoProject instance
const project = new MapeoProject({
Expand All @@ -158,36 +179,40 @@ export class MapeoManager {
await project.$setProjectSettings(settings)

// TODO: Close the project instance instead of keeping it around
// https://github.com/digidem/mapeo-core-next/issues/207
this.#activeProjects.set(projectId, project)
this.#activeProjects.set(projectPublicId, project)

// 6. Return project id
return projectId
// 6. Return project public id
return projectPublicId
}

/**
* @param {string} projectId
* @param {ProjectPublicId} projectPublicId
* @returns {Promise<MapeoProject>}
*/
async getProject(projectId) {
async getProject(projectPublicId) {
// 1. Check for existing active project
const activeProject = this.#activeProjects.get(projectId)
const activeProject = this.#activeProjects.get(projectPublicId)

if (activeProject) return activeProject

// 2. Create project instance
const projectKeysTableResult = this.#db
.select({
projectId: projectKeysTable.projectId,
keysCipher: projectKeysTable.keysCipher,
})
.from(projectKeysTable)
.where(eq(projectKeysTable.projectId, projectId))
.where(eq(projectKeysTable.projectPublicId, projectPublicId))
.get()

if (!projectKeysTableResult) {
throw new Error(`NotFound: project ID ${projectId} not found`)
throw new Error(`NotFound: project ID ${projectPublicId} not found`)
}

const projectId = /** @type {ProjectId} */ (
projectKeysTableResult.projectId
)

const projectKeys = this.#decodeProjectKeysCipher(
projectKeysTableResult.keysCipher,
projectId
Expand All @@ -202,13 +227,13 @@ export class MapeoManager {
})

// 3. Keep track of project instance as we know it's a properly existing project
this.#activeProjects.set(projectId, project)
this.#activeProjects.set(projectPublicId, project)

return project
}

/**
* @returns {Promise<Array<Pick<ProjectValue, 'name'> & { projectId: string, createdAt?: string, updatedAt?: string }>>}
* @returns {Promise<Array<Pick<ProjectValue, 'name'> & { projectId: ProjectPublicId, createdAt?: string, updatedAt?: string }>>}
*/
async listProjects() {
// We use the project keys table as the source of truth for projects that exist
Expand All @@ -217,6 +242,7 @@ export class MapeoManager {
const allProjectKeysResult = this.#db
.select({
projectId: projectKeysTable.projectId,
projectPublicId: projectKeysTable.projectPublicId,
projectInfo: projectKeysTable.projectInfo,
})
.from(projectKeysTable)
Expand All @@ -232,17 +258,21 @@ export class MapeoManager {
.from(projectTable)
.all()

/** @type {Array<Pick<ProjectValue, 'name'> & { projectId: string, createdAt?: string, updatedAt?: string }>} */
/** @type {Array<Pick<ProjectValue, 'name'> & { projectId: ProjectPublicId, createdAt?: string, updatedAt?: string }>} */
const result = []

for (const { projectId, projectInfo } of allProjectKeysResult) {
for (const {
projectId,
projectPublicId,
projectInfo,
} of allProjectKeysResult) {
const existingProject = allProjectsResult.find(
(p) => p.projectId === projectId
)

result.push(
deNullify({
projectId,
projectId: /** @type {ProjectPublicId} */ (projectPublicId),
createdAt: existingProject?.createdAt,
updatedAt: existingProject?.updatedAt,
name: existingProject?.name || projectInfo.name,
Expand All @@ -255,28 +285,30 @@ export class MapeoManager {

/**
* @param {import('./generated/rpc.js').Invite} invite
* @returns {Promise<string>}
* @returns {Promise<ProjectPublicId>}
*/
async addProject({ projectKey, encryptionKeys, projectInfo }) {
const projectId = projectKey.toString('hex')
const projectPublicId = projectKeyToPublicId(projectKey)

// 1. Check for an active project
const activeProject = this.#activeProjects.get(projectId)
const activeProject = this.#activeProjects.get(projectPublicId)

if (activeProject) {
throw new Error(`Project with ID ${projectId} already exists`)
throw new Error(`Project with ID ${projectPublicId} already exists`)
}

// 2. Check if the project exists in the project keys table
// If it does, that means the project has already been either created or added before
const projectId = projectKeyToId(projectKey)

const projectExists = this.#db
.select()
.from(projectKeysTable)
.where(eq(projectKeysTable.projectId, projectId))
.get()

if (projectExists) {
throw new Error(`Project with ID ${projectId} already exists`)
throw new Error(`Project with ID ${projectPublicId} already exists`)
}

// TODO: Relies on completion of https://github.com/digidem/mapeo-core-next/issues/233
Expand All @@ -285,13 +317,14 @@ export class MapeoManager {
// 4. Update the project keys table
this.#saveToProjectKeysTable({
projectId,
projectPublicId,
projectKeys: {
projectKey,
encryptionKeys,
},
projectInfo,
})

return projectId
return projectPublicId
}
}
5 changes: 2 additions & 3 deletions src/mapeo-project.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import {
mapAndValidateCoreOwnership,
} from './core-ownership.js'
import { Capabilities } from './capabilities.js'
import { valueOf } from './utils.js'
import { projectKeyToId, valueOf } from './utils.js'

/** @typedef {Omit<import('@mapeo/schema').ProjectValue, 'schemaName'>} EditableProjectSettings */

Expand Down Expand Up @@ -69,8 +69,7 @@ export class MapeoProject {
projectSecretKey,
encryptionKeys,
}) {
// TODO: Update to use @mapeo/crypto when ready (https://github.com/digidem/mapeo-core-next/issues/171)
this.#projectId = projectKey.toString('hex')
this.#projectId = projectKeyToId(projectKey)

///////// 1. Setup database
const sqlite = new Database(dbPath)
Expand Down
1 change: 1 addition & 0 deletions src/schema/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export const projectTable = sqliteTable('project', toColumns(schemas.project))
export const projectBacklinkTable = backlinkTable(projectTable)
export const projectKeysTable = sqliteTable('projectKeys', {
projectId: text('projectId').notNull().primaryKey(),
projectPublicId: text('projectPublicId').notNull(),
keysCipher: blob('keysCipher', { mode: 'buffer' }).notNull(),
projectInfo: projectInfoColumn('projectInfo')
.default(
Expand Down
5 changes: 5 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type {
ValueOf,
RequireAtLeastOne,
SetOptional,
Opaque,
} from 'type-fest'
import { SUPPORTED_BLOB_VARIANTS } from './blob-store/index.js'
import { MapeoCommon, MapeoDoc, MapeoValue, decode } from '@mapeo/schema'
Expand Down Expand Up @@ -142,6 +143,10 @@ export type TopicKey = Buffer
export type TopicId = string
/** 52 character base32 encoding of `Topic` Buffer */
export type MdnsTopicId = string
/** hex string representation of project key buffer */
export type ProjectId = Opaque<string, 'ProjectId'>
/** z32-encoded hash of project key */
export type ProjectPublicId = Opaque<string, 'ProjectPublicId'>

// TODO: Figure out where those extra fields come from and find more elegant way to represent this
export type RawDhtConnectionStream = Duplex & {
Expand Down
Loading
Loading