From 0013efdeebaa5af7e4f88daa7bf6e53d706622a8 Mon Sep 17 00:00:00 2001 From: Paolo Chillari Date: Wed, 30 Nov 2022 11:49:35 +0000 Subject: [PATCH 001/107] fix: allow update PSA pin requests with same CID (#2125) Resolve #1547 --- packages/api/src/pins.js | 9 +-------- packages/api/src/utils/psa.js | 1 - packages/api/test/pin.spec.js | 16 +++++++++------- 3 files changed, 10 insertions(+), 16 deletions(-) diff --git a/packages/api/src/pins.js b/packages/api/src/pins.js index 56d99b5c2c..e537c9b00e 100644 --- a/packages/api/src/pins.js +++ b/packages/api/src/pins.js @@ -3,7 +3,6 @@ import { JSONResponse } from './utils/json-response.js' import { getPins, PIN_OK_STATUS, waitAndUpdateOkPins } from './utils/pin.js' import { PSAErrorDB, PSAErrorResourceNotFound, PSAErrorInvalidData, PSAErrorRequiredData, PinningServiceApiError } from './errors.js' import { - INVALID_REPLACE, INVALID_REQUEST_ID, PINNING_FAILED, REQUIRED_REQUEST_ID, @@ -244,18 +243,12 @@ export async function pinDelete (request, env, ctx) { * @param {import('./index').Ctx} ctx */ async function replacePin (normalizedCid, newPinData, requestId, authTokenId, env, ctx) { - let existingPinRequest try { - existingPinRequest = await env.db.getPsaPinRequest(authTokenId, requestId) + await env.db.getPsaPinRequest(authTokenId, requestId) } catch (e) { throw new PSAErrorResourceNotFound() } - const existingCid = existingPinRequest.sourceCid - if (newPinData.cid === existingCid) { - throw new PSAErrorInvalidData(INVALID_REPLACE) - } - let pinStatus try { pinStatus = await createPin(normalizedCid, newPinData, authTokenId, env, ctx) diff --git a/packages/api/src/utils/psa.js b/packages/api/src/utils/psa.js index a84f01d5b9..5ea35f60ff 100644 --- a/packages/api/src/utils/psa.js +++ b/packages/api/src/utils/psa.js @@ -64,7 +64,6 @@ export const ERROR_CODE = 400 export const DATA_NOT_FOUND = 'Requested data was not found.' export const INVALID_CID = 'The CID provided is invalid.' export const INVALID_META = 'Meta should be an object with string values' -export const INVALID_REPLACE = 'Existing and replacement CID are the same.' export const INVALID_REQUEST_ID = 'Request id should be a string.' export const PINNING_FAILED = 'PSA_PINNING_FAILED' export const REQUIRED_REQUEST_ID = 'Request id is required.' diff --git a/packages/api/test/pin.spec.js b/packages/api/test/pin.spec.js index afbe475821..aaa2ec5d65 100644 --- a/packages/api/test/pin.spec.js +++ b/packages/api/test/pin.spec.js @@ -7,7 +7,6 @@ import { DATA_NOT_FOUND, ERROR_CODE, INVALID_CID, - INVALID_REPLACE, getEffectivePinStatus } from '../src/utils/psa.js' import { PSAErrorResourceNotFound, PSAErrorInvalidData, PSAErrorRequiredData } from '../src/errors.js' @@ -988,8 +987,9 @@ describe('Pinning APIs endpoints', () => { assert.strictEqual(getResponse.status, 404, 'Pin request was not deleted') }) - it('should not replace the same pin request', async () => { + it('can update existing pin request (with same CID)', async () => { const cid = 'bafybeieppxukl4i4acnjcdj2fueoa5oppuaciseggv2cvplj2iu6d7kx2e' + const aNewName = 'aNewName' const pinRequest = await createPinRequest(cid, token) const res = await fetch(new URL(`pins/${pinRequest.requestid}`, endpoint).toString(), { method: 'POST', @@ -997,15 +997,17 @@ describe('Pinning APIs endpoints', () => { Authorization: `Bearer ${token}` }, body: JSON.stringify({ - cid + cid, + name: aNewName }) }) assert(res, 'Server responded') - assert(!res.ok) - assert.equal(res.status, ERROR_CODE) - const { error } = await res.json() - assert.equal(error.details, INVALID_REPLACE) + assert(res.ok) + const data = await res.json() + assertCorrectPinResponse(data) + assert.strictEqual(data.pin.cid, cid) + assert.strictEqual(data.pin.name, aNewName) }) }) }) From 8b1c1d3eca4a7ae5a557cb29d3f43f9d6ca29b9d Mon Sep 17 00:00:00 2001 From: Paolo Chillari Date: Wed, 30 Nov 2022 11:51:53 +0000 Subject: [PATCH 002/107] fix: PSA compliance fixes (#2091) resolves #1579 --- packages/api/src/pins.js | 7 ++-- packages/api/src/utils/psa.js | 4 +- packages/api/test/pin.spec.js | 71 ++++++++++++++++++++-------------- packages/api/test/user.spec.js | 2 +- packages/db/index.js | 17 ++++++-- 5 files changed, 63 insertions(+), 38 deletions(-) diff --git a/packages/api/src/pins.js b/packages/api/src/pins.js index e537c9b00e..41c48acbf9 100644 --- a/packages/api/src/pins.js +++ b/packages/api/src/pins.js @@ -187,7 +187,7 @@ export async function pinsGet (request, env, ctx) { /** * Transform a PinRequest into a PinStatus * - * @param { Object } pinRequest + * @param { import('../../db/db-client-types.js').PsaPinRequestUpsertOutput } pinRequest * @returns { PsaPinStatusResponse } */ export function toPinStatusResponse (pinRequest) { @@ -197,9 +197,10 @@ export function toPinStatusResponse (pinRequest) { created: pinRequest.created, pin: { cid: pinRequest.sourceCid, - ...pinRequest + ...pinRequest.name && { name: pinRequest.name }, + ...pinRequest.origins && { origins: pinRequest.origins }, + ...pinRequest.meta && { meta: pinRequest.meta } }, - // TODO(https://github.com/web3-storage/web3.storage/issues/792) delegates: [] } } diff --git a/packages/api/src/utils/psa.js b/packages/api/src/utils/psa.js index 5ea35f60ff..e2333ed006 100644 --- a/packages/api/src/utils/psa.js +++ b/packages/api/src/utils/psa.js @@ -74,7 +74,7 @@ export const MAX_PIN_LISTING_LIMIT = 1000 // Validation schemas const listPinsValidator = new Validator({ type: 'object', - required: ['status'], + required: [], properties: { name: { type: 'string', maxLength: 255 }, after: { type: 'string', format: 'date-time' }, @@ -234,7 +234,7 @@ export function validateSearchParams (queryString) { if (result.valid) { // Map statuses for DB compatibility. - opts.statuses = psaStatusesToDBStatuses(opts.status) + opts.statuses = opts.status && psaStatusesToDBStatuses(opts.status) data = opts } else { error = parseValidatorErrors(result.errors) diff --git a/packages/api/test/pin.spec.js b/packages/api/test/pin.spec.js index aaa2ec5d65..660168e529 100644 --- a/packages/api/test/pin.spec.js +++ b/packages/api/test/pin.spec.js @@ -11,6 +11,9 @@ import { } from '../src/utils/psa.js' import { PSAErrorResourceNotFound, PSAErrorInvalidData, PSAErrorRequiredData } from '../src/errors.js' +const REQUEST_EXPECTED_PROPERTIES = ['requestid', 'status', 'created', 'delegates', 'info', 'pin'] +const PIN_EXPECTED_PROPERTIES = ['cid', 'name', 'origins', 'meta'] + /** * * @param {import('../../db/postgres/pg-rest-api-types').definitions['pin']['status']} status @@ -41,24 +44,41 @@ const assertCorrectPinResponse = (data) => { assert.ok(Date.parse(data.created), 'created should be valid date string') assert.ok(Array.isArray(data.delegates), 'delegates should be an array') - if (data.info) { + // @ts-ignore https://github.com/microsoft/TypeScript/issues/44253 + if (Object.hasOwn(data, 'info')) { assert.ok(typeof data.info === 'object', 'info should be an object') } assert.ok(typeof data.pin === 'object', 'pin should be an object') assert.ok(typeof data.pin.cid === 'string', 'pin.cid should be an string') - if (data.pin.name) { + // @ts-ignore https://github.com/microsoft/TypeScript/issues/44253 + if (Object.hasOwn(data.pin, 'name')) { assert.ok(typeof data.pin.name === 'string', 'pin.name should be an string') } - if (data.pin.origins) { + // @ts-ignore https://github.com/microsoft/TypeScript/issues/44253 + if (Object.hasOwn(data.pin, 'origins')) { assert.ok(Array.isArray(data.pin.origins), 'pin.origins should be an array') } - if (data.pin.meta) { + // @ts-ignore https://github.com/microsoft/TypeScript/issues/44253 + if (Object.hasOwn(data.pin, 'meta')) { assert.ok(typeof data.pin.meta === 'object', 'pin.meta should be an object') } + + assert.ok(Object.keys(data).every(property => REQUEST_EXPECTED_PROPERTIES.includes(property)), 'request contains not valid properties') + assert.ok(Object.keys(data.pin).every(property => PIN_EXPECTED_PROPERTIES.includes(property)), 'request.pin contains not valid properties') +} + +/** + * + * @param {object} data + */ +const assertCorretPinResponseList = (data) => { + assert.ok(typeof data.count === 'number', 'count should be a number') + data.results.forEach(r => assertCorrectPinResponse(r)) + Object.keys(data).every(property => ['count', 'results'].includes(property)) } /** @@ -121,24 +141,6 @@ describe('Pinning APIs endpoints', () => { assert.strictEqual(error.details, '#/limit: Instance type "number" is invalid. Expected "integer".') }) - it('requires status', async () => { - const url = new URL(`${baseUrl}`).toString() - const res = await fetch( - url, { - method: 'GET', - headers: { - Authorization: `Bearer ${token}`, - 'Content-Type': 'application/json' - } - }) - - assert(res, 'Server responded') - assert.strictEqual(res.status, ERROR_CODE) - const { error } = await res.json() - assert.strictEqual(error.reason, PSAErrorRequiredData.CODE) - assert.strictEqual(error.details, 'Instance does not have required property "status".') - }) - it('validates meta filter is json object', async () => { const opts = new URLSearchParams({ meta: `[ @@ -251,9 +253,7 @@ describe('Pinning APIs endpoints', () => { }) it('returns only successful pins when no filter values are specified', async () => { - const opts = new URLSearchParams({ - status: 'pinned' - }) + const opts = new URLSearchParams({}) const url = new URL(`${baseUrl}?${opts}`).toString() const res = await fetch( url, { @@ -268,6 +268,7 @@ describe('Pinning APIs endpoints', () => { assert(res.ok, 'Server response is ok') const data = await res.json() assert.strictEqual(data.count, 1) + assertCorretPinResponseList(data) }) it('filters pins on CID, for this user', async () => { @@ -294,6 +295,7 @@ describe('Pinning APIs endpoints', () => { assert(res.ok, 'Server response is ok') const data = await res.json() assert.strictEqual(data.count, 2) + assertCorretPinResponseList(data) }) it('filters case sensitive exact match on name', async () => { @@ -315,6 +317,7 @@ describe('Pinning APIs endpoints', () => { assert(res.ok, 'Server response is ok') const data = await res.json() assert.strictEqual(data.count, 1) + assertCorretPinResponseList(data) }) it('filters case insensitive partial match on name', async () => { @@ -337,6 +340,7 @@ describe('Pinning APIs endpoints', () => { assert(res.ok, 'Server response is ok') const data = await res.json() assert.strictEqual(data.count, 4) + assertCorretPinResponseList(data) }) it('filters pins by status', async () => { @@ -359,6 +363,7 @@ describe('Pinning APIs endpoints', () => { assert.strictEqual(data.count, 1) assert.strictEqual(data.results.length, 1) assert.strictEqual(data.results[0].pin.name, 'FailedPinning.doc') + assertCorretPinResponseList(data) }) it('filters pins by multiple statuses', async () => { @@ -379,6 +384,7 @@ describe('Pinning APIs endpoints', () => { assert(res.ok, 'Server response is ok') const data = await res.json() assert.strictEqual(data.count, 5) + assertCorretPinResponseList(data) }) it('filters pins by queued', async () => { @@ -399,6 +405,7 @@ describe('Pinning APIs endpoints', () => { assert(res.ok, 'Server response is ok') const data = await res.json() assert.strictEqual(data.count, 1) + assertCorretPinResponseList(data) }) it('filters pins created before a date', async () => { @@ -421,6 +428,7 @@ describe('Pinning APIs endpoints', () => { const data = await res.json() assert.strictEqual(data.results.length, 1) assert.strictEqual(data.count, 1) + assertCorretPinResponseList(data) }) it('filters pins created after a date', async () => { @@ -443,6 +451,7 @@ describe('Pinning APIs endpoints', () => { const data = await res.json() assert.strictEqual(data.results.length, 2) assert.strictEqual(data.count, 2) + assertCorretPinResponseList(data) }) it('limits the number of pins returned for this user and includes the total', async () => { @@ -465,6 +474,7 @@ describe('Pinning APIs endpoints', () => { const data = await res.json() assert.strictEqual(data.count, 7) assert.strictEqual(data.results.length, 3) + assertCorretPinResponseList(data) }) it('filters pins by meta', async () => { @@ -487,6 +497,7 @@ describe('Pinning APIs endpoints', () => { const data = await res.json() assert.strictEqual(data.count, 1) assert.strictEqual(data.results[0].pin.name, 'Image.jpeg') + assertCorretPinResponseList(data) }) }) @@ -509,10 +520,12 @@ describe('Pinning APIs endpoints', () => { assert(res, 'Server responded') assert(res.ok, 'Server response ok') const data = await res.json() + assertCorrectPinResponse(data) assert.strictEqual(data.pin.cid, cid) - assert.strictEqual(data.pin.name, null) - assert.strictEqual(data.pin.origins, null) - assert.strictEqual(data.pin.meta, null) + console.log(data.pin.cid, cid) + assert.strictEqual(data.pin.name, undefined) + assert.strictEqual(data.pin.origins, undefined) + assert.strictEqual(data.pin.meta, undefined) }) it('requires cid', async () => { @@ -574,6 +587,7 @@ describe('Pinning APIs endpoints', () => { const data = await res.json() assert(res, 'Server responded') assert(res.ok, 'Server response ok') + assertCorrectPinResponse(data) assert.strictEqual(data.pin.cid, cid) assert.deepStrictEqual(data.pin.name, name) assert.deepStrictEqual(data.pin.origins, origins) @@ -971,6 +985,7 @@ describe('Pinning APIs endpoints', () => { assert(replaceResponse.ok, 'Replace request was not successful') assert.strictEqual(replaceResponse.status, 202) const data = await replaceResponse.json() + assertCorrectPinResponse(data) assert.strictEqual(data.pin.cid, newCid) assert.deepStrictEqual(data.pin.name, name) assert.deepStrictEqual(data.pin.origins, origins) diff --git a/packages/api/test/user.spec.js b/packages/api/test/user.spec.js index 9aa67176ae..c2a1e04b40 100644 --- a/packages/api/test/user.spec.js +++ b/packages/api/test/user.spec.js @@ -417,7 +417,7 @@ describe('GET /user/pins', () => { assert(res.ok) const body = await res.json() - assert.equal([...new Set(body.results.map(x => x.pin.authKey))].length, 2) + assert.equal(body.count, 8) }) }) diff --git a/packages/db/index.js b/packages/db/index.js index fda826b8aa..826560e3f1 100644 --- a/packages/db/index.js +++ b/packages/db/index.js @@ -1168,6 +1168,10 @@ export class DBClient { async listPsaPinRequests (authKey, opts = {}) { const match = opts?.match || 'exact' const limit = opts?.limit || 10 + /** + * @type {Array.|undefined} + */ + let statuses let query = this._client .from(psaPinRequestTableName) @@ -1190,12 +1194,17 @@ export class DBClient { query = query.range(rangeFrom, rangeTo - 1) } - if (!opts.cid && !opts.name && !opts.statuses) { - query = query.eq('content.pins.status', 'Pinned') + // If not specified we default to pinned only if no other filters are provided. + // While slightly inconsistent, that's the current expectation. + // This is being discussed in https://github.com/ipfs-shipyard/pinning-service-compliance/issues/245 + if (!opts.cid && !opts.name && !opts.meta && !opts.statuses) { + statuses = ['Pinned'] + } else { + statuses = opts.statuses } - if (opts.statuses) { - query = query.in('content.pins.status', opts.statuses) + if (statuses) { + query = query.in('content.pins.status', statuses) } if (opts.cid) { From 2056ec6fd692949f112b7b37d48d4da9d6a34523 Mon Sep 17 00:00:00 2001 From: Paolo Chillari Date: Wed, 30 Nov 2022 11:53:35 +0000 Subject: [PATCH 003/107] feat: add tooltip to dashboard storage totals (#2101) Resolves #2081 Co-authored-by: Adam Alton <270255+adamalton@users.noreply.github.com> Co-authored-by: David Choi --- .../account/storageManager/storageManager.js | 5 ++++ .../storageManager/storageManager.scss | 5 ++++ .../website/content/pages/app/account.json | 23 +++++++++++++++---- 3 files changed, 28 insertions(+), 5 deletions(-) diff --git a/packages/website/components/account/storageManager/storageManager.js b/packages/website/components/account/storageManager/storageManager.js index 0b6d0e12f6..2fda38a4fd 100644 --- a/packages/website/components/account/storageManager/storageManager.js +++ b/packages/website/components/account/storageManager/storageManager.js @@ -6,6 +6,8 @@ import Link from 'next/link'; import { useUser } from 'components/contexts/userContext'; import { elementIsInViewport } from 'lib/utils'; import { usePayment } from '../../../hooks/use-payment'; +import Tooltip from 'ZeroComponents/tooltip/tooltip'; +import InfoIcon from 'assets/icons/info'; // Raw TiB number of bytes, to be used in calculations const tebibyte = 1099511627776; @@ -123,6 +125,9 @@ const StorageManager = ({ className = '', content }) => {  of {currentPlan?.baseStorage} used )} + + + )} diff --git a/packages/website/components/account/storageManager/storageManager.scss b/packages/website/components/account/storageManager/storageManager.scss index 60b53b5870..db4a41537e 100644 --- a/packages/website/components/account/storageManager/storageManager.scss +++ b/packages/website/components/account/storageManager/storageManager.scss @@ -168,6 +168,11 @@ .storage-number { font-style: italic; } + .Tooltip { + margin-left: 5px; + font-size: 10px; // The size of the svg icon. + vertical-align: super; + } } .storage-manager-meter-uploaded, diff --git a/packages/website/content/pages/app/account.json b/packages/website/content/pages/app/account.json index 29fe213ca3..18b66a5b67 100644 --- a/packages/website/content/pages/app/account.json +++ b/packages/website/content/pages/app/account.json @@ -76,7 +76,8 @@ }, "loading": "Loading...", "max_space_tib_label": "TiB", - "unlock_label": "1 TiB+" + "unlock_label": "1 TiB+", + "tooltip_total": "
This is the total of all your uploads, including ones marked \"Failed\" (you can read more about what the status means here). You can remove uploads via the table below.
" }, "file_manager": { "tabs": [ @@ -186,10 +187,22 @@ }, "results": { "options": [ - { "label": "View 10 Results", "value": "10" }, - { "label": "View 20 Results", "value": "20" }, - { "label": "View 50 Results", "value": "50" }, - { "label": "View 100 Results", "value": "100" } + { + "label": "View 10 Results", + "value": "10" + }, + { + "label": "View 20 Results", + "value": "20" + }, + { + "label": "View 50 Results", + "value": "50" + }, + { + "label": "View 100 Results", + "value": "100" + } ] } } From e150d1fa86dc0e6600f159fa92ccb7c8e65900e3 Mon Sep 17 00:00:00 2001 From: Paolo Chillari Date: Wed, 30 Nov 2022 12:31:24 +0000 Subject: [PATCH 004/107] feat: list and ability to delete psa request created though deleted tokens (#2054) Resolve #1840 Co-authored-by: Alan Shaw --- package-lock.json | 1209 ++++++++++++++++- packages/api/src/index.js | 4 +- packages/api/src/pins.js | 4 +- packages/api/src/user.js | 44 +- packages/api/test/fixtures/init-data.sql | 4 +- packages/db/db-client-types.ts | 3 + packages/db/index.d.ts | 7 +- packages/db/index.js | 15 +- packages/db/postgres/functions.sql | 5 +- .../migrations/028-update-list-keys.sql | 23 + packages/db/postgres/pg-rest-api-types.d.ts | 208 +-- packages/db/test/pinning.spec.js | 10 +- packages/db/test/user.spec.js | 62 + packages/website/lib/api.js | 10 +- 14 files changed, 1489 insertions(+), 119 deletions(-) create mode 100644 packages/db/postgres/migrations/028-update-list-keys.sql diff --git a/package-lock.json b/package-lock.json index 730230a2b8..bb7df526ab 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11023,6 +11023,36 @@ "esbuild-windows-arm64": "0.13.14" } }, + "node_modules/esbuild-android-64": { + "version": "0.14.51", + "resolved": "https://registry.npmjs.org/esbuild-android-64/-/esbuild-android-64-0.14.51.tgz", + "integrity": "sha512-6FOuKTHnC86dtrKDmdSj2CkcKF8PnqkaIXqvgydqfJmqBazCPdw+relrMlhGjkvVdiiGV70rpdnyFmA65ekBCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-android-arm64": { + "version": "0.13.14", + "resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.13.14.tgz", + "integrity": "sha512-Q+Xhfp827r+ma8/DJgpMRUbDZfefsk13oePFEXEIJ4gxFbNv5+vyiYXYuKm43/+++EJXpnaYmEnu4hAKbAWYbA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "peer": true + }, "node_modules/esbuild-darwin-arm64": { "version": "0.14.51", "resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.51.tgz", @@ -11040,6 +11070,248 @@ "node": ">=12" } }, + "node_modules/esbuild-freebsd-64": { + "version": "0.13.14", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.13.14.tgz", + "integrity": "sha512-BKosI3jtvTfnmsCW37B1TyxMUjkRWKqopR0CE9AF2ratdpkxdR24Vpe3gLKNyWiZ7BE96/SO5/YfhbPUzY8wKw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "peer": true + }, + "node_modules/esbuild-freebsd-arm64": { + "version": "0.13.14", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.13.14.tgz", + "integrity": "sha512-yd2uh0yf+fWv5114+SYTl4/1oDWtr4nN5Op+PGxAkMqHfYfLjFKpcxwCo/QOS/0NWqPVE8O41IYZlFhbEN2B8Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "peer": true + }, + "node_modules/esbuild-linux-32": { + "version": "0.13.14", + "resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.13.14.tgz", + "integrity": "sha512-a8rOnS1oWSfkkYWXoD2yXNV4BdbDKA7PNVQ1klqkY9SoSApL7io66w5H44mTLsfyw7G6Z2vLlaLI2nz9MMAowA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "peer": true + }, + "node_modules/esbuild-linux-64": { + "version": "0.13.14", + "resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.13.14.tgz", + "integrity": "sha512-yPZSoMs9W2MC3Dw+6kflKt5FfQm6Dicex9dGIr1OlHRsn3Hm7yGMUTctlkW53KknnZdOdcdd5upxvbxqymczVQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "peer": true + }, + "node_modules/esbuild-linux-arm": { + "version": "0.13.14", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.13.14.tgz", + "integrity": "sha512-8chZE4pkKRvJ/M/iwsNQ1KqsRg2RyU5eT/x2flNt/f8F2TVrDreR7I0HEeCR50wLla3B1C3wTIOzQBmjuc6uWg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "peer": true + }, + "node_modules/esbuild-linux-arm64": { + "version": "0.13.14", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.13.14.tgz", + "integrity": "sha512-Lvo391ln9PzC334e+jJ2S0Rt0cxP47eoH5gFyv/E8HhOnEJTvm7A+RRnMjjHnejELacTTfYgFGQYPjLsi/jObQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "peer": true + }, + "node_modules/esbuild-linux-mips64le": { + "version": "0.13.14", + "resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.13.14.tgz", + "integrity": "sha512-MZhgxbmrWbpY3TOE029O6l5tokG9+Yoj2hW7vdit/d/VnmneqeGrSHADuDL6qXM8L5jaCiaivb4VhsyVCpdAbQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "peer": true + }, + "node_modules/esbuild-linux-ppc64le": { + "version": "0.13.14", + "resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.13.14.tgz", + "integrity": "sha512-un7KMwS7fX1Un6BjfSZxTT8L5cV/8Uf4SAhM7WYy2XF8o8TI+uRxxD03svZnRNIPsN2J5cl6qV4n7Iwz+yhhVw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "peer": true + }, + "node_modules/esbuild-linux-riscv64": { + "version": "0.14.23", + "resolved": "https://registry.npmjs.org/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.14.23.tgz", + "integrity": "sha512-fbL3ggK2wY0D8I5raPIMPhpCvODFE+Bhb5QGtNP3r5aUsRR6TQV+ZBXIaw84iyvKC8vlXiA4fWLGhghAd/h/Zg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-s390x": { + "version": "0.14.23", + "resolved": "https://registry.npmjs.org/esbuild-linux-s390x/-/esbuild-linux-s390x-0.14.23.tgz", + "integrity": "sha512-GHMDCyfy7+FaNSO8RJ8KCFsnax8fLUsOrj9q5Gi2JmZMY0Zhp75keb5abTFCq2/Oy6KVcT0Dcbyo/bFb4rIFJA==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-netbsd-64": { + "version": "0.13.14", + "resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.13.14.tgz", + "integrity": "sha512-5ekKx/YbOmmlTeNxBjh38Uh5TGn5C4uyqN17i67k18pS3J+U2hTVD7rCxcFcRS1AjNWumkVL3jWqYXadFwMS0Q==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "peer": true + }, + "node_modules/esbuild-openbsd-64": { + "version": "0.13.14", + "resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.13.14.tgz", + "integrity": "sha512-9bzvwewHjct2Cv5XcVoE1yW5YTW12Sk838EYfA46abgnhxGoFSD1mFcaztp5HHC43AsF+hQxbSFG/RilONARUA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "peer": true + }, + "node_modules/esbuild-sunos-64": { + "version": "0.13.14", + "resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.13.14.tgz", + "integrity": "sha512-mjMrZB76M6FmoiTvj/RGWilrioR7gVwtFBRVugr9qLarXMIU1W/pQx+ieEOtflrW61xo8w1fcxyHsVVGRvoQ0w==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "peer": true + }, + "node_modules/esbuild-windows-32": { + "version": "0.13.14", + "resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.13.14.tgz", + "integrity": "sha512-GZa6mrx2rgfbH/5uHg0Rdw50TuOKbdoKCpEBitzmG5tsXBdce+cOL+iFO5joZc6fDVCLW3Y6tjxmSXRk/v20Hg==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "peer": true + }, + "node_modules/esbuild-windows-64": { + "version": "0.13.14", + "resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.13.14.tgz", + "integrity": "sha512-Lsgqah24bT7ClHjLp/Pj3A9wxjhIAJyWQcrOV4jqXAFikmrp2CspA8IkJgw7HFjx6QrJuhpcKVbCAe/xw0i2yw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "peer": true + }, + "node_modules/esbuild-windows-arm64": { + "version": "0.13.14", + "resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.13.14.tgz", + "integrity": "sha512-KP8FHVlWGhM7nzYtURsGnskXb/cBCPTfj0gOKfjKq2tHtYnhDZywsUG57nk7TKhhK0fL11LcejHG3LRW9RF/9A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "peer": true + }, + "node_modules/esbuild/node_modules/esbuild-darwin-64": { + "version": "0.13.14", + "resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.13.14.tgz", + "integrity": "sha512-YmOhRns6QBNSjpVdTahi/yZ8dscx9ai7a6OY6z5ACgOuQuaQ2Qk2qgJ0/siZ6LgD0gJFMV8UINFV5oky5TFNQQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "peer": true + }, "node_modules/esbuild/node_modules/esbuild-darwin-arm64": { "version": "0.13.14", "resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.13.14.tgz", @@ -27835,6 +28107,294 @@ "esbuild-windows-arm64": "0.14.51" } }, + "node_modules/wrangler/node_modules/esbuild-android-arm64": { + "version": "0.14.51", + "resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.14.51.tgz", + "integrity": "sha512-vBtp//5VVkZWmYYvHsqBRCMMi1MzKuMIn5XDScmnykMTu9+TD9v0NMEDqQxvtFToeYmojdo5UCV2vzMQWJcJ4A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/wrangler/node_modules/esbuild-darwin-64": { + "version": "0.14.51", + "resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.14.51.tgz", + "integrity": "sha512-YFmXPIOvuagDcwCejMRtCDjgPfnDu+bNeh5FU2Ryi68ADDVlWEpbtpAbrtf/lvFTWPexbgyKgzppNgsmLPr8PA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/wrangler/node_modules/esbuild-freebsd-64": { + "version": "0.14.51", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.51.tgz", + "integrity": "sha512-cLEI/aXjb6vo5O2Y8rvVSQ7smgLldwYY5xMxqh/dQGfWO+R1NJOFsiax3IS4Ng300SVp7Gz3czxT6d6qf2cw0g==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/wrangler/node_modules/esbuild-freebsd-arm64": { + "version": "0.14.51", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.51.tgz", + "integrity": "sha512-TcWVw/rCL2F+jUgRkgLa3qltd5gzKjIMGhkVybkjk6PJadYInPtgtUBp1/hG+mxyigaT7ib+od1Xb84b+L+1Mg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/wrangler/node_modules/esbuild-linux-32": { + "version": "0.14.51", + "resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.14.51.tgz", + "integrity": "sha512-RFqpyC5ChyWrjx8Xj2K0EC1aN0A37H6OJfmUXIASEqJoHcntuV3j2Efr9RNmUhMfNE6yEj2VpYuDteZLGDMr0w==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/wrangler/node_modules/esbuild-linux-64": { + "version": "0.14.51", + "resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.14.51.tgz", + "integrity": "sha512-dxjhrqo5i7Rq6DXwz5v+MEHVs9VNFItJmHBe1CxROWNf4miOGoQhqSG8StStbDkQ1Mtobg6ng+4fwByOhoQoeA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/wrangler/node_modules/esbuild-linux-arm": { + "version": "0.14.51", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.14.51.tgz", + "integrity": "sha512-LsJynDxYF6Neg7ZC7748yweCDD+N8ByCv22/7IAZglIEniEkqdF4HCaa49JNDLw1UQGlYuhOB8ZT/MmcSWzcWg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/wrangler/node_modules/esbuild-linux-arm64": { + "version": "0.14.51", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.51.tgz", + "integrity": "sha512-D9rFxGutoqQX3xJPxqd6o+kvYKeIbM0ifW2y0bgKk5HPgQQOo2k9/2Vpto3ybGYaFPCE5qTGtqQta9PoP6ZEzw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/wrangler/node_modules/esbuild-linux-mips64le": { + "version": "0.14.51", + "resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.51.tgz", + "integrity": "sha512-vS54wQjy4IinLSlb5EIlLoln8buh1yDgliP4CuEHumrPk4PvvP4kTRIG4SzMXm6t19N0rIfT4bNdAxzJLg2k6A==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/wrangler/node_modules/esbuild-linux-ppc64le": { + "version": "0.14.51", + "resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.51.tgz", + "integrity": "sha512-xcdd62Y3VfGoyphNP/aIV9LP+RzFw5M5Z7ja+zdpQHHvokJM7d0rlDRMN+iSSwvUymQkqZO+G/xjb4/75du8BQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/wrangler/node_modules/esbuild-linux-riscv64": { + "version": "0.14.51", + "resolved": "https://registry.npmjs.org/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.14.51.tgz", + "integrity": "sha512-syXHGak9wkAnFz0gMmRBoy44JV0rp4kVCEA36P5MCeZcxFq8+fllBC2t6sKI23w3qd8Vwo9pTADCgjTSf3L3rA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/wrangler/node_modules/esbuild-linux-s390x": { + "version": "0.14.51", + "resolved": "https://registry.npmjs.org/esbuild-linux-s390x/-/esbuild-linux-s390x-0.14.51.tgz", + "integrity": "sha512-kFAJY3dv+Wq8o28K/C7xkZk/X34rgTwhknSsElIqoEo8armCOjMJ6NsMxm48KaWY2h2RUYGtQmr+RGuUPKBhyw==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/wrangler/node_modules/esbuild-netbsd-64": { + "version": "0.14.51", + "resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.51.tgz", + "integrity": "sha512-ZZBI7qrR1FevdPBVHz/1GSk1x5GDL/iy42Zy8+neEm/HA7ma+hH/bwPEjeHXKWUDvM36CZpSL/fn1/y9/Hb+1A==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/wrangler/node_modules/esbuild-openbsd-64": { + "version": "0.14.51", + "resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.51.tgz", + "integrity": "sha512-7R1/p39M+LSVQVgDVlcY1KKm6kFKjERSX1lipMG51NPcspJD1tmiZSmmBXoY5jhHIu6JL1QkFDTx94gMYK6vfA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/wrangler/node_modules/esbuild-sunos-64": { + "version": "0.14.51", + "resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.14.51.tgz", + "integrity": "sha512-HoHaCswHxLEYN8eBTtyO0bFEWvA3Kdb++hSQ/lLG7TyKF69TeSG0RNoBRAs45x/oCeWaTDntEZlYwAfQlhEtJA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/wrangler/node_modules/esbuild-windows-32": { + "version": "0.14.51", + "resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.14.51.tgz", + "integrity": "sha512-4rtwSAM35A07CBt1/X8RWieDj3ZUHQqUOaEo5ZBs69rt5WAFjP4aqCIobdqOy4FdhYw1yF8Z0xFBTyc9lgPtEg==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/wrangler/node_modules/esbuild-windows-64": { + "version": "0.14.51", + "resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.14.51.tgz", + "integrity": "sha512-HoN/5HGRXJpWODprGCgKbdMvrC3A2gqvzewu2eECRw2sYxOUoh2TV1tS+G7bHNapPGI79woQJGV6pFH7GH7qnA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/wrangler/node_modules/esbuild-windows-arm64": { + "version": "0.14.51", + "resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.51.tgz", + "integrity": "sha512-JQDqPjuOH7o+BsKMSddMfmVJXrnYZxXDHsoLHc0xgmAZkOOCflRmC43q31pk79F9xuyWY45jDBPolb5ZgGOf9g==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, "node_modules/wrangler/node_modules/nanoid": { "version": "3.3.4", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz", @@ -28238,7 +28798,7 @@ }, "packages/api": { "name": "@web3-storage/api", - "version": "7.11.3", + "version": "7.12.0", "license": "(Apache-2.0 OR MIT)", "dependencies": { "@aws-sdk/client-s3": "^3.53.1", @@ -28450,6 +29010,38 @@ "esbuild-windows-arm64": "0.14.23" } }, + "packages/api/node_modules/esbuild-android-arm64": { + "version": "0.14.23", + "resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.14.23.tgz", + "integrity": "sha512-k9sXem++mINrZty1v4FVt6nC5BQCFG4K2geCIUUqHNlTdFnuvcqsY7prcKZLFhqVC1rbcJAr9VSUGFL/vD4vsw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "packages/api/node_modules/esbuild-darwin-64": { + "version": "0.14.23", + "resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.14.23.tgz", + "integrity": "sha512-lB0XRbtOYYL1tLcYw8BoBaYsFYiR48RPrA0KfA/7RFTr4MV7Bwy/J4+7nLsVnv9FGuQummM3uJ93J3ptaTqFug==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, "packages/api/node_modules/esbuild-darwin-arm64": { "version": "0.14.23", "resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.23.tgz", @@ -28467,6 +29059,230 @@ "node": ">=12" } }, + "packages/api/node_modules/esbuild-freebsd-64": { + "version": "0.14.23", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.23.tgz", + "integrity": "sha512-/1xiTjoLuQ+LlbfjJdKkX45qK/M7ARrbLmyf7x3JhyQGMjcxRYVR6Dw81uH3qlMHwT4cfLW4aEVBhP1aNV7VsA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "packages/api/node_modules/esbuild-freebsd-arm64": { + "version": "0.14.23", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.23.tgz", + "integrity": "sha512-uyPqBU/Zcp6yEAZS4LKj5jEE0q2s4HmlMBIPzbW6cTunZ8cyvjG6YWpIZXb1KK3KTJDe62ltCrk3VzmWHp+iLg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "packages/api/node_modules/esbuild-linux-32": { + "version": "0.14.23", + "resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.14.23.tgz", + "integrity": "sha512-37R/WMkQyUfNhbH7aJrr1uCjDVdnPeTHGeDhZPUNhfoHV0lQuZNCKuNnDvlH/u/nwIYZNdVvz1Igv5rY/zfrzQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "packages/api/node_modules/esbuild-linux-64": { + "version": "0.14.23", + "resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.14.23.tgz", + "integrity": "sha512-H0gztDP60qqr8zoFhAO64waoN5yBXkmYCElFklpd6LPoobtNGNnDe99xOQm28+fuD75YJ7GKHzp/MLCLhw2+vQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "packages/api/node_modules/esbuild-linux-arm": { + "version": "0.14.23", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.14.23.tgz", + "integrity": "sha512-x64CEUxi8+EzOAIpCUeuni0bZfzPw/65r8tC5cy5zOq9dY7ysOi5EVQHnzaxS+1NmV+/RVRpmrzGw1QgY2Xpmw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "packages/api/node_modules/esbuild-linux-arm64": { + "version": "0.14.23", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.23.tgz", + "integrity": "sha512-c4MLOIByNHR55n3KoYf9hYDfBRghMjOiHLaoYLhkQkIabb452RWi+HsNgB41sUpSlOAqfpqKPFNg7VrxL3UX9g==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "packages/api/node_modules/esbuild-linux-mips64le": { + "version": "0.14.23", + "resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.23.tgz", + "integrity": "sha512-kHKyKRIAedYhKug2EJpyJxOUj3VYuamOVA1pY7EimoFPzaF3NeY7e4cFBAISC/Av0/tiV0xlFCt9q0HJ68IBIw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "packages/api/node_modules/esbuild-linux-ppc64le": { + "version": "0.14.23", + "resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.23.tgz", + "integrity": "sha512-7ilAiJEPuJJnJp/LiDO0oJm5ygbBPzhchJJh9HsHZzeqO+3PUzItXi+8PuicY08r0AaaOe25LA7sGJ0MzbfBag==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "packages/api/node_modules/esbuild-netbsd-64": { + "version": "0.14.23", + "resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.23.tgz", + "integrity": "sha512-ovk2EX+3rrO1M2lowJfgMb/JPN1VwVYrx0QPUyudxkxLYrWeBxDKQvc6ffO+kB4QlDyTfdtAURrVzu3JeNdA2g==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "packages/api/node_modules/esbuild-openbsd-64": { + "version": "0.14.23", + "resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.23.tgz", + "integrity": "sha512-uYYNqbVR+i7k8ojP/oIROAHO9lATLN7H2QeXKt2H310Fc8FJj4y3Wce6hx0VgnJ4k1JDrgbbiXM8rbEgQyg8KA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "packages/api/node_modules/esbuild-sunos-64": { + "version": "0.14.23", + "resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.14.23.tgz", + "integrity": "sha512-hAzeBeET0+SbScknPzS2LBY6FVDpgE+CsHSpe6CEoR51PApdn2IB0SyJX7vGelXzlyrnorM4CAsRyb9Qev4h9g==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "packages/api/node_modules/esbuild-windows-32": { + "version": "0.14.23", + "resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.14.23.tgz", + "integrity": "sha512-Kttmi3JnohdaREbk6o9e25kieJR379TsEWF0l39PQVHXq3FR6sFKtVPgY8wk055o6IB+rllrzLnbqOw/UV60EA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "packages/api/node_modules/esbuild-windows-64": { + "version": "0.14.23", + "resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.14.23.tgz", + "integrity": "sha512-JtIT0t8ymkpl6YlmOl6zoSWL5cnCgyLaBdf/SiU/Eg3C13r0NbHZWNT/RDEMKK91Y6t79kTs3vyRcNZbfu5a8g==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "packages/api/node_modules/esbuild-windows-arm64": { + "version": "0.14.23", + "resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.23.tgz", + "integrity": "sha512-cTFaQqT2+ik9e4hePvYtRZQ3pqOvKDVNarzql0VFIzhc0tru/ZgdLoXd6epLiKT+SzoSce6V9YJ+nn6RCn6SHw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, "packages/api/node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -31343,7 +32159,7 @@ }, "packages/website": { "name": "@web3-storage/website", - "version": "2.31.0", + "version": "2.32.0", "dependencies": { "@docsearch/react": "^3.0.0", "@fortawesome/free-brands-svg-icons": "^6.1.2", @@ -42838,7 +43654,7 @@ "@nftstorage/ipfs-cluster": "^5.0.1", "@sentry/cli": "^1.72.1", "@types/mocha": "^9.0.0", - "@types/sinon": "*", + "@types/sinon": "^10.0.13", "@web-std/fetch": "^3.0.2", "@web-std/form-data": "^3.0.0", "@web3-storage/db": "^4.0.0", @@ -42992,6 +43808,20 @@ "esbuild-windows-arm64": "0.14.23" } }, + "esbuild-android-arm64": { + "version": "0.14.23", + "resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.14.23.tgz", + "integrity": "sha512-k9sXem++mINrZty1v4FVt6nC5BQCFG4K2geCIUUqHNlTdFnuvcqsY7prcKZLFhqVC1rbcJAr9VSUGFL/vD4vsw==", + "dev": true, + "optional": true + }, + "esbuild-darwin-64": { + "version": "0.14.23", + "resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.14.23.tgz", + "integrity": "sha512-lB0XRbtOYYL1tLcYw8BoBaYsFYiR48RPrA0KfA/7RFTr4MV7Bwy/J4+7nLsVnv9FGuQummM3uJ93J3ptaTqFug==", + "dev": true, + "optional": true + }, "esbuild-darwin-arm64": { "version": "0.14.23", "resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.23.tgz", @@ -42999,6 +43829,104 @@ "dev": true, "optional": true }, + "esbuild-freebsd-64": { + "version": "0.14.23", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.23.tgz", + "integrity": "sha512-/1xiTjoLuQ+LlbfjJdKkX45qK/M7ARrbLmyf7x3JhyQGMjcxRYVR6Dw81uH3qlMHwT4cfLW4aEVBhP1aNV7VsA==", + "dev": true, + "optional": true + }, + "esbuild-freebsd-arm64": { + "version": "0.14.23", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.23.tgz", + "integrity": "sha512-uyPqBU/Zcp6yEAZS4LKj5jEE0q2s4HmlMBIPzbW6cTunZ8cyvjG6YWpIZXb1KK3KTJDe62ltCrk3VzmWHp+iLg==", + "dev": true, + "optional": true + }, + "esbuild-linux-32": { + "version": "0.14.23", + "resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.14.23.tgz", + "integrity": "sha512-37R/WMkQyUfNhbH7aJrr1uCjDVdnPeTHGeDhZPUNhfoHV0lQuZNCKuNnDvlH/u/nwIYZNdVvz1Igv5rY/zfrzQ==", + "dev": true, + "optional": true + }, + "esbuild-linux-64": { + "version": "0.14.23", + "resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.14.23.tgz", + "integrity": "sha512-H0gztDP60qqr8zoFhAO64waoN5yBXkmYCElFklpd6LPoobtNGNnDe99xOQm28+fuD75YJ7GKHzp/MLCLhw2+vQ==", + "dev": true, + "optional": true + }, + "esbuild-linux-arm": { + "version": "0.14.23", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.14.23.tgz", + "integrity": "sha512-x64CEUxi8+EzOAIpCUeuni0bZfzPw/65r8tC5cy5zOq9dY7ysOi5EVQHnzaxS+1NmV+/RVRpmrzGw1QgY2Xpmw==", + "dev": true, + "optional": true + }, + "esbuild-linux-arm64": { + "version": "0.14.23", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.23.tgz", + "integrity": "sha512-c4MLOIByNHR55n3KoYf9hYDfBRghMjOiHLaoYLhkQkIabb452RWi+HsNgB41sUpSlOAqfpqKPFNg7VrxL3UX9g==", + "dev": true, + "optional": true + }, + "esbuild-linux-mips64le": { + "version": "0.14.23", + "resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.23.tgz", + "integrity": "sha512-kHKyKRIAedYhKug2EJpyJxOUj3VYuamOVA1pY7EimoFPzaF3NeY7e4cFBAISC/Av0/tiV0xlFCt9q0HJ68IBIw==", + "dev": true, + "optional": true + }, + "esbuild-linux-ppc64le": { + "version": "0.14.23", + "resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.23.tgz", + "integrity": "sha512-7ilAiJEPuJJnJp/LiDO0oJm5ygbBPzhchJJh9HsHZzeqO+3PUzItXi+8PuicY08r0AaaOe25LA7sGJ0MzbfBag==", + "dev": true, + "optional": true + }, + "esbuild-netbsd-64": { + "version": "0.14.23", + "resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.23.tgz", + "integrity": "sha512-ovk2EX+3rrO1M2lowJfgMb/JPN1VwVYrx0QPUyudxkxLYrWeBxDKQvc6ffO+kB4QlDyTfdtAURrVzu3JeNdA2g==", + "dev": true, + "optional": true + }, + "esbuild-openbsd-64": { + "version": "0.14.23", + "resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.23.tgz", + "integrity": "sha512-uYYNqbVR+i7k8ojP/oIROAHO9lATLN7H2QeXKt2H310Fc8FJj4y3Wce6hx0VgnJ4k1JDrgbbiXM8rbEgQyg8KA==", + "dev": true, + "optional": true + }, + "esbuild-sunos-64": { + "version": "0.14.23", + "resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.14.23.tgz", + "integrity": "sha512-hAzeBeET0+SbScknPzS2LBY6FVDpgE+CsHSpe6CEoR51PApdn2IB0SyJX7vGelXzlyrnorM4CAsRyb9Qev4h9g==", + "dev": true, + "optional": true + }, + "esbuild-windows-32": { + "version": "0.14.23", + "resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.14.23.tgz", + "integrity": "sha512-Kttmi3JnohdaREbk6o9e25kieJR379TsEWF0l39PQVHXq3FR6sFKtVPgY8wk055o6IB+rllrzLnbqOw/UV60EA==", + "dev": true, + "optional": true + }, + "esbuild-windows-64": { + "version": "0.14.23", + "resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.14.23.tgz", + "integrity": "sha512-JtIT0t8ymkpl6YlmOl6zoSWL5cnCgyLaBdf/SiU/Eg3C13r0NbHZWNT/RDEMKK91Y6t79kTs3vyRcNZbfu5a8g==", + "dev": true, + "optional": true + }, + "esbuild-windows-arm64": { + "version": "0.14.23", + "resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.23.tgz", + "integrity": "sha512-cTFaQqT2+ik9e4hePvYtRZQ3pqOvKDVNarzql0VFIzhc0tru/ZgdLoXd6epLiKT+SzoSce6V9YJ+nn6RCn6SHw==", + "dev": true, + "optional": true + }, "escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -53182,6 +54110,14 @@ "esbuild-windows-arm64": "0.13.14" }, "dependencies": { + "esbuild-darwin-64": { + "version": "0.13.14", + "resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.13.14.tgz", + "integrity": "sha512-YmOhRns6QBNSjpVdTahi/yZ8dscx9ai7a6OY6z5ACgOuQuaQ2Qk2qgJ0/siZ6LgD0gJFMV8UINFV5oky5TFNQQ==", + "dev": true, + "optional": true, + "peer": true + }, "esbuild-darwin-arm64": { "version": "0.13.14", "resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.13.14.tgz", @@ -53192,6 +54128,21 @@ } } }, + "esbuild-android-64": { + "version": "0.14.51", + "resolved": "https://registry.npmjs.org/esbuild-android-64/-/esbuild-android-64-0.14.51.tgz", + "integrity": "sha512-6FOuKTHnC86dtrKDmdSj2CkcKF8PnqkaIXqvgydqfJmqBazCPdw+relrMlhGjkvVdiiGV70rpdnyFmA65ekBCQ==", + "dev": true, + "optional": true + }, + "esbuild-android-arm64": { + "version": "0.13.14", + "resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.13.14.tgz", + "integrity": "sha512-Q+Xhfp827r+ma8/DJgpMRUbDZfefsk13oePFEXEIJ4gxFbNv5+vyiYXYuKm43/+++EJXpnaYmEnu4hAKbAWYbA==", + "dev": true, + "optional": true, + "peer": true + }, "esbuild-darwin-arm64": { "version": "0.14.51", "resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.51.tgz", @@ -53199,6 +54150,132 @@ "dev": true, "optional": true }, + "esbuild-freebsd-64": { + "version": "0.13.14", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.13.14.tgz", + "integrity": "sha512-BKosI3jtvTfnmsCW37B1TyxMUjkRWKqopR0CE9AF2ratdpkxdR24Vpe3gLKNyWiZ7BE96/SO5/YfhbPUzY8wKw==", + "dev": true, + "optional": true, + "peer": true + }, + "esbuild-freebsd-arm64": { + "version": "0.13.14", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.13.14.tgz", + "integrity": "sha512-yd2uh0yf+fWv5114+SYTl4/1oDWtr4nN5Op+PGxAkMqHfYfLjFKpcxwCo/QOS/0NWqPVE8O41IYZlFhbEN2B8Q==", + "dev": true, + "optional": true, + "peer": true + }, + "esbuild-linux-32": { + "version": "0.13.14", + "resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.13.14.tgz", + "integrity": "sha512-a8rOnS1oWSfkkYWXoD2yXNV4BdbDKA7PNVQ1klqkY9SoSApL7io66w5H44mTLsfyw7G6Z2vLlaLI2nz9MMAowA==", + "dev": true, + "optional": true, + "peer": true + }, + "esbuild-linux-64": { + "version": "0.13.14", + "resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.13.14.tgz", + "integrity": "sha512-yPZSoMs9W2MC3Dw+6kflKt5FfQm6Dicex9dGIr1OlHRsn3Hm7yGMUTctlkW53KknnZdOdcdd5upxvbxqymczVQ==", + "dev": true, + "optional": true, + "peer": true + }, + "esbuild-linux-arm": { + "version": "0.13.14", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.13.14.tgz", + "integrity": "sha512-8chZE4pkKRvJ/M/iwsNQ1KqsRg2RyU5eT/x2flNt/f8F2TVrDreR7I0HEeCR50wLla3B1C3wTIOzQBmjuc6uWg==", + "dev": true, + "optional": true, + "peer": true + }, + "esbuild-linux-arm64": { + "version": "0.13.14", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.13.14.tgz", + "integrity": "sha512-Lvo391ln9PzC334e+jJ2S0Rt0cxP47eoH5gFyv/E8HhOnEJTvm7A+RRnMjjHnejELacTTfYgFGQYPjLsi/jObQ==", + "dev": true, + "optional": true, + "peer": true + }, + "esbuild-linux-mips64le": { + "version": "0.13.14", + "resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.13.14.tgz", + "integrity": "sha512-MZhgxbmrWbpY3TOE029O6l5tokG9+Yoj2hW7vdit/d/VnmneqeGrSHADuDL6qXM8L5jaCiaivb4VhsyVCpdAbQ==", + "dev": true, + "optional": true, + "peer": true + }, + "esbuild-linux-ppc64le": { + "version": "0.13.14", + "resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.13.14.tgz", + "integrity": "sha512-un7KMwS7fX1Un6BjfSZxTT8L5cV/8Uf4SAhM7WYy2XF8o8TI+uRxxD03svZnRNIPsN2J5cl6qV4n7Iwz+yhhVw==", + "dev": true, + "optional": true, + "peer": true + }, + "esbuild-linux-riscv64": { + "version": "0.14.23", + "resolved": "https://registry.npmjs.org/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.14.23.tgz", + "integrity": "sha512-fbL3ggK2wY0D8I5raPIMPhpCvODFE+Bhb5QGtNP3r5aUsRR6TQV+ZBXIaw84iyvKC8vlXiA4fWLGhghAd/h/Zg==", + "dev": true, + "optional": true + }, + "esbuild-linux-s390x": { + "version": "0.14.23", + "resolved": "https://registry.npmjs.org/esbuild-linux-s390x/-/esbuild-linux-s390x-0.14.23.tgz", + "integrity": "sha512-GHMDCyfy7+FaNSO8RJ8KCFsnax8fLUsOrj9q5Gi2JmZMY0Zhp75keb5abTFCq2/Oy6KVcT0Dcbyo/bFb4rIFJA==", + "dev": true, + "optional": true + }, + "esbuild-netbsd-64": { + "version": "0.13.14", + "resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.13.14.tgz", + "integrity": "sha512-5ekKx/YbOmmlTeNxBjh38Uh5TGn5C4uyqN17i67k18pS3J+U2hTVD7rCxcFcRS1AjNWumkVL3jWqYXadFwMS0Q==", + "dev": true, + "optional": true, + "peer": true + }, + "esbuild-openbsd-64": { + "version": "0.13.14", + "resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.13.14.tgz", + "integrity": "sha512-9bzvwewHjct2Cv5XcVoE1yW5YTW12Sk838EYfA46abgnhxGoFSD1mFcaztp5HHC43AsF+hQxbSFG/RilONARUA==", + "dev": true, + "optional": true, + "peer": true + }, + "esbuild-sunos-64": { + "version": "0.13.14", + "resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.13.14.tgz", + "integrity": "sha512-mjMrZB76M6FmoiTvj/RGWilrioR7gVwtFBRVugr9qLarXMIU1W/pQx+ieEOtflrW61xo8w1fcxyHsVVGRvoQ0w==", + "dev": true, + "optional": true, + "peer": true + }, + "esbuild-windows-32": { + "version": "0.13.14", + "resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.13.14.tgz", + "integrity": "sha512-GZa6mrx2rgfbH/5uHg0Rdw50TuOKbdoKCpEBitzmG5tsXBdce+cOL+iFO5joZc6fDVCLW3Y6tjxmSXRk/v20Hg==", + "dev": true, + "optional": true, + "peer": true + }, + "esbuild-windows-64": { + "version": "0.13.14", + "resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.13.14.tgz", + "integrity": "sha512-Lsgqah24bT7ClHjLp/Pj3A9wxjhIAJyWQcrOV4jqXAFikmrp2CspA8IkJgw7HFjx6QrJuhpcKVbCAe/xw0i2yw==", + "dev": true, + "optional": true, + "peer": true + }, + "esbuild-windows-arm64": { + "version": "0.13.14", + "resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.13.14.tgz", + "integrity": "sha512-KP8FHVlWGhM7nzYtURsGnskXb/cBCPTfj0gOKfjKq2tHtYnhDZywsUG57nk7TKhhK0fL11LcejHG3LRW9RF/9A==", + "dev": true, + "optional": true, + "peer": true + }, "escalade": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", @@ -64888,6 +65965,132 @@ "esbuild-windows-arm64": "0.14.51" } }, + "esbuild-android-arm64": { + "version": "0.14.51", + "resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.14.51.tgz", + "integrity": "sha512-vBtp//5VVkZWmYYvHsqBRCMMi1MzKuMIn5XDScmnykMTu9+TD9v0NMEDqQxvtFToeYmojdo5UCV2vzMQWJcJ4A==", + "dev": true, + "optional": true + }, + "esbuild-darwin-64": { + "version": "0.14.51", + "resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.14.51.tgz", + "integrity": "sha512-YFmXPIOvuagDcwCejMRtCDjgPfnDu+bNeh5FU2Ryi68ADDVlWEpbtpAbrtf/lvFTWPexbgyKgzppNgsmLPr8PA==", + "dev": true, + "optional": true + }, + "esbuild-freebsd-64": { + "version": "0.14.51", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.51.tgz", + "integrity": "sha512-cLEI/aXjb6vo5O2Y8rvVSQ7smgLldwYY5xMxqh/dQGfWO+R1NJOFsiax3IS4Ng300SVp7Gz3czxT6d6qf2cw0g==", + "dev": true, + "optional": true + }, + "esbuild-freebsd-arm64": { + "version": "0.14.51", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.51.tgz", + "integrity": "sha512-TcWVw/rCL2F+jUgRkgLa3qltd5gzKjIMGhkVybkjk6PJadYInPtgtUBp1/hG+mxyigaT7ib+od1Xb84b+L+1Mg==", + "dev": true, + "optional": true + }, + "esbuild-linux-32": { + "version": "0.14.51", + "resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.14.51.tgz", + "integrity": "sha512-RFqpyC5ChyWrjx8Xj2K0EC1aN0A37H6OJfmUXIASEqJoHcntuV3j2Efr9RNmUhMfNE6yEj2VpYuDteZLGDMr0w==", + "dev": true, + "optional": true + }, + "esbuild-linux-64": { + "version": "0.14.51", + "resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.14.51.tgz", + "integrity": "sha512-dxjhrqo5i7Rq6DXwz5v+MEHVs9VNFItJmHBe1CxROWNf4miOGoQhqSG8StStbDkQ1Mtobg6ng+4fwByOhoQoeA==", + "dev": true, + "optional": true + }, + "esbuild-linux-arm": { + "version": "0.14.51", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.14.51.tgz", + "integrity": "sha512-LsJynDxYF6Neg7ZC7748yweCDD+N8ByCv22/7IAZglIEniEkqdF4HCaa49JNDLw1UQGlYuhOB8ZT/MmcSWzcWg==", + "dev": true, + "optional": true + }, + "esbuild-linux-arm64": { + "version": "0.14.51", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.51.tgz", + "integrity": "sha512-D9rFxGutoqQX3xJPxqd6o+kvYKeIbM0ifW2y0bgKk5HPgQQOo2k9/2Vpto3ybGYaFPCE5qTGtqQta9PoP6ZEzw==", + "dev": true, + "optional": true + }, + "esbuild-linux-mips64le": { + "version": "0.14.51", + "resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.51.tgz", + "integrity": "sha512-vS54wQjy4IinLSlb5EIlLoln8buh1yDgliP4CuEHumrPk4PvvP4kTRIG4SzMXm6t19N0rIfT4bNdAxzJLg2k6A==", + "dev": true, + "optional": true + }, + "esbuild-linux-ppc64le": { + "version": "0.14.51", + "resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.51.tgz", + "integrity": "sha512-xcdd62Y3VfGoyphNP/aIV9LP+RzFw5M5Z7ja+zdpQHHvokJM7d0rlDRMN+iSSwvUymQkqZO+G/xjb4/75du8BQ==", + "dev": true, + "optional": true + }, + "esbuild-linux-riscv64": { + "version": "0.14.51", + "resolved": "https://registry.npmjs.org/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.14.51.tgz", + "integrity": "sha512-syXHGak9wkAnFz0gMmRBoy44JV0rp4kVCEA36P5MCeZcxFq8+fllBC2t6sKI23w3qd8Vwo9pTADCgjTSf3L3rA==", + "dev": true, + "optional": true + }, + "esbuild-linux-s390x": { + "version": "0.14.51", + "resolved": "https://registry.npmjs.org/esbuild-linux-s390x/-/esbuild-linux-s390x-0.14.51.tgz", + "integrity": "sha512-kFAJY3dv+Wq8o28K/C7xkZk/X34rgTwhknSsElIqoEo8armCOjMJ6NsMxm48KaWY2h2RUYGtQmr+RGuUPKBhyw==", + "dev": true, + "optional": true + }, + "esbuild-netbsd-64": { + "version": "0.14.51", + "resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.51.tgz", + "integrity": "sha512-ZZBI7qrR1FevdPBVHz/1GSk1x5GDL/iy42Zy8+neEm/HA7ma+hH/bwPEjeHXKWUDvM36CZpSL/fn1/y9/Hb+1A==", + "dev": true, + "optional": true + }, + "esbuild-openbsd-64": { + "version": "0.14.51", + "resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.51.tgz", + "integrity": "sha512-7R1/p39M+LSVQVgDVlcY1KKm6kFKjERSX1lipMG51NPcspJD1tmiZSmmBXoY5jhHIu6JL1QkFDTx94gMYK6vfA==", + "dev": true, + "optional": true + }, + "esbuild-sunos-64": { + "version": "0.14.51", + "resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.14.51.tgz", + "integrity": "sha512-HoHaCswHxLEYN8eBTtyO0bFEWvA3Kdb++hSQ/lLG7TyKF69TeSG0RNoBRAs45x/oCeWaTDntEZlYwAfQlhEtJA==", + "dev": true, + "optional": true + }, + "esbuild-windows-32": { + "version": "0.14.51", + "resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.14.51.tgz", + "integrity": "sha512-4rtwSAM35A07CBt1/X8RWieDj3ZUHQqUOaEo5ZBs69rt5WAFjP4aqCIobdqOy4FdhYw1yF8Z0xFBTyc9lgPtEg==", + "dev": true, + "optional": true + }, + "esbuild-windows-64": { + "version": "0.14.51", + "resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.14.51.tgz", + "integrity": "sha512-HoN/5HGRXJpWODprGCgKbdMvrC3A2gqvzewu2eECRw2sYxOUoh2TV1tS+G7bHNapPGI79woQJGV6pFH7GH7qnA==", + "dev": true, + "optional": true + }, + "esbuild-windows-arm64": { + "version": "0.14.51", + "resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.51.tgz", + "integrity": "sha512-JQDqPjuOH7o+BsKMSddMfmVJXrnYZxXDHsoLHc0xgmAZkOOCflRmC43q31pk79F9xuyWY45jDBPolb5ZgGOf9g==", + "dev": true, + "optional": true + }, "nanoid": { "version": "3.3.4", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz", diff --git a/packages/api/src/index.js b/packages/api/src/index.js index ed65d8c6b6..3bbadc188e 100644 --- a/packages/api/src/index.js +++ b/packages/api/src/index.js @@ -21,7 +21,8 @@ import { userUploadsDelete, userUploadGet, userUploadsGet, - userUploadsRename + userUploadsRename, + userPinDelete } from './user.js' import { pinDelete, pinGet, pinPost, pinsGet } from './pins.js' import { blogSubscriptionCreate } from './blog.js' @@ -109,6 +110,7 @@ router.delete('/user/tokens/:id', auth['👤🗑️'](userTokensDelete)) router.get('/user/account', auth['👤'](userAccountGet)) router.get('/user/info', auth['👤'](userInfoGet)) router.get('/user/pins', auth['📌⚠️'](userPinsGet)) +router.delete('/user/pins/:requestId', auth['👤🗑️'](userPinDelete)) router.get('/user/payment', auth['👤'](userPaymentGet)) router.put('/user/payment', auth['👤'](userPaymentPut)) diff --git a/packages/api/src/pins.js b/packages/api/src/pins.js index 41c48acbf9..e70a2268a0 100644 --- a/packages/api/src/pins.js +++ b/packages/api/src/pins.js @@ -226,7 +226,7 @@ export async function pinDelete (request, env, ctx) { try { // Update deleted_at (and updated_at) timestamp for the pin request. - await env.db.deletePsaPinRequest(requestId, authToken._id) + await env.db.deletePsaPinRequest(requestId, [authToken._id]) } catch (e) { console.error(e) throw new PSAErrorResourceNotFound() @@ -259,7 +259,7 @@ async function replacePin (normalizedCid, newPinData, requestId, authTokenId, en } try { - await env.db.deletePsaPinRequest(requestId, authTokenId) + await env.db.deletePsaPinRequest(requestId, [authTokenId]) } catch (e) { console.error(e) throw new PSAErrorDB() diff --git a/packages/api/src/user.js b/packages/api/src/user.js index adc3a6cef5..709371f854 100644 --- a/packages/api/src/user.js +++ b/packages/api/src/user.js @@ -1,7 +1,7 @@ import * as JWT from './utils/jwt.js' import { JSONResponse, notFound } from './utils/json-response.js' import { JWT_ISSUER } from './constants.js' -import { HTTPError, RangeNotSatisfiableError } from './errors.js' +import { HTTPError, PSAErrorInvalidData, PSAErrorRequiredData, PSAErrorResourceNotFound, RangeNotSatisfiableError } from './errors.js' import { getTagValue, hasPendingTagProposal, hasTag } from './utils/tags.js' import { NO_READ_OR_WRITE, @@ -11,7 +11,7 @@ import { } from './maintenance.js' import { pagination } from './utils/pagination.js' import { toPinStatusResponse } from './pins.js' -import { validateSearchParams } from './utils/psa.js' +import { INVALID_REQUEST_ID, REQUIRED_REQUEST_ID, validateSearchParams } from './utils/psa.js' import { magicLinkBypassForE2ETestingInTestmode } from './magic.link.js' import { CustomerNotFound, getPaymentSettings, initializeBillingForNewUser, isStoragePriceName, savePaymentSettings } from './utils/billing.js' @@ -330,7 +330,6 @@ export async function userRequestPost (request, env) { * @param {import('./env').Env} env */ export async function userTokensGet (request, env) { - // @ts-ignore const tokens = await env.db.listKeys(request.auth.user._id) return new JSONResponse(tokens) @@ -529,8 +528,7 @@ export async function userPinsGet (request, env) { throw psaParams.error } - // @ts-ignore - const tokens = (await env.db.listKeys(request.auth.user._id)).map((key) => key._id) + const tokens = (await env.db.listKeys(request.auth.user._id, { includeDeleted: true })).map((key) => key._id) let pinRequests @@ -584,6 +582,42 @@ export async function userPinsGet (request, env) { }, { headers }) } +/** + * List a user's pins regardless of the token used. + * As we don't want to scope the Pinning Service API to users + * we need a new endpoint as an umbrella. + * + * @param {import('./user').AuthenticatedRequest} request + * @param {import('./env').Env} env + * @param {import('./index').Ctx} ctx + * @return {Promise} + */ +export async function userPinDelete (request, env, ctx) { + // @ts-ignore + const requestId = request.params?.requestId + + if (!requestId) { + throw new PSAErrorRequiredData(REQUIRED_REQUEST_ID) + } + + if (typeof requestId !== 'string') { + throw new PSAErrorInvalidData(INVALID_REQUEST_ID) + } + + // TODO: Improve me, it is un-optimal to get the tokens in a separate request to the db. + const tokens = (await env.db.listKeys(request.auth.user._id, { includeDeleted: true })).map((key) => key._id) + + try { + // Update deleted_at (and updated_at) timestamp for the pin request. + await env.db.deletePsaPinRequest(requestId, tokens) + } catch (e) { + console.error(e) + throw new PSAErrorResourceNotFound() + } + + return new JSONResponse({}, { status: 202 }) +} + /** * @param {string} userProposalForm * @param {string} tagName diff --git a/packages/api/test/fixtures/init-data.sql b/packages/api/test/fixtures/init-data.sql index ad90856b4d..e5c9c70ecf 100644 --- a/packages/api/test/fixtures/init-data.sql +++ b/packages/api/test/fixtures/init-data.sql @@ -70,8 +70,8 @@ VALUES (6, 'test-pinning-and-restriction', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ (7, 'test-restriction', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ0ZXN0LXJlc3RyaWN0aW9uIiwiaXNzIjoid2ViMy1zdG9yYWdlIiwiaWF0IjoxNjMzOTU3Mzg5ODcyLCJuYW1lIjoidGVzdC1yZXN0cmljdGlvbiJ9.uAklG7dHOxRD85c564RBcBqeFGUGNBper7VLaXBGnFg', 10000000000000007); -- Used to test pinning from an existing user with a different token -INSERT INTO auth_key (id, name, secret, user_id) -VALUES (8, 'test-pinning-b', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ0ZXN0LXBpbm5pbmctYiIsImlzcyI6IndlYjMtc3RvcmFnZSIsImlhdCI6MTYzMzk1NzM4OTg3MiwibmFtZSI6InRlc3QtcGlubmluZy1iIn0.B2qeE5mHW93saJx-Nu8p8Io_z7_3VRaHoBAVxPbCP5c', 10000000000000004); +INSERT INTO auth_key (id, name, secret, user_id, deleted_at) +VALUES (8, 'test-pinning-b', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ0ZXN0LXBpbm5pbmctYiIsImlzcyI6IndlYjMtc3RvcmFnZSIsImlhdCI6MTYzMzk1NzM4OTg3MiwibmFtZSI6InRlc3QtcGlubmluZy1iIn0.B2qeE5mHW93saJx-Nu8p8Io_z7_3VRaHoBAVxPbCP5c', 10000000000000004, '2021-07-14T19:27:14.934572+00:00'); -- Reset the primary key next value SELECT pg_catalog.setval(pg_get_serial_sequence('auth_key', 'id'), MAX(id)) FROM auth_key; diff --git a/packages/db/db-client-types.ts b/packages/db/db-client-types.ts index 7201be5e0c..1b98b4c0c3 100644 --- a/packages/db/db-client-types.ts +++ b/packages/db/db-client-types.ts @@ -354,4 +354,7 @@ export type LogEmailSentInput = { messageId: string } +export type ListKeysOptions = { + includeDeleted: boolean +} export type AgreementKind = 'web3.storage-tos-v1' diff --git a/packages/db/index.d.ts b/packages/db/index.d.ts index 591e456075..c4b8a5dddb 100644 --- a/packages/db/index.d.ts +++ b/packages/db/index.d.ts @@ -31,6 +31,7 @@ import type { EmailSentInput, LogEmailSentInput, GetUserOptions, + ListKeysOptions, AgreementKind, } from './db-client-types' @@ -61,12 +62,12 @@ export class DBClient { getDealsForCids (cids: string[]): Promise> createKey (key: CreateAuthKeyInput): Promise getKey (issuer: string, secret: string): Promise - listKeys (userId: number): Promise> + listKeys (userId: string, opts?: ListKeysOptions): Promise> createPsaPinRequest (pinRequest: PsaPinRequestUpsertInput): Promise getPsaPinRequest (authKey: string, pinRequestId: string) : Promise listPsaPinRequests (authKey: string, opts?: ListPsaPinRequestOptions ) : Promise - deletePsaPinRequest (pinRequestId: string, authKey: string) : Promise - deleteKey (id: number): Promise + deletePsaPinRequest (pinRequestId: string, authKey: Array) : Promise + deleteKey (userId: number, keyId: number): Promise query(document: RequestDocument, variables: V): Promise createUserTag(userId: string, tag: UserTagInput): Promise getUserTags(userId: string): Promise diff --git a/packages/db/index.js b/packages/db/index.js index 826560e3f1..c6eb477c01 100644 --- a/packages/db/index.js +++ b/packages/db/index.js @@ -1014,13 +1014,15 @@ export class DBClient { /** * List auth keys of a given user. * - * @param {number} userId + * @param {string} userId + * @param {import('./db-client-types').ListKeysOptions} opts * @return {Promise>} */ - async listKeys (userId) { + async listKeys (userId, { includeDeleted } = { includeDeleted: false }) { /** @type {{ error: PostgrestError, data: Array }} */ const { data, error } = await this._client.rpc('user_auth_keys_list', { - query_user_id: userId + query_user_id: userId, + include_deleted: includeDeleted }) if (error) { @@ -1263,9 +1265,9 @@ export class DBClient { * Delete a user PA pin request. * * @param {number} requestId - * @param {string} authKey + * @param {string[]} authKeys */ - async deletePsaPinRequest (requestId, authKey) { + async deletePsaPinRequest (requestId, authKeys) { const date = new Date().toISOString() /** @type {{ data: import('./db-client-types').PsaPinRequestItem, error: PostgrestError }} */ const { data, error } = await this._client @@ -1274,7 +1276,8 @@ export class DBClient { deleted_at: date, updated_at: date }) - .match({ auth_key_id: authKey, id: requestId }) + .match({ id: requestId }) + .in('auth_key_id', authKeys) .filter('deleted_at', 'is', null) .single() diff --git a/packages/db/postgres/functions.sql b/packages/db/postgres/functions.sql index f91b40dcc5..337a1cd438 100644 --- a/packages/db/postgres/functions.sql +++ b/packages/db/postgres/functions.sql @@ -376,7 +376,7 @@ BEGIN END $$; -CREATE OR REPLACE FUNCTION user_auth_keys_list(query_user_id BIGINT) +CREATE OR REPLACE FUNCTION user_auth_keys_list(query_user_id BIGINT, include_deleted BOOLEAN default false) RETURNS TABLE ( "id" text, @@ -394,7 +394,8 @@ SELECT (ak.id)::TEXT AS id, ak.inserted_at AS created, EXISTS(SELECT 42 FROM upload u WHERE u.auth_key_id = ak.id) AS has_uploads FROM auth_key ak - WHERE ak.user_id = query_user_id AND ak.deleted_at IS NULL + WHERE ak.user_id = query_user_id AND + (include_deleted OR ak.deleted_at IS NULL) $$; CREATE OR REPLACE FUNCTION find_deals_by_content_cids(cids text[]) diff --git a/packages/db/postgres/migrations/028-update-list-keys.sql b/packages/db/postgres/migrations/028-update-list-keys.sql new file mode 100644 index 0000000000..56f8c5abdf --- /dev/null +++ b/packages/db/postgres/migrations/028-update-list-keys.sql @@ -0,0 +1,23 @@ +DROP FUNCTION IF EXISTS user_auth_keys_list; + +CREATE OR REPLACE FUNCTION user_auth_keys_list(query_user_id BIGINT, include_deleted BOOLEAN default false) + RETURNS TABLE + ( + "id" text, + "name" text, + "secret" text, + "created" timestamptz, + "has_uploads" boolean + ) + LANGUAGE sql +AS +$$ +SELECT (ak.id)::TEXT AS id, + ak.name AS name, + ak.secret AS secret, + ak.inserted_at AS created, + EXISTS(SELECT 42 FROM upload u WHERE u.auth_key_id = ak.id) AS has_uploads + FROM auth_key ak + WHERE ak.user_id = query_user_id AND + (include_deleted OR ak.deleted_at IS NULL) +$$; diff --git a/packages/db/postgres/pg-rest-api-types.d.ts b/packages/db/postgres/pg-rest-api-types.d.ts index ec99da6693..162fb2e734 100644 --- a/packages/db/postgres/pg-rest-api-types.d.ts +++ b/packages/db/postgres/pg-rest-api-types.d.ts @@ -1592,23 +1592,13 @@ export interface paths { }; }; }; - "/rpc/upsert_user": { + "/rpc/upsert_pins": { post: { parameters: { body: { args: { - /** Format: text */ - _github: string; - /** Format: text */ - _email: string; - /** Format: text */ - _name: string; - /** Format: text */ - _picture: string; - /** Format: text */ - _issuer: string; - /** Format: text */ - _public_address: string; + /** Format: json */ + data: string; }; }; header: { @@ -1622,14 +1612,11 @@ export interface paths { }; }; }; - "/rpc/user_auth_keys_list": { + "/rpc/uuid_ns_url": { post: { parameters: { body: { - args: { - /** Format: bigint */ - query_user_id: number; - }; + args: { [key: string]: unknown }; }; header: { /** Preference */ @@ -1642,14 +1629,11 @@ export interface paths { }; }; }; - "/rpc/create_psa_pin_request": { + "/rpc/pgrst_watch": { post: { parameters: { body: { - args: { - /** Format: json */ - data: string; - }; + args: { [key: string]: unknown }; }; header: { /** Preference */ @@ -1662,14 +1646,11 @@ export interface paths { }; }; }; - "/rpc/user_used_storage": { + "/rpc/uuid_nil": { post: { parameters: { body: { - args: { - /** Format: bigint */ - query_user_id: number; - }; + args: { [key: string]: unknown }; }; header: { /** Preference */ @@ -1682,14 +1663,11 @@ export interface paths { }; }; }; - "/rpc/json_arr_to_json_element_array": { + "/rpc/uuid_generate_v1": { post: { parameters: { body: { - args: { - /** Format: json */ - _json: string; - }; + args: { [key: string]: unknown }; }; header: { /** Preference */ @@ -1702,7 +1680,7 @@ export interface paths { }; }; }; - "/rpc/create_key": { + "/rpc/create_content": { post: { parameters: { body: { @@ -1722,13 +1700,30 @@ export interface paths { }; }; }; - "/rpc/json_arr_to_text_arr": { + "/rpc/uuid_ns_oid": { + post: { + parameters: { + body: { + args: { [key: string]: unknown }; + }; + header: { + /** Preference */ + Prefer?: parameters["preferParams"]; + }; + }; + responses: { + /** OK */ + 200: unknown; + }; + }; + }; + "/rpc/user_uploaded_storage": { post: { parameters: { body: { args: { - /** Format: json */ - _json: string; + /** Format: bigint */ + query_user_id: number; }; }; header: { @@ -1742,11 +1737,20 @@ export interface paths { }; }; }; - "/rpc/uuid_generate_v1mc": { + "/rpc/users_by_storage_used": { post: { parameters: { body: { - args: { [key: string]: unknown }; + args: { + /** Format: integer */ + to_percent?: number; + /** Format: bigint */ + user_id_gt?: number; + /** Format: integer */ + from_percent: number; + /** Format: bigint */ + user_id_lte?: number; + }; }; header: { /** Preference */ @@ -1759,11 +1763,16 @@ export interface paths { }; }; }; - "/rpc/uuid_generate_v1": { + "/rpc/user_auth_keys_list": { post: { parameters: { body: { - args: { [key: string]: unknown }; + args: { + /** Format: boolean */ + include_deleted?: boolean; + /** Format: bigint */ + query_user_id: number; + }; }; header: { /** Preference */ @@ -1776,11 +1785,14 @@ export interface paths { }; }; }; - "/rpc/uuid_generate_v4": { + "/rpc/find_deals_by_content_cids": { post: { parameters: { body: { - args: { [key: string]: unknown }; + args: { + /** Format: text[] */ + cids: string; + }; }; header: { /** Preference */ @@ -1793,13 +1805,15 @@ export interface paths { }; }; }; - "/rpc/create_upload": { + "/rpc/uuid_generate_v5": { post: { parameters: { body: { args: { - /** Format: json */ - data: string; + /** Format: uuid */ + namespace: string; + /** Format: text */ + name: string; }; }; header: { @@ -1833,14 +1847,11 @@ export interface paths { }; }; }; - "/rpc/find_deals_by_content_cids": { + "/rpc/uuid_ns_x500": { post: { parameters: { body: { - args: { - /** Format: text[] */ - cids: string; - }; + args: { [key: string]: unknown }; }; header: { /** Preference */ @@ -1853,19 +1864,13 @@ export interface paths { }; }; }; - "/rpc/users_by_storage_used": { + "/rpc/json_arr_to_text_arr": { post: { parameters: { body: { args: { - /** Format: integer */ - to_percent?: number; - /** Format: bigint */ - user_id_gt?: number; - /** Format: integer */ - from_percent: number; - /** Format: bigint */ - user_id_lte?: number; + /** Format: json */ + _json: string; }; }; header: { @@ -1879,11 +1884,16 @@ export interface paths { }; }; }; - "/rpc/uuid_ns_url": { + "/rpc/uuid_generate_v3": { post: { parameters: { body: { - args: { [key: string]: unknown }; + args: { + /** Format: uuid */ + namespace: string; + /** Format: text */ + name: string; + }; }; header: { /** Preference */ @@ -1896,15 +1906,23 @@ export interface paths { }; }; }; - "/rpc/uuid_generate_v5": { + "/rpc/upsert_user": { post: { parameters: { body: { args: { /** Format: text */ - name: string; - /** Format: uuid */ - namespace: string; + _issuer: string; + /** Format: text */ + _github: string; + /** Format: text */ + _name: string; + /** Format: text */ + _email: string; + /** Format: text */ + _public_address: string; + /** Format: text */ + _picture: string; }; }; header: { @@ -1918,7 +1936,7 @@ export interface paths { }; }; }; - "/rpc/create_content": { + "/rpc/create_key": { post: { parameters: { body: { @@ -1938,7 +1956,7 @@ export interface paths { }; }; }; - "/rpc/uuid_ns_oid": { + "/rpc/uuid_generate_v4": { post: { parameters: { body: { @@ -1955,7 +1973,7 @@ export interface paths { }; }; }; - "/rpc/pgrst_watch": { + "/rpc/uuid_ns_dns": { post: { parameters: { body: { @@ -1972,11 +1990,14 @@ export interface paths { }; }; }; - "/rpc/uuid_ns_x500": { + "/rpc/json_arr_to_json_element_array": { post: { parameters: { body: { - args: { [key: string]: unknown }; + args: { + /** Format: json */ + _json: string; + }; }; header: { /** Preference */ @@ -1989,11 +2010,14 @@ export interface paths { }; }; }; - "/rpc/uuid_nil": { + "/rpc/user_psa_storage": { post: { parameters: { body: { - args: { [key: string]: unknown }; + args: { + /** Format: bigint */ + query_user_id: number; + }; }; header: { /** Preference */ @@ -2006,11 +2030,14 @@ export interface paths { }; }; }; - "/rpc/uuid_ns_dns": { + "/rpc/create_psa_pin_request": { post: { parameters: { body: { - args: { [key: string]: unknown }; + args: { + /** Format: json */ + data: string; + }; }; header: { /** Preference */ @@ -2023,13 +2050,13 @@ export interface paths { }; }; }; - "/rpc/upsert_pins": { + "/rpc/user_used_storage": { post: { parameters: { body: { args: { - /** Format: json */ - data: string; + /** Format: bigint */ + query_user_id: number; }; }; header: { @@ -2043,15 +2070,30 @@ export interface paths { }; }; }; - "/rpc/uuid_generate_v3": { + "/rpc/uuid_generate_v1mc": { + post: { + parameters: { + body: { + args: { [key: string]: unknown }; + }; + header: { + /** Preference */ + Prefer?: parameters["preferParams"]; + }; + }; + responses: { + /** OK */ + 200: unknown; + }; + }; + }; + "/rpc/create_upload": { post: { parameters: { body: { args: { - /** Format: text */ - name: string; - /** Format: uuid */ - namespace: string; + /** Format: json */ + data: string; }; }; header: { diff --git a/packages/db/test/pinning.spec.js b/packages/db/test/pinning.spec.js index 17e63f0ee3..cac85f9549 100644 --- a/packages/db/test/pinning.spec.js +++ b/packages/db/test/pinning.spec.js @@ -496,7 +496,7 @@ describe('Pin Request', () => { it('unlists deleted pins', async () => { const { results: prs } = await client.listPsaPinRequests(authKeyPinList, { limit: 20 }) assert.strictEqual(prs.length, totalPinned) - await client.deletePsaPinRequest(createdPinningRequests[10]._id, authKeyPinList) + await client.deletePsaPinRequest(createdPinningRequests[10]._id, [authKeyPinList]) const { results: res } = await client.listPsaPinRequests(authKeyPinList, { limit: 20 }) assert.strictEqual(res.length, totalPinned - 1) }) @@ -504,18 +504,18 @@ describe('Pin Request', () => { describe('Delete Pin', () => { it('throws if the request id does not exist', async () => { - assert.rejects(client.deletePsaPinRequest(1000, authKey)) + assert.rejects(client.deletePsaPinRequest(1000, [authKey])) }) it('throws if the auth key does not belong to the pin request', async () => { - assert.rejects(client.deletePsaPinRequest(aPinRequestOutput._id, 'fakeAuth')) + assert.rejects(client.deletePsaPinRequest(aPinRequestOutput._id, ['fakeAuth'])) }) it('returns the id of the deleted pin request', async () => { const aPinRequestOutputId = aPinRequestOutput._id const pinRequest = await client.getPsaPinRequest(authKey, aPinRequestOutputId) assert.ok(!pinRequest.deleted, 'is null') - const deletedPinRequest = await client.deletePsaPinRequest(aPinRequestOutputId, authKey) + const deletedPinRequest = await client.deletePsaPinRequest(aPinRequestOutputId, [authKey]) assert.ok(deletedPinRequest) assert.equal(deletedPinRequest._id, pinRequest._id) }) @@ -525,7 +525,7 @@ describe('Pin Request', () => { }) it('cannot delete a pin request which is already deleted', async () => { - assert.rejects(client.deletePsaPinRequest(aPinRequestOutput._id, authKey)) + assert.rejects(client.deletePsaPinRequest(aPinRequestOutput._id, [authKey])) }) }) }) diff --git a/packages/db/test/user.spec.js b/packages/db/test/user.spec.js index d5674d8fab..a7c70e492c 100644 --- a/packages/db/test/user.spec.js +++ b/packages/db/test/user.spec.js @@ -153,6 +153,68 @@ describe('user operations', () => { assert(error, 'should fail to delete auth key again') }) + it('does not list deleted keys by default', async () => { + const name = 'test-key-name-2' + const secret = 'test-secret' + const authKey1 = await client.createKey({ + name, + secret, + user: user._id + }) + + const authKey2 = await client.createKey({ + name, + secret, + user: user._id + }) + + const authKey3 = await client.createKey({ + name, + secret, + user: user._id + }) + + const notDeletedKeys = [authKey1._id, authKey2._id] + + await client.deleteKey(user._id, authKey3._id) + + const keys = await client.listKeys(user._id) + + assert.strictEqual(keys.length, 2, 'should list only not deleted by default') + assert(keys.every(item => notDeletedKeys.includes(item._id))) + }) + + it('lists deleted keys if requested', async () => { + const name = 'test-key-name-2' + const secret = 'test-secret' + const authKey1 = await client.createKey({ + name, + secret, + user: user._id + }) + + const authKey2 = await client.createKey({ + name, + secret, + user: user._id + }) + + const authKey3 = await client.createKey({ + name, + secret, + user: user._id + }) + + const notDeletedKeys = [authKey1._id, authKey2._id, authKey3._id] + + await client.deleteKey(user._id, authKey3._id) + + const keys = await client.listKeys(user._id, { includeDeleted: true }) + + assert.strictEqual(keys.length, 3, 'should list only not deleted by default') + assert(keys.every(item => notDeletedKeys.includes(item._id))) + }) + it('can track user used storage and has uploads', async () => { const authKey = await client.createKey({ name: 'test-key-name-3', diff --git a/packages/website/lib/api.js b/packages/website/lib/api.js index 005817ab79..c4f107c842 100644 --- a/packages/website/lib/api.js +++ b/packages/website/lib/api.js @@ -282,17 +282,13 @@ export async function getPinRequests({ status, size, page }) { * @param {string} requestid */ export async function deletePinRequest(requestid) { - const tokens = await getTokens(); - if (!tokens[0]) { - throw new Error('missing API token'); - } - const res = await fetch(`${API}/pins/${encodeURIComponent(requestid)}`, { + const res = await fetch(`${API}/user/pins/${encodeURIComponent(requestid)}`, { method: 'DELETE', headers: { 'Content-Type': 'application/json', - Authorization: 'Bearer ' + tokens[0].secret, + Authorization: 'Bearer ' + (await getToken()), }, - }); + }) if (!res.ok) { throw new Error(`failed to delete pin request: ${await res.text()}`); From bf8201f27ea4f5a6f7c76ac11cf179a7c09c0876 Mon Sep 17 00:00:00 2001 From: Paolo Chillari Date: Wed, 30 Nov 2022 16:02:28 +0000 Subject: [PATCH 005/107] fix: update key list function migration number (#2138) --- .../{028-update-list-keys.sql => 031-update-list-keys.sql} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename packages/db/postgres/migrations/{028-update-list-keys.sql => 031-update-list-keys.sql} (100%) diff --git a/packages/db/postgres/migrations/028-update-list-keys.sql b/packages/db/postgres/migrations/031-update-list-keys.sql similarity index 100% rename from packages/db/postgres/migrations/028-update-list-keys.sql rename to packages/db/postgres/migrations/031-update-list-keys.sql From 04b66bc27826aab4b10cc9c9b487126ccc7375b0 Mon Sep 17 00:00:00 2001 From: Adam Munns Date: Thu, 1 Dec 2022 13:44:32 -0800 Subject: [PATCH 006/107] fix: styles missing from custom storage form (#2135) Screenshot 2022-11-29 at 2 47 42 PM --- .../website/components/customStorageForm/customStorageForm.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/website/components/customStorageForm/customStorageForm.scss b/packages/website/components/customStorageForm/customStorageForm.scss index a174d81b1f..cc13116184 100644 --- a/packages/website/components/customStorageForm/customStorageForm.scss +++ b/packages/website/components/customStorageForm/customStorageForm.scss @@ -1,4 +1,4 @@ -.custom-storage-form .kwes-form { +.custom-storage-form.kwes-form { .fields { display: grid; width: 100%; From 4d5a1a0e07db3c6749efed0072ab5b4c2fcb828e Mon Sep 17 00:00:00 2001 From: Adam Munns Date: Thu, 1 Dec 2022 13:52:41 -0800 Subject: [PATCH 007/107] feat: simple analytics (#2110) Swap countly for simple analytics. --- .../account/filesManager/pinRequestsTable.js | 13 +- .../account/filesManager/uploadsTable.js | 14 +- packages/website/components/button/button.js | 6 +- packages/website/components/card/card-tier.js | 6 +- packages/website/components/card/card.js | 8 +- packages/website/components/footer/footer.js | 4 +- .../components/navigation/navigation.js | 5 +- .../website/components/textblock/textblock.js | 10 +- .../tokens/tokenCreator/tokenCreator.js | 10 +- .../tokens/tokensManager/tokensManager.js | 8 +- packages/website/lib/analytics.js | 48 ++++++ packages/website/lib/countly.js | 163 ------------------ packages/website/lib/types.d.ts | 2 +- .../modules/docs-theme/feedback/feedback.js | 5 +- .../components/accordion/accordionSection.js | 18 +- .../zero/components/dropdown/dropdown.js | 28 +-- packages/website/pages/404.js | 5 +- packages/website/pages/_app.js | 11 ++ packages/website/pages/account/index.js | 8 +- packages/website/pages/login/index.js | 10 +- packages/website/pages/pricing.js | 10 +- 21 files changed, 152 insertions(+), 240 deletions(-) create mode 100644 packages/website/lib/analytics.js delete mode 100644 packages/website/lib/countly.js diff --git a/packages/website/components/account/filesManager/pinRequestsTable.js b/packages/website/components/account/filesManager/pinRequestsTable.js index 671411e79c..0bbf63ba43 100644 --- a/packages/website/components/account/filesManager/pinRequestsTable.js +++ b/packages/website/components/account/filesManager/pinRequestsTable.js @@ -1,7 +1,7 @@ import clsx from 'clsx'; import React, { useCallback, useEffect, useState } from 'react'; -import countly from 'lib/countly'; +import analytics, { saEvent } from 'lib/analytics'; import Loading from 'components/loading/loading'; import Button, { ButtonVariant } from 'components/button/button'; import Dropdown from 'ZeroComponents/dropdown/dropdown'; @@ -10,9 +10,9 @@ import Modal from 'modules/zero/components/modal/modal'; import CloseIcon from 'assets/icons/close'; import { useUser } from 'components/contexts/userContext'; import RefreshIcon from 'assets/icons/refresh'; +import { usePinRequests } from 'components/contexts/pinRequestsContext'; import PinRequestRowItem from './pinRequestRowItem'; import GradientBackground from '../../gradientbackground/gradientbackground.js'; -import { usePinRequests } from 'components/contexts/pinRequestsContext'; /** * @typedef {Object} PinRequestsTableProps @@ -76,8 +76,8 @@ const PinRequestsTable = ({ content, hidden, onUpdatingChange, showCheckOverlay const closeDeleteModal = useCallback(() => { deleteModalState[1](false); - countly.trackEvent(countly.events.FILE_DELETE_CLICK, { - ui: countly.ui.FILES, + saEvent(analytics.events.FILE_DELETE_CLICK, { + ui: analytics.ui.FILES, totalDeleted: 0, }); }, [deleteModalState]); @@ -92,9 +92,8 @@ const PinRequestsTable = ({ content, hidden, onUpdatingChange, showCheckOverlay await Promise.all(selectedPinRequests.map(({ requestid }) => deletePinRequest(requestid))); } } catch (e) {} - - countly.trackEvent(countly.events.FILE_DELETE_CLICK, { - ui: countly.ui.FILES, + saEvent(analytics.events.FILE_DELETE_CLICK, { + ui: analytics.ui.FILES, totalDeleted: selectedPinRequests.length, }); diff --git a/packages/website/components/account/filesManager/uploadsTable.js b/packages/website/components/account/filesManager/uploadsTable.js index b067476a74..7760c8b548 100644 --- a/packages/website/components/account/filesManager/uploadsTable.js +++ b/packages/website/components/account/filesManager/uploadsTable.js @@ -5,7 +5,7 @@ import { useRouter } from 'next/router'; import CloseIcon from 'assets/icons/close'; import RefreshIcon from 'assets/icons/refresh'; -import countly from 'lib/countly'; +import analytics, { saEvent } from 'lib/analytics'; import { formatTimestamp, formatTimestampFull } from 'lib/utils'; import Modal from 'modules/zero/components/modal/modal'; import Dropdown from 'ZeroComponents/dropdown/dropdown'; @@ -154,8 +154,8 @@ const UploadsTable = ({ content, hidden, onFileUpload, onUpdatingChange, showChe /** Closes the delete dialog modal. */ const closeDeleteModal = useCallback(() => { deleteModalState[1](false); - countly.trackEvent(countly.events.FILE_DELETE_CLICK, { - ui: countly.ui.FILES, + saEvent(analytics.events.FILE_DELETE_CLICK, { + ui: analytics.ui.FILES, totalDeleted: 0, }); }, [deleteModalState]); @@ -172,8 +172,8 @@ const UploadsTable = ({ content, hidden, onFileUpload, onUpdatingChange, showChe } } catch (e) {} - countly.trackEvent(countly.events.FILE_DELETE_CLICK, { - ui: countly.ui.FILES, + saEvent(analytics.events.FILE_DELETE_CLICK, { + ui: analytics.ui.FILES, totalDeleted: selectedUploads.length, }); @@ -370,7 +370,7 @@ const UploadsTable = ({ content, hidden, onFileUpload, onUpdatingChange, showChe onClick={onFileUpload} variant={content?.upload.theme} tracking={{ - ui: countly.ui[content?.upload.ui], + ui: analytics.ui[content?.upload.ui], action: content?.upload.action, data: { isFirstFile: false }, }} @@ -435,7 +435,7 @@ const UploadsTable = ({ content, hidden, onFileUpload, onUpdatingChange, showChe onClick={onFileUpload} variant={content?.table.cta.theme} tracking={{ - ui: countly.ui[content?.table.cta.ui], + ui: analytics.ui[content?.table.cta.ui], action: content?.table.cta.action, data: { isFirstFile: true }, }} diff --git a/packages/website/components/button/button.js b/packages/website/components/button/button.js index 1a638e3cc8..43312c4586 100644 --- a/packages/website/components/button/button.js +++ b/packages/website/components/button/button.js @@ -2,7 +2,7 @@ import clsx from 'clsx'; import React, { useCallback } from 'react'; import ZeroButton from 'ZeroComponents/button/button'; -import { trackEvent, events } from 'lib/countly'; +import { events, saEvent } from 'lib/analytics'; import Tooltip from 'ZeroComponents/tooltip/tooltip'; export const ButtonVariant = { @@ -17,7 +17,7 @@ export const ButtonVariant = { }; /** * @typedef {Object} TrackingProps - * @prop {string} [ui] UI section id. One of countly.ui. + * @prop {string} [ui] UI section id. One of analytics.ui. * @prop {string} [action] Action id. used to uniquely identify an action within a ui section. * @prop {string} [event] Custom event name to be used instead of the default CTA_LINK_CLICK. * @prop {Record} [data] The data attached to this tracking event @@ -54,7 +54,7 @@ const Button = ({ const onClickHandler = useCallback( event => { tracking && - trackEvent(tracking.event || events.CTA_LINK_CLICK, { + saEvent(tracking.event || events.CTA_LINK_CLICK, { ui: tracking.ui, action: tracking.action, link: props.href || '', diff --git a/packages/website/components/card/card-tier.js b/packages/website/components/card/card-tier.js index 9eef579250..d43447232a 100644 --- a/packages/website/components/card/card-tier.js +++ b/packages/website/components/card/card-tier.js @@ -4,7 +4,7 @@ import { useRouter } from 'next/router'; import clsx from 'clsx'; import Button from '../button/button'; -import countly from '../../lib/countly'; +import analytics from '../../lib/analytics'; import { elementIsInViewport } from '../../lib/utils.js'; // ====================================================================== Params @@ -23,10 +23,10 @@ export default function Card({ card, cardsGroup = [], index = 0, onCardLoad }) { const tracking = {}; if (typeof card.cta === 'object') { if (card.cta.event) { - tracking.event = countly.events[card.cta.event]; + tracking.event = analytics.events[card.cta.event]; } if (card.cta.ui) { - tracking.ui = countly.ui[card.cta.ui]; + tracking.ui = analytics.ui[card.cta.ui]; } if (card.cta.action) { tracking.action = card.cta.action; diff --git a/packages/website/components/card/card.js b/packages/website/components/card/card.js index 391f32b878..8b6046abb6 100644 --- a/packages/website/components/card/card.js +++ b/packages/website/components/card/card.js @@ -9,7 +9,7 @@ import CardTier from './card-tier'; import Button from '../button/button'; import NpmIcon from '../../assets/icons/npmicon'; import Windows from '../../assets/icons/windows'; -import countly from '../../lib/countly'; +import analytics, { saEvent } from '../../lib/analytics'; // ====================================================================== Params /** @@ -27,10 +27,10 @@ export default function Card({ card, cardsGroup = [], index = 0, targetClass, on const tracking = {}; if (typeof card.cta === 'object') { if (card.cta.event) { - tracking.event = countly.events[card.cta.event]; + tracking.event = analytics.events[card.cta.event]; } if (card.cta.ui) { - tracking.ui = countly.ui[card.cta.ui]; + tracking.ui = analytics.ui[card.cta.ui]; } if (card.cta.action) { tracking.action = card.cta.action; @@ -44,7 +44,7 @@ export default function Card({ card, cardsGroup = [], index = 0, targetClass, on }, [onCardLoad]); const onLinkClick = useCallback(e => { - countly.trackCustomLinkClick(countly.events.LINK_CLICK_EXPLORE_DOCS, e.currentTarget); + saEvent(analytics.events.LINK_CLICK_EXPLORE_DOCS, { link_text: e.currentTarget }); }, []); const handleButtonClick = useCallback( diff --git a/packages/website/components/footer/footer.js b/packages/website/components/footer/footer.js index 6d43c7294c..8f318f9124 100644 --- a/packages/website/components/footer/footer.js +++ b/packages/website/components/footer/footer.js @@ -3,7 +3,7 @@ import { useCallback } from 'react'; import { useRouter } from 'next/router'; import clsx from 'clsx'; -import { trackCustomLinkClick, events } from '../../lib/countly'; +import { events, saEvent } from '../../lib/analytics'; import Link, { useIsExternalHref } from '../link/link'; import SiteLogo from '../../assets/icons/w3storage-logo.js'; import Button from '../button/button'; @@ -30,7 +30,7 @@ export default function Footer({ isProductApp }) { // ================================================================= Functions const onLinkClick = useCallback(e => { - trackCustomLinkClick(events.LINK_CLICK_FOOTER, e.currentTarget); + saEvent(events.LINK_CLICK_FOOTER, { target: e.currentTarget }); }, []); const handleButtonClick = useCallback( diff --git a/packages/website/components/navigation/navigation.js b/packages/website/components/navigation/navigation.js index 9e384ce033..33f8ae54c2 100644 --- a/packages/website/components/navigation/navigation.js +++ b/packages/website/components/navigation/navigation.js @@ -6,7 +6,7 @@ import clsx from 'clsx'; import { useAuthorization } from 'components/contexts/authorizationContext'; import ZeroAccordion from 'ZeroComponents/accordion/accordion'; import ZeroAccordionSection from 'ZeroComponents/accordion/accordionSection'; -import { trackCustomLinkClick, events, ui } from 'lib/countly'; +import { events, saEvent, ui } from 'lib/analytics'; import Loading from 'components/loading/loading'; import Breadcrumbs from 'components/breadcrumbs/breadcrumbs'; import Sidebar from 'modules/docs-theme/sidebar/sidebar'; @@ -50,7 +50,8 @@ export default function Navigation({ breadcrumbs, isProductApp }) { const onLinkClick = useCallback( e => { - trackCustomLinkClick(events.LINK_CLICK_NAVBAR, e.currentTarget); + saEvent(events.LINK_CLICK_NAVBAR, { target: e.currentTarget }); + if (isMenuOpen) { setMenuOpen(false); } diff --git a/packages/website/components/textblock/textblock.js b/packages/website/components/textblock/textblock.js index 2859cf764e..1578fa9bd8 100644 --- a/packages/website/components/textblock/textblock.js +++ b/packages/website/components/textblock/textblock.js @@ -4,7 +4,7 @@ import { useRouter } from 'next/router'; import clsx from 'clsx'; import Button from '../button/button'; -import countly from '../../lib/countly'; +import analytics from '../../lib/analytics'; // ====================================================================== Params /** @@ -19,10 +19,10 @@ export default function TextBlock({ block }) { const tracking2 = {}; if (typeof block.cta === 'object') { if (block.cta.event) { - tracking.event = countly.events[block.cta.event]; + tracking.event = analytics.events[block.cta.event]; } if (block.cta.ui) { - tracking.ui = countly.ui[block.cta.ui]; + tracking.ui = analytics.ui[block.cta.ui]; } if (block.cta.action) { tracking.action = block.cta.action; @@ -30,10 +30,10 @@ export default function TextBlock({ block }) { } if (typeof block.cta22 === 'object') { if (block.cta2.event) { - tracking2.event = countly.events[block.cta2.event]; + tracking2.event = analytics.events[block.cta2.event]; } if (block.cta2.ui) { - tracking2.ui = countly.ui[block.cta2.ui]; + tracking2.ui = analytics.ui[block.cta2.ui]; } if (block.cta2.action) { tracking2.action = block.cta2.action; diff --git a/packages/website/components/tokens/tokenCreator/tokenCreator.js b/packages/website/components/tokens/tokenCreator/tokenCreator.js index 2274ad6439..e39a4a65f1 100644 --- a/packages/website/components/tokens/tokenCreator/tokenCreator.js +++ b/packages/website/components/tokens/tokenCreator/tokenCreator.js @@ -3,7 +3,7 @@ import clsx from 'clsx'; import { useRouter } from 'next/router'; import { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react'; -import countly from 'lib/countly'; +import analytics, { saEvent } from 'lib/analytics'; import Button, { ButtonVariant } from 'components/button/button'; import { useTokens } from 'components/contexts/tokensContext'; import { useUser } from 'hooks/use-user'; @@ -30,15 +30,15 @@ const TokenCreator = ({ content }) => { const onTokenCreate = useCallback( async e => { // Tracking - countly.trackEvent( - countly.events.TOKEN_CREATE, + saEvent( + analytics.events.TOKEN_CREATE, !tokens.length ? { - ui: countly.ui.TOKENS_EMPTY, + ui: analytics.ui.TOKENS_EMPTY, action: 'New API Token', } : { - ui: countly.ui.NEW_TOKEN, + ui: analytics.ui.NEW_TOKEN, action: 'Create new token', } ); diff --git a/packages/website/components/tokens/tokensManager/tokensManager.js b/packages/website/components/tokens/tokensManager/tokensManager.js index 9cbfc463ec..fa43f524e4 100644 --- a/packages/website/components/tokens/tokensManager/tokensManager.js +++ b/packages/website/components/tokens/tokensManager/tokensManager.js @@ -5,7 +5,7 @@ import { useRouter } from 'next/router'; import Link from 'components/link/link'; import TokenRowItem from './tokenRowItem'; -import countly from 'lib/countly'; +import analytics, { saEvent } from 'lib/analytics'; import Loading from 'components/loading/loading'; import Button, { ButtonVariant } from 'components/button/button'; import { useTokens } from 'components/contexts/tokensContext'; @@ -74,8 +74,8 @@ const TokensManager = ({ content }) => { } finally { await queryClient.invalidateQueries('get-tokens'); - countly.trackEvent(countly.events.TOKEN_DELETE, { - ui: countly.ui.TOKENS, + saEvent(analytics.events.TOKEN_DELETE, { + ui: analytics.ui.TOKENS, }); await getTokens(); @@ -144,7 +144,7 @@ const TokensManager = ({ content }) => { className={clsx(isCreating && 'isDisabled')} href={content.table.cta.link} variant={content.table.cta.theme} - tracking={{ ui: countly.ui[content.table.cta.ui], action: content.table.cta.action }} + tracking={{ ui: analytics.ui[content.table.cta.ui], action: content.table.cta.action }} > {content.table.cta.text} diff --git a/packages/website/lib/analytics.js b/packages/website/lib/analytics.js new file mode 100644 index 0000000000..e4c4f62544 --- /dev/null +++ b/packages/website/lib/analytics.js @@ -0,0 +1,48 @@ +/** @constant */ +export const events = { + LINK_CLICK_BANNER_NFTSTORAGE: 'linkClickBannerNFTStorage', + LINK_CLICK_NAVBAR: 'linkClickNavbar', + LINK_CLICK_FOOTER: 'linkClickFooter', + // Used for CTAs that are only linking to other pages/resources + CTA_LINK_CLICK: 'ctaLinkClick', + // Other custom action events + LOGIN_CLICK: 'loginClick', + LOGOUT_CLICK: 'logoutClick', + FILE_UPLOAD_CLICK: 'fileUploadClick', + FILE_DELETE_CLICK: 'fileDeleteClick', + FILES_NAVIGATION_CLICK: 'filesNavigationClick', + FILES_REFRESH: 'filesRefreshClick', + TOKEN_CREATE: 'tokenCreate', + TOKEN_COPY: 'tokenCopy', + TOKEN_DELETE: 'tokenDelete', + NOT_FOUND: 'notFound', + FEEDBACK_HELPFUL: 'feedbackHelpful', + FEEDBACK_IMPORTANT: 'feedbackImportant', + FEEDBACK_GENERAL: 'feedbackGeneral', +}; + +/** @constant */ +export const ui = { + HOME_HERO: 'home/hero', + HOME_GET_STARTED: 'home/get-started', + NAVBAR: 'navbar', + LOGIN: 'login', + FILES: 'files', + UPLOAD: 'upload', + NEW_TOKEN: 'new-token', + TOKENS: 'tokens', + TOKENS_EMPTY: 'tokens/empty', + PROFILE_GETTING_STARTED: 'profile/getting-started', + PROFILE_API_TOKENS: 'profile/api-tokens', +}; + +export default { + events, + ui +}; + +export const saEvent = (eventName, metadata) => { + + // @ts-ignore + if (window && window.sa_event) return window.sa_event(eventName, metadata); +}; diff --git a/packages/website/lib/countly.js b/packages/website/lib/countly.js deleted file mode 100644 index 82a54ba7e9..0000000000 --- a/packages/website/lib/countly.js +++ /dev/null @@ -1,163 +0,0 @@ -/* eslint-disable */ -import { useEffect } from 'react'; -import countly from 'countly-sdk-web'; -import Router from 'next/router'; - -const config = { - key: process.env.NEXT_PUBLIC_COUNTLY_KEY, - url: process.env.NEXT_PUBLIC_COUNTLY_URL, -}; - -/** @constant */ -export const events = { - LINK_CLICK_BANNER_NFTSTORAGE: 'linkClickBannerNFTStorage', - LINK_CLICK_NAVBAR: 'linkClickNavbar', - LINK_CLICK_FOOTER: 'linkClickFooter', - // Used for CTAs that are only linking to other pages/resources - CTA_LINK_CLICK: 'ctaLinkClick', - // Other custom action events - LOGIN_CLICK: 'loginClick', - LOGOUT_CLICK: 'logoutClick', - FILE_UPLOAD_CLICK: 'fileUploadClick', - FILE_DELETE_CLICK: 'fileDeleteClick', - FILES_NAVIGATION_CLICK: 'filesNavigationClick', - FILES_REFRESH: 'filesRefreshClick', - TOKEN_CREATE: 'tokenCreate', - TOKEN_COPY: 'tokenCopy', - TOKEN_DELETE: 'tokenDelete', - NOT_FOUND: 'notFound', - FEEDBACK_HELPFUL: 'feedbackHelpful', - FEEDBACK_IMPORTANT: 'feedbackImportant', - FEEDBACK_GENERAL: 'feedbackGeneral', -}; - -/** @constant */ -export const ui = { - HOME_HERO: 'home/hero', - HOME_GET_STARTED: 'home/get-started', - NAVBAR: 'navbar', - LOGIN: 'login', - FILES: 'files', - UPLOAD: 'upload', - NEW_TOKEN: 'new-token', - TOKENS: 'tokens', - TOKENS_EMPTY: 'tokens/empty', - PROFILE_GETTING_STARTED: 'profile/getting-started', - PROFILE_API_TOKENS: 'profile/api-tokens', -}; - -/** - * Initialize countly analytics object - */ -export function init() { - if (typeof window === 'undefined') { - return; - } - - if (ready) { - return; - } - - if (!config.key || !config.url) { - console.warn('[lib/countly]', 'Countly config not found.'); - - return; - } - - // dont load on localhost - if (document.location.origin.match(/\/\/localhost(\W|$)/)) { return } - - countly.init({ app_key: config.key, url: config.url, debug: false }); - - countly.track_sessions(); - countly.track_pageview(); - countly.track_clicks(); - countly.track_links(); - countly.track_scrolls(); - - ready = true; - - // Track other not-so-easy to access links - // NFT.Storage Banner link - document.querySelector('div > a[href*="https://nft.storage"]')?.addEventListener('click', event => { - const target = /** @type {HTMLLinkElement} **/ (event?.currentTarget); - - trackEvent(events.LINK_CLICK_BANNER_NFTSTORAGE, { - link: target?.href, - text: target?.innerText, - }); - }); -} - -/** - * Track an event to countly with custom data - * - * @param {string} event Event name to be sent to countly. - * @param {Object} [segmentation] Custom data object to be used as segmentation data in countly. - */ -export function trackEvent(event, segmentation = {}) { - if (!ready) { - init(); - } - - ready && - countly.add_event({ - key: event, - segmentation: { - path: location.pathname, - ...segmentation, - }, - }); -} - -/** - * Track page view to countly. - * - * @param {string} [path] Page route to track. Defaults to window.location.pathname if not provided. - */ -export function trackPageView(path) { - if (!ready) { - init(); - } - - ready && countly.track_pageview(path); -} - -/** - * Track custom link click. - * - * @param {string} event Event name to be sent to countly. - * @param {HTMLLinkElement} target DOM element target of the clicked link. - */ -export function trackCustomLinkClick(event, target) { - if (!ready) { - init(); - } - - ready && - trackEvent(event, { - link: target.href.includes(location.origin) ? new URL(target.href).pathname : target.href, - text: target.innerText, - }); -} - -export function useCountly() { - useEffect(() => { - init(); - Router.events.on('routeChangeComplete', route => { - trackPageView(route); - }); - }, []); -} - -export let ready = false; - -export default { - events, - ui, - init, - trackEvent, - trackPageView, - trackCustomLinkClick, - ready, -}; diff --git a/packages/website/lib/types.d.ts b/packages/website/lib/types.d.ts index 170a7bbbeb..e4743b3864 100644 --- a/packages/website/lib/types.d.ts +++ b/packages/website/lib/types.d.ts @@ -5,4 +5,4 @@ declare module '*.svg' { export const ReactComponent: React.SFC>; const src: string; export default src; -} +} \ No newline at end of file diff --git a/packages/website/modules/docs-theme/feedback/feedback.js b/packages/website/modules/docs-theme/feedback/feedback.js index d96d6930c9..aa7a4dbbd0 100644 --- a/packages/website/modules/docs-theme/feedback/feedback.js +++ b/packages/website/modules/docs-theme/feedback/feedback.js @@ -1,7 +1,7 @@ import React, { useState } from 'react'; import { useRouter } from 'next/router'; -import { trackEvent, events } from '../../../lib/countly'; +import { events, saEvent } from '../../../lib/analytics'; function Feedback({ strings: { title, yes, no, thanks, helpUsImprove } }) { const router = useRouter(); @@ -21,9 +21,8 @@ function Feedback({ strings: { title, yes, no, thanks, helpUsImprove } }) { open an issue ); - const sendFeedback = (answer, answerText) => { - trackEvent(events.FEEDBACK_HELPFUL, { + saEvent(events.FEEDBACK_HELPFUL, { path: window.location.pathname, question: title, answer, diff --git a/packages/website/modules/zero/components/accordion/accordionSection.js b/packages/website/modules/zero/components/accordion/accordionSection.js index a896d22b1f..a3f4a6325f 100644 --- a/packages/website/modules/zero/components/accordion/accordionSection.js +++ b/packages/website/modules/zero/components/accordion/accordionSection.js @@ -2,20 +2,21 @@ import { useRouter } from 'next/router'; import React, { useState, useEffect, useRef } from 'react'; import clsx from 'clsx'; import ZeroAccordionHeader from 'ZeroComponents/accordion/accordionHeader'; +import { saEvent } from 'lib/analytics'; /** * @param {any} props TODO: Define props * @callback props.toggle * @param Boolean props.toggleOnLoad */ -function Header({}) { +function Header({ }) { return null; } /** * @param {any} props TODO: Define props */ -function Content({}) { +function Content({ }) { return null; } @@ -44,6 +45,15 @@ function AccordionSection({ active, toggle, toggleOnLoad, reportUID, slug, disab reportUID(uid); }, [reportUID, uid]); + useEffect(() => { + if (open && header) { + const headerLabel = header.props.children.find(child => { + return child.props.className === 'accordion-header-text'; + }); + saEvent('accordion_opened', { title: headerLabel.props.children }); + } + }, [header, open]); + useEffect(() => { if (!!slug && !!router.query.section && router.query.section === slug) { const element = document.getElementById(`accordion-section_${slug}`); @@ -62,8 +72,8 @@ function AccordionSection({ active, toggle, toggleOnLoad, reportUID, slug, disab toggle={ disabled ? () => { - return null; - } + return null; + } : toggle } > diff --git a/packages/website/modules/zero/components/dropdown/dropdown.js b/packages/website/modules/zero/components/dropdown/dropdown.js index 91451a97b5..ce6fdf556e 100644 --- a/packages/website/modules/zero/components/dropdown/dropdown.js +++ b/packages/website/modules/zero/components/dropdown/dropdown.js @@ -49,7 +49,7 @@ const Dropdown = ({ }, [queryParam, setQueryValue, setDropdownValue, isOpen, setIsOpen]) useEffect(() => { - if(!currentItem || !currentItem.value) return + if (!currentItem || !currentItem.value) return const selectEl = selectElRef.current selectEl.value = currentItem.value onChange && onChange(currentItem.value) @@ -59,30 +59,30 @@ const Dropdown = ({ const selectEl = selectElRef.current const currentIndex = options.findIndex(option => option.value === selectEl.value) - if(event.key === 'Escape') { + if (event.key === 'Escape') { setIsOpen(false) } - if(event.key === 'Enter') { + if (event.key === 'Enter') { setCurrentItem(options[currentIndex].value) } - else if(event.key === 'ArrowUp') { - if(!isOpen) + else if (event.key === 'ArrowUp') { + if (!isOpen) setIsOpen(true) - else if(currentIndex > 0) + else if (currentIndex > 0) setCurrentItem(options[currentIndex - 1].value, false) } - else if(event.key === 'ArrowDown') { - if(!isOpen) + else if (event.key === 'ArrowDown') { + if (!isOpen) setIsOpen(true) - else if(currentIndex < options.length - 1) + else if (currentIndex < options.length - 1) setCurrentItem(options[currentIndex + 1].value, false) } - else if(event.key === 'ArrowLeft') { - if(currentIndex > 0) + else if (event.key === 'ArrowLeft') { + if (currentIndex > 0) setCurrentItem(options[currentIndex - 1].value) } - else if(event.key === 'ArrowRight') { - if(currentIndex < options.length - 1) + else if (event.key === 'ArrowRight') { + if (currentIndex < options.length - 1) setCurrentItem(options[currentIndex + 1].value) } }, [options, setIsOpen, setCurrentItem]) @@ -106,7 +106,7 @@ const Dropdown = ({
-
+
{options.map((option) => (
{ - countly.trackEvent(countly.events.NOT_FOUND, { + saEvent(analytics.events.NOT_FOUND, { path: window.location?.pathname ?? 'unknown', referrer: typeof window !== 'undefined' ? document.referrer : null, }); diff --git a/packages/website/pages/_app.js b/packages/website/pages/_app.js index c0b9964c6a..8239a8a214 100644 --- a/packages/website/pages/_app.js +++ b/packages/website/pages/_app.js @@ -2,6 +2,7 @@ import '../styles/global.scss'; import { useRouter } from 'next/router'; import { useEffect } from 'react'; import clsx from 'clsx'; +import Script from 'next/script'; import Metadata from 'components/general/metadata'; import RestrictedRoute from 'components/general/restrictedRoute'; @@ -27,6 +28,16 @@ const App = ({ Component, pageProps }) => { +