├── src ├── api.js ├── stores │ ├── api.js │ ├── parts.js │ ├── lib.js │ ├── subscriptions.js │ ├── car.js │ ├── delegations.js │ ├── rate-limits.js │ ├── revocations.js │ ├── claims.js │ ├── usage.js │ ├── plans.js │ ├── api.ts │ ├── blob.js │ ├── store.js │ ├── upload.js │ ├── provisions.js │ └── transactional.js ├── validated-email.html ├── validate-email.html ├── banner.txt ├── util.js ├── http.js ├── api.ts ├── config.js ├── claims.js ├── server.js └── index.js ├── .gitignore ├── Dockerfile ├── .env.template ├── .github └── workflows │ └── ci.yml ├── package.json ├── README.md ├── pail.js ├── test └── stores │ └── transactional.spec.js ├── LICENSE.md └── pnpm-lock.yaml /src/api.js: -------------------------------------------------------------------------------- 1 | export {} 2 | -------------------------------------------------------------------------------- /src/stores/api.js: -------------------------------------------------------------------------------- 1 | export {} 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env 3 | data 4 | -------------------------------------------------------------------------------- /src/validated-email.html: -------------------------------------------------------------------------------- 1 | 2 |

Email Validated!

3 | -------------------------------------------------------------------------------- /src/validate-email.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 |
5 | -------------------------------------------------------------------------------- /src/banner.txt: -------------------------------------------------------------------------------- 1 | _ _ _ 2 | | | ___ ___ __ _| | ___| |_ ___ _ __ __ _ __ _ ___ 3 | | |/ _ \ / __/ _` | | / __| __/ _ \| '__/ _` |/ _` |/ _ \ 4 | | | (_) | (_| (_| | |_\__ \ || (_) | | | (_| | (_| | __/ 5 | |_|\___/ \___\__,_|_(⁂)___/\__\___/|_| \__,_|\__, |\___| 6 | |___/ 7 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Dockerfile for running local.storage service. 2 | # CircleCI just gives us a nice Node image; this isn't for CI. 3 | FROM cimg/node:20.11.1 4 | USER circleci 5 | RUN mkdir -p /home/circleci/app 6 | WORKDIR /home/circleci/app 7 | COPY --chown=circleci:circleci package*.json ./ 8 | COPY --chown=circleci:circleci src ./src 9 | EXPOSE 3000 10 | RUN npm install 11 | CMD [ "npm", "start" ] 12 | -------------------------------------------------------------------------------- /src/util.js: -------------------------------------------------------------------------------- 1 | /** @param {string} k */ 2 | export const mustGetEnv = k => { 3 | const v = process.env[k] 4 | if (v == null) throw new Error(`missing environment variable: ${k}`) 5 | return v 6 | } 7 | 8 | /** @param {ReadableStream} readable */ 9 | export const concatStream = async readable => { 10 | const chunks = [] 11 | await readable.pipeTo(new WritableStream({ write: chunk => { chunks.push(chunk) } })) 12 | return new Uint8Array(await new Blob(chunks).arrayBuffer()) 13 | } 14 | -------------------------------------------------------------------------------- /.env.template: -------------------------------------------------------------------------------- 1 | ### required 2 | 3 | # multibase base64pad encoded ed25519 private key 4 | # (you can use `w3 key create` to generate, see https://web3.storage/docs/w3cli/) 5 | PRIVATE_KEY='Mg...' 6 | 7 | ### optional 8 | 9 | # directory where to read/write data to 10 | DATA_DIR=./data 11 | # port the service should bind to 12 | API_PORT=3000 13 | # Public URL where UCAN invocations can be sent 14 | PUBLIC_API_URL='http://localhost:3000' 15 | # Public URL where uploads will be received 16 | # i.e. a HTTP PUT request to {PUBLIC_UPLOAD_URL}/blob/{CID} 17 | PUBLIC_UPLOAD_URL='http://localhost:3000' 18 | -------------------------------------------------------------------------------- /src/http.js: -------------------------------------------------------------------------------- 1 | import * as API from './api.js' 2 | import httpRangeParse from 'http-range-parse' 3 | 4 | /** 5 | * Convert a HTTP Range header to a range object. 6 | * @param {string} value 7 | * @returns {API.Range} 8 | */ 9 | export const parseRange = value => { 10 | const result = httpRangeParse(value) 11 | if (result.ranges) throw new Error('Multipart ranges not supported') 12 | const { unit, first, last, suffix } = result 13 | if (unit !== 'bytes') throw new Error(`Unsupported range unit: ${unit}`) 14 | return suffix != null 15 | ? { suffix } 16 | : { offset: first, length: last != null ? last - first + 1 : undefined } 17 | } 18 | -------------------------------------------------------------------------------- /src/stores/parts.js: -------------------------------------------------------------------------------- 1 | import * as API from './api.js' 2 | import * as UploadAPI from '@web3-storage/upload-api/types' 3 | import { Link } from '@ucanto/server' 4 | import { Set as LinkSet } from 'lnset' 5 | 6 | /** @implements {UploadAPI.DudewhereBucket} */ 7 | export class PartsStore { 8 | #store 9 | 10 | /** 11 | * @param {API.TransactionalStore>} store 12 | */ 13 | constructor (store) { 14 | this.#store = store 15 | } 16 | 17 | /** 18 | * @param {string} root 19 | * @param {string} shard 20 | */ 21 | put (root, shard) { 22 | return this.#store.transact(async s => { 23 | const key = `d/${root}` 24 | let record = await s.get(key) 25 | if (!record) { 26 | record = [] 27 | } 28 | const parts = new LinkSet(record) 29 | parts.add(Link.parse(shard)) 30 | await s.put(key, [...parts.values()]) 31 | }) 32 | } 33 | } -------------------------------------------------------------------------------- /src/stores/lib.js: -------------------------------------------------------------------------------- 1 | import { Failure } from '@ucanto/server' 2 | import { coerce } from 'multiformats/bytes' 3 | 4 | export class RecordNotFound extends Failure { 5 | constructor () { 6 | super() 7 | this.name = /** @type {const} */ ('RecordNotFound') 8 | } 9 | 10 | describe () { 11 | return 'record not found' 12 | } 13 | } 14 | 15 | export class RecordKeyConflict extends Failure { 16 | constructor () { 17 | super() 18 | this.name = /** @type {const} */ ('RecordKeyConflict') 19 | } 20 | 21 | describe () { 22 | return 'record key conflict' 23 | } 24 | } 25 | 26 | /** @type {import('multiformats').BlockCodec<0x0202, Uint8Array>} */ 27 | export const CARCodec = { 28 | name: 'car', 29 | code: 0x0202, 30 | encode: d => coerce(d), 31 | decode: d => coerce(d) 32 | } 33 | 34 | /** @type {import('multiformats').BlockCodec<0x0401, Uint8Array>} */ 35 | export const MultihashIndexSortedCodec = { 36 | name: 'car-multihash-index-sorted', 37 | code: 0x0401, 38 | encode: d => coerce(d), 39 | decode: d => coerce(d) 40 | } 41 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | paths: 8 | - 'src/**' 9 | - 'test/**' 10 | - '.github/workflows/ci.yml' 11 | pull_request: 12 | branches: 13 | - main 14 | paths: 15 | - 'src/**' 16 | - 'test/**' 17 | - '.github/workflows/ci.yml' 18 | workflow_dispatch: 19 | 20 | jobs: 21 | test: 22 | name: Test 23 | strategy: 24 | matrix: 25 | node-version: 26 | - 20 27 | os: 28 | - ubuntu-latest 29 | runs-on: ${{ matrix.os }} 30 | steps: 31 | - name: Checkout 32 | uses: actions/checkout@v2 33 | - uses: pnpm/action-setup@v2 34 | with: 35 | version: 'latest' 36 | - name: Setup node ${{ matrix.node-version }} 37 | uses: actions/setup-node@v2 38 | with: 39 | node-version: ${{ matrix.node-version }} 40 | cache: 'pnpm' 41 | - name: Install dependencies 42 | run: pnpm install 43 | - name: Build 44 | run: pnpm run --if-present build 45 | - name: Test 46 | run: pnpm test 47 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "local.storage", 3 | "version": "0.0.0", 4 | "description": "web3.storage running locally", 5 | "main": "src/index.js", 6 | "type": "module", 7 | "scripts": { 8 | "start": "node src/index.js", 9 | "test": "entail 'test/**/*.spec.js'" 10 | }, 11 | "author": "Alan Shaw", 12 | "license": "Apache-2.0 OR MIT", 13 | "dependencies": { 14 | "@ipld/dag-cbor": "^9.2.0", 15 | "@ucanto/principal": "^9.0.0", 16 | "@ucanto/server": "^9.0.1", 17 | "@ucanto/transport": "^9.1.0", 18 | "@web3-storage/content-claims": "^4.0.3", 19 | "@web3-storage/did-mailto": "^2.1.0", 20 | "@web3-storage/pail": "0.6.0-alpha.4", 21 | "@web3-storage/upload-api": "^8.4.1", 22 | "blockstore-fs": "^1.1.10", 23 | "cardex": "^3.0.2", 24 | "carstream": "^2.0.0", 25 | "dotenv": "^16.4.5", 26 | "http-range-parse": "^1.0.0", 27 | "lnmap": "^2.0.0", 28 | "lnset": "^1.2.0", 29 | "multiformats": "^13.1.0", 30 | "nanoid": "^5.0.6", 31 | "p-defer": "^4.0.0", 32 | "p-queue": "^8.0.1" 33 | }, 34 | "devDependencies": { 35 | "@types/node": "^20.11.29", 36 | "@ucanto/interface": "^9.0.0", 37 | "cli-table3": "^0.6.4", 38 | "entail": "^2.1.2", 39 | "sade": "^1.8.1" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/stores/subscriptions.js: -------------------------------------------------------------------------------- 1 | import * as API from './api.js' 2 | import * as UploadAPI from '@web3-storage/upload-api/types' 3 | 4 | /** @implements {UploadAPI.SubscriptionsStorage} */ 5 | export class SubscriptionsStore { 6 | #store 7 | 8 | /** 9 | * @param {API.TransactionalStore} store 10 | */ 11 | constructor (store) { 12 | this.#store = store 13 | } 14 | 15 | /** @param {UploadAPI.AccountDID} customer */ 16 | list (customer) { 17 | return this.#store.transact(async s => { 18 | /** @type {Record} */ 19 | const subs = {} 20 | for await (const [, v] of s.entries({ prefix: `i/cu/${customer}/` })) { 21 | subs[v.subscription] = subs[v.subscription] || [] 22 | subs[v.subscription].push(v) 23 | } 24 | 25 | /** @type {import('@web3-storage/upload-api').SubscriptionListItem[]} */ 26 | const subscriptions = [] 27 | for (const [subscription, consumers] of Object.entries(subs)) { 28 | subscriptions.push({ 29 | subscription, 30 | provider: consumers[0].provider, 31 | consumers: consumers.map(c => c.consumer) 32 | }) 33 | } 34 | 35 | return { ok: { results: subscriptions } } 36 | }) 37 | } 38 | } -------------------------------------------------------------------------------- /src/api.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AccessServiceContext, 3 | ConsoleServiceContext, 4 | ConsumerServiceContext, 5 | CustomerServiceContext, 6 | ProviderServiceContext, 7 | SpaceServiceContext, 8 | StoreServiceContext, 9 | SubscriptionServiceContext, 10 | RateLimitServiceContext, 11 | RevocationServiceContext, 12 | PlanServiceContext, 13 | UploadServiceContext, 14 | UsageServiceContext, 15 | ErrorReporter, 16 | Signer 17 | } from '@web3-storage/upload-api' 18 | import { ServiceContext as ClaimServiceContext } from '@web3-storage/content-claims/server/service/api' 19 | 20 | export interface ServiceContext 21 | extends AccessServiceContext, 22 | ClaimServiceContext, 23 | ConsoleServiceContext, 24 | ConsumerServiceContext, 25 | CustomerServiceContext, 26 | ProviderServiceContext, 27 | SpaceServiceContext, 28 | StoreServiceContext, 29 | SubscriptionServiceContext, 30 | RateLimitServiceContext, 31 | RevocationServiceContext, 32 | PlanServiceContext, 33 | UploadServiceContext, 34 | UsageServiceContext {} 35 | 36 | export interface UcantoServerContext extends ServiceContext { 37 | id: Signer 38 | errorReporter: ErrorReporter 39 | } 40 | 41 | export type Range = { offset: number, length?: number } | { offset?: number, length: number } | { suffix: number } 42 | -------------------------------------------------------------------------------- /src/stores/car.js: -------------------------------------------------------------------------------- 1 | import * as Link from 'multiformats/link' 2 | import * as UploadAPI from '@web3-storage/upload-api/types' 3 | import { ok } from '@ucanto/server' 4 | import { BlobStore } from './blob.js' 5 | 6 | export class CARPutEvent extends Event { 7 | /** @param {UploadAPI.UnknownLink} link */ 8 | constructor (link) { 9 | super('put') 10 | this.link = link 11 | } 12 | } 13 | 14 | /** @implements {UploadAPI.CarStoreBucket} */ 15 | export class CARStore extends EventTarget { 16 | #store 17 | 18 | /** @param {BlobStore} store */ 19 | constructor (store) { 20 | super() 21 | this.#store = store 22 | } 23 | 24 | /** 25 | * @param {Uint8Array} bytes 26 | * @returns {Promise>} 27 | */ 28 | async put (bytes) { 29 | const result = await this.#store.put(bytes) 30 | if (!result.ok) return result 31 | const link = Link.create(0x0202, result.ok.digest) 32 | this.dispatchEvent(new CARPutEvent(link)) 33 | return ok({ link }) 34 | } 35 | 36 | /** @param {UploadAPI.UnknownLink} link */ 37 | has (link) { 38 | return this.#store.has(link.multihash) 39 | } 40 | 41 | /** 42 | * @param {UploadAPI.UnknownLink} link 43 | * @param {number} size 44 | */ 45 | createUploadUrl (link, size) { 46 | return this.#store.createUploadURL(link.multihash, size) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/config.js: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs' 2 | import * as ed25519 from '@ucanto/principal/ed25519' 3 | import { DID } from '@ucanto/server' 4 | import { base32 } from 'multiformats/bases/base32' 5 | import * as Digest from 'multiformats/hashes/digest' 6 | import dotenv from 'dotenv' 7 | import * as Link from 'multiformats/link' 8 | import { mustGetEnv } from './util.js' 9 | 10 | dotenv.config() 11 | 12 | /** @see https://github.com/web3-storage/w3infra/blob/a1f171de30748d5bb4068cf54302bf92f2b076f1/upload-api/service.js#L12-L16 */ 13 | const MAX_UPLOAD_SIZE = 127*(1<<25) 14 | const LIBP2P_KEY_CODE = 0x72 15 | 16 | const signer = ed25519.parse(mustGetEnv('PRIVATE_KEY')) 17 | // @ts-expect-error 18 | const libp2pKey = Link.create(LIBP2P_KEY_CODE, Digest.create(0xed, signer.verifier)) 19 | const did = DID.parse(`did:web:${libp2pKey.toString(base32)}.local.web3.storage`).did() 20 | 21 | const pkg = JSON.parse(fs.readFileSync(`${import.meta.dirname}/../package.json`, 'utf8')) 22 | const banner = fs.readFileSync(`${import.meta.dirname}/banner.txt`, 'utf8') 23 | const apiPort = process.env.API_PORT ?? 3000 24 | const publicApiURL = new URL(process.env.PUBLIC_API_URL ?? `http://localhost:${apiPort}`) 25 | const publicUploadURL = new URL(process.env.PUBLIC_UPLOAD_URL ?? publicApiURL) 26 | 27 | export const config = { 28 | pkg, 29 | banner, 30 | apiPort, 31 | dataDir: process.env.DATA_DIR ?? './data', 32 | signer: signer.withDID(did), 33 | publicApiURL, 34 | publicUploadURL, 35 | maxUploadSize: MAX_UPLOAD_SIZE 36 | } 37 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # local.storage 2 | 3 | Run web3.storage locally! 4 | 5 | ## Usage 6 | 7 | Install Node.js v20.11+ from [nodejs.org](https://nodejs.org). 8 | 9 | Clone repo and install dependencies: 10 | 11 | ```sh 12 | git clone https://github.com/w3s-project/local.storage.git 13 | cd local.storage 14 | npm install 15 | ``` 16 | 17 | Copy `.env.template` to `.env` and set environment variables: 18 | 19 | ```sh 20 | ### required 21 | 22 | # multibase base64pad encoded ed25519 private key 23 | # (you can use `w3 key create` to generate, see https://web3.storage/docs/w3cli/) 24 | PRIVATE_KEY='Mg...' 25 | 26 | ### optional 27 | 28 | # directory where to read/write data to 29 | DATA_DIR=./data 30 | # port the service should bind to 31 | API_PORT=3000 32 | # Public URL where UCAN invocations can be sent 33 | PUBLIC_API_URL='http://localhost:3000' 34 | # Public URL where uploads will be received 35 | # i.e. a HTTP PUT request to {PUBLIC_UPLOAD_URL}/blob/{CID} 36 | PUBLIC_UPLOAD_URL='http://localhost:3000' 37 | ``` 38 | 39 | Start the service: 40 | 41 | ```sh 42 | npm start 43 | ``` 44 | 45 | To use with [w3cli](https://web3.storage/docs/w3cli/), you'll need to set the following environment variables: 46 | 47 | ```sh 48 | W3UP_SERVICE_URL=http://localhost:3000 49 | W3UP_SERVICE_DID=did:web:XXX.local.web3.storage 50 | # (replace XXX with your service DID, printed when the service starts) 51 | W3_STORE_NAME=w3cli-local.storage 52 | ``` 53 | ## Contributing 54 | 55 | All welcome! web3.storage is open-source. 56 | 57 | ## License 58 | 59 | Dual-licensed under [MIT + Apache 2.0](LICENSE.md) 60 | -------------------------------------------------------------------------------- /src/stores/delegations.js: -------------------------------------------------------------------------------- 1 | import * as API from './api.js' 2 | import * as UploadAPI from '@web3-storage/upload-api/types' 3 | import { ok, Delegation } from '@ucanto/server' 4 | 5 | /** @implements {UploadAPI.DelegationsStorage} */ 6 | export class DelegationsStore { 7 | #store 8 | 9 | /** 10 | * @param {API.TransactionalStore} store 11 | */ 12 | constructor (store) { 13 | this.#store = store 14 | } 15 | 16 | 17 | /** 18 | * @param {Array>>} delegations 19 | * @param {{ cause?: UploadAPI.Link }} [options] 20 | */ 21 | putMany (delegations, options) { 22 | return this.#store.transact(async s => { 23 | for (const d of delegations) { 24 | const archive = await d.archive() 25 | if (!archive.ok) return archive 26 | 27 | await s.put(`d/${d.audience.did()}/${d.cid}`, Object.assign({ 28 | bytes: archive.ok, 29 | insertedAt: new Date().toISOString() 30 | }, options?.cause ? { cause: options.cause } : {})) 31 | } 32 | return ok({}) 33 | }) 34 | } 35 | 36 | /** @param {UploadAPI.DelegationsStorageQuery} query */ 37 | find (query) { 38 | return this.#store.transact(async s => { 39 | const delegations = [] 40 | for await (const [, v] of s.entries({ prefix: `d/${query.audience}/` })) { 41 | const delegation = await Delegation.extract(v.bytes) 42 | if (!delegation.ok) return delegation 43 | delegations.push(delegation.ok) 44 | } 45 | return ok(delegations) 46 | }) 47 | } 48 | 49 | // AFAIK this is in the interface but unused 50 | async count () { 51 | return 0n 52 | } 53 | } -------------------------------------------------------------------------------- /src/stores/rate-limits.js: -------------------------------------------------------------------------------- 1 | import * as API from './api.js' 2 | import * as UploadAPI from '@web3-storage/upload-api/types' 3 | import { ok, error } from '@ucanto/server' 4 | import { nanoid } from 'nanoid' 5 | import { RecordNotFound } from './lib.js' 6 | 7 | /** @implements {UploadAPI.RateLimitsStorage} */ 8 | export class RateLimitsStore { 9 | #store 10 | 11 | /** 12 | * @param {API.TransactionalStore} store 13 | */ 14 | constructor (store) { 15 | this.#store = store 16 | } 17 | 18 | /** 19 | * @param {string} subject 20 | * @param {number} rate 21 | */ 22 | add (subject, rate) { 23 | return this.#store.transact(async s => { 24 | const insertedAt = new Date().toISOString() 25 | const id = nanoid() 26 | const record = { 27 | id, 28 | subject, 29 | rate, 30 | insertedAt 31 | } 32 | await s.put(`d/${id}`, record) 33 | await s.put(`i/${subject}/${id}`, record) 34 | return ok({ id }) 35 | }) 36 | } 37 | 38 | /** @param {string} subject */ 39 | list (subject) { 40 | return this.#store.transact(async s => { 41 | const rateLimits = [] 42 | for await (const [, v] of s.entries({ prefix: `i/${subject}/` })) { 43 | rateLimits.push({ id: v.id, rate: v.rate }) 44 | } 45 | return ok(rateLimits) 46 | }) 47 | } 48 | 49 | /** @param {string} id */ 50 | remove (id) { 51 | return this.#store.transact(async s => { 52 | const record = await s.get(`d/${id}`) 53 | if (!record) return error(new RecordNotFound()) 54 | 55 | await s.del(`d/${id}`) 56 | await s.del(`i/${record.subject}/${id}`) 57 | return ok({}) 58 | }) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/stores/revocations.js: -------------------------------------------------------------------------------- 1 | import * as API from './api.js' 2 | import * as UploadAPI from '@web3-storage/upload-api/types' 3 | import { ok } from '@ucanto/server' 4 | 5 | /** @implements {UploadAPI.RevocationsStorage} */ 6 | export class RevocationsStore { 7 | #store 8 | 9 | /** 10 | * @param {API.TransactionalStore} store 11 | */ 12 | constructor (store) { 13 | this.#store = store 14 | } 15 | 16 | /** @param {UploadAPI.Revocation} revocation */ 17 | add (revocation) { 18 | return this.#store.transact(async s => { 19 | const key = `d/${revocation.revoke}` 20 | let record = await s.get(key) 21 | if (!record) { 22 | record = { revoke: revocation.revoke, scopes: {} } 23 | } 24 | record.scopes[revocation.scope] = { cause: revocation.cause } 25 | await s.put(key, record) 26 | return ok({}) 27 | }) 28 | } 29 | 30 | /** @param {UploadAPI.Revocation} revocation */ 31 | reset (revocation) { 32 | return this.#store.transact(async s => { 33 | await s.put(`d/${revocation.revoke}`, /** @type {API.RevocationRecord} */ ({ 34 | revoke: revocation.revoke, 35 | scopes: { [revocation.scope]: { cause: revocation.cause } } 36 | })) 37 | return ok({}) 38 | }) 39 | } 40 | 41 | /** @param {UploadAPI.RevocationQuery} query */ 42 | query (query) { 43 | return this.#store.transact(async s => { 44 | /** @type {UploadAPI.MatchingRevocations} */ 45 | const matches = {} 46 | for (const revoke of Object.keys(query)) { 47 | const record = await s.get(`d/${revoke}`) 48 | if (!record) continue 49 | matches[revoke] = record.scopes 50 | } 51 | return ok(matches) 52 | }) 53 | } 54 | } -------------------------------------------------------------------------------- /src/stores/claims.js: -------------------------------------------------------------------------------- 1 | import * as API from './api.js' 2 | import * as ClaimsAPI from '@web3-storage/content-claims/server/api' 3 | import { Delegation } from '@ucanto/server' 4 | import { base58btc } from 'multiformats/bases/base58' 5 | import * as Digest from 'multiformats/hashes/digest' 6 | 7 | /** @implements {ClaimsAPI.ClaimStore} */ 8 | export class ClaimStore { 9 | #store 10 | 11 | /** 12 | * @param {API.TransactionalStore} store 13 | */ 14 | constructor (store) { 15 | this.#store = store 16 | } 17 | 18 | /** 19 | * @param {import('@ucanto/interface').UnknownLink} link 20 | */ 21 | get (link) { 22 | return this.#store.transact(async s => { 23 | /** @type {ClaimsAPI.Claim[]} */ 24 | const claims = [] 25 | for await (const [, v] of s.entries({ prefix: `d/${base58btc.encode(link.multihash.bytes)}` })) { 26 | const claim = await Delegation.extract(v.bytes) 27 | if (claim.error) { 28 | console.error('failed to extract claim from archive', claim.error) 29 | continue 30 | } 31 | const value = /** @type {ClaimsAPI.AnyAssertCap} */ (claim.ok.capabilities[0]) 32 | claims.push({ 33 | claim: v.cause, 34 | bytes: v.bytes, 35 | content: Digest.decode(v.content), 36 | value, 37 | expiration: claim.ok.expiration 38 | }) 39 | } 40 | return claims 41 | }) 42 | } 43 | 44 | 45 | /** @param {ClaimsAPI.Claim} claim */ 46 | put (claim) { 47 | return this.#store.transact(async s => { 48 | await s.put(`d/${base58btc.encode(claim.content.bytes)}/${claim.claim}`, { 49 | cause: claim.claim, 50 | bytes: claim.bytes, 51 | content: claim.content.bytes 52 | }) 53 | }) 54 | } 55 | } -------------------------------------------------------------------------------- /src/claims.js: -------------------------------------------------------------------------------- 1 | import { Assert } from '@web3-storage/content-claims/capability' 2 | 3 | /** 4 | * @typedef {{ 5 | * issuer: import('@ucanto/interface').Signer 6 | * audience: import('@ucanto/interface').Principal 7 | * proofs: import('@ucanto/interface').Proof[] 8 | * }} InvocationConfig 9 | */ 10 | 11 | /** 12 | * @param {InvocationConfig} conf 13 | * @param {import('multiformats').UnknownLink} content 14 | * @param {URL} location 15 | */ 16 | export const createLocationClaim = (conf, content, location) => { 17 | return Assert.location.delegate({ 18 | issuer: conf.issuer, 19 | audience: conf.audience, 20 | with: conf.audience.did(), 21 | nb: { 22 | content, 23 | location: [/** @type {import('@ucanto/interface').URI} */(location.toString())] 24 | }, 25 | expiration: Infinity, 26 | proofs: conf.proofs 27 | }) 28 | } 29 | 30 | /** 31 | * @param {InvocationConfig} conf 32 | * @param {import('multiformats').UnknownLink} content 33 | * @param {import('multiformats').Link} includes 34 | */ 35 | export const createInclusionClaim = (conf, content, includes) => { 36 | return Assert.inclusion.delegate({ 37 | issuer: conf.issuer, 38 | audience: conf.audience, 39 | with: conf.audience.did(), 40 | nb: { 41 | content, 42 | includes 43 | }, 44 | expiration: Infinity, 45 | proofs: conf.proofs 46 | }) 47 | } 48 | 49 | /** 50 | * @param {InvocationConfig} conf 51 | * @param {import('multiformats').UnknownLink} content 52 | * @param {import('multiformats').Link[]} parts 53 | */ 54 | export const createPartitionClaim = (conf, content, parts) => { 55 | return Assert.partition.delegate({ 56 | issuer: conf.issuer, 57 | audience: conf.audience, 58 | with: conf.audience.did(), 59 | nb: { 60 | content, 61 | parts 62 | }, 63 | expiration: Infinity, 64 | proofs: conf.proofs 65 | }) 66 | } 67 | -------------------------------------------------------------------------------- /src/stores/usage.js: -------------------------------------------------------------------------------- 1 | import * as API from './api.js' 2 | import * as UploadAPI from '@web3-storage/upload-api/types' 3 | import { ok, error } from '@ucanto/server' 4 | 5 | /** @implements {UploadAPI.UsageStorage} */ 6 | export class UsageStore { 7 | #spaceDiffStore 8 | #spaceSnapshotStore 9 | 10 | /** 11 | * @param {API.TransactionalStore} spaceDiffStore 12 | * @param {API.TransactionalStore} spaceSnapshotStore 13 | */ 14 | constructor (spaceDiffStore, spaceSnapshotStore) { 15 | this.#spaceDiffStore = spaceDiffStore 16 | this.#spaceSnapshotStore = spaceSnapshotStore 17 | } 18 | 19 | /** 20 | * @param {UploadAPI.ProviderDID} provider 21 | * @param {UploadAPI.SpaceDID} space 22 | * @param {{ from: Date, to: Date }} period 23 | */ 24 | async report (provider, space, period) { 25 | const initial = await this.#spaceSnapshotStore.transact(async s => { 26 | const record = await s.get(`d/${provider}/${space}/${period.from.toISOString()}`) 27 | return record?.size ?? 0n 28 | }) 29 | 30 | return this.#spaceDiffStore.transact(async s => { 31 | let final = initial 32 | const events = [] 33 | const prefix = `d/${provider}/${space}/` 34 | 35 | for await (const [k, v] of s.entries({ gt: `${prefix}/${period.from.toISOString()}` })) { 36 | if (!k.startsWith(prefix)) break 37 | if (new Date(v.receiptAt).getTime() > period.to.getTime()) break 38 | events.push({ 39 | cause: v.cause, 40 | delta: v.delta, 41 | receiptAt: v.receiptAt 42 | }) 43 | final += BigInt(v.delta) 44 | } 45 | 46 | 47 | if (final > Number.MAX_SAFE_INTEGER) { 48 | return error(new Error('space is bigger than MAX_SAFE_INTEGER')) 49 | } 50 | 51 | const report = { 52 | provider, 53 | space, 54 | period: { 55 | from: period.from.toISOString(), 56 | to: period.to.toISOString() 57 | }, 58 | size: { 59 | initial: Number(initial), 60 | final: Number(final) 61 | }, 62 | events, 63 | } 64 | return ok(report) 65 | }) 66 | } 67 | } -------------------------------------------------------------------------------- /src/stores/plans.js: -------------------------------------------------------------------------------- 1 | import * as API from './api.js' 2 | import * as UploadAPI from '@web3-storage/upload-api/types' 3 | import { ok, error } from '@ucanto/server' 4 | 5 | /** @implements {UploadAPI.PlansStorage} */ 6 | export class PlansStore { 7 | #store 8 | 9 | /** 10 | * @param {API.TransactionalStore} store 11 | */ 12 | constructor (store) { 13 | this.#store = store 14 | } 15 | 16 | /** 17 | * @param {UploadAPI.AccountDID} customer 18 | * @param {string} account 19 | * @param {UploadAPI.DID} product 20 | */ 21 | initialize (customer, account, product) { 22 | return this.#store.transact(async s => { 23 | const key = `d/${customer}` 24 | const exists = await s.get(key) 25 | if (exists) return error({ 26 | name: /** @type {const} */ ('CustomerExists'), 27 | message: `Customer already exists: ${customer}` 28 | }) 29 | 30 | await s.put(key, { 31 | customer, 32 | account, 33 | product, 34 | insertedAt: new Date().toISOString() 35 | }) 36 | return ok({}) 37 | }) 38 | } 39 | 40 | /** @param {UploadAPI.AccountDID} customer */ 41 | get (customer) { 42 | return this.#store.transact(async s => { 43 | const key = `d/${customer}` 44 | const record = await s.get(key) 45 | if (!record) return error({ 46 | name: /** @type {const} */ ('PlanNotFound'), 47 | message: `Plan not found` 48 | }) 49 | 50 | return ok({ 51 | product: record.product, 52 | updatedAt: record.updatedAt ?? record.insertedAt 53 | }) 54 | }) 55 | } 56 | 57 | /** 58 | * @param {UploadAPI.AccountDID} customer 59 | * @param {UploadAPI.DID} product 60 | */ 61 | set (customer, product) { 62 | return this.#store.transact(async s => { 63 | const key = `d/${customer}` 64 | const record = await s.get(key) 65 | if (!record) return error({ 66 | name: /** @type {const} */ ('CustomerNotFound'), 67 | message: `Customer not found: ${customer}` 68 | }) 69 | 70 | await s.put(key, { 71 | ...record, 72 | product, 73 | updatedAt: new Date().toISOString() 74 | }) 75 | return ok({}) 76 | }) 77 | } 78 | } -------------------------------------------------------------------------------- /src/stores/api.ts: -------------------------------------------------------------------------------- 1 | import { AccountDID, DID, ISO8601Date, ProviderDID, SpaceDID, UCANLink } from '@web3-storage/upload-api' 2 | import { EntriesOptions } from '@web3-storage/pail/api' 3 | 4 | export { EntriesOptions } 5 | 6 | export interface Store { 7 | put: (key: string, value: T) => Promise 8 | get: (key: string) => Promise 9 | has: (key: string) => Promise 10 | del: (key: string) => Promise 11 | entries: (options?: EntriesOptions) => AsyncIterable<[string, T]> 12 | } 13 | 14 | export interface TransactionalStore { 15 | transact (fn: (store: Store) => Promise): Promise 16 | } 17 | 18 | /////////////////////////////////////////////////////////////////////////////// 19 | 20 | export interface SubscriptionRecord { 21 | customer: AccountDID 22 | provider: ProviderDID 23 | subscription: string 24 | cause: UCANLink 25 | insertedAt: ISO8601Date 26 | } 27 | 28 | export interface ConsumerRecord { 29 | consumer: SpaceDID 30 | customer: AccountDID 31 | provider: ProviderDID 32 | subscription: string 33 | cause: UCANLink 34 | insertedAt: ISO8601Date 35 | } 36 | 37 | export interface SpaceMetricRecord { 38 | value: number 39 | space: SpaceDID 40 | } 41 | 42 | export interface DelegationRecord { 43 | cause?: UCANLink 44 | bytes: Uint8Array 45 | insertedAt: ISO8601Date 46 | } 47 | 48 | export interface RateLimitRecord { 49 | id: string 50 | subject: string 51 | rate: number 52 | insertedAt: ISO8601Date 53 | updatedAt?: ISO8601Date 54 | } 55 | 56 | export interface CustomerRecord { 57 | customer: AccountDID 58 | account: string 59 | product: DID 60 | insertedAt: ISO8601Date 61 | updatedAt?: ISO8601Date 62 | } 63 | 64 | export interface RevocationRecord { 65 | revoke: UCANLink 66 | scopes: { [scope: DID]: { cause: UCANLink } } 67 | } 68 | 69 | export interface SpaceDiffRecord { 70 | provider: ProviderDID 71 | space: SpaceDID 72 | subscription: string 73 | cause: UCANLink 74 | delta: number 75 | receiptAt: ISO8601Date 76 | insertedAt: ISO8601Date 77 | } 78 | 79 | export interface SpaceSnapshotRecord { 80 | provider: ProviderDID 81 | space: SpaceDID 82 | size: bigint 83 | recordedAt: ISO8601Date 84 | insertedAt: ISO8601Date 85 | } 86 | 87 | export interface ClaimRecord { 88 | cause: UCANLink 89 | bytes: Uint8Array 90 | content: Uint8Array // multihash 91 | } 92 | -------------------------------------------------------------------------------- /pail.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import fs from 'fs' 3 | import sade from 'sade' 4 | import * as Link from 'multiformats/link' 5 | import { get, entries } from '@web3-storage/pail' 6 | import Table from 'cli-table3' 7 | import { BlockStore } from './src/stores/transactional.js' 8 | 9 | const cli = sade('pail') 10 | .option('--path', 'Path to data store.', './data') 11 | 12 | cli.command('get ') 13 | .describe('Get the stored value for the given key from the pail. If the key is not found, `undefined` is returned.') 14 | .action(async (key, opts) => { 15 | const blocks = new BlockStore(`${opts.path}/blocks`) 16 | const root = Link.decode(fs.readFileSync(`${opts.path}/root`)) 17 | console.log(`Reading pail with root: ${root}`) 18 | // @ts-expect-error 19 | const value = await get(blocks, root, key) 20 | if (value) console.log(value.toString()) 21 | }) 22 | 23 | cli.command('ls') 24 | .describe('List entries in the pail.') 25 | .alias('list') 26 | .option('-p, --prefix', 'Key prefix to filter by.') 27 | .option('--gt', 'Filter results by keys greater than this string.') 28 | .option('--lt', 'Filter results by keys less than this string.') 29 | .option('--json', 'Format output as newline delimted JSON.') 30 | .action(async (opts) => { 31 | const blocks = new BlockStore(`${opts.path}/blocks`) 32 | const root = Link.decode(fs.readFileSync(`${opts.path}/root`)) 33 | console.log(`Reading pail with root: ${root}`) 34 | const { columns } = process.stdout 35 | const keyColWidth = columns < 128 36 | ? Math.max(3, Math.floor(columns / 2) - 2) 37 | : Math.max(3, columns - 62 - 4) 38 | const valColWidth = columns < 128 39 | ? Math.max(3, Math.floor(columns / 2) - 2) 40 | : 62 41 | const table = new Table({ 42 | head: ['Key', 'Value'], 43 | colWidths: [keyColWidth, valColWidth], 44 | wordWrap: true, 45 | wrapOnWordBoundary: false 46 | }) 47 | let n = 0 48 | // @ts-expect-error 49 | for await (const [k, v] of entries(blocks, root, { prefix: opts.prefix, gt: opts.gt, lt: opts.lt })) { 50 | if (opts.json) { 51 | console.log(JSON.stringify({ key: k, value: v.toString() })) 52 | } else { 53 | table.push([k, v.toString()]) 54 | } 55 | n++ 56 | } 57 | if (!opts.json) { 58 | table.push([{ content: `Total: ${n.toLocaleString()}`, colSpan: 2}]) 59 | console.log(table.toString()) 60 | } 61 | }) 62 | 63 | cli.parse(process.argv) 64 | -------------------------------------------------------------------------------- /src/server.js: -------------------------------------------------------------------------------- 1 | import * as API from './api.js' 2 | import * as Server from '@ucanto/server' 3 | import { CAR } from '@ucanto/transport' 4 | import { create as createRevocationChecker } from '@web3-storage/upload-api/utils/revocation' 5 | import { createService as createStoreService } from '@web3-storage/upload-api/store' 6 | import { createService as createUploadService } from '@web3-storage/upload-api/upload' 7 | import { createService as createConsoleService } from '@web3-storage/upload-api/console' 8 | import { createService as createAccessService } from '@web3-storage/upload-api/access' 9 | import { createService as createConsumerService } from '@web3-storage/upload-api/consumer' 10 | import { createService as createCustomerService } from '@web3-storage/upload-api/customer' 11 | import { createService as createSpaceService } from '@web3-storage/upload-api/space' 12 | import { createService as createProviderService } from '@web3-storage/upload-api/provider' 13 | import { createService as createSubscriptionService } from '@web3-storage/upload-api/subscription' 14 | import { createService as createAdminService } from '@web3-storage/upload-api/admin' 15 | import { createService as createRateLimitService } from '@web3-storage/upload-api/rate-limit' 16 | import { createService as createUcanService } from '@web3-storage/upload-api/ucan' 17 | import { createService as createPlanService } from '@web3-storage/upload-api/plan' 18 | import { createService as createUsageService } from '@web3-storage/upload-api/usage' 19 | import { createService as createClaimsService } from '@web3-storage/content-claims/server/service' 20 | 21 | /** @param {API.UcantoServerContext} options */ 22 | export const createServer = ({ id, ...context }) => Server.create({ 23 | ...createRevocationChecker(context), 24 | id, 25 | codec: CAR.inbound, 26 | service: createService(context), 27 | catch: error => context.errorReporter.catch(error) 28 | }) 29 | 30 | /** @param {API.ServiceContext} context */ 31 | export const createService = context => ({ 32 | access: createAccessService(context), 33 | console: createConsoleService(context), 34 | consumer: createConsumerService(context), 35 | customer: createCustomerService(context), 36 | provider: createProviderService(context), 37 | 'rate-limit': createRateLimitService(context), 38 | admin: createAdminService(context), 39 | space: createSpaceService(context), 40 | store: createStoreService(context), 41 | subscription: createSubscriptionService(context), 42 | upload: createUploadService(context), 43 | ucan: createUcanService(context), 44 | plan: createPlanService(context), 45 | usage: createUsageService(context), 46 | ...createClaimsService(context) 47 | }) 48 | -------------------------------------------------------------------------------- /src/stores/blob.js: -------------------------------------------------------------------------------- 1 | import { base64pad } from 'multiformats/bases/base64' 2 | import { base58btc } from 'multiformats/bases/base58' 3 | import { sha256 } from 'multiformats/hashes/sha2' 4 | import * as UploadAPI from '@web3-storage/upload-api/types' 5 | import { ok, error } from '@ucanto/server' 6 | import * as API from './api.js' 7 | import { RecordNotFound } from './lib.js' 8 | 9 | export class BlobPutEvent extends Event { 10 | /** @param {import('multiformats').MultihashDigest} digest */ 11 | constructor (digest) { 12 | super('put') 13 | this.digest = digest 14 | } 15 | } 16 | 17 | export class BlobStore extends EventTarget { 18 | #store 19 | #signer 20 | #url 21 | 22 | /** 23 | * @param {API.TransactionalStore} store 24 | * @param {import('@ucanto/server').Signer} signer 25 | * @param {URL} url 26 | */ 27 | constructor (store, signer, url) { 28 | super() 29 | this.#store = store 30 | this.#signer = signer 31 | this.#url = url 32 | } 33 | 34 | /** 35 | * @param {Uint8Array} bytes 36 | * @returns {Promise>} 37 | */ 38 | async put (bytes) { 39 | const digest = await sha256.digest(bytes) 40 | await this.#store.transact(s => s.put(`d/${base58btc.encode(digest.bytes)}`, bytes)) 41 | this.dispatchEvent(new BlobPutEvent(digest)) 42 | return ok({ digest }) 43 | } 44 | 45 | /** @param {import('multiformats').MultihashDigest} digest */ 46 | has (digest) { 47 | return this.#store.transact(s => s.has(`d/${base58btc.encode(digest.bytes)}`)) 48 | } 49 | 50 | /** 51 | * @param {import('multiformats').MultihashDigest} digest 52 | * @param {{ range?: import('../api.js').Range }} [options] 53 | * @returns {Promise, import('@web3-storage/upload-api').RecordNotFound>>} 54 | */ 55 | async stream (digest, options) { 56 | const { range } = options ?? {} 57 | const bytes = await this.#store.transact(s => s.get(`d/${base58btc.encode(digest.bytes)}`)) 58 | if (!bytes) return error(new RecordNotFound()) 59 | return ok( 60 | /** @type {ReadableStream} */ 61 | new ReadableStream({ 62 | pull (controller) { 63 | if (range) { 64 | if ('suffix' in range) { 65 | controller.enqueue(bytes.slice(-range.suffix)) 66 | } else if (range.offset && range.length) { 67 | controller.enqueue(bytes.slice(range.offset, range.offset + range.length)) 68 | } else if (range.offset) { 69 | controller.enqueue(bytes.slice(range.offset)) 70 | } else if (range.length) { 71 | controller.enqueue(bytes.slice(0, range.length)) 72 | } 73 | } else { 74 | controller.enqueue(bytes) 75 | } 76 | controller.close() 77 | } 78 | }) 79 | ) 80 | } 81 | 82 | /** 83 | * @param {import('multiformats').MultihashDigest} digest 84 | * @param {number} size 85 | */ 86 | async createUploadURL (digest, size) { 87 | // TODO: sign 88 | return { 89 | url: new URL(`blob/${base58btc.encode(digest.bytes)}`, this.#url), 90 | headers: { 91 | 'x-amz-checksum-sha256': base64pad.baseEncode(digest.digest), 92 | 'content-length': String(size), 93 | }, 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/stores/store.js: -------------------------------------------------------------------------------- 1 | import * as API from './api.js' 2 | import * as UploadAPI from '@web3-storage/upload-api/types' 3 | import { ok, error } from '@ucanto/server' 4 | import { RecordKeyConflict, RecordNotFound } from './lib.js' 5 | 6 | /** @implements {UploadAPI.StoreTable} */ 7 | export class StoreStore { 8 | #store 9 | 10 | /** 11 | * @param {API.TransactionalStore} store 12 | */ 13 | constructor (store) { 14 | this.#store = store 15 | } 16 | 17 | /** @param {UploadAPI.UnknownLink} link */ 18 | inspect (link) { 19 | return this.#store.transact(async s => { 20 | const spaces = [] 21 | for await (const [, v] of s.entries({ prefix: `i/${link}` })) { 22 | spaces.push({ did: v.space, insertedAt: v.insertedAt }) 23 | } 24 | return ok({ spaces }) 25 | }) 26 | } 27 | 28 | /** 29 | * @param {UploadAPI.DID} space 30 | * @param {UploadAPI.UnknownLink} link 31 | */ 32 | exists (space, link) { 33 | return this.#store.transact(async s => ok(await s.has(`d/${space}/${link}`))) 34 | } 35 | 36 | /** 37 | * @param {UploadAPI.DID} space 38 | * @param {UploadAPI.UnknownLink} link 39 | */ 40 | get (space, link) { 41 | return this.#store.transact(async s => { 42 | const res = await s.get(`d/${space}/${link}`) 43 | return res ? ok(res) : error(new RecordNotFound()) 44 | }) 45 | } 46 | 47 | /** 48 | * @param {UploadAPI.StoreAddInput} item 49 | */ 50 | insert (item) { 51 | return this.#store.transact(async s => { 52 | const record = { ...item, insertedAt: new Date().toISOString() } 53 | const exists = await s.get(`d/${record.space}/${record.link}`) 54 | if (exists) { 55 | return error(new RecordKeyConflict()) 56 | } 57 | if ('origin' in record && record.origin == null) { 58 | delete record.origin 59 | } 60 | await s.put(`d/${record.space}/${record.link}`, record) 61 | await s.put(`i/${record.link}/${record.space}`, record) 62 | return ok({ 63 | link: item.link, 64 | size: item.size, 65 | origin: item.origin 66 | }) 67 | }) 68 | } 69 | 70 | /** 71 | * @param {UploadAPI.DID} space 72 | * @param {UploadAPI.UnknownLink} link 73 | */ 74 | remove (space, link) { 75 | return this.#store.transact(async s => { 76 | const item = await s.get(`d/${space}/${link}`) 77 | if (!item) { 78 | return error(new RecordNotFound()) 79 | } 80 | 81 | await s.del(`d/${space}/${link}`) 82 | await s.del(`i/${link}/${space}`) 83 | return ok({ size: item.size }) 84 | }) 85 | } 86 | 87 | /** 88 | * @param {UploadAPI.DID} space 89 | * @param {UploadAPI.ListOptions} [options] 90 | */ 91 | list (space, options) { 92 | if (options?.pre) { 93 | // pail not support listing entries backwards 94 | return Promise.resolve(error(new Error('pre not implemented'))) 95 | } 96 | return this.#store.transact(async s => { 97 | const size = options?.size ?? 20 98 | const gt = `d/${space}/${options?.cursor ?? ''}` 99 | const lt = `d/${space}/~` 100 | 101 | const results = [] 102 | let more = false 103 | for await (const [, v] of s.entries({ gt, lt })) { 104 | if (results.length + 1 > size) { 105 | more = true 106 | break 107 | } 108 | 109 | results.push({ 110 | link: v.link, 111 | size: v.size, 112 | origin: v.origin, 113 | insertedAt: v.insertedAt 114 | }) 115 | } 116 | 117 | const before = results.at(0)?.link.toString() 118 | const after = more ? results.at(-1)?.link.toString() : undefined 119 | return ok(Object.assign( 120 | { size: results.length, results }, 121 | before ? { before } : {}, 122 | after ? { cursor: after, after } : {}, 123 | )) 124 | }) 125 | } 126 | } -------------------------------------------------------------------------------- /test/stores/transactional.spec.js: -------------------------------------------------------------------------------- 1 | import os from 'node:os' 2 | import path from 'node:path' 3 | import fs from 'node:fs' 4 | import { Store } from '../../src/stores/transactional.js' 5 | 6 | const tmpDir = () => path.join(os.tmpdir(), `${Date.now()}.${Math.random()}.test.local.storage`) 7 | 8 | /** 9 | * @param {(assert: import('entail').Assert, dir: string) => Promise} testfn 10 | */ 11 | const withTmpDir = testfn => { 12 | return async (/** @type {import('entail').assert} */ assert) => { 13 | const dir = tmpDir() 14 | try { 15 | await testfn(assert, dir) 16 | } finally { 17 | await fs.promises.rm(dir, { recursive: true, maxRetries: 10 }) 18 | } 19 | } 20 | } 21 | 22 | export const testTransactionalStore = { 23 | 'store some things': withTmpDir(async (assert, dir) => { 24 | /** @type {Array<[string, any]>} */ 25 | const items = [ 26 | ['foo', { bar: 'baz' }], 27 | ['bar', 'boz'] 28 | ] 29 | const store = new Store(dir) 30 | await store.transact(async s => { 31 | for (const [k, v] of items) { 32 | await s.put(k, v) 33 | } 34 | }) 35 | 36 | await store.transact(async s => { 37 | for (const [k, v] of items) { 38 | const value = await s.get(k) 39 | assert.deepEqual(value, v) 40 | } 41 | }) 42 | }), 43 | 44 | 'multiple transactions consistent data': withTmpDir(async (assert, dir) => { 45 | const store = new Store(dir) 46 | await Promise.all([ 47 | store.transact(async s => { 48 | await s.put('foo', 123) 49 | await s.put('bar', '🍻') 50 | }), 51 | store.transact(s => s.put('baz', {})), 52 | store.transact(s => s.put('boz', { test: 1138 })), 53 | store.transact(s => s.del('baz')) 54 | ]) 55 | const items = await store.transact(async s => { 56 | /** @type {Array<[string, any]>} */ 57 | const arr = [] 58 | for await (const [k, v] of s.entries()) { 59 | arr.push([k, v]) 60 | } 61 | return arr 62 | }) 63 | assert.equal(items.length, 3) 64 | assert.equal(items[0][0], 'bar') 65 | assert.equal(items[0][1], '🍻') 66 | assert.equal(items[1][0], 'boz') 67 | assert.deepEqual(items[1][1], { test: 1138 }) 68 | assert.equal(items[2][0], 'foo') 69 | assert.equal(items[2][1], 123) 70 | }), 71 | 72 | 'sublevels': withTmpDir(async (assert, dir) => { 73 | /** @type {Array<[string, any]>} */ 74 | const items = [ 75 | ['foo', { bar: 'baz' }], 76 | ['bar', 'boz'] 77 | ] 78 | const store = new Store(dir) 79 | const prefix0 = 'sub0/' 80 | const subStore0 = store.partition(prefix0) 81 | const prefix1 = 'sub1/' 82 | const subStore1 = store.partition(prefix1) 83 | 84 | await subStore0.transact(async s => { 85 | for (const [k, v] of items) { 86 | await s.put(k, v) 87 | } 88 | }) 89 | 90 | await subStore0.transact(async s => { 91 | for (const [k, v] of items) { 92 | const value = await s.get(k) 93 | assert.deepEqual(value, v) 94 | } 95 | for await (const [k, v] of s.entries()) { 96 | assert.deepEqual(items.find(i => i[0] === k), [k, v]) 97 | } 98 | }) 99 | 100 | await subStore1.transact(async s => { 101 | for (const [k, v] of items) { 102 | await s.put(k, v) 103 | } 104 | }) 105 | 106 | await subStore1.transact(async s => { 107 | for (const [k, v] of items) { 108 | const value = await s.get(k) 109 | assert.deepEqual(value, v) 110 | } 111 | for await (const [k, v] of s.entries()) { 112 | assert.deepEqual(items.find(i => i[0] === k), [k, v]) 113 | } 114 | }) 115 | 116 | await store.transact(async s => { 117 | for (const p of [prefix0, prefix1]) { 118 | for (const [k, v] of items) { 119 | const value0 = await s.get(`${p}${k}`) 120 | assert.deepEqual(value0, v) 121 | } 122 | } 123 | }) 124 | }) 125 | } -------------------------------------------------------------------------------- /src/stores/upload.js: -------------------------------------------------------------------------------- 1 | import * as API from './api.js' 2 | import * as UploadAPI from '@web3-storage/upload-api/types' 3 | import { ok, error } from '@ucanto/server' 4 | import { Set as LinkSet } from 'lnset' 5 | import { RecordNotFound } from './lib.js' 6 | 7 | export class UploadAddEvent extends Event { 8 | /** 9 | * @param {import('multiformats').UnknownLink} root 10 | * @param {import('multiformats').Link[]} shards 11 | */ 12 | constructor (root, shards) { 13 | super('add') 14 | this.root = root 15 | this.shards = shards 16 | } 17 | } 18 | 19 | /** @implements {UploadAPI.UploadTable} */ 20 | export class UploadStore extends EventTarget { 21 | #store 22 | 23 | /** 24 | * @param {API.TransactionalStore} store 25 | */ 26 | constructor (store) { 27 | super() 28 | this.#store = store 29 | } 30 | 31 | /** @param {UploadAPI.UnknownLink} root */ 32 | inspect (root) { 33 | return this.#store.transact(async s => { 34 | const spaces = [] 35 | for await (const [, v] of s.entries({ prefix: `i/${root}` })) { 36 | spaces.push({ did: v.space, insertedAt: v.insertedAt }) 37 | } 38 | return ok({ spaces }) 39 | }) 40 | } 41 | 42 | /** 43 | * @param {UploadAPI.DID} space 44 | * @param {UploadAPI.UnknownLink} root 45 | */ 46 | exists (space, root) { 47 | return this.#store.transact(async s => ok(await s.has(`d/${space}/${root}`))) 48 | } 49 | 50 | /** 51 | * @param {UploadAPI.DID} space 52 | * @param {UploadAPI.UnknownLink} root 53 | */ 54 | get (space, root) { 55 | return this.#store.transact(async s => { 56 | const res = await s.get(`d/${space}/${root}`) 57 | return res ? ok(res) : error(new RecordNotFound()) 58 | }) 59 | } 60 | 61 | /** 62 | * @param {UploadAPI.UploadAddInput} item 63 | */ 64 | upsert (item) { 65 | return this.#store.transact(async s => { 66 | let record = await s.get(`d/${item.space}/${item.root}`) 67 | const now = new Date().toISOString() 68 | if (record) { 69 | const shards = new LinkSet(record.shards) 70 | for (const s of item.shards ?? []) { 71 | shards.add(s) 72 | } 73 | record = { ...record, shards: [...shards.values()], updatedAt: now } 74 | } else { 75 | record = { ...item, insertedAt: now, updatedAt: now } 76 | } 77 | 78 | await s.put(`d/${record.space}/${record.root}`, record) 79 | await s.put(`i/${record.root}/${record.space}`, record) 80 | this.dispatchEvent(new UploadAddEvent(record.root, record.shards ?? [])) 81 | return ok({ root: record.root, shards: record.shards }) 82 | }) 83 | } 84 | 85 | /** 86 | * @param {UploadAPI.DID} space 87 | * @param {UploadAPI.UnknownLink} root 88 | */ 89 | remove (space, root) { 90 | return this.#store.transact(async s => { 91 | const record = await s.get(`d/${space}/${root}`) 92 | if (!record) { 93 | return error(new RecordNotFound()) 94 | } 95 | 96 | await s.del(`d/${space}/${root}`) 97 | await s.del(`i/${root}/${space}`) 98 | return ok({ root: record.root, shards: record.shards }) 99 | }) 100 | } 101 | 102 | /** 103 | * @param {UploadAPI.DID} space 104 | * @param {UploadAPI.ListOptions} [options] 105 | */ 106 | list (space, options) { 107 | if (options?.pre) { 108 | // pail not support listing entries backwards 109 | return Promise.resolve(error(new Error('pre not implemented'))) 110 | } 111 | return this.#store.transact(async s => { 112 | const size = options?.size ?? 20 113 | const gt = `d/${space}/${options?.cursor ?? ''}` 114 | const lt = `d/${space}/~` 115 | 116 | const results = [] 117 | let more = false 118 | for await (const [, v] of s.entries({ gt, lt })) { 119 | if (results.length + 1 > size) { 120 | more = true 121 | break 122 | } 123 | 124 | results.push({ 125 | root: v.root, 126 | shards: v.shards, 127 | insertedAt: v.insertedAt, 128 | updatedAt: v.updatedAt 129 | }) 130 | } 131 | 132 | const before = results.at(0)?.root.toString() 133 | const after = more ? results.at(-1)?.root.toString() : undefined 134 | return ok(Object.assign( 135 | { size: results.length, results }, 136 | before ? { before } : {}, 137 | after ? { cursor: after, after } : {}, 138 | )) 139 | }) 140 | } 141 | } -------------------------------------------------------------------------------- /src/stores/provisions.js: -------------------------------------------------------------------------------- 1 | import * as API from './api.js' 2 | import * as UploadAPI from '@web3-storage/upload-api/types' 3 | import { ok, error, CBOR } from '@ucanto/server' 4 | import { RecordKeyConflict } from './lib.js' 5 | 6 | /** @implements {UploadAPI.ProvisionsStorage} */ 7 | export class ProvisionsStore { 8 | #subscriptionStore 9 | #consumerStore 10 | #spaceMetricsStore 11 | 12 | /** 13 | * @param {API.TransactionalStore} subscriptionStore 14 | * @param {API.TransactionalStore} consumerStore 15 | * @param {API.TransactionalStore} spaceMetricsStore 16 | * @param {UploadAPI.ProviderDID[]} services 17 | */ 18 | constructor (subscriptionStore, consumerStore, spaceMetricsStore, services) { 19 | this.#subscriptionStore = subscriptionStore 20 | this.#consumerStore = consumerStore 21 | this.#spaceMetricsStore = spaceMetricsStore 22 | this.services = services 23 | } 24 | 25 | /** @param {UploadAPI.SpaceDID} consumer */ 26 | getStorageProviders (consumer) { 27 | return this.#consumerStore.transact(async s => { 28 | /** @type {UploadAPI.ProviderDID[]} */ 29 | const providers = [] 30 | for await (const [k, v] of s.entries({ prefix: `i/co/${consumer}/` })) { 31 | providers.push(v.provider) 32 | } 33 | return ok(providers) 34 | }) 35 | } 36 | 37 | /** @param {UploadAPI.SpaceDID} consumer */ 38 | hasStorageProvider (consumer) { 39 | return this.#consumerStore.transact(async s => { 40 | for await (const _ of s.entries({ prefix: `i/co/${consumer}/` })) { 41 | return ok(true) 42 | } 43 | return ok(false) 44 | }) 45 | } 46 | 47 | /** @param {UploadAPI.Provision} item */ 48 | async put (item) { 49 | const { cause, consumer, customer, provider } = item 50 | const subscription = await encodeSubscriptionID(item) 51 | 52 | await this.#subscriptionStore.transact(async s => { 53 | const key = `d/${subscription}/${provider}` 54 | const exists = await s.get(key) 55 | if (exists) return 56 | 57 | const record = { 58 | cause: cause.cid, 59 | provider, 60 | customer, 61 | subscription, 62 | insertedAt: new Date().toISOString() 63 | } 64 | await s.put(key, record) 65 | await s.put(`i/c/${customer}/${provider}/${subscription}`, record) 66 | }) 67 | 68 | return this.#consumerStore.transact(async s => { 69 | const key = `d/${subscription}/${provider}` 70 | const exists = await s.get(key) 71 | if (exists) return error(new RecordKeyConflict()) 72 | 73 | const record = { 74 | cause: cause.cid, 75 | provider, 76 | consumer, 77 | customer, 78 | subscription, 79 | insertedAt: new Date().toISOString() 80 | } 81 | await s.put(key, record) 82 | await s.put(`i/co/${consumer}/${provider}`, record) 83 | await s.put(`i/cu/${customer}/${provider}/${subscription}`, record) 84 | return ok({ id: subscription }) 85 | }) 86 | } 87 | 88 | /** 89 | * @param {UploadAPI.ProviderDID} provider 90 | * @param {UploadAPI.SpaceDID} consumer 91 | */ 92 | async getConsumer (provider, consumer) { 93 | const [record, allocated] = await Promise.all([ 94 | this.#consumerStore.transact(s => s.get(`i/co/${consumer}/${provider}`)), 95 | this.#spaceMetricsStore.transact(async s => { 96 | const record = await s.get(`d/${consumer}/store/add-size-total`) 97 | return record?.value ?? 0 98 | }) 99 | ]) 100 | if (!record) { 101 | return error({ name: 'ConsumerNotFound', message: `Consumer not found: ${consumer}` }) 102 | } 103 | return ok({ 104 | did: consumer, 105 | allocated, 106 | limit: 1_000_000_000, 107 | subscription: record.subscription 108 | }) 109 | } 110 | 111 | /** 112 | * @param {UploadAPI.ProviderDID} provider 113 | * @param {UploadAPI.AccountDID} customer 114 | */ 115 | async getCustomer (provider, customer) { 116 | const subscriptions = await this.#subscriptionStore.transact(async s => { 117 | const subscriptions = [] 118 | for await (const [k, v] of s.entries({ prefix: `i/c/${customer}/${provider}/` })) { 119 | subscriptions.push(v.subscription) 120 | } 121 | return subscriptions 122 | }) 123 | return ok({ did: customer, subscriptions }) 124 | } 125 | 126 | /** 127 | * @param {UploadAPI.ProviderDID} provider 128 | * @param {string} subscription 129 | */ 130 | async getSubscription (provider, subscription) { 131 | return await this.#consumerStore.transact(async s => { 132 | const record = await s.get(`d/${subscription}/${provider}`) 133 | if (!record) { 134 | return error({ name: 'SubscriptionNotFound', message: `Subscription not found: ${subscription}` }) 135 | } 136 | return ok({ customer: record.customer, consumer: record.consumer }) 137 | }) 138 | } 139 | 140 | // AFAIK this is in the interface but unused 141 | async count () { 142 | return 0n 143 | } 144 | } 145 | 146 | /** 147 | * Create a subscription ID for a given provision. Currently 148 | * uses a CID generated from `consumer` which ensures a space 149 | * can be provisioned at most once. 150 | * 151 | * @param {UploadAPI.Provision} item 152 | */ 153 | export const encodeSubscriptionID = async ({ consumer }) => 154 | (await CBOR.write({ consumer })).cid.toString() 155 | -------------------------------------------------------------------------------- /src/stores/transactional.js: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs' 2 | import path from 'node:path' 3 | import * as Link from 'multiformats/link' 4 | import * as Block from 'multiformats/block' 5 | import * as codec from '@ipld/dag-cbor' 6 | import { sha256 as hasher } from 'multiformats/hashes/sha2' 7 | import * as Pail from '@web3-storage/pail' 8 | import { MultiBlockFetcher } from '@web3-storage/pail/block' 9 | import { ShardBlock } from '@web3-storage/pail/shard' 10 | import { FsBlockstore } from 'blockstore-fs' 11 | import Queue from 'p-queue' 12 | import defer from 'p-defer' 13 | import { Map as LinkMap } from 'lnmap' 14 | import * as API from './api.js' 15 | 16 | /** 17 | * @template T 18 | * @typedef {API.TransactionalStore} ITransactionalStore 19 | */ 20 | 21 | /** 22 | * @template T 23 | * @typedef {API.Store} IStore 24 | */ 25 | 26 | /** 27 | * A store that uses transactions to ensure data consistency. 28 | * 29 | * @template T 30 | * @implements {ITransactionalStore} 31 | */ 32 | export class Store { 33 | /** @type {LinkStore} */ 34 | #root 35 | #queue 36 | #blocks 37 | #codec 38 | 39 | /** 40 | * Creates a new store that uses transactions to ensure data consistency. 41 | * 42 | * @param {string} dir 43 | * @param {{ codec?: import('multiformats').BlockCodec }} [options] 44 | */ 45 | constructor (dir, options) { 46 | this.#root = new LinkStore(path.join(dir, 'root')) 47 | this.#blocks = new BlockStore(path.join(dir, 'blocks')) 48 | this.#queue = new Queue({ concurrency: 1 }) 49 | this.#codec = options?.codec ?? codec 50 | } 51 | 52 | /** 53 | * @template R 54 | * @param {(store: IStore) => Promise} fn 55 | * @returns {Promise} 56 | */ 57 | transact (fn) { 58 | return transact({ 59 | queue: this.#queue, 60 | root: this.#root, 61 | blocks: this.#blocks, 62 | codec: this.#codec 63 | }, fn) 64 | } 65 | 66 | /** 67 | * Partitions a transactional store using the provided keyspace prefix. 68 | * 69 | * @template P 70 | * @param {string} prefix 71 | * @param {{ codec?: import('multiformats').BlockCodec }} [options] 72 | * @returns {ITransactionalStore

} 73 | */ 74 | partition (prefix, options) { 75 | const config = { 76 | queue: this.#queue, 77 | root: this.#root, 78 | blocks: this.#blocks, 79 | codec: this.#codec, 80 | ...options 81 | } 82 | return { 83 | /** 84 | * @template R 85 | * @param {(store: IStore

) => Promise} fn 86 | * @returns {Promise} 87 | */ 88 | transact (fn) { 89 | return transact(config, store => fn(new SubTxnStore(prefix, store))) 90 | } 91 | } 92 | } 93 | } 94 | 95 | /** 96 | * @template T 97 | * @template R 98 | * @param {object} config 99 | * @param {Queue} config.queue Transaction queue. 100 | * @param {LinkStore} config.root Storage for the DAG root. 101 | * @param {BlockStore} config.blocks 102 | * @param {import('multiformats').BlockCodec} config.codec 103 | * @param {(store: IStore) => Promise} fn 104 | * @returns {Promise} 105 | */ 106 | const transact = async ({ queue, root, blocks, codec }, fn) => { 107 | /** @type {import('p-defer').DeferredPromise} */ 108 | const { promise, resolve, reject } = defer() 109 | await queue.add(async () => { 110 | try { 111 | let rootLink = await root.get() 112 | if (!rootLink) { 113 | const block = await ShardBlock.create() 114 | await blocks.put(block.cid, block.bytes) 115 | await root.set(block.cid) 116 | rootLink = block.cid 117 | } 118 | const txn = new TxnStore({ root: rootLink, blocks, codec }) 119 | const result = await fn(txn) 120 | for (const a of txn.additions) { 121 | await blocks.put(a.cid, a.bytes) 122 | } 123 | await root.set(txn.root) 124 | for (const r of txn.removals) { 125 | await blocks.del(r.cid) 126 | } 127 | resolve(result) 128 | if (rootLink.toString() !== txn.root.toString()) { 129 | console.log(`Transaction commit: ${txn.root}`) 130 | } 131 | } catch (err) { 132 | reject(err) 133 | } 134 | }) 135 | return promise 136 | } 137 | 138 | /** 139 | * @template T 140 | * @implements {IStore} 141 | */ 142 | class TxnStore { 143 | #root 144 | #blocks 145 | /** @type {Map} */ 146 | #additions 147 | /** @type {Map} */ 148 | #removals 149 | #codec 150 | 151 | /** 152 | * @param {object} params 153 | * @param {import('@web3-storage/pail/api').ShardLink} params.root 154 | * @param {import('@web3-storage/pail/api').BlockFetcher} params.blocks 155 | * @param {import('multiformats').BlockCodec} params.codec 156 | */ 157 | constructor ({ root, blocks, codec }) { 158 | this.#blocks = new MultiBlockFetcher({ 159 | // @ts-expect-error 160 | get: async cid => this.#additions.get(cid) 161 | }, blocks) 162 | this.#root = root 163 | this.#additions = new LinkMap() 164 | this.#removals = new LinkMap() 165 | this.#codec = codec 166 | } 167 | 168 | get root () { 169 | return this.#root 170 | } 171 | 172 | get additions () { 173 | return [...this.#additions.values()] 174 | } 175 | 176 | get removals () { 177 | return [...this.#removals.values()] 178 | } 179 | 180 | /** 181 | * @param {string} key 182 | * @param {T} value 183 | */ 184 | async put (key, value) { 185 | const valueBlock = await Block.encode({ value, codec: this.#codec, hasher }) 186 | this.#additions.set(valueBlock.cid, valueBlock) 187 | if (this.#removals.has(valueBlock.cid)) { 188 | this.#removals.delete(valueBlock.cid) 189 | } 190 | 191 | // TODO: remove the value when putting to an existing key? 192 | 193 | const res = await Pail.put(this.#blocks, this.#root, key, valueBlock.cid) 194 | this.#applyDiff(res) 195 | } 196 | 197 | /** @param {string} key */ 198 | async del (key) { 199 | const valueLink = await Pail.get(this.#blocks, this.#root, key) 200 | if (!valueLink) return 201 | 202 | const valueBlock = await this.#blocks.get(valueLink) 203 | if (!valueBlock) throw new Error(`missing value for key: ${key}: ${valueLink}`) 204 | 205 | // @ts-expect-error 206 | this.#additions.delete(valueBlock.cid) 207 | // TODO: this could be referenced somewhere else in the pail 208 | // this.#removals.set(valueBlock.cid, valueBlock) 209 | 210 | const res = await Pail.del(this.#blocks, this.#root, key) 211 | this.#applyDiff(res) 212 | } 213 | 214 | /** 215 | * @param {{ root: import('@web3-storage/pail/api').ShardLink } & import('@web3-storage/pail/api').ShardDiff} diff 216 | */ 217 | #applyDiff (diff) { 218 | for (const a of diff.additions) { 219 | if (this.#removals.has(a.cid)) { 220 | this.#removals.delete(a.cid) 221 | } 222 | this.#additions.set(a.cid, a) 223 | } 224 | for (const r of diff.removals) { 225 | if (this.#additions.has(r.cid)) { 226 | this.#additions.delete(r.cid) 227 | } 228 | this.#removals.set(r.cid, r) 229 | } 230 | this.#root = diff.root 231 | } 232 | 233 | /** @param {string} key */ 234 | async get (key) { 235 | const valueLink = await Pail.get(this.#blocks, this.#root, key) 236 | if (!valueLink) return 237 | const valueBlock = await this.#blocks.get(valueLink) 238 | if (!valueBlock) throw new Error(`missing value for key: ${key}: ${valueLink}`) 239 | return this.#codec.decode(valueBlock.bytes) 240 | } 241 | 242 | /** @param {string} key */ 243 | async has (key) { 244 | const exists = await Pail.get(this.#blocks, this.#root, key) 245 | return Boolean(exists) 246 | } 247 | 248 | /** 249 | * @param {API.EntriesOptions} [options] 250 | * @returns {AsyncIterable<[string, T]>} 251 | */ 252 | async * entries (options) { 253 | for await (const [k, v] of Pail.entries(this.#blocks, this.#root, options)) { 254 | const valueBlock = await this.#blocks.get(v) 255 | if (!valueBlock) throw new Error('missing value for key') 256 | yield [k, this.#codec.decode(valueBlock.bytes)] 257 | } 258 | } 259 | } 260 | 261 | /** 262 | * @template T 263 | * @implements {IStore} 264 | */ 265 | class SubTxnStore { 266 | #prefix 267 | #store 268 | 269 | /** 270 | * @param {string} prefix 271 | * @param {IStore} store 272 | */ 273 | constructor (prefix, store) { 274 | this.#prefix = prefix 275 | this.#store = store 276 | } 277 | 278 | /** 279 | * @param {string} key 280 | * @param {T} value 281 | */ 282 | put (key, value) { 283 | return this.#store.put(`${this.#prefix}${key}`, value) 284 | } 285 | 286 | /** @param {string} key */ 287 | del (key) { 288 | return this.#store.del(`${this.#prefix}${key}`) 289 | } 290 | 291 | /** @param {string} key */ 292 | get (key) { 293 | return this.#store.get(`${this.#prefix}${key}`) 294 | } 295 | 296 | /** @param {string} key */ 297 | has (key) { 298 | return this.#store.has(`${this.#prefix}${key}`) 299 | } 300 | 301 | /** 302 | * @param {API.EntriesOptions} [options] 303 | * @returns {AsyncIterable<[string, T]>} 304 | */ 305 | async * entries (options) { 306 | if (options) { 307 | if ('prefix' in options && options.prefix) { 308 | options = { prefix: `${this.#prefix}${options.prefix}` } 309 | } else { 310 | if ('gt' in options && options.gt) { 311 | options = { ...options, gt: `${this.#prefix}${options.gt}` } 312 | } 313 | if ('gte' in options && options.gte) { 314 | options = { ...options, gte: `${this.#prefix}${options.gte}` } 315 | } 316 | if ('lt' in options && options.lt) { 317 | options = { ...options, lt: `${this.#prefix}${options.lt}` } 318 | } 319 | if ('lte' in options && options.lte) { 320 | options = { ...options, lte: `${this.#prefix}${options.lte}` } 321 | } 322 | } 323 | } 324 | for await (const [k, v] of this.#store.entries(options)) { 325 | yield [k.slice(this.#prefix.length), v] 326 | } 327 | } 328 | } 329 | 330 | /** 331 | * A store for a single CID. 332 | * 333 | * @template {import('multiformats').Link} T 334 | */ 335 | class LinkStore { 336 | #link 337 | #filepath 338 | 339 | /** @param {string} filepath */ 340 | constructor (filepath) { 341 | this.#filepath = filepath 342 | try { 343 | this.#link = /** @type {T} */ (Link.decode(fs.readFileSync(filepath))) 344 | } catch (err) { 345 | if (err.code !== 'ENOENT') throw err 346 | } 347 | } 348 | 349 | async get () { 350 | return this.#link 351 | } 352 | 353 | /** @param {T} link */ 354 | async set (link) { 355 | await fs.promises.writeFile(this.#filepath, link.bytes) 356 | this.#link = link 357 | } 358 | } 359 | 360 | export class BlockStore { 361 | #bs 362 | 363 | /** @param {string} dir */ 364 | constructor (dir) { 365 | fs.mkdirSync(dir, { recursive: true }) 366 | this.#bs = new FsBlockstore(dir) 367 | } 368 | 369 | /** 370 | * @param {import('multiformats').UnknownLink} cid 371 | * @param {Uint8Array} bytes 372 | */ 373 | put (cid, bytes) { 374 | // @ts-expect-error 375 | return this.#bs.put(cid, bytes) 376 | } 377 | 378 | /** 379 | * @template {unknown} T 380 | * @template {number} C 381 | * @template {number} A 382 | * @template {import('multiformats').Version} V 383 | * @param {import('multiformats').Link} cid 384 | * @returns {Promise | undefined>} 385 | */ 386 | async get (cid) { 387 | try { 388 | // @ts-expect-error 389 | const bytes = await this.#bs.get(cid) 390 | return { cid, bytes } 391 | } catch {} 392 | } 393 | 394 | /** @param {import('multiformats').UnknownLink} cid */ 395 | del (cid) { 396 | // @ts-expect-error 397 | return this.#bs.delete(cid) 398 | } 399 | } 400 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The contents of this repository are Copyright (c) corresponding authors and 2 | contributors, licensed under the `Permissive License Stack` meaning either of: 3 | 4 | - Apache-2.0 Software License: https://www.apache.org/licenses/LICENSE-2.0 5 | ([...4tr2kfsq](https://dweb.link/ipfs/bafkreiankqxazcae4onkp436wag2lj3ccso4nawxqkkfckd6cg4tr2kfsq)) 6 | 7 | - MIT Software License: https://opensource.org/licenses/MIT 8 | ([...vljevcba](https://dweb.link/ipfs/bafkreiepofszg4gfe2gzuhojmksgemsub2h4uy2gewdnr35kswvljevcba)) 9 | 10 | You may not use the contents of this repository except in compliance 11 | with one of the listed Licenses. For an extended clarification of the 12 | intent behind the choice of Licensing please refer to 13 | https://protocol.ai/blog/announcing-the-permissive-license-stack/ 14 | 15 | Unless required by applicable law or agreed to in writing, software 16 | distributed under the terms listed in this notice is distributed on 17 | an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, 18 | either express or implied. See each License for the specific language 19 | governing permissions and limitations under that License. 20 | 21 | 22 | 23 | `SPDX-License-Identifier: Apache-2.0 OR MIT` 24 | 25 | Verbatim copies of both licenses are included below: 26 | 27 |

Apache-2.0 Software License 28 | 29 | ``` 30 | Apache License 31 | Version 2.0, January 2004 32 | http://www.apache.org/licenses/ 33 | 34 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 35 | 36 | 1. Definitions. 37 | 38 | "License" shall mean the terms and conditions for use, reproduction, 39 | and distribution as defined by Sections 1 through 9 of this document. 40 | 41 | "Licensor" shall mean the copyright owner or entity authorized by 42 | the copyright owner that is granting the License. 43 | 44 | "Legal Entity" shall mean the union of the acting entity and all 45 | other entities that control, are controlled by, or are under common 46 | control with that entity. For the purposes of this definition, 47 | "control" means (i) the power, direct or indirect, to cause the 48 | direction or management of such entity, whether by contract or 49 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 50 | outstanding shares, or (iii) beneficial ownership of such entity. 51 | 52 | "You" (or "Your") shall mean an individual or Legal Entity 53 | exercising permissions granted by this License. 54 | 55 | "Source" form shall mean the preferred form for making modifications, 56 | including but not limited to software source code, documentation 57 | source, and configuration files. 58 | 59 | "Object" form shall mean any form resulting from mechanical 60 | transformation or translation of a Source form, including but 61 | not limited to compiled object code, generated documentation, 62 | and conversions to other media types. 63 | 64 | "Work" shall mean the work of authorship, whether in Source or 65 | Object form, made available under the License, as indicated by a 66 | copyright notice that is included in or attached to the work 67 | (an example is provided in the Appendix below). 68 | 69 | "Derivative Works" shall mean any work, whether in Source or Object 70 | form, that is based on (or derived from) the Work and for which the 71 | editorial revisions, annotations, elaborations, or other modifications 72 | represent, as a whole, an original work of authorship. For the purposes 73 | of this License, Derivative Works shall not include works that remain 74 | separable from, or merely link (or bind by name) to the interfaces of, 75 | the Work and Derivative Works thereof. 76 | 77 | "Contribution" shall mean any work of authorship, including 78 | the original version of the Work and any modifications or additions 79 | to that Work or Derivative Works thereof, that is intentionally 80 | submitted to Licensor for inclusion in the Work by the copyright owner 81 | or by an individual or Legal Entity authorized to submit on behalf of 82 | the copyright owner. For the purposes of this definition, "submitted" 83 | means any form of electronic, verbal, or written communication sent 84 | to the Licensor or its representatives, including but not limited to 85 | communication on electronic mailing lists, source code control systems, 86 | and issue tracking systems that are managed by, or on behalf of, the 87 | Licensor for the purpose of discussing and improving the Work, but 88 | excluding communication that is conspicuously marked or otherwise 89 | designated in writing by the copyright owner as "Not a Contribution." 90 | 91 | "Contributor" shall mean Licensor and any individual or Legal Entity 92 | on behalf of whom a Contribution has been received by Licensor and 93 | subsequently incorporated within the Work. 94 | 95 | 2. Grant of Copyright License. Subject to the terms and conditions of 96 | this License, each Contributor hereby grants to You a perpetual, 97 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 98 | copyright license to reproduce, prepare Derivative Works of, 99 | publicly display, publicly perform, sublicense, and distribute the 100 | Work and such Derivative Works in Source or Object form. 101 | 102 | 3. Grant of Patent License. Subject to the terms and conditions of 103 | this License, each Contributor hereby grants to You a perpetual, 104 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 105 | (except as stated in this section) patent license to make, have made, 106 | use, offer to sell, sell, import, and otherwise transfer the Work, 107 | where such license applies only to those patent claims licensable 108 | by such Contributor that are necessarily infringed by their 109 | Contribution(s) alone or by combination of their Contribution(s) 110 | with the Work to which such Contribution(s) was submitted. If You 111 | institute patent litigation against any entity (including a 112 | cross-claim or counterclaim in a lawsuit) alleging that the Work 113 | or a Contribution incorporated within the Work constitutes direct 114 | or contributory patent infringement, then any patent licenses 115 | granted to You under this License for that Work shall terminate 116 | as of the date such litigation is filed. 117 | 118 | 4. Redistribution. You may reproduce and distribute copies of the 119 | Work or Derivative Works thereof in any medium, with or without 120 | modifications, and in Source or Object form, provided that You 121 | meet the following conditions: 122 | 123 | (a) You must give any other recipients of the Work or 124 | Derivative Works a copy of this License; and 125 | 126 | (b) You must cause any modified files to carry prominent notices 127 | stating that You changed the files; and 128 | 129 | (c) You must retain, in the Source form of any Derivative Works 130 | that You distribute, all copyright, patent, trademark, and 131 | attribution notices from the Source form of the Work, 132 | excluding those notices that do not pertain to any part of 133 | the Derivative Works; and 134 | 135 | (d) If the Work includes a "NOTICE" text file as part of its 136 | distribution, then any Derivative Works that You distribute must 137 | include a readable copy of the attribution notices contained 138 | within such NOTICE file, excluding those notices that do not 139 | pertain to any part of the Derivative Works, in at least one 140 | of the following places: within a NOTICE text file distributed 141 | as part of the Derivative Works; within the Source form or 142 | documentation, if provided along with the Derivative Works; or, 143 | within a display generated by the Derivative Works, if and 144 | wherever such third-party notices normally appear. The contents 145 | of the NOTICE file are for informational purposes only and 146 | do not modify the License. You may add Your own attribution 147 | notices within Derivative Works that You distribute, alongside 148 | or as an addendum to the NOTICE text from the Work, provided 149 | that such additional attribution notices cannot be construed 150 | as modifying the License. 151 | 152 | You may add Your own copyright statement to Your modifications and 153 | may provide additional or different license terms and conditions 154 | for use, reproduction, or distribution of Your modifications, or 155 | for any such Derivative Works as a whole, provided Your use, 156 | reproduction, and distribution of the Work otherwise complies with 157 | the conditions stated in this License. 158 | 159 | 5. Submission of Contributions. Unless You explicitly state otherwise, 160 | any Contribution intentionally submitted for inclusion in the Work 161 | by You to the Licensor shall be under the terms and conditions of 162 | this License, without any additional terms or conditions. 163 | Notwithstanding the above, nothing herein shall supersede or modify 164 | the terms of any separate license agreement you may have executed 165 | with Licensor regarding such Contributions. 166 | 167 | 6. Trademarks. This License does not grant permission to use the trade 168 | names, trademarks, service marks, or product names of the Licensor, 169 | except as required for reasonable and customary use in describing the 170 | origin of the Work and reproducing the content of the NOTICE file. 171 | 172 | 7. Disclaimer of Warranty. Unless required by applicable law or 173 | agreed to in writing, Licensor provides the Work (and each 174 | Contributor provides its Contributions) on an "AS IS" BASIS, 175 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 176 | implied, including, without limitation, any warranties or conditions 177 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 178 | PARTICULAR PURPOSE. You are solely responsible for determining the 179 | appropriateness of using or redistributing the Work and assume any 180 | risks associated with Your exercise of permissions under this License. 181 | 182 | 8. Limitation of Liability. In no event and under no legal theory, 183 | whether in tort (including negligence), contract, or otherwise, 184 | unless required by applicable law (such as deliberate and grossly 185 | negligent acts) or agreed to in writing, shall any Contributor be 186 | liable to You for damages, including any direct, indirect, special, 187 | incidental, or consequential damages of any character arising as a 188 | result of this License or out of the use or inability to use the 189 | Work (including but not limited to damages for loss of goodwill, 190 | work stoppage, computer failure or malfunction, or any and all 191 | other commercial damages or losses), even if such Contributor 192 | has been advised of the possibility of such damages. 193 | 194 | 9. Accepting Warranty or Additional Liability. While redistributing 195 | the Work or Derivative Works thereof, You may choose to offer, 196 | and charge a fee for, acceptance of support, warranty, indemnity, 197 | or other liability obligations and/or rights consistent with this 198 | License. However, in accepting such obligations, You may act only 199 | on Your own behalf and on Your sole responsibility, not on behalf 200 | of any other Contributor, and only if You agree to indemnify, 201 | defend, and hold each Contributor harmless for any liability 202 | incurred by, or claims asserted against, such Contributor by reason 203 | of your accepting any such warranty or additional liability. 204 | 205 | END OF TERMS AND CONDITIONS 206 | ``` 207 | 208 |
209 | 210 |
MIT Software License 211 | 212 | ``` 213 | Permission is hereby granted, free of charge, to any person obtaining a copy 214 | of this software and associated documentation files (the "Software"), to deal 215 | in the Software without restriction, including without limitation the rights 216 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 217 | copies of the Software, and to permit persons to whom the Software is 218 | furnished to do so, subject to the following conditions: 219 | 220 | The above copyright notice and this permission notice shall be included in 221 | all copies or substantial portions of the Software. 222 | 223 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 224 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 225 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 226 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 227 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 228 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 229 | THE SOFTWARE. 230 | ``` 231 | 232 |
233 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs' 2 | import http from 'node:http' 3 | import { Writable } from 'node:stream' 4 | import { authorize } from '@web3-storage/upload-api/validate' 5 | import * as DIDMailto from '@web3-storage/did-mailto' 6 | import { walkClaims } from '@web3-storage/content-claims/server' 7 | import * as Digest from 'multiformats/hashes/digest' 8 | import * as raw from 'multiformats/codecs/raw' 9 | import * as Link from 'multiformats/link' 10 | import { base58btc } from 'multiformats/bases/base58' 11 | import { CARReaderStream, CARWriterStream } from 'carstream' 12 | import { MultihashIndexSortedWriter } from 'cardex' 13 | import { Store } from './stores/transactional.js' 14 | import { StoreStore } from './stores/store.js' 15 | import { UploadAddEvent, UploadStore } from './stores/upload.js' 16 | import { ProvisionsStore } from './stores/provisions.js' 17 | import { DelegationsStore } from './stores/delegations.js' 18 | import { RateLimitsStore } from './stores/rate-limits.js' 19 | import { PlansStore } from './stores/plans.js' 20 | import { SubscriptionsStore } from './stores/subscriptions.js' 21 | import { RevocationsStore } from './stores/revocations.js' 22 | import { UsageStore } from './stores/usage.js' 23 | import { BlobStore } from './stores/blob.js' 24 | import { CARPutEvent, CARStore } from './stores/car.js' 25 | import { PartsStore } from './stores/parts.js' 26 | import { ClaimStore } from './stores/claims.js' 27 | import { parseRange } from './http.js' 28 | import { config } from './config.js' 29 | import { createServer } from './server.js' 30 | import { createInclusionClaim, createLocationClaim, createPartitionClaim } from './claims.js' 31 | import { concatStream } from './util.js' 32 | 33 | const store = new Store(config.dataDir) 34 | const storeStore = store.partition('store/') 35 | const uploadStore = store.partition('upload/') 36 | const subscriptionStore = store.partition('subscription/') 37 | const consumerStore = store.partition('consumer/') 38 | const spaceMetricsStore = store.partition('space-metric/') 39 | const delegationsStore = store.partition('delegation/') 40 | const rateLimitsStore = store.partition('rate-limit/') 41 | const customerStore = store.partition('customer/') 42 | const revocationsStore = store.partition('revocation/') 43 | const spaceDiffStore = store.partition('space-diff/') 44 | const spaceSnapshotStore = store.partition('space-snapshot/') 45 | const blobStore = store.partition('blob/', { codec: raw }) 46 | const partsStore = store.partition('part/') 47 | const claimStore = store.partition('claim/') 48 | 49 | const blobStorage = new BlobStore(blobStore, config.signer, config.publicUploadURL) 50 | 51 | const context = { 52 | // Ucanto config 53 | id: config.signer, 54 | signer: config.signer, 55 | errorReporter: { catch: err => console.error(err) }, 56 | 57 | // Access service config 58 | email: { sendValidation: async input => console.log('Sending email:', input) }, 59 | url: config.publicApiURL, // URL used in validation emails 60 | 61 | // store/add config 62 | maxUploadSize: config.maxUploadSize, 63 | 64 | // Stores 65 | storeTable: new StoreStore(storeStore), 66 | uploadTable: new UploadStore(uploadStore), 67 | provisionsStorage: new ProvisionsStore(subscriptionStore, consumerStore, spaceMetricsStore, [config.signer.did()]), 68 | delegationsStorage: new DelegationsStore(delegationsStore), 69 | rateLimitsStorage: new RateLimitsStore(rateLimitsStore), 70 | plansStorage: new PlansStore(customerStore), 71 | subscriptionsStorage: new SubscriptionsStore(consumerStore), 72 | revocationsStorage: new RevocationsStore(revocationsStore), 73 | usageStorage: new UsageStore(spaceDiffStore, spaceSnapshotStore), 74 | // used on store/add to determine if status = 'upload' or status = 'done' in response [X] 75 | carStoreBucket: new CARStore(blobStorage), 76 | // on upload/add we write root => CAR(s) mapping [X] 77 | dudewhereBucket: new PartsStore(partsStore), 78 | 79 | // filecoin storefront 80 | // taskStore, // [X] 81 | // receiptStore, // [X] 82 | 83 | // aggregatorId, // aggregator service DID, will be replaced with aggregator service config [X] 84 | // pieceStore, // [X] 85 | // filecoinSubmitQueue, // [X] 86 | // pieceOfferQueue, // [X] 87 | 88 | // dealTrackerService, // connection and invocation config for deal tracker service 89 | 90 | // Content Claims 91 | claimStore: new ClaimStore(claimStore) 92 | } 93 | 94 | // Create a location claim when a CAR is added 95 | context.carStoreBucket.addEventListener('put', async e => { 96 | if (e instanceof CARPutEvent) { 97 | await createAndStoreLocationClaim(e.link) 98 | } 99 | }) 100 | 101 | // Create CAR index when CAR is added 102 | context.carStoreBucket.addEventListener('put', async e => { 103 | if (e instanceof CARPutEvent) { 104 | const carStream = await blobStorage.stream(e.link.multihash) 105 | if (!carStream.ok) return console.error('failed to stream CAR from blob store', carStream.error) 106 | 107 | const { readable, writable } = new TransformStream() 108 | const writer = MultihashIndexSortedWriter.createWriter({ writer: writable.getWriter() }) 109 | 110 | const [, bytes] = await Promise.all([ 111 | carStream.ok 112 | .pipeThrough(new CARReaderStream()) 113 | .pipeTo(new WritableStream({ 114 | write: async block => { await writer.add(block.cid, block.offset) }, 115 | close: async () => { await writer.close() } 116 | })), 117 | concatStream(readable) 118 | ]) 119 | 120 | const putResult = await blobStorage.put(bytes) 121 | if (!putResult.ok) return console.error('failed to store CAR index', putResult.error) 122 | 123 | const { digest } = putResult.ok 124 | const includes = Link.create(MultihashIndexSortedWriter.codec, digest) 125 | 126 | console.log(`Indexed CAR: ${e.link} => ${includes}`) 127 | 128 | await createAndStoreLocationClaim(includes) 129 | 130 | const claim = await createInclusionClaim({ 131 | issuer: config.signer, 132 | audience: config.signer, 133 | proofs: [] 134 | }, e.link, includes) 135 | 136 | const archive = await claim.archive() 137 | if (archive.error) return console.error('failed to archive delegation', archive.error) 138 | 139 | await context.claimStore.put({ 140 | claim: claim.cid, 141 | bytes: archive.ok, 142 | content: e.link.multihash, 143 | // @ts-expect-error 144 | value: claim.capabilities[0] 145 | }) 146 | 147 | console.log(`Content inclusion claimed: ${e.link} includes: ${includes}`) 148 | } 149 | }) 150 | 151 | // Create a partition claim when `upload/add` is invoked 152 | context.uploadTable.addEventListener('add', async e => { 153 | if (e instanceof UploadAddEvent) { 154 | const claim = await createPartitionClaim({ 155 | issuer: config.signer, 156 | audience: config.signer, 157 | proofs: [] 158 | }, e.root, e.shards) 159 | 160 | const archive = await claim.archive() 161 | if (archive.error) return console.error('failed to archive delegation', archive.error) 162 | 163 | await context.claimStore.put({ 164 | claim: claim.cid, 165 | bytes: archive.ok, 166 | content: e.root.multihash, 167 | // @ts-expect-error 168 | value: claim.capabilities[0] 169 | }) 170 | 171 | console.log(`Content partition claimed: ${e.root} parts: ${e.shards.map(s => String(s))}`) 172 | } 173 | }) 174 | 175 | /** 176 | * @param {import('multiformats').UnknownLink} content 177 | */ 178 | const createAndStoreLocationClaim = async (content) => { 179 | const location = new URL(`/blob/${base58btc.encode(content.multihash.bytes)}`, config.publicUploadURL) 180 | const claim = await createLocationClaim({ 181 | issuer: config.signer, 182 | audience: config.signer, 183 | proofs: [] 184 | }, content, location) 185 | 186 | const archive = await claim.archive() 187 | if (archive.error) return console.error('failed to archive delegation', archive.error) 188 | 189 | await context.claimStore.put({ 190 | claim: claim.cid, 191 | bytes: archive.ok, 192 | content: content.multihash, 193 | // @ts-expect-error 194 | value: claim.capabilities[0] 195 | }) 196 | console.log(`Content location claimed: ${content} location: ${location}`) 197 | } 198 | 199 | const server = createServer(context) 200 | 201 | console.log(config.banner) 202 | console.log(`Service DID: ${config.signer.did()} (${config.signer.toDIDKey()})`) 203 | 204 | const httpServer = http.createServer(async (req, res) => { 205 | if (req.url !== '/') console.log(`${req.method} ${req.url}`) 206 | 207 | // GET /validate-email ////////////////////////////////////////////////////// 208 | if (req.method === 'GET' && req.url === '/version') { 209 | res.statusCode = 200 210 | res.setHeader('Content-Type', 'application/json') 211 | res.write(JSON.stringify({ 212 | name: config.pkg.name, 213 | version: config.pkg.version, 214 | did: config.signer.did(), 215 | publicKey: config.signer.toDIDKey() 216 | })) 217 | return res.end() 218 | } 219 | if (req.method === 'GET' && req.url?.startsWith('/validate-email?')) { 220 | res.statusCode = 200 221 | res.setHeader('Content-Type', 'text/html') 222 | res.write(await fs.promises.readFile(`${import.meta.dirname}/validate-email.html`)) 223 | return res.end() 224 | } 225 | 226 | // POST /validate-email ///////////////////////////////////////////////////// 227 | if (req.method === 'POST' && req.url?.startsWith('/validate-email?')) { 228 | const { searchParams } = new URL(req.url, config.publicApiURL) 229 | const authResult = await authorize(searchParams.get('ucan') ?? '', context) 230 | if (authResult.error) { 231 | console.error(new Error('failed authorization', { cause: authResult.error })) 232 | res.statusCode = 500 233 | res.setHeader('Content-Type', 'text/html') 234 | res.write(`Oops something went wrong: ${authResult.error.message}`) 235 | return res.end() 236 | } 237 | 238 | const customer = DIDMailto.fromEmail(authResult.ok.email) 239 | const account = `placeholder:acc-${Date.now()}` 240 | const product = 'did:web:starter.local.web3.storage' 241 | console.log(`Skipping payment flow and initializing ${customer} with plan ${product}`) 242 | const initResult = await context.plansStorage.initialize(customer, account, product) 243 | if (initResult.error && initResult.error.name !== 'CustomerExists') { 244 | console.error(new Error('failed customer initialization', { cause: initResult.error })) 245 | res.statusCode = 500 246 | res.setHeader('Content-Type', 'text/html') 247 | res.write(`Oops something went wrong: ${initResult.error.message}`) 248 | return res.end() 249 | } 250 | 251 | res.statusCode = 200 252 | res.setHeader('Content-Type', 'text/html') 253 | res.write(await fs.promises.readFile(`${import.meta.dirname}/validated-email.html`)) 254 | return res.end() 255 | } 256 | 257 | // PUT /blob/:multihash ///////////////////////////////////////////////////// 258 | if (req.method === 'PUT' && req.url?.startsWith('/blob/')) { 259 | // TODO: validate signed URL 260 | const chunks = [] 261 | for await (const chunk of req) { 262 | chunks.push(chunk) 263 | } 264 | const bytes = Buffer.concat(chunks) 265 | const result = await context.carStoreBucket.put(bytes) 266 | if (result.ok) console.log(`Stored: ${result.ok.link} (${bytes.length} bytes)`) 267 | return res.end() 268 | } 269 | 270 | // GET /blob/:multihash ///////////////////////////////////////////////////// 271 | if (req.method === 'GET' && req.url?.startsWith('/blob/')) { 272 | let digest 273 | try { 274 | const url = new URL(req.url, config.publicApiURL) 275 | digest = Digest.decode(base58btc.decode(url.pathname.split('/')[2])) 276 | } catch (err) { 277 | res.statusCode = 400 278 | res.write(`invalid multihash: ${err.message}`) 279 | return res.end() 280 | } 281 | 282 | let range 283 | if (req.headers.range) { 284 | try { 285 | range = parseRange(req.headers.range) 286 | console.log(`Range: ${req.headers.range}`) 287 | } catch (err) { 288 | res.statusCode = 400 289 | res.write(`invalid range: ${err.message}`) 290 | return res.end() 291 | } 292 | } 293 | const result = await blobStorage.stream(digest, { range }) 294 | if (result.error) { 295 | console.error('failed to read blob', result.error, range) 296 | res.statusCode = result.error.name === 'RecordNotFound' ? 404 : 500 297 | res.write('failed to read blob') 298 | return res.end() 299 | } 300 | 301 | return await result.ok.pipeTo(Writable.toWeb(res)) 302 | } 303 | 304 | // GET /claims/:cid ///////////////////////////////////////////////////////// 305 | if (req.method === 'GET' && req.url?.startsWith('/claims/')) { 306 | const url = new URL(req.url, config.publicApiURL) 307 | 308 | let link 309 | try { 310 | link = Link.parse(url.pathname.split('/')[2]) 311 | } catch (err) { 312 | res.statusCode = 400 313 | res.write(`invalid CID: ${err.message}`) 314 | return res.end() 315 | } 316 | 317 | const walkcsv = url.searchParams.get('walk') 318 | const walk = new Set(walkcsv ? walkcsv.split(',') : []) 319 | const readable = walkClaims({ claimFetcher: context.claimStore }, link, walk) 320 | res.setHeader('Content-Type', 'application/vnd.ipld.car; version=1;') 321 | return await readable.pipeThrough(new CARWriterStream()).pipeTo(Writable.toWeb(res)) 322 | } 323 | 324 | // POST / /////////////////////////////////////////////////////////////////// 325 | if (req.method === 'POST' && req.url === '/') { 326 | const chunks = [] 327 | for await (const chunk of req) { 328 | chunks.push(chunk) 329 | } 330 | const response = await server.request({ 331 | method: req.method ?? 'POST', 332 | // @ts-expect-error 333 | headers: req.headers, 334 | body: Buffer.concat(chunks) 335 | }) 336 | res.statusCode = response.status ?? 200 337 | for (const [k, v] of Object.entries(response.headers)) { 338 | res.setHeader(k, v) 339 | } 340 | res.write(response.body) 341 | return res.end() 342 | } 343 | 344 | res.statusCode = 404 345 | res.end() 346 | }) 347 | 348 | httpServer.listen(config.apiPort, () => { 349 | console.log(`Server listening on :${config.apiPort}`) 350 | }) 351 | -------------------------------------------------------------------------------- /pnpm-lock.yaml: -------------------------------------------------------------------------------- 1 | lockfileVersion: '6.0' 2 | 3 | settings: 4 | autoInstallPeers: true 5 | excludeLinksFromLockfile: false 6 | 7 | dependencies: 8 | '@ipld/dag-cbor': 9 | specifier: ^9.2.0 10 | version: 9.2.0 11 | '@ucanto/principal': 12 | specifier: ^9.0.0 13 | version: 9.0.0 14 | '@ucanto/server': 15 | specifier: ^9.0.1 16 | version: 9.0.1 17 | '@ucanto/transport': 18 | specifier: ^9.1.0 19 | version: 9.1.0 20 | '@web3-storage/content-claims': 21 | specifier: ^4.0.3 22 | version: 4.0.3 23 | '@web3-storage/did-mailto': 24 | specifier: ^2.1.0 25 | version: 2.1.0 26 | '@web3-storage/pail': 27 | specifier: 0.6.0-alpha.4 28 | version: 0.6.0-alpha.4 29 | '@web3-storage/upload-api': 30 | specifier: ^8.4.1 31 | version: 8.4.1 32 | blockstore-fs: 33 | specifier: ^1.1.10 34 | version: 1.1.10 35 | cardex: 36 | specifier: ^3.0.2 37 | version: 3.0.2 38 | carstream: 39 | specifier: ^2.0.0 40 | version: 2.0.0 41 | dotenv: 42 | specifier: ^16.4.5 43 | version: 16.4.5 44 | http-range-parse: 45 | specifier: ^1.0.0 46 | version: 1.0.0 47 | lnmap: 48 | specifier: ^2.0.0 49 | version: 2.0.0 50 | lnset: 51 | specifier: ^1.2.0 52 | version: 1.2.0 53 | multiformats: 54 | specifier: ^13.1.0 55 | version: 13.1.0 56 | nanoid: 57 | specifier: ^5.0.6 58 | version: 5.0.6 59 | p-defer: 60 | specifier: ^4.0.0 61 | version: 4.0.0 62 | p-queue: 63 | specifier: ^8.0.1 64 | version: 8.0.1 65 | 66 | devDependencies: 67 | '@types/node': 68 | specifier: ^20.11.29 69 | version: 20.11.29 70 | '@ucanto/interface': 71 | specifier: ^9.0.0 72 | version: 9.0.0 73 | cli-table3: 74 | specifier: ^0.6.4 75 | version: 0.6.4 76 | entail: 77 | specifier: ^2.1.2 78 | version: 2.1.2 79 | sade: 80 | specifier: ^1.8.1 81 | version: 1.8.1 82 | 83 | packages: 84 | 85 | /@chainsafe/is-ip@2.0.2: 86 | resolution: {integrity: sha512-ndGqEMG1W5WkGagaqOZHpPU172AGdxr+LD15sv3WIUvT5oCFUrG1Y0CW/v2Egwj4JXEvSibaIIIqImsm98y1nA==} 87 | dev: false 88 | 89 | /@chainsafe/netmask@2.0.0: 90 | resolution: {integrity: sha512-I3Z+6SWUoaljh3TBzCnCxjlUyN8tA+NAk5L6m9IxvCf1BENQTePzPMis97CoN/iMW1St3WN+AWCCRp+TTBRiDg==} 91 | dependencies: 92 | '@chainsafe/is-ip': 2.0.2 93 | dev: false 94 | 95 | /@colors/colors@1.5.0: 96 | resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==} 97 | engines: {node: '>=0.1.90'} 98 | requiresBuild: true 99 | dev: true 100 | optional: true 101 | 102 | /@ipld/car@5.3.0: 103 | resolution: {integrity: sha512-OB8LVvJeVAFFGluNIkZeDZ/aGeoekFKsuIvNT9I5sJIb5WekQuW5+lekjQ7Z7mZ7DBKuke/kI4jBT1j0/akU1w==} 104 | engines: {node: '>=16.0.0', npm: '>=7.0.0'} 105 | dependencies: 106 | '@ipld/dag-cbor': 9.2.0 107 | cborg: 4.1.1 108 | multiformats: 13.1.0 109 | varint: 6.0.0 110 | dev: false 111 | 112 | /@ipld/dag-cbor@9.2.0: 113 | resolution: {integrity: sha512-N14oMy0q4gM6OuZkIpisKe0JBSjf1Jb39VI+7jMLiWX9124u1Z3Fdj/Tag1NA0cVxxqWDh0CqsjcVfOKtelPDA==} 114 | engines: {node: '>=16.0.0', npm: '>=7.0.0'} 115 | dependencies: 116 | cborg: 4.1.1 117 | multiformats: 13.1.0 118 | 119 | /@ipld/dag-json@10.2.0: 120 | resolution: {integrity: sha512-O9YLUrl3d3WbVz7v1WkajFkyfOLEe2Fep+wor4fgVe0ywxzrivrj437NiPcVyB+2EDdFn/Q7tCHFf8YVhDf8ZA==} 121 | engines: {node: '>=16.0.0', npm: '>=7.0.0'} 122 | dependencies: 123 | cborg: 4.1.1 124 | multiformats: 13.1.0 125 | 126 | /@ipld/dag-ucan@3.4.0: 127 | resolution: {integrity: sha512-sW4R43w3DbEdoGWWJZCwsblwXa600HCanG9p2w1MJPVBNTNjhvqc3XI0uEqKhT2oqKWrND7uInVtcPmZme7hhA==} 128 | dependencies: 129 | '@ipld/dag-cbor': 9.2.0 130 | '@ipld/dag-json': 10.2.0 131 | multiformats: 11.0.2 132 | 133 | /@leichtgewicht/ip-codec@2.0.4: 134 | resolution: {integrity: sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A==} 135 | dev: false 136 | 137 | /@libp2p/interface@1.1.4: 138 | resolution: {integrity: sha512-gJXQycTF50tI02X/IlReAav4XoGPs3Yr917vNXsTUsZQRzQaPjbvKfXqA5hkLFpZ1lnxQ8wto/EVw4ca4XaL1A==} 139 | dependencies: 140 | '@multiformats/multiaddr': 12.2.1 141 | it-pushable: 3.2.3 142 | it-stream-types: 2.0.1 143 | multiformats: 13.1.0 144 | progress-events: 1.0.0 145 | uint8arraylist: 2.4.8 146 | dev: false 147 | 148 | /@libp2p/logger@4.0.7: 149 | resolution: {integrity: sha512-oyICns7G18S4eDhbFHUwZ7gLQnZTBVQtUMmMgEmrs8LnQu2GvXADxmQAPPkKtLNSCvRudg4hN3hP04Y+vNvlBQ==} 150 | dependencies: 151 | '@libp2p/interface': 1.1.4 152 | '@multiformats/multiaddr': 12.2.1 153 | debug: 4.3.4 154 | interface-datastore: 8.2.11 155 | multiformats: 13.1.0 156 | transitivePeerDependencies: 157 | - supports-color 158 | dev: false 159 | 160 | /@multiformats/dns@1.0.4: 161 | resolution: {integrity: sha512-ecv1m13cIPZmMqr5ZYESnr5ZSaPBpIjp7dDlQEiKIrnBzpV+lm8a20hvC3odRLK23QDfhVLO7dTq2VJIaBR5tw==} 162 | dependencies: 163 | '@types/dns-packet': 5.6.5 164 | buffer: 6.0.3 165 | dns-packet: 5.6.1 166 | hashlru: 2.3.0 167 | p-queue: 8.0.1 168 | progress-events: 1.0.0 169 | uint8arrays: 5.0.3 170 | dev: false 171 | 172 | /@multiformats/multiaddr@12.2.1: 173 | resolution: {integrity: sha512-UwjoArBbv64FlaetV4DDwh+PUMfzXUBltxQwdh+uTYnGFzVa8ZfJsn1vt1RJlJ6+Xtrm3RMekF/B+K338i2L5Q==} 174 | dependencies: 175 | '@chainsafe/is-ip': 2.0.2 176 | '@chainsafe/netmask': 2.0.0 177 | '@libp2p/interface': 1.1.4 178 | '@multiformats/dns': 1.0.4 179 | multiformats: 13.1.0 180 | uint8-varint: 2.0.4 181 | uint8arrays: 5.0.3 182 | dev: false 183 | 184 | /@noble/curves@1.4.0: 185 | resolution: {integrity: sha512-p+4cb332SFCrReJkCYe8Xzm0OWi4Jji5jVdIZRL/PmacmDkFNw6MrrV+gGpiPxLHbV+zKFRywUWbaseT+tZRXg==} 186 | dependencies: 187 | '@noble/hashes': 1.4.0 188 | dev: false 189 | 190 | /@noble/ed25519@1.7.3: 191 | resolution: {integrity: sha512-iR8GBkDt0Q3GyaVcIu7mSsVIqnFbkbRzGLWlvhwunacoLwt4J3swfKhfaM6rN6WY+TBGoYT1GtT1mIh2/jGbRQ==} 192 | dev: false 193 | 194 | /@noble/hashes@1.3.3: 195 | resolution: {integrity: sha512-V7/fPHgl+jsVPXqqeOzT8egNj2iBIVt+ECeMMG8TdcnTikP3oaBtUVqpT/gYCR68aEBJSF+XbYUxStjbFMqIIA==} 196 | engines: {node: '>= 16'} 197 | dev: false 198 | 199 | /@noble/hashes@1.4.0: 200 | resolution: {integrity: sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==} 201 | engines: {node: '>= 16'} 202 | dev: false 203 | 204 | /@nodelib/fs.scandir@2.1.5: 205 | resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} 206 | engines: {node: '>= 8'} 207 | dependencies: 208 | '@nodelib/fs.stat': 2.0.5 209 | run-parallel: 1.2.0 210 | dev: true 211 | 212 | /@nodelib/fs.stat@2.0.5: 213 | resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} 214 | engines: {node: '>= 8'} 215 | dev: true 216 | 217 | /@nodelib/fs.walk@1.2.8: 218 | resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} 219 | engines: {node: '>= 8'} 220 | dependencies: 221 | '@nodelib/fs.scandir': 2.1.5 222 | fastq: 1.17.1 223 | dev: true 224 | 225 | /@scure/base@1.1.5: 226 | resolution: {integrity: sha512-Brj9FiG2W1MRQSTB212YVPRrcbjkv48FoZi/u4l/zds/ieRrqsh7aUf6CLwkAq61oKXr/ZlTzlY66gLIj3TFTQ==} 227 | dev: false 228 | 229 | /@scure/bip39@1.2.2: 230 | resolution: {integrity: sha512-HYf9TUXG80beW+hGAt3TRM8wU6pQoYur9iNypTROm42dorCGmLnFe3eWjz3gOq6G62H2WRh0FCzAR1PI+29zIA==} 231 | dependencies: 232 | '@noble/hashes': 1.3.3 233 | '@scure/base': 1.1.5 234 | dev: false 235 | 236 | /@types/dns-packet@5.6.5: 237 | resolution: {integrity: sha512-qXOC7XLOEe43ehtWJCMnQXvgcIpv6rPmQ1jXT98Ad8A3TB1Ue50jsCbSSSyuazScEuZ/Q026vHbrOTVkmwA+7Q==} 238 | dependencies: 239 | '@types/node': 20.11.29 240 | dev: false 241 | 242 | /@types/node@20.11.29: 243 | resolution: {integrity: sha512-P99thMkD/1YkCvAtOd6/zGedKNA0p2fj4ZpjCzcNiSCBWgm3cNRTBfa/qjFnsKkkojxu4vVLtWpesnZ9+ap+gA==} 244 | dependencies: 245 | undici-types: 5.26.5 246 | 247 | /@types/retry@0.12.1: 248 | resolution: {integrity: sha512-xoDlM2S4ortawSWORYqsdU+2rxdh4LRW9ytc3zmT37RIKQh6IHyKwwtKhKis9ah8ol07DCkZxPt8BBvPjC6v4g==} 249 | dev: false 250 | 251 | /@ucanto/client@9.0.0: 252 | resolution: {integrity: sha512-Fl8ZGuWoVQygBtLISPlFb5Ej/LKUofghTTAT4kjFNc8WB9bD7AS+yvSPowwd+4uTnxfEOeKWV2lzO1+gRxQF0w==} 253 | dependencies: 254 | '@ucanto/core': 9.0.1 255 | '@ucanto/interface': 9.0.0 256 | dev: false 257 | 258 | /@ucanto/core@9.0.1: 259 | resolution: {integrity: sha512-SsYvKCO3FD27roTVcg8ASxnixjn+j96sPlijpVq1uBUxq7SmuNxNPYFZqpxXKj2R4gty/Oc8XTse12ebB9Kofg==} 260 | dependencies: 261 | '@ipld/car': 5.3.0 262 | '@ipld/dag-cbor': 9.2.0 263 | '@ipld/dag-ucan': 3.4.0 264 | '@ucanto/interface': 9.0.0 265 | multiformats: 11.0.2 266 | dev: false 267 | 268 | /@ucanto/interface@9.0.0: 269 | resolution: {integrity: sha512-Y9185yj+CRNpT43EAHTe9MpskCgU9DyWvmYyLMMmF40w+ujp6EYy5JVI/gVjJAsh+2Y9ruvWHOF0M+21TnLQyg==} 270 | dependencies: 271 | '@ipld/dag-ucan': 3.4.0 272 | multiformats: 11.0.2 273 | 274 | /@ucanto/principal@9.0.0: 275 | resolution: {integrity: sha512-3KpaZ0mNycDnDx2WJ9p5qnhTlc4YLFqmuClBpNJcGLk+begaeH7dUlzfxNtloSvZAeB67G03Y883CqiVhN6ZmA==} 276 | dependencies: 277 | '@ipld/dag-ucan': 3.4.0 278 | '@noble/curves': 1.4.0 279 | '@noble/ed25519': 1.7.3 280 | '@noble/hashes': 1.4.0 281 | '@ucanto/interface': 9.0.0 282 | multiformats: 11.0.2 283 | one-webcrypto: 1.0.3 284 | dev: false 285 | 286 | /@ucanto/server@9.0.1: 287 | resolution: {integrity: sha512-EGhgKLjPgvM39j86WxSD7UoR0rr7jpTMclCOcpOEVC9r91sob8BReW2i7cm1zPvhSNFqS8rLjlGEgUIAhdAxmg==} 288 | dependencies: 289 | '@ucanto/core': 9.0.1 290 | '@ucanto/interface': 9.0.0 291 | '@ucanto/principal': 9.0.0 292 | '@ucanto/validator': 9.0.1 293 | dev: false 294 | 295 | /@ucanto/transport@9.1.0: 296 | resolution: {integrity: sha512-3pLXEg9YIH0NN1faBh0Xaioxbb2JtPL+4AFtQtmO8LnRyqGnTahZwwaM8XFL5eMBAp0pYDoZaQ6wdMce0t1cAQ==} 297 | dependencies: 298 | '@ucanto/core': 9.0.1 299 | '@ucanto/interface': 9.0.0 300 | dev: false 301 | 302 | /@ucanto/validator@9.0.1: 303 | resolution: {integrity: sha512-H9GMOXHNW3vCv36eQZN1/h8zOXHEljRV5yNZ/huyOaJLVAKxt7Va1Ww8VBf2Ho/ac6P7jwvQRT7WgxaXx1/3Hg==} 304 | dependencies: 305 | '@ipld/car': 5.3.0 306 | '@ipld/dag-cbor': 9.2.0 307 | '@ucanto/core': 9.0.1 308 | '@ucanto/interface': 9.0.0 309 | multiformats: 11.0.2 310 | dev: false 311 | 312 | /@web3-storage/access@18.2.0: 313 | resolution: {integrity: sha512-EME/HViwasLsXmhGC3j93nxPwL3hfWHb5uD5sOw79PBDIGcPHYE3L9w3TQSDeKPXbA+mA4J7uWbAoTcoDkElSg==} 314 | dependencies: 315 | '@ipld/car': 5.3.0 316 | '@ipld/dag-ucan': 3.4.0 317 | '@scure/bip39': 1.2.2 318 | '@ucanto/client': 9.0.0 319 | '@ucanto/core': 9.0.1 320 | '@ucanto/interface': 9.0.0 321 | '@ucanto/principal': 9.0.0 322 | '@ucanto/transport': 9.1.0 323 | '@ucanto/validator': 9.0.1 324 | '@web3-storage/capabilities': 13.1.1 325 | '@web3-storage/did-mailto': 2.1.0 326 | bigint-mod-arith: 3.3.1 327 | conf: 11.0.2 328 | multiformats: 12.1.3 329 | one-webcrypto: github.com/web3-storage/one-webcrypto/5148cd14d5489a8ac4cd38223870e02db15a2382 330 | p-defer: 4.0.0 331 | type-fest: 4.12.0 332 | uint8arrays: 4.0.10 333 | dev: false 334 | 335 | /@web3-storage/capabilities@13.1.1: 336 | resolution: {integrity: sha512-yQwrjhqwXGc1z8FCs7dCMsNp+G1LCrPq8RWCrflHA0rlISyMez6DQQpOJrCfao/MSk30nzPSzIm+FX/k3+8knw==} 337 | dependencies: 338 | '@ucanto/core': 9.0.1 339 | '@ucanto/interface': 9.0.0 340 | '@ucanto/principal': 9.0.0 341 | '@ucanto/transport': 9.1.0 342 | '@ucanto/validator': 9.0.1 343 | '@web3-storage/data-segment': 3.2.0 344 | dev: false 345 | 346 | /@web3-storage/content-claims@4.0.3: 347 | resolution: {integrity: sha512-PtGYLlS5258uOC57K4kJKJHPWdDeYjI3o38i+uxiHfM8cra5CwszE/82/peyUNyxm2VZVPNj8+LltB4HBQbaZA==} 348 | dependencies: 349 | '@ucanto/client': 9.0.0 350 | '@ucanto/interface': 9.0.0 351 | '@ucanto/server': 9.0.1 352 | '@ucanto/transport': 9.1.0 353 | carstream: 1.1.1 354 | multiformats: 12.1.3 355 | dev: false 356 | 357 | /@web3-storage/data-segment@3.2.0: 358 | resolution: {integrity: sha512-SM6eNumXzrXiQE2/J59+eEgCRZNYPxKhRoHX2QvV3/scD4qgcf4g+paWBc3UriLEY1rCboygGoPsnqYJNyZyfA==} 359 | dependencies: 360 | '@ipld/dag-cbor': 9.2.0 361 | multiformats: 11.0.2 362 | sync-multihash-sha2: 1.0.0 363 | dev: false 364 | 365 | /@web3-storage/data-segment@4.0.0: 366 | resolution: {integrity: sha512-AnNyJp3wHMa7LBzguQzm4rmXSi8vQBz4uFs+jiXnSNtLR5dAqHfhMvi9XdWonWPYvxNvT5ZhYCSF0mpDjymqKg==} 367 | dependencies: 368 | '@ipld/dag-cbor': 9.2.0 369 | multiformats: 11.0.2 370 | sync-multihash-sha2: 1.0.0 371 | dev: false 372 | 373 | /@web3-storage/did-mailto@2.1.0: 374 | resolution: {integrity: sha512-TRmfSXj1IhtX3ESurSNOylZSBKi0z/VJNoMLpof+AVRdovgZjjocpiePQTs2pfHKqHTHfJXc9AboWyK4IKTWMw==} 375 | engines: {node: '>=16.15'} 376 | dev: false 377 | 378 | /@web3-storage/filecoin-api@4.4.0: 379 | resolution: {integrity: sha512-mT3QRC1Cbu0SDHMQjo1ZJFEH8dL4+8H6++wZOIm401bFSHXEamTkydQT3DfX9IHXAFSab/7kwkQ93yDwOgtXiA==} 380 | engines: {node: '>=16.15'} 381 | dependencies: 382 | '@ipld/dag-ucan': 3.4.0 383 | '@ucanto/client': 9.0.0 384 | '@ucanto/core': 9.0.1 385 | '@ucanto/interface': 9.0.0 386 | '@ucanto/server': 9.0.1 387 | '@ucanto/transport': 9.1.0 388 | '@web3-storage/capabilities': 13.1.1 389 | '@web3-storage/data-segment': 4.0.0 390 | p-map: 6.0.0 391 | dev: false 392 | 393 | /@web3-storage/pail@0.6.0-alpha.4: 394 | resolution: {integrity: sha512-pB77kAsV+dPamT3MmrGxsjPDpm6tRhYrm6Hj44ezxiHUI8EKU9eoMgjSSjd9Fj3gMsFdROL653aoiNHgkxngUg==} 395 | hasBin: true 396 | dependencies: 397 | '@ipld/car': 5.3.0 398 | '@ipld/dag-cbor': 9.2.0 399 | archy: 1.0.0 400 | cli-color: 2.0.4 401 | multiformats: 12.1.3 402 | sade: 1.8.1 403 | dev: false 404 | 405 | /@web3-storage/upload-api@8.4.1: 406 | resolution: {integrity: sha512-ZN8ttHwWQlap3dBGWOhtG+JR42z7vS2l2Ni0ouZfkS6PcsH3sJE8l9ntm8QK4dcw4hMsuk0P6BN4z3Z6X1/hCA==} 407 | engines: {node: '>=16.15'} 408 | dependencies: 409 | '@ucanto/client': 9.0.0 410 | '@ucanto/interface': 9.0.0 411 | '@ucanto/principal': 9.0.0 412 | '@ucanto/server': 9.0.1 413 | '@ucanto/transport': 9.1.0 414 | '@ucanto/validator': 9.0.1 415 | '@web3-storage/access': 18.2.0 416 | '@web3-storage/capabilities': 13.1.1 417 | '@web3-storage/did-mailto': 2.1.0 418 | '@web3-storage/filecoin-api': 4.4.0 419 | multiformats: 12.1.3 420 | p-retry: 5.1.2 421 | dev: false 422 | 423 | /ajv-formats@2.1.1(ajv@8.12.0): 424 | resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==} 425 | peerDependencies: 426 | ajv: ^8.0.0 427 | peerDependenciesMeta: 428 | ajv: 429 | optional: true 430 | dependencies: 431 | ajv: 8.12.0 432 | dev: false 433 | 434 | /ajv@8.12.0: 435 | resolution: {integrity: sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==} 436 | dependencies: 437 | fast-deep-equal: 3.1.3 438 | json-schema-traverse: 1.0.0 439 | require-from-string: 2.0.2 440 | uri-js: 4.4.1 441 | dev: false 442 | 443 | /ansi-regex@5.0.1: 444 | resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} 445 | engines: {node: '>=8'} 446 | dev: true 447 | 448 | /archy@1.0.0: 449 | resolution: {integrity: sha512-Xg+9RwCg/0p32teKdGMPTPnVXKD0w3DfHnFTficozsAgsvq2XenPJq/MYpzzQ/v8zrOyJn6Ds39VA4JIDwFfqw==} 450 | dev: false 451 | 452 | /atomically@2.0.2: 453 | resolution: {integrity: sha512-Xfmb4q5QV7uqTlVdMSTtO5eF4DCHfNOdaPyKlbFShkzeNP+3lj3yjjcbdjSmEY4+pDBKJ9g26aP+ImTe88UHoQ==} 454 | dependencies: 455 | stubborn-fs: 1.2.5 456 | when-exit: 2.1.2 457 | dev: false 458 | 459 | /balanced-match@1.0.2: 460 | resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} 461 | dev: false 462 | 463 | /base64-js@1.5.1: 464 | resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} 465 | dev: false 466 | 467 | /bigint-mod-arith@3.3.1: 468 | resolution: {integrity: sha512-pX/cYW3dCa87Jrzv6DAr8ivbbJRzEX5yGhdt8IutnX/PCIXfpx+mabWNK/M8qqh+zQ0J3thftUBHW0ByuUlG0w==} 469 | engines: {node: '>=10.4.0'} 470 | dev: false 471 | 472 | /blockstore-core@4.4.0: 473 | resolution: {integrity: sha512-tjOJAJMPWlqahqCjn5awLJz2eZeJnrGOBA0OInBFK69/FfPZbSID2t7s5jFcBRhGaglca56BzG4t5XOV3MPxOQ==} 474 | dependencies: 475 | '@libp2p/logger': 4.0.7 476 | err-code: 3.0.1 477 | interface-blockstore: 5.2.10 478 | interface-store: 5.1.8 479 | it-drain: 3.0.5 480 | it-filter: 3.0.4 481 | it-merge: 3.0.3 482 | it-pushable: 3.2.3 483 | multiformats: 13.1.0 484 | transitivePeerDependencies: 485 | - supports-color 486 | dev: false 487 | 488 | /blockstore-fs@1.1.10: 489 | resolution: {integrity: sha512-Dg0mbdma0OY4NEk78efcAAiG5ZrMcIVrM7s+0e2p4uavnvrcBT6vDj5ITfnRfid3idKHOoCYShGEi9ENNgJg1A==} 490 | dependencies: 491 | blockstore-core: 4.4.0 492 | fast-write-atomic: 0.2.1 493 | interface-blockstore: 5.2.10 494 | interface-store: 5.1.8 495 | it-glob: 2.0.6 496 | it-map: 3.0.5 497 | it-parallel-batch: 3.0.4 498 | multiformats: 13.1.0 499 | transitivePeerDependencies: 500 | - supports-color 501 | dev: false 502 | 503 | /brace-expansion@2.0.1: 504 | resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} 505 | dependencies: 506 | balanced-match: 1.0.2 507 | dev: false 508 | 509 | /braces@3.0.2: 510 | resolution: {integrity: sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==} 511 | engines: {node: '>=8'} 512 | dependencies: 513 | fill-range: 7.0.1 514 | dev: true 515 | 516 | /buffer@6.0.3: 517 | resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} 518 | dependencies: 519 | base64-js: 1.5.1 520 | ieee754: 1.2.1 521 | dev: false 522 | 523 | /cardex@3.0.2: 524 | resolution: {integrity: sha512-0/w/34Uroad916W5Kn0CEAeIKH5Nw69Yv8Z4c2iVSSRzQPMOZbjUEygFZOkSDbDKXy6SgHinDoXrs72idZZ5WQ==} 525 | hasBin: true 526 | dependencies: 527 | '@ipld/car': 5.3.0 528 | multiformats: 13.1.0 529 | sade: 1.8.1 530 | uint8arrays: 5.0.3 531 | varint: 6.0.0 532 | dev: false 533 | 534 | /carstream@1.1.1: 535 | resolution: {integrity: sha512-cgn3TqHo6SPsHBTfM5QgXngv6HtwgO1bKCHcdS35vBrweLcYrIG/+UboCbvnIGA0k8NtAYl/DvDdej/9pZGZxQ==} 536 | dependencies: 537 | '@ipld/dag-cbor': 9.2.0 538 | multiformats: 12.1.3 539 | uint8arraylist: 2.4.8 540 | dev: false 541 | 542 | /carstream@2.0.0: 543 | resolution: {integrity: sha512-vmxqp4Quj4rVdkoXBzcQ3UzeH0JR7aXLr9rkx07pLp0gtPJyRxIU+9HZb94lKsaXxogM6EFRApEHusINPjsUuw==} 544 | dependencies: 545 | '@ipld/dag-cbor': 9.2.0 546 | multiformats: 13.1.0 547 | uint8arraylist: 2.4.8 548 | dev: false 549 | 550 | /cborg@4.1.1: 551 | resolution: {integrity: sha512-XBEXWdr1N0pjAX0l2a+BKmWGIMDYoLrrqgAWWETtaqn3HC4zn0dZoNO8vC+8oDZfDLMyVRfRD3kMuoYgT67KBQ==} 552 | hasBin: true 553 | 554 | /cli-color@2.0.4: 555 | resolution: {integrity: sha512-zlnpg0jNcibNrO7GG9IeHH7maWFeCz+Ja1wx/7tZNU5ASSSSZ+/qZciM0/LHCYxSdqv5h2sdbQ/PXYdOuetXvA==} 556 | engines: {node: '>=0.10'} 557 | dependencies: 558 | d: 1.0.2 559 | es5-ext: 0.10.64 560 | es6-iterator: 2.0.3 561 | memoizee: 0.4.15 562 | timers-ext: 0.1.7 563 | dev: false 564 | 565 | /cli-table3@0.6.4: 566 | resolution: {integrity: sha512-Lm3L0p+/npIQWNIiyF/nAn7T5dnOwR3xNTHXYEBFBFVPXzCVNZ5lqEC/1eo/EVfpDsQ1I+TX4ORPQgp+UI0CRw==} 567 | engines: {node: 10.* || >= 12.*} 568 | dependencies: 569 | string-width: 4.2.3 570 | optionalDependencies: 571 | '@colors/colors': 1.5.0 572 | dev: true 573 | 574 | /conf@11.0.2: 575 | resolution: {integrity: sha512-jjyhlQ0ew/iwmtwsS2RaB6s8DBifcE2GYBEaw2SJDUY/slJJbNfY4GlDVzOs/ff8cM/Wua5CikqXgbFl5eu85A==} 576 | engines: {node: '>=14.16'} 577 | dependencies: 578 | ajv: 8.12.0 579 | ajv-formats: 2.1.1(ajv@8.12.0) 580 | atomically: 2.0.2 581 | debounce-fn: 5.1.2 582 | dot-prop: 7.2.0 583 | env-paths: 3.0.0 584 | json-schema-typed: 8.0.1 585 | semver: 7.6.0 586 | dev: false 587 | 588 | /d@1.0.2: 589 | resolution: {integrity: sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw==} 590 | engines: {node: '>=0.12'} 591 | dependencies: 592 | es5-ext: 0.10.64 593 | type: 2.7.2 594 | dev: false 595 | 596 | /debounce-fn@5.1.2: 597 | resolution: {integrity: sha512-Sr4SdOZ4vw6eQDvPYNxHogvrxmCIld/VenC5JbNrFwMiwd7lY/Z18ZFfo+EWNG4DD9nFlAujWAo/wGuOPHmy5A==} 598 | engines: {node: '>=12'} 599 | dependencies: 600 | mimic-fn: 4.0.0 601 | dev: false 602 | 603 | /debug@4.3.4: 604 | resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} 605 | engines: {node: '>=6.0'} 606 | peerDependencies: 607 | supports-color: '*' 608 | peerDependenciesMeta: 609 | supports-color: 610 | optional: true 611 | dependencies: 612 | ms: 2.1.2 613 | dev: false 614 | 615 | /dequal@2.0.3: 616 | resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} 617 | engines: {node: '>=6'} 618 | dev: true 619 | 620 | /diff@5.2.0: 621 | resolution: {integrity: sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==} 622 | engines: {node: '>=0.3.1'} 623 | dev: true 624 | 625 | /dir-glob@3.0.1: 626 | resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} 627 | engines: {node: '>=8'} 628 | dependencies: 629 | path-type: 4.0.0 630 | dev: true 631 | 632 | /dns-packet@5.6.1: 633 | resolution: {integrity: sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw==} 634 | engines: {node: '>=6'} 635 | dependencies: 636 | '@leichtgewicht/ip-codec': 2.0.4 637 | dev: false 638 | 639 | /dot-prop@7.2.0: 640 | resolution: {integrity: sha512-Ol/IPXUARn9CSbkrdV4VJo7uCy1I3VuSiWCaFSg+8BdUOzF9n3jefIpcgAydvUZbTdEBZs2vEiTiS9m61ssiDA==} 641 | engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} 642 | dependencies: 643 | type-fest: 2.19.0 644 | dev: false 645 | 646 | /dotenv@16.4.5: 647 | resolution: {integrity: sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==} 648 | engines: {node: '>=12'} 649 | dev: false 650 | 651 | /emoji-regex@8.0.0: 652 | resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} 653 | dev: true 654 | 655 | /entail@2.1.2: 656 | resolution: {integrity: sha512-/icW51VHeJo5j6z6/80vO6R7zAdvHDODHYcc2jItrhRvP/zfTlm2b+xcEkp/Vt3UI6R5651Stw0AGpE1Gzkm6Q==} 657 | hasBin: true 658 | dependencies: 659 | dequal: 2.0.3 660 | globby: 13.1.4 661 | kleur: 4.1.5 662 | sade: 1.8.1 663 | uvu: 0.5.6 664 | dev: true 665 | 666 | /env-paths@3.0.0: 667 | resolution: {integrity: sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A==} 668 | engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} 669 | dev: false 670 | 671 | /err-code@3.0.1: 672 | resolution: {integrity: sha512-GiaH0KJUewYok+eeY05IIgjtAe4Yltygk9Wqp1V5yVWLdhf0hYZchRjNIT9bb0mSwRcIusT3cx7PJUf3zEIfUA==} 673 | dev: false 674 | 675 | /es5-ext@0.10.64: 676 | resolution: {integrity: sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg==} 677 | engines: {node: '>=0.10'} 678 | requiresBuild: true 679 | dependencies: 680 | es6-iterator: 2.0.3 681 | es6-symbol: 3.1.4 682 | esniff: 2.0.1 683 | next-tick: 1.1.0 684 | dev: false 685 | 686 | /es6-iterator@2.0.3: 687 | resolution: {integrity: sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==} 688 | dependencies: 689 | d: 1.0.2 690 | es5-ext: 0.10.64 691 | es6-symbol: 3.1.4 692 | dev: false 693 | 694 | /es6-symbol@3.1.4: 695 | resolution: {integrity: sha512-U9bFFjX8tFiATgtkJ1zg25+KviIXpgRvRHS8sau3GfhVzThRQrOeksPeT0BWW2MNZs1OEWJ1DPXOQMn0KKRkvg==} 696 | engines: {node: '>=0.12'} 697 | dependencies: 698 | d: 1.0.2 699 | ext: 1.7.0 700 | dev: false 701 | 702 | /es6-weak-map@2.0.3: 703 | resolution: {integrity: sha512-p5um32HOTO1kP+w7PRnB+5lQ43Z6muuMuIMffvDN8ZB4GcnjLBV6zGStpbASIMk4DCAvEaamhe2zhyCb/QXXsA==} 704 | dependencies: 705 | d: 1.0.2 706 | es5-ext: 0.10.64 707 | es6-iterator: 2.0.3 708 | es6-symbol: 3.1.4 709 | dev: false 710 | 711 | /esniff@2.0.1: 712 | resolution: {integrity: sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg==} 713 | engines: {node: '>=0.10'} 714 | dependencies: 715 | d: 1.0.2 716 | es5-ext: 0.10.64 717 | event-emitter: 0.3.5 718 | type: 2.7.2 719 | dev: false 720 | 721 | /event-emitter@0.3.5: 722 | resolution: {integrity: sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==} 723 | dependencies: 724 | d: 1.0.2 725 | es5-ext: 0.10.64 726 | dev: false 727 | 728 | /eventemitter3@5.0.1: 729 | resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==} 730 | dev: false 731 | 732 | /ext@1.7.0: 733 | resolution: {integrity: sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==} 734 | dependencies: 735 | type: 2.7.2 736 | dev: false 737 | 738 | /fast-deep-equal@3.1.3: 739 | resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} 740 | dev: false 741 | 742 | /fast-glob@3.3.2: 743 | resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==} 744 | engines: {node: '>=8.6.0'} 745 | dependencies: 746 | '@nodelib/fs.stat': 2.0.5 747 | '@nodelib/fs.walk': 1.2.8 748 | glob-parent: 5.1.2 749 | merge2: 1.4.1 750 | micromatch: 4.0.5 751 | dev: true 752 | 753 | /fast-write-atomic@0.2.1: 754 | resolution: {integrity: sha512-WvJe06IfNYlr+6cO3uQkdKdy3Cb1LlCJSF8zRs2eT8yuhdbSlR9nIt+TgQ92RUxiRrQm+/S7RARnMfCs5iuAjw==} 755 | dev: false 756 | 757 | /fastq@1.17.1: 758 | resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==} 759 | dependencies: 760 | reusify: 1.0.4 761 | dev: true 762 | 763 | /fill-range@7.0.1: 764 | resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==} 765 | engines: {node: '>=8'} 766 | dependencies: 767 | to-regex-range: 5.0.1 768 | dev: true 769 | 770 | /glob-parent@5.1.2: 771 | resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} 772 | engines: {node: '>= 6'} 773 | dependencies: 774 | is-glob: 4.0.3 775 | dev: true 776 | 777 | /globby@13.1.4: 778 | resolution: {integrity: sha512-iui/IiiW+QrJ1X1hKH5qwlMQyv34wJAYwH1vrf8b9kBA4sNiif3gKsMHa+BrdnOpEudWjpotfa7LrTzB1ERS/g==} 779 | engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} 780 | dependencies: 781 | dir-glob: 3.0.1 782 | fast-glob: 3.3.2 783 | ignore: 5.3.1 784 | merge2: 1.4.1 785 | slash: 4.0.0 786 | dev: true 787 | 788 | /hashlru@2.3.0: 789 | resolution: {integrity: sha512-0cMsjjIC8I+D3M44pOQdsy0OHXGLVz6Z0beRuufhKa0KfaD2wGwAev6jILzXsd3/vpnNQJmWyZtIILqM1N+n5A==} 790 | dev: false 791 | 792 | /http-range-parse@1.0.0: 793 | resolution: {integrity: sha512-8xfObVjVGz3VGdiMbbsxoMhxgZrcLOagvzf5rcTfwU2OmmyWJib6Y9rtyjjIFFJ8J/UUEaik4U6AmvnC70YLiw==} 794 | dev: false 795 | 796 | /ieee754@1.2.1: 797 | resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} 798 | dev: false 799 | 800 | /ignore@5.3.1: 801 | resolution: {integrity: sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==} 802 | engines: {node: '>= 4'} 803 | dev: true 804 | 805 | /interface-blockstore@5.2.10: 806 | resolution: {integrity: sha512-9K48hTvBCGsKVD3pF4ILgDcf+W2P/gq0oxLcsHGB6E6W6nDutYkzR+7k7bCs9REHrBEfKzcVDEKieiuNM9WRZg==} 807 | dependencies: 808 | interface-store: 5.1.8 809 | multiformats: 13.1.0 810 | dev: false 811 | 812 | /interface-datastore@8.2.11: 813 | resolution: {integrity: sha512-9E0iXehfp/j0UbZ2mvlYB4K9pP7uQBCppfuy8WHs1EHF6wLQrM9+zwyX+8Qt6HnH4GKZRyXX/CNXm6oD4+QYgA==} 814 | dependencies: 815 | interface-store: 5.1.8 816 | uint8arrays: 5.0.3 817 | dev: false 818 | 819 | /interface-store@5.1.8: 820 | resolution: {integrity: sha512-7na81Uxkl0vqk0CBPO5PvyTkdaJBaezwUJGsMOz7riPOq0rJt+7W31iaopaMICWea/iykUsvNlPx/Tc+MxC3/w==} 821 | dev: false 822 | 823 | /is-extglob@2.1.1: 824 | resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} 825 | engines: {node: '>=0.10.0'} 826 | dev: true 827 | 828 | /is-fullwidth-code-point@3.0.0: 829 | resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} 830 | engines: {node: '>=8'} 831 | dev: true 832 | 833 | /is-glob@4.0.3: 834 | resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} 835 | engines: {node: '>=0.10.0'} 836 | dependencies: 837 | is-extglob: 2.1.1 838 | dev: true 839 | 840 | /is-number@7.0.0: 841 | resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} 842 | engines: {node: '>=0.12.0'} 843 | dev: true 844 | 845 | /is-promise@2.2.2: 846 | resolution: {integrity: sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==} 847 | dev: false 848 | 849 | /it-batch@3.0.4: 850 | resolution: {integrity: sha512-WRu2mqOYIs+T9k7+yxSK9VJdk0UE4R0jKQsWQcti5c6vhb1FhjC2+yCB5XBrctQ9edNfCMU/wVzdDj8qSwimbA==} 851 | dev: false 852 | 853 | /it-drain@3.0.5: 854 | resolution: {integrity: sha512-qYFe4SWdvs9oJGUY5bSjvmiLUMLzFEODNOQUdYdCIkuIgQF+AUB2INhM4yQ09buJ2rhHKDFxvTD/+yUq6qg0XA==} 855 | dev: false 856 | 857 | /it-filter@3.0.4: 858 | resolution: {integrity: sha512-e0sz+st4sudK/zH6GZ/gRTRP8A/ADuJFCYDmRgMbZvR79y5+v4ZXav850bBZk5wL9zXaYZFxS1v/6Qi+Vjwh5g==} 859 | dependencies: 860 | it-peekable: 3.0.3 861 | dev: false 862 | 863 | /it-glob@2.0.6: 864 | resolution: {integrity: sha512-4C6ccz4nhqrq7yZMzBr3MsKhyL+rlnLXIPceyGG6ogl3Lx3eeWMv1RtlySJwFi6q+jVcPyTpeYt/xftwI2JEQQ==} 865 | dependencies: 866 | minimatch: 9.0.3 867 | dev: false 868 | 869 | /it-map@3.0.5: 870 | resolution: {integrity: sha512-hB0TDXo/h4KSJJDSRLgAPmDroiXP6Fx1ck4Bzl3US9hHfZweTKsuiP0y4gXuTMcJlS6vj0bb+f70rhkD47ZA3w==} 871 | dependencies: 872 | it-peekable: 3.0.3 873 | dev: false 874 | 875 | /it-merge@3.0.3: 876 | resolution: {integrity: sha512-FYVU15KC5pb/GQX1Ims+lee8d4pdqGVCpWr0lkNj8o4xuNo7jY71k6GuEiWdP+T7W1bJqewSxX5yoTy5yZpRVA==} 877 | dependencies: 878 | it-pushable: 3.2.3 879 | dev: false 880 | 881 | /it-parallel-batch@3.0.4: 882 | resolution: {integrity: sha512-O1omh8ss8+UtXiMjE+8kM5C20DT0Ma4VtKVfrSHOJU0UHZ+iWBXarabzPYEp+WiuQmrv+klDPPlTZ9KaLN9xOA==} 883 | dependencies: 884 | it-batch: 3.0.4 885 | dev: false 886 | 887 | /it-peekable@3.0.3: 888 | resolution: {integrity: sha512-Wx21JX/rMzTEl9flx3DGHuPV1KQFGOl8uoKfQtmZHgPQtGb89eQ6RyVd82h3HuP9Ghpt0WgBDlmmdWeHXqyx7w==} 889 | dev: false 890 | 891 | /it-pushable@3.2.3: 892 | resolution: {integrity: sha512-gzYnXYK8Y5t5b/BnJUr7glfQLO4U5vyb05gPx/TyTw+4Bv1zM9gFk4YsOrnulWefMewlphCjKkakFvj1y99Tcg==} 893 | dependencies: 894 | p-defer: 4.0.0 895 | dev: false 896 | 897 | /it-stream-types@2.0.1: 898 | resolution: {integrity: sha512-6DmOs5r7ERDbvS4q8yLKENcj6Yecr7QQTqWApbZdfAUTEC947d+PEha7PCqhm//9oxaLYL7TWRekwhoXl2s6fg==} 899 | engines: {node: '>=16.0.0', npm: '>=7.0.0'} 900 | dev: false 901 | 902 | /json-schema-traverse@1.0.0: 903 | resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} 904 | dev: false 905 | 906 | /json-schema-typed@8.0.1: 907 | resolution: {integrity: sha512-XQmWYj2Sm4kn4WeTYvmpKEbyPsL7nBsb647c7pMe6l02/yx2+Jfc4dT6UZkEXnIUb5LhD55r2HPsJ1milQ4rDg==} 908 | dev: false 909 | 910 | /kleur@4.1.5: 911 | resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} 912 | engines: {node: '>=6'} 913 | dev: true 914 | 915 | /lnmap@2.0.0: 916 | resolution: {integrity: sha512-3s4tWz2VfK/mUlI82070Xy67qi1o7lok9fI5hle3paLLT2Rlp9Zy8wBz+yir9YZPxvhbhkYZqoZhOrrVcfnVeQ==} 917 | dependencies: 918 | multiformats: 13.1.0 919 | dev: false 920 | 921 | /lnset@1.2.0: 922 | resolution: {integrity: sha512-Rx1y8a6N/x6D/BQIm+a44niZzxDpcbAIkBAFC2KVHjevbxyDbHUTxrltK9/5paHsQUk5/z+0FPwuzw4rAdH1vg==} 923 | dependencies: 924 | multiformats: 12.1.3 925 | dev: false 926 | 927 | /lru-cache@6.0.0: 928 | resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} 929 | engines: {node: '>=10'} 930 | dependencies: 931 | yallist: 4.0.0 932 | dev: false 933 | 934 | /lru-queue@0.1.0: 935 | resolution: {integrity: sha512-BpdYkt9EvGl8OfWHDQPISVpcl5xZthb+XPsbELj5AQXxIC8IriDZIQYjBJPEm5rS420sjZ0TLEzRcq5KdBhYrQ==} 936 | dependencies: 937 | es5-ext: 0.10.64 938 | dev: false 939 | 940 | /memoizee@0.4.15: 941 | resolution: {integrity: sha512-UBWmJpLZd5STPm7PMUlOw/TSy972M+z8gcyQ5veOnSDRREz/0bmpyTfKt3/51DhEBqCZQn1udM/5flcSPYhkdQ==} 942 | dependencies: 943 | d: 1.0.2 944 | es5-ext: 0.10.64 945 | es6-weak-map: 2.0.3 946 | event-emitter: 0.3.5 947 | is-promise: 2.2.2 948 | lru-queue: 0.1.0 949 | next-tick: 1.1.0 950 | timers-ext: 0.1.7 951 | dev: false 952 | 953 | /merge2@1.4.1: 954 | resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} 955 | engines: {node: '>= 8'} 956 | dev: true 957 | 958 | /micromatch@4.0.5: 959 | resolution: {integrity: sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==} 960 | engines: {node: '>=8.6'} 961 | dependencies: 962 | braces: 3.0.2 963 | picomatch: 2.3.1 964 | dev: true 965 | 966 | /mimic-fn@4.0.0: 967 | resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==} 968 | engines: {node: '>=12'} 969 | dev: false 970 | 971 | /minimatch@9.0.3: 972 | resolution: {integrity: sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==} 973 | engines: {node: '>=16 || 14 >=14.17'} 974 | dependencies: 975 | brace-expansion: 2.0.1 976 | dev: false 977 | 978 | /mri@1.2.0: 979 | resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} 980 | engines: {node: '>=4'} 981 | 982 | /ms@2.1.2: 983 | resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} 984 | dev: false 985 | 986 | /multiformats@11.0.2: 987 | resolution: {integrity: sha512-b5mYMkOkARIuVZCpvijFj9a6m5wMVLC7cf/jIPd5D/ARDOfLC5+IFkbgDXQgcU2goIsTD/O9NY4DI/Mt4OGvlg==} 988 | engines: {node: '>=16.0.0', npm: '>=7.0.0'} 989 | 990 | /multiformats@12.1.3: 991 | resolution: {integrity: sha512-eajQ/ZH7qXZQR2AgtfpmSMizQzmyYVmCql7pdhldPuYQi4atACekbJaQplk6dWyIi10jCaFnd6pqvcEFXjbaJw==} 992 | engines: {node: '>=16.0.0', npm: '>=7.0.0'} 993 | dev: false 994 | 995 | /multiformats@13.1.0: 996 | resolution: {integrity: sha512-HzdtdBwxsIkzpeXzhQ5mAhhuxcHbjEHH+JQoxt7hG/2HGFjjwyolLo7hbaexcnhoEuV4e0TNJ8kkpMjiEYY4VQ==} 997 | 998 | /nanoid@5.0.6: 999 | resolution: {integrity: sha512-rRq0eMHoGZxlvaFOUdK1Ev83Bd1IgzzR+WJ3IbDJ7QOSdAxYjlurSPqFs9s4lJg29RT6nPwizFtJhQS6V5xgiA==} 1000 | engines: {node: ^18 || >=20} 1001 | hasBin: true 1002 | dev: false 1003 | 1004 | /next-tick@1.1.0: 1005 | resolution: {integrity: sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==} 1006 | dev: false 1007 | 1008 | /one-webcrypto@1.0.3: 1009 | resolution: {integrity: sha512-fu9ywBVBPx0gS9K0etIROTiCkvI5S1TDjFsYFb3rC1ewFxeOqsbzq7aIMBHsYfrTHBcGXJaONXXjTl8B01cW1Q==} 1010 | dev: false 1011 | 1012 | /p-defer@4.0.0: 1013 | resolution: {integrity: sha512-Vb3QRvQ0Y5XnF40ZUWW7JfLogicVh/EnA5gBIvKDJoYpeI82+1E3AlB9yOcKFS0AhHrWVnAQO39fbR0G99IVEQ==} 1014 | engines: {node: '>=12'} 1015 | dev: false 1016 | 1017 | /p-map@6.0.0: 1018 | resolution: {integrity: sha512-T8BatKGY+k5rU+Q/GTYgrEf2r4xRMevAN5mtXc2aPc4rS1j3s+vWTaO2Wag94neXuCAUAs8cxBL9EeB5EA6diw==} 1019 | engines: {node: '>=16'} 1020 | dev: false 1021 | 1022 | /p-queue@8.0.1: 1023 | resolution: {integrity: sha512-NXzu9aQJTAzbBqOt2hwsR63ea7yvxJc0PwN/zobNAudYfb1B7R08SzB4TsLeSbUCuG467NhnoT0oO6w1qRO+BA==} 1024 | engines: {node: '>=18'} 1025 | dependencies: 1026 | eventemitter3: 5.0.1 1027 | p-timeout: 6.1.2 1028 | dev: false 1029 | 1030 | /p-retry@5.1.2: 1031 | resolution: {integrity: sha512-couX95waDu98NfNZV+i/iLt+fdVxmI7CbrrdC2uDWfPdUAApyxT4wmDlyOtR5KtTDmkDO0zDScDjDou9YHhd9g==} 1032 | engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} 1033 | dependencies: 1034 | '@types/retry': 0.12.1 1035 | retry: 0.13.1 1036 | dev: false 1037 | 1038 | /p-timeout@6.1.2: 1039 | resolution: {integrity: sha512-UbD77BuZ9Bc9aABo74gfXhNvzC9Tx7SxtHSh1fxvx3jTLLYvmVhiQZZrJzqqU0jKbN32kb5VOKiLEQI/3bIjgQ==} 1040 | engines: {node: '>=14.16'} 1041 | dev: false 1042 | 1043 | /path-type@4.0.0: 1044 | resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} 1045 | engines: {node: '>=8'} 1046 | dev: true 1047 | 1048 | /picomatch@2.3.1: 1049 | resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} 1050 | engines: {node: '>=8.6'} 1051 | dev: true 1052 | 1053 | /progress-events@1.0.0: 1054 | resolution: {integrity: sha512-zIB6QDrSbPfRg+33FZalluFIowkbV5Xh1xSuetjG+rlC5he6u2dc6VQJ0TbMdlN3R1RHdpOqxEFMKTnQ+itUwA==} 1055 | engines: {node: '>=16.0.0', npm: '>=7.0.0'} 1056 | dev: false 1057 | 1058 | /punycode@2.3.1: 1059 | resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} 1060 | engines: {node: '>=6'} 1061 | dev: false 1062 | 1063 | /queue-microtask@1.2.3: 1064 | resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} 1065 | dev: true 1066 | 1067 | /require-from-string@2.0.2: 1068 | resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} 1069 | engines: {node: '>=0.10.0'} 1070 | dev: false 1071 | 1072 | /retry@0.13.1: 1073 | resolution: {integrity: sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==} 1074 | engines: {node: '>= 4'} 1075 | dev: false 1076 | 1077 | /reusify@1.0.4: 1078 | resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} 1079 | engines: {iojs: '>=1.0.0', node: '>=0.10.0'} 1080 | dev: true 1081 | 1082 | /run-parallel@1.2.0: 1083 | resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} 1084 | dependencies: 1085 | queue-microtask: 1.2.3 1086 | dev: true 1087 | 1088 | /sade@1.8.1: 1089 | resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==} 1090 | engines: {node: '>=6'} 1091 | dependencies: 1092 | mri: 1.2.0 1093 | 1094 | /semver@7.6.0: 1095 | resolution: {integrity: sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==} 1096 | engines: {node: '>=10'} 1097 | hasBin: true 1098 | dependencies: 1099 | lru-cache: 6.0.0 1100 | dev: false 1101 | 1102 | /slash@4.0.0: 1103 | resolution: {integrity: sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==} 1104 | engines: {node: '>=12'} 1105 | dev: true 1106 | 1107 | /string-width@4.2.3: 1108 | resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} 1109 | engines: {node: '>=8'} 1110 | dependencies: 1111 | emoji-regex: 8.0.0 1112 | is-fullwidth-code-point: 3.0.0 1113 | strip-ansi: 6.0.1 1114 | dev: true 1115 | 1116 | /strip-ansi@6.0.1: 1117 | resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} 1118 | engines: {node: '>=8'} 1119 | dependencies: 1120 | ansi-regex: 5.0.1 1121 | dev: true 1122 | 1123 | /stubborn-fs@1.2.5: 1124 | resolution: {integrity: sha512-H2N9c26eXjzL/S/K+i/RHHcFanE74dptvvjM8iwzwbVcWY/zjBbgRqF3K0DY4+OD+uTTASTBvDoxPDaPN02D7g==} 1125 | dev: false 1126 | 1127 | /sync-multihash-sha2@1.0.0: 1128 | resolution: {integrity: sha512-A5gVpmtKF0ov+/XID0M0QRJqF2QxAsj3x/LlDC8yivzgoYCoWkV+XaZPfVu7Vj1T/hYzYS1tfjwboSbXjqocug==} 1129 | dependencies: 1130 | '@noble/hashes': 1.4.0 1131 | dev: false 1132 | 1133 | /timers-ext@0.1.7: 1134 | resolution: {integrity: sha512-b85NUNzTSdodShTIbky6ZF02e8STtVVfD+fu4aXXShEELpozH+bCpJLYMPZbsABN2wDH7fJpqIoXxJpzbf0NqQ==} 1135 | dependencies: 1136 | es5-ext: 0.10.64 1137 | next-tick: 1.1.0 1138 | dev: false 1139 | 1140 | /to-regex-range@5.0.1: 1141 | resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} 1142 | engines: {node: '>=8.0'} 1143 | dependencies: 1144 | is-number: 7.0.0 1145 | dev: true 1146 | 1147 | /type-fest@2.19.0: 1148 | resolution: {integrity: sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==} 1149 | engines: {node: '>=12.20'} 1150 | dev: false 1151 | 1152 | /type-fest@4.12.0: 1153 | resolution: {integrity: sha512-5Y2/pp2wtJk8o08G0CMkuFPCO354FGwk/vbidxrdhRGZfd0tFnb4Qb8anp9XxXriwBgVPjdWbKpGl4J9lJY2jQ==} 1154 | engines: {node: '>=16'} 1155 | dev: false 1156 | 1157 | /type@2.7.2: 1158 | resolution: {integrity: sha512-dzlvlNlt6AXU7EBSfpAscydQ7gXB+pPGsPnfJnZpiNJBDj7IaJzQlBZYGdEi4R9HmPdBv2XmWJ6YUtoTa7lmCw==} 1159 | dev: false 1160 | 1161 | /uint8-varint@2.0.4: 1162 | resolution: {integrity: sha512-FwpTa7ZGA/f/EssWAb5/YV6pHgVF1fViKdW8cWaEarjB8t7NyofSWBdOTyFPaGuUG4gx3v1O3PQ8etsiOs3lcw==} 1163 | dependencies: 1164 | uint8arraylist: 2.4.8 1165 | uint8arrays: 5.0.3 1166 | dev: false 1167 | 1168 | /uint8arraylist@2.4.8: 1169 | resolution: {integrity: sha512-vc1PlGOzglLF0eae1M8mLRTBivsvrGsdmJ5RbK3e+QRvRLOZfZhQROTwH/OfyF3+ZVUg9/8hE8bmKP2CvP9quQ==} 1170 | dependencies: 1171 | uint8arrays: 5.0.3 1172 | dev: false 1173 | 1174 | /uint8arrays@4.0.10: 1175 | resolution: {integrity: sha512-AnJNUGGDJAgFw/eWu/Xb9zrVKEGlwJJCaeInlf3BkecE/zcTobk5YXYIPNQJO1q5Hh1QZrQQHf0JvcHqz2hqoA==} 1176 | dependencies: 1177 | multiformats: 12.1.3 1178 | dev: false 1179 | 1180 | /uint8arrays@5.0.3: 1181 | resolution: {integrity: sha512-6LBuKji28kHjgPJMkQ6GDaBb1lRwIhyOYq6pDGwYMoDPfImE9SkuYENVmR0yu9yGgs2clHUSY9fKDukR+AXfqQ==} 1182 | dependencies: 1183 | multiformats: 13.1.0 1184 | dev: false 1185 | 1186 | /undici-types@5.26.5: 1187 | resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} 1188 | 1189 | /uri-js@4.4.1: 1190 | resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} 1191 | dependencies: 1192 | punycode: 2.3.1 1193 | dev: false 1194 | 1195 | /uvu@0.5.6: 1196 | resolution: {integrity: sha512-+g8ENReyr8YsOc6fv/NVJs2vFdHBnBNdfE49rshrTzDWOlUx4Gq7KOS2GD8eqhy2j+Ejq29+SbKH8yjkAqXqoA==} 1197 | engines: {node: '>=8'} 1198 | hasBin: true 1199 | dependencies: 1200 | dequal: 2.0.3 1201 | diff: 5.2.0 1202 | kleur: 4.1.5 1203 | sade: 1.8.1 1204 | dev: true 1205 | 1206 | /varint@6.0.0: 1207 | resolution: {integrity: sha512-cXEIW6cfr15lFv563k4GuVuW/fiwjknytD37jIOLSdSWuOI6WnO/oKwmP2FQTU2l01LP8/M5TSAJpzUaGe3uWg==} 1208 | dev: false 1209 | 1210 | /when-exit@2.1.2: 1211 | resolution: {integrity: sha512-u9J+toaf3CCxCAzM/484qNAxQE75rFdVgiFEEV8Xps2gzYhf0tx73s1WXDQhkwV17E3MxRMz40m7Ekd2/121Lg==} 1212 | dev: false 1213 | 1214 | /yallist@4.0.0: 1215 | resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} 1216 | dev: false 1217 | 1218 | github.com/web3-storage/one-webcrypto/5148cd14d5489a8ac4cd38223870e02db15a2382: 1219 | resolution: {tarball: https://codeload.github.com/web3-storage/one-webcrypto/tar.gz/5148cd14d5489a8ac4cd38223870e02db15a2382} 1220 | name: one-webcrypto 1221 | version: 1.0.3 1222 | dev: false 1223 | --------------------------------------------------------------------------------