diff --git a/.env.example b/.env.example index 06c46bcd7..dfe089120 100755 --- a/.env.example +++ b/.env.example @@ -113,7 +113,7 @@ RUN=1 # Enable google api functionalities GOOGLE_CLIENT_ID= GOOGLE_CLIENT_SECRET= # Unnecessary for now, not doing serverside 2step -GOOGLE_DEV_API_KEY= # Unnecessary for now, not doing serverside 2step +GOOGLE_DEV_API_KEY= # Unnecessary for now, not doing serverside 2step ## Configure RPC nodes (open an issue/ping us to access DeSci Labs' nodes) ETHEREUM_RPC_URL=http://host.docker.internal:8545 @@ -134,7 +134,7 @@ MAX_LOCK_TIME=3600 # 1 hour DOI_PREFIX=10.62891 CROSSREF_DOI_URL=https://doi.org -# Cross ref api +# Cross ref api CROSSREF_METADATA_API=https://test.crossref.org/servlet/deposit CROSSREF_ADMIN_API=https://test.crossref.org CROSSREF_EMAIL= @@ -162,5 +162,9 @@ ES_DB_NAME= ES_DB_USER= ES_DB_PASSWORD= -### open Alex Database - Postgres -OPEN_ALEX_DATABASE_URL=postgresql://username:password@host/database?schema=openalex \ No newline at end of file +### open Alex Database - Postgres +OPEN_ALEX_DATABASE_URL=postgresql://username:password@host/database?schema=openalex + +CLOUDFLARE_WORKER_API=http://host.docker.internal:5445 +CLOUDFLARE_WORKER_API_SECRET=auth-token +ENABLE_WORKERS_API=true diff --git a/.env.test b/.env.test index a983b8529..470f80046 100644 --- a/.env.test +++ b/.env.test @@ -27,7 +27,7 @@ REDIS_PORT=6379 REDIS_URL=redis://host.docker.internal:6379 # LOCAL DEV -# http://localhost:1984 +# http://localhost:1984 # # LIVE: # host: 'arweave.net', @@ -77,7 +77,7 @@ REPO_SERVER_URL=http://host.docker.internal:5485 DOI_PREFIX=10.62891 CROSSREF_DOI_URL=https://doi.org -# Cross ref api +# Cross ref api CROSSREF_METADATA_API=https://test.crossref.org/servlet/deposit CROSSREF_ADMIN_API=https://test.crossref.org CROSSREF_API_KEY= @@ -90,4 +90,8 @@ CROSSREF_NOTIFY_ENDPOINT=endpoint # Automated metadata AUTOMATED_METADATA_API= -AUTOMATED_METADATA_API_KEY= \ No newline at end of file +AUTOMATED_METADATA_API_KEY= + +CLOUDFLARE_WORKER_API=http://host.docker.internal:5446 +CLOUDFLARE_WORKER_API_SECRET=test-api-secret +ENABLE_WORKERS_API=true diff --git a/.github/workflows/deploy-sync-server.yaml b/.github/workflows/deploy-sync-server.yaml index 2036cb03b..5e7f0b6d0 100644 --- a/.github/workflows/deploy-sync-server.yaml +++ b/.github/workflows/deploy-sync-server.yaml @@ -1,6 +1,6 @@ name: Deploy Worker on: - pull_request: + push: paths: - .github/workflows/deploy-sync-server.yaml - sync-server/** diff --git a/desci-repo/src/controllers/nodes/documents.ts b/desci-repo/src/controllers/nodes/documents.ts index 97ab229c5..8c90793c4 100644 --- a/desci-repo/src/controllers/nodes/documents.ts +++ b/desci-repo/src/controllers/nodes/documents.ts @@ -1,6 +1,6 @@ import { Doc } from '@automerge/automerge'; import { AutomergeUrl, DocumentId } from '@automerge/automerge-repo'; -import { ManifestActions } from '@desci-labs/desci-models'; +import { ManifestActions, ResearchObjectV1 } from '@desci-labs/desci-models'; import { Request, Response } from 'express'; import { ZodError } from 'zod'; @@ -13,7 +13,7 @@ import { ResearchObjectDocument } from '../../types.js'; import { actionsSchema } from '../../validators/schema.js'; import { ensureUuidEndsWithDot } from './utils.js'; -import { IS_DEV, IS_TEST, PARTY_SERVER_HOST, PARTY_SERVER_TOKEN } from '../../config.js'; +import { ENABLE_PARTYKIT_FEATURE, IS_DEV, IS_TEST, PARTY_SERVER_HOST, PARTY_SERVER_TOKEN } from '../../config.js'; const protocol = IS_TEST || IS_DEV ? 'http://' : 'https://'; @@ -68,10 +68,24 @@ export const getLatestNodeManifest = async function (req: Request, res: Response // todo: add support for documentId params and skip querying node // fast track call if documentId is available if (documentId) { - const document = await getDocument(documentId as DocumentId); - if (document) { - res.status(200).send({ ok: true, document }); + if (ENABLE_PARTYKIT_FEATURE) { + const response = await fetch(`${protocol}${PARTY_SERVER_HOST}/api/documents?documentId=${documentId}`, { + // body: JSON.stringify({ uuid, documentId }), + headers: { + 'x-api-key': PARTY_SERVER_TOKEN!, + }, + }); + const data = (await response.json()) as { document: ResearchObjectV1 }; + + logger.trace({ document: !!data.document, ENABLE_PARTYKIT_FEATURE }, 'Document Retrieved'); + res.status(200).send({ ok: true, document: data.document }); return; + } else { + const document = await getDocument(documentId as DocumentId); + if (document) { + res.status(200).send({ ok: true, document }); + return; + } } } @@ -97,10 +111,24 @@ export const getLatestNodeManifest = async function (req: Request, res: Response return; } - const document = await getDocument(node.manifestDocumentId as DocumentId); + if (ENABLE_PARTYKIT_FEATURE) { + const response = await fetch(`${protocol}${PARTY_SERVER_HOST}/api/documents?documentId=${documentId}`, { + headers: { + 'x-api-key': PARTY_SERVER_TOKEN!, + }, + }); + const data = (await response.json()) as { document: ResearchObjectV1 }; - logger.trace({ document: !!document }, 'return DOCUMENT'); - res.status(200).send({ ok: true, document }); + logger.trace({ document: !!data.document, ENABLE_PARTYKIT_FEATURE }, 'Document Retrieved'); + res.status(200).send({ ok: true, document: data.document }); + return; + } else { + const document = await getDocument(node.manifestDocumentId as DocumentId); + + logger.trace({ document: !!document, ENABLE_PARTYKIT_FEATURE }, 'return DOCUMENT'); + res.status(200).send({ ok: true, document }); + return; + } } catch (err) { logger.error({ err }, 'Error'); res.status(500).send({ ok: false, message: JSON.stringify(err) }); diff --git a/desci-server/kubernetes/deployment_dev.yaml b/desci-server/kubernetes/deployment_dev.yaml index 1703430a3..d0cdc2f7b 100644 --- a/desci-server/kubernetes/deployment_dev.yaml +++ b/desci-server/kubernetes/deployment_dev.yaml @@ -95,6 +95,9 @@ spec: export ELASTIC_SEARCH_USER="{{ .Data.ELASTIC_SEARCH_USER }}" export ELASTIC_SEARCH_PW="{{ .Data.ELASTIC_SEARCH_PW }}" export OPEN_ALEX_DATABASE_URL="{{ .Data.OPEN_ALEX_DATABASE_URL }}" + CLOUDFLARE_WORKER_API=nodes-dev-sync.desci.com + CLOUDFLARE_WORKER_API_SECRET=auth-token + ENABLE_WORKERS_API=true export DEBUG_TEST=0; echo "appfinish"; {{- end -}} diff --git a/desci-server/kubernetes/deployment_prod.yaml b/desci-server/kubernetes/deployment_prod.yaml index 0cfe2e829..0f3224c3e 100755 --- a/desci-server/kubernetes/deployment_prod.yaml +++ b/desci-server/kubernetes/deployment_prod.yaml @@ -95,6 +95,9 @@ spec: export ELASTIC_SEARCH_USER="{{ .Data.ELASTIC_SEARCH_USER }}" export ELASTIC_SEARCH_PW="{{ .Data.ELASTIC_SEARCH_PW }}" export OPEN_ALEX_DATABASE_URL="{{ .Data.OPEN_ALEX_DATABASE_URL }}" + CLOUDFLARE_WORKER_API=nodes-sync.desci.com + CLOUDFLARE_WORKER_API_SECRET=auth-token + ENABLE_WORKERS_API=true export IGNORE_LINE=0; export DEBUG_TEST=0; echo "appfinish"; diff --git a/desci-server/kubernetes/deployment_staging.yaml b/desci-server/kubernetes/deployment_staging.yaml index d94eca4fc..87dbf6c23 100644 --- a/desci-server/kubernetes/deployment_staging.yaml +++ b/desci-server/kubernetes/deployment_staging.yaml @@ -107,6 +107,9 @@ spec: export ELASTIC_SEARCH_USER="{{ .Data.ELASTIC_SEARCH_USER }}" export ELASTIC_SEARCH_PW="{{ .Data.ELASTIC_SEARCH_PW }}" export OPEN_ALEX_DATABASE_URL="{{ .Data.OPEN_ALEX_DATABASE_URL }}" + CLOUDFLARE_WORKER_API=nodes-sync.desci.com + CLOUDFLARE_WORKER_API_SECRET=auth-token + ENABLE_WORKERS_API=true export DEBUG_TEST=0; echo "appfinish"; {{- end -}} diff --git a/desci-server/src/services/repoService.ts b/desci-server/src/services/repoService.ts index 609400c7b..82cf03896 100644 --- a/desci-server/src/services/repoService.ts +++ b/desci-server/src/services/repoService.ts @@ -9,6 +9,10 @@ import { NodeUuid } from './manifestRepo.js'; const logger = parentLogger.child({ module: 'Repo Service' }); +const cloudflareWorkerApi = process.env.CLOUDFLARE_WORKER_API; +const cloudflareWorkerApiSecret = process.env.CLOUDFLARE_WORKER_API_SECRET; +const enableWorkersApi = process.env.ENABLE_WORKERS_API == 'true'; + type ApiResponse = { ok: boolean } & B; class RepoService { @@ -36,21 +40,24 @@ class RepoService { } async dispatchAction(arg: { uuid: NodeUuid | string; documentId: DocumentId; actions: ManifestActions[] }) { - logger.info({ arg }, 'Disatch Changes'); + logger.info({ arg, enableWorkersApi, cloudflareWorkerApi }, 'Disatch Changes'); const response = await this.#client.post<{ ok: boolean; document: ResearchObjectDocument }>( - `${this.baseUrl}/v1/nodes/documents/dispatch`, + enableWorkersApi + ? `${cloudflareWorkerApi}/api/documents/dispatch` + : `${this.baseUrl}/v1/nodes/documents/dispatch`, arg, { headers: { 'x-api-remote-traceid': (als.getStore() as any)?.traceId, + ...(enableWorkersApi && { 'x-api-key': cloudflareWorkerApiSecret }), }, }, ); - logger.info({ arg, ok: response.data.ok }, 'Disatch Changes Response'); + logger.trace({ arg, ok: response.data.ok }, 'Disatch Actions Response'); if (response.status === 200 && response.data.ok) { return response.data.document; } else { - // logger.info({ response: response.data }, 'Disatch Changes Response'); + logger.trace({ response: response.data }, 'Disatch Actions Failed'); return null; } } @@ -59,11 +66,14 @@ class RepoService { logger.info({ arg }, 'Disatch Actions'); try { const response = await this.#client.post<{ ok: boolean; document: ResearchObjectDocument }>( - `${this.baseUrl}/v1/nodes/documents/actions`, + enableWorkersApi + ? `${cloudflareWorkerApi}/api/documents/actions` + : `${this.baseUrl}/v1/nodes/documents/actions`, arg, { headers: { 'x-api-remote-traceid': (als.getStore() as any)?.traceId, + ...(enableWorkersApi && { 'x-api-key': cloudflareWorkerApiSecret }), }, }, ); @@ -83,19 +93,20 @@ class RepoService { try { const response = await this.#client.post< ApiResponse<{ documentId: DocumentId; document: ResearchObjectDocument }> - >(`${this.baseUrl}/v1/nodes/documents`, arg, { + >(enableWorkersApi ? `${cloudflareWorkerApi}/api/documents` : `${this.baseUrl}/v1/nodes/documents`, arg, { headers: { 'x-api-remote-traceid': (als.getStore() as any)?.traceId, + ...(enableWorkersApi && { 'x-api-key': cloudflareWorkerApiSecret }), }, }); - logger.info({ response: response.data }, 'Create Draft Response'); - if (response.status === 200 && response.data.ok) { + logger.info({ status: response.status, response: response.data }, 'Create Draft Response'); + if (response.status === 200) { return response.data; } else { return null; } } catch (err) { - logger.error({ err }, 'Create Draft Error'); + logger.error({ err, enableWorkersApi, cloudflareWorkerApi }, 'Create Draft Error'); return null; } } @@ -106,27 +117,24 @@ class RepoService { return null; } try { - // const controller = new AbortController(); - // setTimeout(() => { - // logger.trace('Abort request'); - // controller.abort(); - // }, arg.timeout ?? this.defaultTimeoutInMilliseconds); logger.trace( { timout: arg.timeout || this.defaultTimeoutInMilliseconds, uuid: arg.uuid, documentId: arg.documentId }, '[getDraftDocument]', ); const response = await this.#client.get>( - `${this.baseUrl}/v1/nodes/documents/draft/${arg.uuid}?documentId=${arg.documentId}`, + enableWorkersApi + ? `${cloudflareWorkerApi}/api/documents?documentId=${arg.documentId}` + : `${this.baseUrl}/v1/nodes/documents/draft/${arg.uuid}?documentId=${arg.documentId}`, { headers: { 'x-api-remote-traceid': (als.getStore() as any)?.traceId, + ...(enableWorkersApi && { 'x-api-key': cloudflareWorkerApiSecret }), }, - // timeout: arg.timeout ?? this.defaultTimeoutInMilliseconds, - signal: AbortSignal.timeout(arg.timeout ?? this.defaultTimeoutInMilliseconds), // controller.signal, + signal: AbortSignal.timeout(arg.timeout ?? this.defaultTimeoutInMilliseconds), timeoutErrorMessage: this.timeoutErrorMessage, }, ); - logger.info({ arg }, 'Retrieve Draft Document'); + logger.info({ arg, doc: response.data }, 'Retrieve Draft Document'); if (response.status === 200 && response.data.ok) { return response.data.document; } else { diff --git a/desci-server/test/integration/automerge.test.ts b/desci-server/test/integration/automerge.test.ts index 35b20fd0a..dabed43a4 100644 --- a/desci-server/test/integration/automerge.test.ts +++ b/desci-server/test/integration/automerge.test.ts @@ -50,7 +50,7 @@ const createDraftNode = async (user: User, baseManifest: ResearchObjectV1, baseM return { node: updatedNode || node, documentId: response?.documentId }; }; -describe('Automerge Integration', () => { +describe.only('Automerge Integration', () => { let user: User; let unauthedUser: User; // let node: Node; diff --git a/desci-server/test/integration/data.test.ts b/desci-server/test/integration/data.test.ts index 886bcc1c6..c1c1fec3e 100644 --- a/desci-server/test/integration/data.test.ts +++ b/desci-server/test/integration/data.test.ts @@ -64,7 +64,7 @@ const createDraftNode = async (user: User, baseManifest: ResearchObjectV1, baseM return { node: updatedNode || node, documentId: response?.documentId }; }; -describe('Data Controllers', () => { +describe.only('Data Controllers', () => { let user: User; let unauthedUser: User; // let node: Node; diff --git a/sync-server/package.json b/sync-server/package.json index 1428fdddb..654f51fb0 100644 --- a/sync-server/package.json +++ b/sync-server/package.json @@ -64,13 +64,15 @@ "@automerge/automerge-repo-network-websocket": "^1.2.1", "@automerge/automerge-repo-react-hooks": "^1.0.19", "@cloudflare/workers-types": "^4.20241022.0", - "@desci-labs/desci-models": "^0.2.18", + "@desci-labs/desci-models": "^0.2.19", + "deep-equal": "^2.2.3", "isomorphic-ws": "^5.0.0", "partykit": "^0.0.111", "partyserver": "^0.0.57", "partysocket": "^1.0.2", "pg": "^8.13.1", "pino-std-serializers": "^7.0.0", - "ws": "^8.14.2" + "ws": "^8.14.2", + "zod": "^3.24.1" } } diff --git a/sync-server/src/automerge-repo-storage-postgres/PostgresStorageAdapter.ts b/sync-server/src/automerge-repo-storage-postgres/PostgresStorageAdapter.ts index 80e72cb2f..f961804c2 100644 --- a/sync-server/src/automerge-repo-storage-postgres/PostgresStorageAdapter.ts +++ b/sync-server/src/automerge-repo-storage-postgres/PostgresStorageAdapter.ts @@ -33,7 +33,7 @@ export class PostgresStorageAdapter implements StorageAdapterInterface { async save(keyArray: StorageKey, binary: Uint8Array): Promise { const key = getKey(keyArray); this.cache[key] = binary; - console.log('[save]', { key }); + try { await this.query( `INSERT INTO "${this.tableName}" (key, value) VALUES ($1, $2) ON CONFLICT(key) DO UPDATE SET value = $2 RETURNING key`, @@ -87,10 +87,7 @@ export class PostgresStorageAdapter implements StorageAdapterInterface { } private async loadRangeKeys(keyPrefix: string[]): Promise { - console.log('LoadRange Keys', { keyPrefix }); const response = await this.query(`SELECT key FROM "${this.tableName}" WHERE key LIKE $1`, [`${keyPrefix}%`]); - // console.log({ keyPrefix, response: response?.length }, '[LOADED RANGE Keys]'); - return response ? response.map((row) => row.key) : []; } } diff --git a/sync-server/src/index.ts b/sync-server/src/index.ts index b4f4543c5..2bebd51dd 100644 --- a/sync-server/src/index.ts +++ b/sync-server/src/index.ts @@ -1,8 +1,15 @@ -import { type PeerId, Repo } from '@automerge/automerge-repo/slim'; +import { + Doc, + DocHandleChangePayload, + DocHandleEvents, + DocumentId, + type PeerId, + Repo, +} from '@automerge/automerge-repo/slim'; import { DurableObjectState } from '@cloudflare/workers-types'; import { routePartykitRequest, Server as PartyServer, Connection, ConnectionContext, WSMessage } from 'partyserver'; -import { err as serialiseErr } from 'pino-std-serializers'; -import { ResearchObjectV1 } from '@desci-labs/desci-models'; +import { req, err as serialiseErr } from 'pino-std-serializers'; +import { ManifestActions, ResearchObjectV1 } from '@desci-labs/desci-models'; import { PartyKitWSServerAdapter } from './automerge-repo-network-websocket/PartykitWsServerAdapter.js'; @@ -11,6 +18,9 @@ import { PostgresStorageAdapter } from './automerge-repo-storage-postgres/Postgr import { Env } from './types.js'; import { ensureUuidEndsWithDot } from './utils.js'; import { assert } from './automerge-repo-network-websocket/assert.js'; +import { actionsSchema } from './lib/schema.js'; +import { getAutomergeUrl, getDocumentUpdater } from './manifestRepo.js'; +import { ZodError } from 'zod'; interface ResearchObjectDocument { manifest: ResearchObjectV1; @@ -56,12 +66,51 @@ export class AutomergeServer extends PartyServer { // Since this is a server, we don't share generously — meaning we only sync documents they already // know about and can ask for by ID. sharePolicy: async (peer, docId) => { - console.log('SharePolicy called', { peer, docId }); + // console.log('SharePolicy called', { peer, docId }); return true; }, }; this.repo = new Repo(config); + + const handleChange = async (change: DocHandleChangePayload) => { + console.log({ change: change.handle.documentId, uuid: change.patchInfo.after.uuid }, 'Document Changed'); + const newTitle = change.patchInfo.after.manifest.title; + const newCover = change.patchInfo.after.manifest.coverImage; + const uuid = ensureUuidEndsWithDot(change.doc.uuid); + // console.log({ uuid: uuid, newTitle }, 'UPDATE NODE'); + + try { + // TODO: Check if update message is 'UPDATE TITLE' + if (newTitle) { + const result = await query('UPDATE "Node" SET title = $1 WHERE uuid = $2', [newTitle, uuid]); + // console.info({ newTitle, result }, 'TITLE UPDATED'); + } + + // TODO: Check if update message is 'UPDATE TITLE' + // Update the cover image url in the db for fetching collection + if (newCover) { + const coverUrl = process.env.IPFS_RESOLVER_OVERRIDE + '/' + newCover; + const result = await query( + 'INSERT INTO "NodeCover" (url, cid, "nodeUuid", version) VALUES ($1, $2, $3, $4) ON CONFLICT("nodeUuid", version) DO UPDATE SET url = $1, cid = $2', + [coverUrl, newCover as string, uuid, 0], + ); + // console.info({ uuid, coverUrl, result }, 'COVER UPDATED'); + } + } catch (err) { + console.error('[Error in DOCUMENT repo.ts::handleChange CALLBACK]', err); + // console.error({err}, '[Error in DOCUMENT repo.ts::handleChange CALLBACK]'); + } + }; + + this.repo.on('document', async (doc) => { + console.log({ documentId: doc.handle.documentId }, 'DOCUMENT Ready'); + doc.handle.on>('change', handleChange); + }); + + this.repo.off('document', async (doc) => { + doc.handle.off('change', handleChange); + }); } async onConnect(connection: Connection, ctx: ConnectionContext): Promise { @@ -157,10 +206,181 @@ async function handleCreateDocument(request: Request, env: Env) { return new Response(JSON.stringify({ documentId: handle.documentId, document }), { status: 200 }); } +async function dispatchDocumentChanges(request: Request, env: Env) { + try { + console.log('[Request]::handleCreateDocument ', { env }); + const environment = env.ENVIRONMENT || 'dev'; + const localDbUrl = + env.DATABASE_URL ?? process.env.WRANGLER_HYPERDRIVE_LOCAL_CONNECTION_STRING_NODES_DB ?? ''; + const DATABASE_URL = environment === 'dev' ? localDbUrl : env.NODES_DB.connectionString; + + const { Repo } = await import('@automerge/automerge-repo'); + const { query } = await database.init(DATABASE_URL); + const config = { + storage: new PostgresStorageAdapter(query), + peerId: `cloudflare-ephemeral-peer` as PeerId, + sharePolicy: async () => true, + }; + + const repo = new Repo(config); + + let body = (await request.clone().json()) as { uuid: string; documentId: DocumentId; actions: ManifestActions[] }; + const actions = body.actions as ManifestActions[]; + const documentId = body.documentId as DocumentId; + + if (!(actions && actions.length > 0)) { + console.error({ body }, 'No actions to dispatch'); + return new Response(JSON.stringify({ ok: false, message: 'No actions to dispatch' }), { status: 400 }); + } + + let document: Doc | undefined; + + const dispatchChange = await getDocumentUpdater(repo, documentId); + + for (const action of actions) { + document = await dispatchChange(action); + } + + if (!document) { + console.error({ document }, 'Document not found'); + return new Response(JSON.stringify({ ok: false, message: 'Document not found' }), { status: 400 }); + } + + await repo.flush(); + + return new Response(JSON.stringify({ document, ok: true }), { status: 200 }); + } catch (err) { + console.error(err, 'Error [dispatchDocumentChange]'); + + if (err instanceof ZodError) { + // res.status(400).send({ ok: false, error: err }); + return new Response(JSON.stringify({ ok: false, message: JSON.stringify(err) }), { status: 400 }); + } + + return new Response(JSON.stringify({ ok: false, message: JSON.stringify(err) }), { status: 500 }); + } +} + +async function handleAutomergeActions(request: Request, env: Env) { + try { + console.log('[Request]::handleCreateDocument ', { env }); + const environment = env.ENVIRONMENT || 'dev'; + const localDbUrl = + env.DATABASE_URL ?? process.env.WRANGLER_HYPERDRIVE_LOCAL_CONNECTION_STRING_NODES_DB ?? ''; + const DATABASE_URL = environment === 'dev' ? localDbUrl : env.NODES_DB.connectionString; + + const { Repo } = await import('@automerge/automerge-repo'); + const { query } = await database.init(DATABASE_URL); + const config = { + storage: new PostgresStorageAdapter(query), + peerId: `cloudflare-ephemeral-peer` as PeerId, + sharePolicy: async () => true, + }; + + const repo = new Repo(config); + + let body = (await request.clone().json()) as { uuid: string; documentId: DocumentId; actions: ManifestActions[] }; + const actions = body.actions as ManifestActions[]; + const documentId = body.documentId as DocumentId; + + if (!(actions && actions.length > 0)) { + console.error({ body }, 'No actions to dispatch'); + return new Response(JSON.stringify({ ok: false, message: 'No actions to dispatch' }), { status: 400 }); + } + + const validatedActions = await actionsSchema.parseAsync(actions); + console.log({ validatedActions }, 'Actions validated'); + + let document: Doc | undefined; + + const dispatchChange = await getDocumentUpdater(repo, documentId); + + for (const action of actions) { + document = await dispatchChange(action); + } + + if (!document) { + console.error({ document }, 'Document not found'); + return new Response(JSON.stringify({ ok: false, message: 'Document not found' }), { status: 400 }); + } + + await repo.flush(); + + return new Response(JSON.stringify({ document, ok: true }), { status: 200 }); + } catch (err) { + console.error(err, 'Error [dispatchDocumentChange]'); + + if (err instanceof ZodError) { + // res.status(400).send({ ok: false, error: err }); + return new Response(JSON.stringify({ ok: false, message: JSON.stringify(err) }), { status: 400 }); + } + + return new Response(JSON.stringify({ ok: false, message: JSON.stringify(err) }), { status: 500 }); + } +} + +async function getLatestDocument(request: Request, env: Env) { + try { + console.log('[Request]::getLatestDocument ', { env }); + const environment = env.ENVIRONMENT || 'dev'; + const localDbUrl = + env.DATABASE_URL ?? process.env.WRANGLER_HYPERDRIVE_LOCAL_CONNECTION_STRING_NODES_DB ?? ''; + const DATABASE_URL = environment === 'dev' ? localDbUrl : env.NODES_DB.connectionString; + + const { Repo } = await import('@automerge/automerge-repo'); + const { query } = await database.init(DATABASE_URL); + const config = { + storage: new PostgresStorageAdapter(query), + peerId: `cloudflare-ephemeral-peer` as PeerId, + sharePolicy: async () => true, + }; + + const repo = new Repo(config); + const url = new URL(request.url); + const documentId = url.searchParams.get('documentId') as DocumentId; + if (!documentId) { + console.error('No DocumentID found'); + return new Response(JSON.stringify({ ok: false, message: 'Invalid body' }), { status: 400 }); + } + + let document: Doc | undefined; + + const handle = repo.find(getAutomergeUrl(documentId)); + document = await handle.doc(); + console.log('Document Retrieved', { document: !!document }); + + return new Response(JSON.stringify({ document, ok: true }), { status: 200 }); + } catch (err) { + console.error({ err }, 'Error [getLatestDocument]'); + + return new Response(JSON.stringify({ ok: false, message: JSON.stringify(err) }), { status: 500 }); + } +} + export default { fetch(request: Request, env) { + console.log('Request Fetch:', { + env, + url: request.url, + }); + const secretKey = env.ENVIRONMENT === null ? 'test-api-secret' : env.API_TOKEN; + if (request.url.includes('/api/') && request.headers.get('x-api-key') != secretKey) { + console.log('[Error]::Api key error'); + return new Response('UnAuthorized', { status: 401 }); + } + + if (request.url.includes('/api/documents') && request.method.toLowerCase() === 'get') + return getLatestDocument(request, env); + + if (request.url.includes('/api/documents/actions') && request.method.toLowerCase() === 'post') + return handleAutomergeActions(request, env); + + if (request.url.includes('/api/documents/dispatch') && request.method.toLowerCase() === 'post') + return dispatchDocumentChanges(request, env); + if (request.url.includes('/api/documents') && request.method.toLowerCase() === 'post') return handleCreateDocument(request, env); + if (!request.url.includes('/parties/automerge')) return new Response('Not found', { status: 404 }); return routePartykitRequest(request, env) || new Response('Not found', { status: 404 }); }, diff --git a/sync-server/src/lib/schema.ts b/sync-server/src/lib/schema.ts new file mode 100644 index 000000000..e745cd841 --- /dev/null +++ b/sync-server/src/lib/schema.ts @@ -0,0 +1,104 @@ +import { + ResearchObjectComponentType, + ResearchObjectV1AuthorRole, + ResearchObjectV1Component, + ResearchObjectV1Dpid, + ResearchObjectV1Organization, + ManifestActions, + ResearchObjectReference, +} from '@desci-labs/desci-models'; +import { z } from 'zod'; + +const researchObject = z + .object({ + id: z.string(), + version: z.union([z.literal('desci-nodes-0.1.0'), z.literal('desci-nodes-0.2.0'), z.literal(1)]), + name: z.string(), + payload: z.object({ path: z.string() }).passthrough(), + components: z.array(z.object({ id: z.string() }).passthrough()), + }) + .passthrough(); + +export interface ResearchObjectV1Author { + name: string; + orcid?: string | undefined; + googleScholar?: string | undefined; + role: ResearchObjectV1AuthorRole; + organizations?: ResearchObjectV1Organization[] | undefined; + github?: string | undefined; +} + +const contributor: z.ZodType = z.object({ + name: z.string(), + orcid: z.string().optional(), + googleScholar: z.string().optional(), + role: z.nativeEnum(ResearchObjectV1AuthorRole), + organizations: z.array(z.object({ id: z.string(), name: z.string() })).optional(), + github: z.string().optional(), +}); +// .passthrough(); + +const dpid: z.ZodType = z.object({ prefix: z.string(), id: z.string() }).required(); +const componentType = z.nativeEnum(ResearchObjectComponentType); +const componentTypeMap = z.record(componentType); + +const commonPayloadSchema = z.object({ + title: z.string().optional(), + keywords: z.array(z.string()).optional(), + description: z.string().optional(), + licenseType: z.string().optional(), + path: z.string().optional(), + url: z.string().url().optional(), +}); + +const componentSchema: z.ZodType = z + .object({ + id: z.string(), + name: z.string(), + payload: commonPayloadSchema.passthrough(), + type: z.union([componentType, componentTypeMap]), + starred: z.boolean(), + }) + .passthrough(); + +export const DPID_PATH_REGEX = + /^https:\/\/(?dev-beta|beta)\.dpid\.org(?\/\d+)(?\/v\d+)?(?\/root.*)?/m; + +export const DOI_REGEX = /(https:\/\/doi.org\/)?(?10.\d{4,9}\/[-+<>._;()/:A-Z0-9]+$)/i; + +const referenceSchema: z.ZodType = z + .object({ + type: z.union([z.literal('doi'), z.literal('dpid')]), + id: z.string().refine((id) => DPID_PATH_REGEX.test(id) || DOI_REGEX.test(id)), + }) + .refine((arg) => { + if (arg.type === 'doi') return DOI_REGEX.test(arg.id); + return DPID_PATH_REGEX.test(arg.id); + }); + +type Action = ManifestActions['type']; + +export const actionsSchema = z.array( + z.discriminatedUnion('type', [ + z.object({ type: z.literal('Publish Dpid'), dpid: dpid }), + z.object({ type: z.literal('Remove Dpid') }), + z.object({ type: z.literal('Update Title'), title: z.string() }), + z.object({ type: z.literal('Update Description'), description: z.string() }), + z.object({ type: z.literal('Update License'), defaultLicense: z.string() }), + z.object({ type: z.literal('Update ResearchFields'), researchFields: z.array(z.string()) }), + z.object({ type: z.literal('Add Component'), component: componentSchema }), + z.object({ type: z.literal('Delete Component'), path: z.string() }), + z.object({ type: z.literal('Update Component'), component: componentSchema }), + z.object({ type: z.literal('Add Contributor'), author: contributor }), + z.object({ type: z.literal('Add Contributors'), contributors: z.array(contributor) }), + z.object({ type: z.literal('Set Contributors'), contributors: z.array(contributor) }), + z.object({ type: z.literal('Remove Contributor'), contributorIndex: z.number() }), + z.object({ type: z.literal('Pin Component'), componentIndex: z.number() }), + z.object({ type: z.literal('UnPin Component'), componentIndex: z.number() }), + z.object({ type: z.literal('Update CoverImage'), cid: z.string().optional() }), + z.object({ type: z.literal('Add Reference'), reference: referenceSchema }), + z.object({ type: z.literal('Add References'), reference: z.array(referenceSchema) }), + // z.object({ type: z.literal('Set References'), references: z.array(referenceSchema) }), + z.object({ type: z.literal('Delete Reference'), referenceId: z.string() }), + ]), +); diff --git a/sync-server/src/manifestRepo.ts b/sync-server/src/manifestRepo.ts new file mode 100644 index 000000000..47609d0b5 --- /dev/null +++ b/sync-server/src/manifestRepo.ts @@ -0,0 +1,536 @@ +import { Doc, getHeads } from '@automerge/automerge'; +import { AutomergeUrl, DocumentId, Repo } from '@automerge/automerge-repo'; +import { + CodeComponent, + DataComponent, + ExternalLinkComponent, + PdfComponent, + ResearchObjectComponentType, + ResearchObjectComponentTypeMap, + ResearchObjectV1Component, + ResearchObjectV1Dpid, + isResearchObjectComponentTypeMap, + ManifestActions, + ResearchObjectV1, +} from '@desci-labs/desci-models'; +import isEqual from 'deep-equal'; + +// import { logger as parentLogger } from '../logger.js'; +// import { backendRepo, repoManager } from '../repo.js'; + +// const logger = parentLogger.child({ module: 'manifestRepo.ts' }); + +export type NodeUuid = string & { _kind: 'uuid' }; + +export interface ResearchObjectDocument { + manifest: ResearchObjectV1; + uuid: string; + driveClock: string; +} + +export const getAutomergeUrl = (documentId: DocumentId): AutomergeUrl => { + return `automerge:${documentId}` as AutomergeUrl; +}; + +export function assertNever(value: never) { + console.error('Unknown value', value); + throw Error('Not Possible'); +} + +export const getDocumentUpdater = async (repo: Repo, documentId: DocumentId) => { + const handle = repo.find(`automerge:${documentId}` as AutomergeUrl); + console.trace({ handle: handle.isReady() }, 'Retrieved handle'); + + return async (action: ManifestActions) => { + if (!handle) return; + console.trace({ documentId, action }, 'get doc'); + let latestDocument = await handle.doc(); + console.trace({ latestDocument }, 'retrieved doc'); + + if (!latestDocument) { + console.error({ node: documentId }, 'Automerge document not found'); + return; + } + + const heads = getHeads(latestDocument); + console.trace({ action, heads }, `DocumentUpdater::Dispatched`); + + switch (action.type) { + case 'Add Components': + const uniqueComponents = action.components.filter( + (componentToAdd) => + !latestDocument?.manifest.components.some((c) => c.payload?.path === componentToAdd.payload?.path), + ); + if (uniqueComponents.length > 0) { + handle.change( + (document) => { + uniqueComponents.forEach((component) => { + document.manifest.components.push(component); + }); + }, + { time: Date.now(), message: action.type }, + ); + } + break; + case 'Rename Component': + handle.change( + (document) => { + const component = document.manifest.components.find((c) => c.payload?.path === action.path); + if (component) component.name = action.fileName; + }, + { time: Date.now(), message: action.type }, + ); + break; + case 'Delete Component': + const deleteIdx = latestDocument.manifest.components.findIndex((c) => c.payload?.path === action.path); + if (deleteIdx !== -1) { + console.info({ action, deleteIdx }, `DocumentUpdater::Deleteing`); + handle.change( + (document) => { + document.manifest.components.splice(deleteIdx, 1); + }, + { time: Date.now(), message: action.type }, + ); + } + break; + case 'Delete Components': + const componentEntries = latestDocument.manifest.components + .map((c) => (action.paths.includes(c.payload?.path) ? c.payload?.path : null)) + .filter(Boolean) as string[]; + if (componentEntries.length > 0) { + console.info({ action, componentEntries }, `DocumentUpdater::Delete Components`); + handle.change( + (document) => { + for (const path of componentEntries) { + const deleteIdx = document.manifest.components.findIndex((c) => c.payload?.path === path); + console.info({ path, deleteIdx }, `DocumentUpdater::Delete`); + if (deleteIdx !== -1) document.manifest.components.splice(deleteIdx, 1); + } + }, + { time: Date.now(), message: action.type }, + ); + } + break; + case 'Rename Component Path': + const components = latestDocument.manifest.components.filter( + (component) => + component.payload?.path?.startsWith(action.oldPath + '/') || component.payload?.path === action.oldPath, + ); + if (components.length > 0) { + handle.change( + (document) => { + const components = document.manifest.components.filter( + (component) => + component.payload?.path.startsWith(action.oldPath + '/') || + component.payload?.path === action.oldPath, + ); + for (const component of components) { + component.payload.path = component.payload?.path.replace(action.oldPath, action.newPath); + } + }, + { time: Date.now(), message: action.type }, + ); + } + break; + case 'Update Component': + handle.change( + (document) => { + updateManifestComponent(document, action.component, action.componentIndex); + }, + { time: Date.now(), message: action.type }, + ); + break; + case 'Assign Component Type': + handle.change( + (document) => { + updateComponentTypeMap(document, action.component.payload?.path, action.componentTypeMap); + }, + { time: Date.now(), message: action.type }, + ); + break; + case 'Set Drive Clock': + handle.change( + (document) => { + if (document.driveClock && document.driveClock === action.time) return; // Don't update if already the latest + document.driveClock = action.time; + }, + { time: Date.now(), message: action.type }, + ); + break; + case 'Update License': + handle.change( + (document) => { + document.manifest.defaultLicense = action.defaultLicense; + }, + { time: Date.now(), message: action.type }, + ); + break; + case 'Update Description': + handle.change( + (document) => { + if (!document.manifest.description) document.manifest.description = ''; + document.manifest.description = action.description; + }, + { time: Date.now(), message: action.type }, + ); + break; + case 'Update Title': + handle.change( + (document) => { + document.manifest.title = action.title; + }, + { time: Date.now(), message: action.type }, + ); + break; + case 'Update ResearchFields': + handle.change( + (document) => { + document.manifest.researchFields = action.researchFields; + }, + { time: Date.now(), message: action.type }, + ); + break; + case 'Add Component': + handle.change( + (document) => { + addManifestComponent(document, action.component); + }, + { time: Date.now(), message: action.type }, + ); + break; + case 'Upsert Component': + handle.change( + (document) => { + upsertManifestComponent(document, action.component); + }, + { time: Date.now(), message: action.type }, + ); + break; + case 'Upsert Components': + action.components.forEach((component) => { + handle.change( + (document) => { + upsertManifestComponent(document, component); + }, + { time: Date.now(), message: 'Upsert Component' }, + ); + }); + break; + case 'Publish Dpid': + handle.change( + (document) => { + addDpid(document, action.dpid); + }, + { time: Date.now(), message: action.type }, + ); + break; + case 'Remove Dpid': + handle.change( + (document) => { + removeDpid(document); + }, + { time: Date.now(), message: action.type }, + ); + break; + case 'Pin Component': + const componentIndex = latestDocument?.manifest.components.findIndex((c) => c.payload?.path === action.path); + if (componentIndex && componentIndex != -1) { + handle.change( + (document) => { + togglePin(document, componentIndex, true); + }, + { time: Date.now(), message: action.type }, + ); + } + break; + case 'UnPin Component': + const index = latestDocument?.manifest.components.findIndex((c) => c.payload?.path === action.path); + if (index && index != -1) { + handle.change( + (document) => { + togglePin(document, index, false); + }, + { time: Date.now(), message: action.type }, + ); + } + break; + case 'Remove Contributor': + handle.change( + (document) => { + document.manifest.authors?.splice(action.contributorIndex, 1); + }, + { time: Date.now(), message: action.type }, + ); + break; + case 'Add Contributor': + handle.change( + (document) => { + if (!document.manifest.authors) document.manifest.authors = []; + document.manifest.authors?.push(action.author); + }, + { time: Date.now(), message: action.type }, + ); + break; + case 'Add Contributors': + handle.change( + (document) => { + if (!document.manifest.authors) document.manifest.authors = []; + document.manifest.authors?.push(...action.contributors); + }, + { time: Date.now(), message: action.type }, + ); + break; + case 'Set Contributors': + handle.change( + (document) => { + if (!document.manifest.authors) document.manifest.authors = []; + document.manifest.authors = action.contributors; + }, + { time: Date.now(), message: action.type }, + ); + break; + case 'Update CoverImage': + handle.change( + (document) => { + if (!action.cid) { + delete document.manifest.coverImage; + } else { + document.manifest.coverImage = action.cid; + } + }, + { time: Date.now(), message: action.type }, + ); + break; + case 'Add Reference': + const exists = + latestDocument.manifest?.references && + latestDocument.manifest?.references?.find((ref) => ref.id === action.reference.id); + + if (!exists) { + handle.change((document) => { + if (!document.manifest.references) { + document.manifest.references = []; + } + + document.manifest.references.push(action.reference); + }); + } + break; + case 'Add References': + handle.change((document) => { + if (!document.manifest.references) { + document.manifest.references = []; + } + + for (const reference of action.references) { + if (!document.manifest.references.find((ref) => ref.id === reference.id)) + document.manifest.references.push(reference); + } + }); + break; + case 'Set References': + handle.change((document) => { + if (!document.manifest.references) { + document.manifest.references = []; + } + document.manifest.references = action.references; + }); + break; + case 'Delete Reference': + if (!action.referenceId) return; + const deletedIdx = latestDocument.manifest.references?.findIndex((ref) => ref.id === action.referenceId); + if (deletedIdx !== undefined && deletedIdx !== -1) { + handle.change((document) => { + document.manifest.references?.splice(deletedIdx, 1); + }); + } + break; + default: + assertNever(action); + } + + // console.trace({ documentId }, 'get updated doc'); + latestDocument = await handle.doc(); + // console.trace({ action }, 'retrieved updated doc'); + + if (latestDocument) { + const updatedHeads = getHeads(latestDocument); + console.trace({ action, heads: updatedHeads }, `DocumentUpdater::Exit`); + } + return latestDocument; + }; +}; + +const updateComponentTypeMap = ( + doc: Doc, + path: string, + compTypeMap: ResearchObjectComponentTypeMap, +): void => { + const currentComponent = doc.manifest.components.find((c) => c.payload?.path === path); + if (!currentComponent) return; + + const existingType = currentComponent.type; + if (!isResearchObjectComponentTypeMap(existingType)) { + currentComponent.type = {}; + } + + const componentType = currentComponent.type; + const update = { + ...(isResearchObjectComponentTypeMap(existingType) && { ...existingType }), + ...compTypeMap, + }; + + Object.entries(update).forEach(([key, value]) => { + if (!componentType[key]) componentType[key] = ''; + componentType[key] = value; + }); +}; + +const addManifestComponent = (doc: Doc, component: ResearchObjectV1Component): void => { + doc.manifest.components.push(component); +}; + +const deleteComponent = (doc: Doc, path: string): void => { + const deleteIdx = doc.manifest.components.findIndex((component) => component?.payload?.path === path); + if (deleteIdx !== -1) doc.manifest.components.splice(deleteIdx, 1); +}; + +const togglePin = (doc: Doc, componentIndex: number, pin: boolean): void => { + const currentComponent = doc.manifest.components[componentIndex]; + currentComponent.starred = pin; +}; + +const addDpid = (doc: Doc, dpid: ResearchObjectV1Dpid): void => { + if (doc.manifest.dpid) return; + doc.manifest.dpid = dpid; +}; + +/** In an unavailable optimistic dPID was written to the manifest, it must + * be removed again. + */ +const removeDpid = (doc: Doc): void => { + delete doc.manifest.dpid; +}; + +const updateManifestComponent = ( + doc: Doc, + component: ResearchObjectV1Component, + componentIndex: number, +): void => { + if (componentIndex === -1 || componentIndex === undefined) return; + + const currentComponent = doc.manifest.components[componentIndex]; + currentComponent.type = component?.type || currentComponent.type; + + if (!currentComponent.starred) currentComponent.starred = false; + currentComponent.starred = component?.starred || currentComponent.starred; + + if (component.name) { + currentComponent.name = component.name; + } + + if ('subtype' in component) { + if (component.subtype) { + if (isPdfComponent(component, currentComponent)) { + (currentComponent as PdfComponent).subtype = component.subtype; + /* Only pdf and external links component have subtypes in the model */ + // } else if (isDataComponent(component, currentComponent)) { + // (currentComponent as DataComponent).subtype = component.subtype; + // } else if (isCodeComponent(component, currentComponent)) { + // (currentComponent as CodeComponent).subtype = component.subtype; + } else if (isExternalLinkComponent(component, currentComponent)) { + (currentComponent as ExternalLinkComponent).subtype = component.subtype; + } + } else { + if (isPdfComponent(component, currentComponent)) { + delete (currentComponent as PdfComponent).subtype; + /* Only pdf and external links component have subtypes in the model */ + // } else if (isDataComponent(component, currentComponent)) { + // delete (currentComponent as DataComponent).subtype; + // } else if (isCodeComponent(component, currentComponent)) { + // delete (currentComponent as CodeComponent).subtype; + } else if (isExternalLinkComponent(component, currentComponent)) { + delete (currentComponent as ExternalLinkComponent).subtype; + } else { + delete currentComponent?.['subtype']; + } + } + } + + const currentPayload = currentComponent.payload; + if ('payload' in component) { + // Prevent previous payload overwrite + Object.entries(component.payload).forEach(([key, value]) => { + if (component.payload[key] === null || component.payload[key] === undefined) return; + if (isEqual(currentPayload[key], value)) return; + if (!currentPayload[key]) currentPayload[key] = getTypeDefault(value); + currentPayload[key] = value; + }); + } +}; + +const upsertManifestComponent = (doc: Doc, component: ResearchObjectV1Component): void => { + // Check for existing component + const existingComponentIndex = doc.manifest.components.findIndex( + (c) => c.id === component.id || c.payload?.path === component.payload?.path, + ); + // Apply changess + if (existingComponentIndex !== -1) { + const existingComponent = doc.manifest.components[existingComponentIndex]; + doc.manifest.components[existingComponentIndex] = { + ...existingComponent, + ...component, + payload: { ...existingComponent.payload, ...component.payload }, + }; + } else { + // Push the component + doc.manifest.components.push(component); + } +}; + +// eslint-disable-next-line @typescript-eslint/ban-types +type TypeInitialisers = {} | '' | 0 | []; + +const getTypeDefault = (value: unknown): TypeInitialisers => { + if (Array.isArray(value)) return []; + if (typeof value === 'string') return ''; + if (typeof value === 'number') return 0; + if (typeof value === 'object') return {}; + return ''; +}; + +const isPdfComponent = ( + component: ResearchObjectV1Component, + currentComponent: ResearchObjectV1Component, +): component is PdfComponent => { + return ( + component.type === ResearchObjectComponentType.PDF || currentComponent.type === ResearchObjectComponentType.PDF + ); +}; + +const isDataComponent = ( + component: ResearchObjectV1Component, + currentComponent: ResearchObjectV1Component, +): component is DataComponent => { + return ( + component.type === ResearchObjectComponentType.DATA || currentComponent.type === ResearchObjectComponentType.DATA + ); +}; + +const isCodeComponent = ( + component: ResearchObjectV1Component, + currentComponent: ResearchObjectV1Component, +): component is CodeComponent => { + return ( + component.type === ResearchObjectComponentType.CODE || currentComponent.type === ResearchObjectComponentType.CODE + ); +}; + +const isExternalLinkComponent = ( + component: ResearchObjectV1Component, + currentComponent: ResearchObjectV1Component, +): component is ExternalLinkComponent => { + return ( + component.type === ResearchObjectComponentType.LINK || currentComponent.type === ResearchObjectComponentType.LINK + ); +}; diff --git a/sync-server/wrangler.toml b/sync-server/wrangler.toml index a0973272a..c24921168 100644 --- a/sync-server/wrangler.toml +++ b/sync-server/wrangler.toml @@ -20,17 +20,17 @@ head_sampling_rate = 1 # optional. default = 1. [[hyperdrive]] binding = "NODES_DB" id = "856af129675e4140accc0ccabbc2ee20" -localConnectionString = "postgresql://walter:white@host.docker.internal:5433/boilerplate" +localConnectionString = "postgresql://walter:white@host.docker.internal:5434/boilerplate" [vars] DB_TABLE= "DocumentStore" NODES_API= "http://localhost:5420" ENVIRONMENT="dev" -DATABASE_URL= "postgresql://walter:white@host.docker.internal:5433/boilerplate" +DATABASE_URL= "postgresql://walter:white@host.docker.internal:5434/boilerplate" [env.dev] durable_objects.bindings = [{ name = "Automerge", class_name = "AutomergeServer" }] -vars = { DB_TABLE= "DocumentStore", NODES_API= "http://localhost:5420", ENVIRONMENT="dev", DATABASE_URL= "postgresql://walter:white@host.docker.internal:5433/boilerplate" } +vars = { DB_TABLE= "DocumentStore", NODES_API= "http://localhost:5420", ENVIRONMENT="dev", DATABASE_URL= "postgresql://walter:white@host.docker.internal:5434/boilerplate" } hyperdrive = [{ binding = "NODES_DB", id = "856af129675e4140accc0ccabbc2ee20" }] [env.staging] diff --git a/sync-server/yarn.lock b/sync-server/yarn.lock index 15af3a169..cc04be1e5 100644 --- a/sync-server/yarn.lock +++ b/sync-server/yarn.lock @@ -233,10 +233,10 @@ dependencies: "@jridgewell/trace-mapping" "0.3.9" -"@desci-labs/desci-models@^0.2.18": - version "0.2.18" - resolved "https://registry.yarnpkg.com/@desci-labs/desci-models/-/desci-models-0.2.18.tgz#d4873db35fdeaa5133beb75ebcb263587d7e2e4b" - integrity sha512-VypYv1gtN7ttSnfsqrTZf9EzXl50fCaYmg+3qOnZ2x+yNPIZPEhTY4YGmTLXvPqspG8kVlCtX+zG2NTUcu8fTg== +"@desci-labs/desci-models@^0.2.19": + version "0.2.19" + resolved "https://registry.yarnpkg.com/@desci-labs/desci-models/-/desci-models-0.2.19.tgz#430f8e6b6a1967d6b06fc8a36e6c32536e1a4a42" + integrity sha512-XaZyLom2z4vi+A47qS5G1ecc65fw3MxfAKutH1iXrb92AXmxQG7TcH313O3qv4206pIDdNfmJNfgjo4h78cVJw== dependencies: jsonld "^8.1.1" schema-dts "^1.1.2" @@ -893,7 +893,7 @@ argparse@^2.0.1: resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== -array-buffer-byte-length@^1.0.1: +array-buffer-byte-length@^1.0.0, array-buffer-byte-length@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz#1e5583ec16763540a27ae52eed99ff899223568f" integrity sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg== @@ -1055,6 +1055,14 @@ buffer-from@^1.0.0: resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== +call-bind-apply-helpers@^1.0.0, call-bind-apply-helpers@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.1.tgz#32e5892e6361b29b0b545ba6f7763378daca2840" + integrity sha512-BhYE+WDaywFg2TBWYNXAE+8B1ATnThNBqXHP5nQu0jWJdVvY2hvkpyB3qOmtmDePiS5/BDQ8wASEWGMWRG148g== + dependencies: + es-errors "^1.3.0" + function-bind "^1.1.2" + call-bind@^1.0.2, call-bind@^1.0.5, call-bind@^1.0.6, call-bind@^1.0.7: version "1.0.7" resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.7.tgz#06016599c40c56498c18769d2730be242b6fa3b9" @@ -1066,6 +1074,24 @@ call-bind@^1.0.2, call-bind@^1.0.5, call-bind@^1.0.6, call-bind@^1.0.7: get-intrinsic "^1.2.4" set-function-length "^1.2.1" +call-bind@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.8.tgz#0736a9660f537e3388826f440d5ec45f744eaa4c" + integrity sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww== + dependencies: + call-bind-apply-helpers "^1.0.0" + es-define-property "^1.0.0" + get-intrinsic "^1.2.4" + set-function-length "^1.2.2" + +call-bound@^1.0.2, call-bound@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/call-bound/-/call-bound-1.0.3.tgz#41cfd032b593e39176a71533ab4f384aa04fd681" + integrity sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA== + dependencies: + call-bind-apply-helpers "^1.0.1" + get-intrinsic "^1.2.6" + callsites@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" @@ -1359,6 +1385,30 @@ deep-eql@^3.0.1: dependencies: type-detect "^4.0.0" +deep-equal@^2.2.3: + version "2.2.3" + resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-2.2.3.tgz#af89dafb23a396c7da3e862abc0be27cf51d56e1" + integrity sha512-ZIwpnevOurS8bpT4192sqAowWM76JDKSHYzMLty3BZGSswgq6pBaH3DhCSW5xVAZICZyKdOBPjwww5wfgT/6PA== + dependencies: + array-buffer-byte-length "^1.0.0" + call-bind "^1.0.5" + es-get-iterator "^1.1.3" + get-intrinsic "^1.2.2" + is-arguments "^1.1.1" + is-array-buffer "^3.0.2" + is-date-object "^1.0.5" + is-regex "^1.1.4" + is-shared-array-buffer "^1.0.2" + isarray "^2.0.5" + object-is "^1.1.5" + object-keys "^1.1.1" + object.assign "^4.1.4" + regexp.prototype.flags "^1.5.1" + side-channel "^1.0.4" + which-boxed-primitive "^1.0.2" + which-collection "^1.0.1" + which-typed-array "^1.1.13" + deep-is@^0.1.3: version "0.1.4" resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831" @@ -1443,6 +1493,15 @@ dotenv@^16.3.0: resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.4.5.tgz#cdd3b3b604cb327e286b4762e13502f717cb099f" integrity sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg== +dunder-proto@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/dunder-proto/-/dunder-proto-1.0.1.tgz#d7ae667e1dc83482f8b70fd0f6eefc50da30f58a" + integrity sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A== + dependencies: + call-bind-apply-helpers "^1.0.1" + es-errors "^1.3.0" + gopd "^1.2.0" + dynamic-dedupe@^0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/dynamic-dedupe/-/dynamic-dedupe-0.3.0.tgz#06e44c223f5e4e94d78ef9db23a6515ce2f962a1" @@ -1546,11 +1605,31 @@ es-define-property@^1.0.0: dependencies: get-intrinsic "^1.2.4" +es-define-property@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.1.tgz#983eb2f9a6724e9303f61addf011c72e09e0b0fa" + integrity sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g== + es-errors@^1.2.1, es-errors@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f" integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== +es-get-iterator@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/es-get-iterator/-/es-get-iterator-1.1.3.tgz#3ef87523c5d464d41084b2c3c9c214f1199763d6" + integrity sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw== + dependencies: + call-bind "^1.0.2" + get-intrinsic "^1.1.3" + has-symbols "^1.0.3" + is-arguments "^1.1.1" + is-map "^2.0.2" + is-set "^2.0.2" + is-string "^1.0.7" + isarray "^2.0.5" + stop-iteration-iterator "^1.0.0" + es-object-atoms@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/es-object-atoms/-/es-object-atoms-1.0.0.tgz#ddb55cd47ac2e240701260bc2a8e31ecb643d941" @@ -2047,6 +2126,22 @@ get-intrinsic@^1.1.3, get-intrinsic@^1.2.1, get-intrinsic@^1.2.3, get-intrinsic@ has-symbols "^1.0.3" hasown "^2.0.0" +get-intrinsic@^1.2.2, get-intrinsic@^1.2.5, get-intrinsic@^1.2.6: + version "1.2.6" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.6.tgz#43dd3dd0e7b49b82b2dfcad10dc824bf7fc265d5" + integrity sha512-qxsEs+9A+u85HhllWJJFicJfPDhRmjzoYdl64aMWW9yRIJmSyxdn8IEkuIM530/7T+lv0TIHd8L6Q/ra0tEoeA== + dependencies: + call-bind-apply-helpers "^1.0.1" + dunder-proto "^1.0.0" + es-define-property "^1.0.1" + es-errors "^1.3.0" + es-object-atoms "^1.0.0" + function-bind "^1.1.2" + gopd "^1.2.0" + has-symbols "^1.1.0" + hasown "^2.0.2" + math-intrinsics "^1.0.0" + get-own-enumerable-property-symbols@^3.0.0: version "3.0.2" resolved "https://registry.yarnpkg.com/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.2.tgz#b5fde77f22cbe35f390b4e089922c50bce6ef664" @@ -2174,6 +2269,11 @@ gopd@^1.0.1: dependencies: get-intrinsic "^1.1.3" +gopd@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.2.0.tgz#89f56b8217bdbc8802bd299df6d7f1081d7e51a1" + integrity sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg== + graceful-fs@^4.1.2: version "4.2.11" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" @@ -2216,6 +2316,11 @@ has-symbols@^1.0.2, has-symbols@^1.0.3: resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== +has-symbols@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.1.0.tgz#fc9c6a783a084951d0b971fe1018de813707a338" + integrity sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ== + has-tostringtag@^1.0.0, has-tostringtag@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz#2cdc42d40bef2e5b4eeab7c01a73c54ce7ab5abc" @@ -2305,6 +2410,32 @@ internal-slot@^1.0.7: hasown "^2.0.0" side-channel "^1.0.4" +internal-slot@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.1.0.tgz#1eac91762947d2f7056bc838d93e13b2e9604961" + integrity sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw== + dependencies: + es-errors "^1.3.0" + hasown "^2.0.2" + side-channel "^1.1.0" + +is-arguments@^1.1.1: + version "1.2.0" + resolved "https://registry.yarnpkg.com/is-arguments/-/is-arguments-1.2.0.tgz#ad58c6aecf563b78ef2bf04df540da8f5d7d8e1b" + integrity sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA== + dependencies: + call-bound "^1.0.2" + has-tostringtag "^1.0.2" + +is-array-buffer@^3.0.2: + version "3.0.5" + resolved "https://registry.yarnpkg.com/is-array-buffer/-/is-array-buffer-3.0.5.tgz#65742e1e687bd2cc666253068fd8707fe4d44280" + integrity sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A== + dependencies: + call-bind "^1.0.8" + call-bound "^1.0.3" + get-intrinsic "^1.2.6" + is-array-buffer@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/is-array-buffer/-/is-array-buffer-3.0.4.tgz#7a1f92b3d61edd2bc65d24f130530ea93d7fae98" @@ -2416,7 +2547,7 @@ is-inside-container@^1.0.0: dependencies: is-docker "^3.0.0" -is-map@^2.0.3: +is-map@^2.0.2, is-map@^2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/is-map/-/is-map-2.0.3.tgz#ede96b7fe1e270b3c4465e3a465658764926d62e" integrity sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw== @@ -2466,7 +2597,7 @@ is-regexp@^1.0.0: resolved "https://registry.yarnpkg.com/is-regexp/-/is-regexp-1.0.0.tgz#fd2d883545c46bac5a633e7b9a09e87fa2cb5069" integrity sha512-7zjFAPO4/gwyQAAgRRmqeEeyIICSdmCqa3tsVHMdBzaXXRiqopZL4Cyghg/XulGWrtABTpbnYYzzIRffLkP4oA== -is-set@^2.0.3: +is-set@^2.0.2, is-set@^2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/is-set/-/is-set-2.0.3.tgz#8ab209ea424608141372ded6e0cb200ef1d9d01d" integrity sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg== @@ -2776,6 +2907,11 @@ make-error@^1.1.1: resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2" integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== +math-intrinsics@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/math-intrinsics/-/math-intrinsics-1.0.0.tgz#4e04bf87c85aa51e90d078dac2252b4eb5260817" + integrity sha512-4MqMiKP90ybymYvsut0CH2g4XWbfLtmlCkXmtmdcDCxNB+mQcu1w/1+L/VD7vi/PSv7X2JYV7SCcR+jiPXnQtA== + memorystream@^0.3.1: version "0.3.1" resolved "https://registry.yarnpkg.com/memorystream/-/memorystream-0.3.1.tgz#86d7090b30ce455d63fbae12dda51a47ddcaf9b2" @@ -3063,12 +3199,20 @@ object-inspect@^1.13.1, object-inspect@^1.13.3: resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.3.tgz#f14c183de51130243d6d18ae149375ff50ea488a" integrity sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA== +object-is@^1.1.5: + version "1.1.6" + resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.1.6.tgz#1a6a53aed2dd8f7e6775ff870bea58545956ab07" + integrity sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + object-keys@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== -object.assign@^4.1.5: +object.assign@^4.1.4, object.assign@^4.1.5: version "4.1.5" resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.5.tgz#3a833f9ab7fdb80fc9e8d2300c803d216d8fdbb0" integrity sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ== @@ -3607,7 +3751,7 @@ regenerator-runtime@^0.14.0: resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz#356ade10263f685dda125100cd862c1db895327f" integrity sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw== -regexp.prototype.flags@^1.5.3: +regexp.prototype.flags@^1.5.1, regexp.prototype.flags@^1.5.3: version "1.5.3" resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.5.3.tgz#b3ae40b1d2499b8350ab2c3fe6ef3845d3a96f42" integrity sha512-vqlC04+RQoFalODCbCumG2xIOvapzVMHwsyIGM/SIE8fRhFFsXeH8/QQ+s0T0kDAhKc4k30s73/0ydkHQz6HlQ== @@ -3798,7 +3942,7 @@ serialize-javascript@^6.0.2: dependencies: randombytes "^2.1.0" -set-function-length@^1.2.1: +set-function-length@^1.2.1, set-function-length@^1.2.2: version "1.2.2" resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.2.2.tgz#aac72314198eaed975cf77b2c3b6b880695e5449" integrity sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg== @@ -3854,6 +3998,35 @@ shell-quote@^1.6.1: resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.8.1.tgz#6dbf4db75515ad5bac63b4f1894c3a154c766680" integrity sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA== +side-channel-list@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/side-channel-list/-/side-channel-list-1.0.0.tgz#10cb5984263115d3b7a0e336591e290a830af8ad" + integrity sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA== + dependencies: + es-errors "^1.3.0" + object-inspect "^1.13.3" + +side-channel-map@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/side-channel-map/-/side-channel-map-1.0.1.tgz#d6bb6b37902c6fef5174e5f533fab4c732a26f42" + integrity sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA== + dependencies: + call-bound "^1.0.2" + es-errors "^1.3.0" + get-intrinsic "^1.2.5" + object-inspect "^1.13.3" + +side-channel-weakmap@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz#11dda19d5368e40ce9ec2bdc1fb0ecbc0790ecea" + integrity sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A== + dependencies: + call-bound "^1.0.2" + es-errors "^1.3.0" + get-intrinsic "^1.2.5" + object-inspect "^1.13.3" + side-channel-map "^1.0.1" + side-channel@^1.0.4: version "1.0.6" resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.6.tgz#abd25fb7cd24baf45466406b1096b7831c9215f2" @@ -3864,6 +4037,17 @@ side-channel@^1.0.4: get-intrinsic "^1.2.4" object-inspect "^1.13.1" +side-channel@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.1.0.tgz#c3fcff9c4da932784873335ec9765fa94ff66bc9" + integrity sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw== + dependencies: + es-errors "^1.3.0" + object-inspect "^1.13.3" + side-channel-list "^1.0.0" + side-channel-map "^1.0.1" + side-channel-weakmap "^1.0.2" + signal-exit@^3.0.2, signal-exit@^3.0.3: version "3.0.7" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" @@ -3961,6 +4145,14 @@ stacktracey@^2.1.8: as-table "^1.0.36" get-source "^2.0.12" +stop-iteration-iterator@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz#f481ff70a548f6124d0312c3aa14cbfa7aa542ad" + integrity sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ== + dependencies: + es-errors "^1.3.0" + internal-slot "^1.1.0" + stoppable@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/stoppable/-/stoppable-1.1.0.tgz#32da568e83ea488b08e4d7ea2c3bcc9d75015d5b" @@ -4430,7 +4622,7 @@ which-builtin-type@^1.1.4: which-collection "^1.0.2" which-typed-array "^1.1.15" -which-collection@^1.0.2: +which-collection@^1.0.1, which-collection@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/which-collection/-/which-collection-1.0.2.tgz#627ef76243920a107e7ce8e96191debe4b16c2a0" integrity sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw== @@ -4440,6 +4632,17 @@ which-collection@^1.0.2: is-weakmap "^2.0.2" is-weakset "^2.0.3" +which-typed-array@^1.1.13: + version "1.1.16" + resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.16.tgz#db4db429c4706feca2f01677a144278e4a8c216b" + integrity sha512-g+N+GAWiRj66DngFwHvISJd+ITsyphZvD1vChfVg6cEdnzy53GzB3oy0fUNlvhz7H7+MiqhYr26qxQShCpKTTQ== + dependencies: + available-typed-arrays "^1.0.7" + call-bind "^1.0.7" + for-each "^0.3.3" + gopd "^1.0.1" + has-tostringtag "^1.0.2" + which-typed-array@^1.1.14, which-typed-array@^1.1.15: version "1.1.15" resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.15.tgz#264859e9b11a649b388bfaaf4f767df1f779b38d" @@ -4721,3 +4924,8 @@ zod@^3.22.3: version "3.23.8" resolved "https://registry.yarnpkg.com/zod/-/zod-3.23.8.tgz#e37b957b5d52079769fb8097099b592f0ef4067d" integrity sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g== + +zod@^3.24.1: + version "3.24.1" + resolved "https://registry.yarnpkg.com/zod/-/zod-3.24.1.tgz#27445c912738c8ad1e9de1bea0359fa44d9d35ee" + integrity sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A==