From 55812b03940852f1f91cd0a46b5c093601c854a9 Mon Sep 17 00:00:00 2001 From: devin ivy Date: Fri, 21 Jun 2024 12:41:06 -0400 Subject: [PATCH] Bsky short link service (#4542) * bskylink: scaffold service w/ initial config and schema * bskylink: implement link creation and redirects * bskylink: tidy * bskylink: tests * bskylink: tidy, add error handler * bskylink: add dockerfile * bskylink: add build * bskylink: fix some express plumbing * bskyweb: proxy fallthrough routes to link service redirects * bskyweb: build w/ link proxy * Add AASA to bskylink (#4588) --------- Co-authored-by: Hailey --- .../workflows/build-and-push-bskyweb-aws.yaml | 1 + .../workflows/build-and-push-link-aws.yaml | 55 + Dockerfile.bskylink | 41 + bskylink/package.json | 26 + bskylink/src/bin.ts | 24 + bskylink/src/config.ts | 82 ++ bskylink/src/context.ts | 33 + bskylink/src/db/index.ts | 174 +++ bskylink/src/db/migrations/001-init.ts | 15 + bskylink/src/db/migrations/index.ts | 5 + bskylink/src/db/migrations/provider.ts | 8 + bskylink/src/db/schema.ts | 17 + bskylink/src/index.ts | 45 + bskylink/src/logger.ts | 4 + bskylink/src/routes/create.ts | 111 ++ bskylink/src/routes/health.ts | 20 + bskylink/src/routes/index.ts | 17 + bskylink/src/routes/redirect.ts | 40 + bskylink/src/routes/siteAssociation.ts | 13 + bskylink/src/routes/util.ts | 23 + bskylink/src/util.ts | 8 + bskylink/tests/index.ts | 84 ++ bskylink/tests/infra/_common.sh | 157 +++ bskylink/tests/infra/docker-compose.yaml | 27 + bskylink/tests/infra/with-test-db.sh | 9 + bskylink/tsconfig.json | 10 + bskylink/yarn.lock | 1027 +++++++++++++++++ bskyweb/cmd/bskyweb/main.go | 9 +- bskyweb/cmd/bskyweb/server.go | 34 + 29 files changed, 2118 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/build-and-push-link-aws.yaml create mode 100644 Dockerfile.bskylink create mode 100644 bskylink/package.json create mode 100644 bskylink/src/bin.ts create mode 100644 bskylink/src/config.ts create mode 100644 bskylink/src/context.ts create mode 100644 bskylink/src/db/index.ts create mode 100644 bskylink/src/db/migrations/001-init.ts create mode 100644 bskylink/src/db/migrations/index.ts create mode 100644 bskylink/src/db/migrations/provider.ts create mode 100644 bskylink/src/db/schema.ts create mode 100644 bskylink/src/index.ts create mode 100644 bskylink/src/logger.ts create mode 100644 bskylink/src/routes/create.ts create mode 100644 bskylink/src/routes/health.ts create mode 100644 bskylink/src/routes/index.ts create mode 100644 bskylink/src/routes/redirect.ts create mode 100644 bskylink/src/routes/siteAssociation.ts create mode 100644 bskylink/src/routes/util.ts create mode 100644 bskylink/src/util.ts create mode 100644 bskylink/tests/index.ts create mode 100755 bskylink/tests/infra/_common.sh create mode 100644 bskylink/tests/infra/docker-compose.yaml create mode 100755 bskylink/tests/infra/with-test-db.sh create mode 100644 bskylink/tsconfig.json create mode 100644 bskylink/yarn.lock diff --git a/.github/workflows/build-and-push-bskyweb-aws.yaml b/.github/workflows/build-and-push-bskyweb-aws.yaml index 6eb9485b..bcd759b0 100644 --- a/.github/workflows/build-and-push-bskyweb-aws.yaml +++ b/.github/workflows/build-and-push-bskyweb-aws.yaml @@ -4,6 +4,7 @@ on: push: branches: - main + - divy/bskylink env: REGISTRY: ${{ secrets.AWS_ECR_REGISTRY_USEAST2_PACKAGES_REGISTRY }} diff --git a/.github/workflows/build-and-push-link-aws.yaml b/.github/workflows/build-and-push-link-aws.yaml new file mode 100644 index 00000000..f91af487 --- /dev/null +++ b/.github/workflows/build-and-push-link-aws.yaml @@ -0,0 +1,55 @@ +name: build-and-push-link-aws +on: + push: + branches: + - divy/bskylink + +env: + REGISTRY: ${{ secrets.AWS_ECR_REGISTRY_USEAST2_PACKAGES_REGISTRY }} + USERNAME: ${{ secrets.AWS_ECR_REGISTRY_USEAST2_PACKAGES_USERNAME }} + PASSWORD: ${{ secrets.AWS_ECR_REGISTRY_USEAST2_PACKAGES_PASSWORD }} + IMAGE_NAME: bskylink + +jobs: + link-container-aws: + if: github.repository == 'bluesky-social/social-app' + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + id-token: write + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Setup Docker buildx + uses: docker/setup-buildx-action@v1 + + - name: Log into registry ${{ env.REGISTRY }} + uses: docker/login-action@v2 + with: + registry: ${{ env.REGISTRY }} + username: ${{ env.USERNAME}} + password: ${{ env.PASSWORD }} + + - name: Extract Docker metadata + id: meta + uses: docker/metadata-action@v4 + with: + images: | + ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=sha,enable=true,priority=100,prefix=,suffix=,format=long + + - name: Build and push Docker image + id: build-and-push + uses: docker/build-push-action@v4 + with: + context: . + push: ${{ github.event_name != 'pull_request' }} + file: ./Dockerfile.bskylink + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max diff --git a/Dockerfile.bskylink b/Dockerfile.bskylink new file mode 100644 index 00000000..acde2322 --- /dev/null +++ b/Dockerfile.bskylink @@ -0,0 +1,41 @@ +FROM node:20.11-alpine3.18 as build + +# Move files into the image and install +WORKDIR /app + +COPY ./bskylink/package.json ./ +COPY ./bskylink/yarn.lock ./ +RUN yarn install --frozen-lockfile + +COPY ./bskylink ./ + +# build then prune dev deps +RUN yarn build +RUN yarn install --production --ignore-scripts --prefer-offline + +# Uses assets from build stage to reduce build size +FROM node:20.11-alpine3.18 + +RUN apk add --update dumb-init + +# Avoid zombie processes, handle signal forwarding +ENTRYPOINT ["dumb-init", "--"] + +WORKDIR /app +COPY --from=build /app /app +RUN mkdir /app/data && chown node /app/data + +VOLUME /app/data +EXPOSE 3000 +ENV LINK_PORT=3000 +ENV NODE_ENV=production +# potential perf issues w/ io_uring on this version of node +ENV UV_USE_IO_URING=0 + +# https://github.com/nodejs/docker-node/blob/master/docs/BestPractices.md#non-root-user +USER node +CMD ["node", "--heapsnapshot-signal=SIGUSR2", "--enable-source-maps", "dist/bin.js"] + +LABEL org.opencontainers.image.source=https://github.com/bluesky-social/social-app +LABEL org.opencontainers.image.description="Bsky Link Service" +LABEL org.opencontainers.image.licenses=UNLICENSED diff --git a/bskylink/package.json b/bskylink/package.json new file mode 100644 index 00000000..5fdee206 --- /dev/null +++ b/bskylink/package.json @@ -0,0 +1,26 @@ +{ + "name": "bskylink", + "version": "0.0.0", + "type": "module", + "main": "index.ts", + "scripts": { + "test": "./tests/infra/with-test-db.sh node --loader ts-node/esm --test ./tests/index.ts", + "build": "tsc" + }, + "dependencies": { + "@atproto/common": "^0.4.0", + "body-parser": "^1.20.2", + "cors": "^2.8.5", + "express": "^4.19.2", + "http-terminator": "^3.2.0", + "kysely": "^0.27.3", + "pg": "^8.12.0", + "pino": "^9.2.0", + "uint8arrays": "^5.1.0" + }, + "devDependencies": { + "@types/cors": "^2.8.17", + "@types/pg": "^8.11.6", + "typescript": "^5.4.5" + } +} diff --git a/bskylink/src/bin.ts b/bskylink/src/bin.ts new file mode 100644 index 00000000..17f06884 --- /dev/null +++ b/bskylink/src/bin.ts @@ -0,0 +1,24 @@ +import {Database, envToCfg, httpLogger, LinkService, readEnv} from './index.js' + +async function main() { + const env = readEnv() + const cfg = envToCfg(env) + if (cfg.db.migrationUrl) { + const migrateDb = Database.postgres({ + url: cfg.db.migrationUrl, + schema: cfg.db.schema, + }) + await migrateDb.migrateToLatestOrThrow() + await migrateDb.close() + } + const link = await LinkService.create(cfg) + await link.start() + httpLogger.info('link service is running') + process.on('SIGTERM', async () => { + httpLogger.info('link service is stopping') + await link.destroy() + httpLogger.info('link service is stopped') + }) +} + +main() diff --git a/bskylink/src/config.ts b/bskylink/src/config.ts new file mode 100644 index 00000000..ce409ccc --- /dev/null +++ b/bskylink/src/config.ts @@ -0,0 +1,82 @@ +import {envInt, envList, envStr} from '@atproto/common' + +export type Config = { + service: ServiceConfig + db: DbConfig +} + +export type ServiceConfig = { + port: number + version?: string + hostnames: string[] + appHostname: string +} + +export type DbConfig = { + url: string + migrationUrl?: string + pool: DbPoolConfig + schema?: string +} + +export type DbPoolConfig = { + size: number + maxUses: number + idleTimeoutMs: number +} + +export type Environment = { + port?: number + version?: string + hostnames: string[] + appHostname?: string + dbPostgresUrl?: string + dbPostgresMigrationUrl?: string + dbPostgresSchema?: string + dbPostgresPoolSize?: number + dbPostgresPoolMaxUses?: number + dbPostgresPoolIdleTimeoutMs?: number +} + +export const readEnv = (): Environment => { + return { + port: envInt('LINK_PORT'), + version: envStr('LINK_VERSION'), + hostnames: envList('LINK_HOSTNAMES'), + appHostname: envStr('LINK_APP_HOSTNAME'), + dbPostgresUrl: envStr('LINK_DB_POSTGRES_URL'), + dbPostgresMigrationUrl: envStr('LINK_DB_POSTGRES_MIGRATION_URL'), + dbPostgresSchema: envStr('LINK_DB_POSTGRES_SCHEMA'), + dbPostgresPoolSize: envInt('LINK_DB_POSTGRES_POOL_SIZE'), + dbPostgresPoolMaxUses: envInt('LINK_DB_POSTGRES_POOL_MAX_USES'), + dbPostgresPoolIdleTimeoutMs: envInt( + 'LINK_DB_POSTGRES_POOL_IDLE_TIMEOUT_MS', + ), + } +} + +export const envToCfg = (env: Environment): Config => { + const serviceCfg: ServiceConfig = { + port: env.port ?? 3000, + version: env.version, + hostnames: env.hostnames, + appHostname: env.appHostname || 'bsky.app', + } + if (!env.dbPostgresUrl) { + throw new Error('Must configure postgres url (LINK_DB_POSTGRES_URL)') + } + const dbCfg: DbConfig = { + url: env.dbPostgresUrl, + migrationUrl: env.dbPostgresMigrationUrl, + schema: env.dbPostgresSchema, + pool: { + idleTimeoutMs: env.dbPostgresPoolIdleTimeoutMs ?? 10000, + maxUses: env.dbPostgresPoolMaxUses ?? Infinity, + size: env.dbPostgresPoolSize ?? 10, + }, + } + return { + service: serviceCfg, + db: dbCfg, + } +} diff --git a/bskylink/src/context.ts b/bskylink/src/context.ts new file mode 100644 index 00000000..7e6f2f34 --- /dev/null +++ b/bskylink/src/context.ts @@ -0,0 +1,33 @@ +import {Config} from './config.js' +import Database from './db/index.js' + +export type AppContextOptions = { + cfg: Config + db: Database +} + +export class AppContext { + cfg: Config + db: Database + abortController = new AbortController() + + constructor(private opts: AppContextOptions) { + this.cfg = this.opts.cfg + this.db = this.opts.db + } + + static async fromConfig(cfg: Config, overrides?: Partial) { + const db = Database.postgres({ + url: cfg.db.url, + schema: cfg.db.schema, + poolSize: cfg.db.pool.size, + poolMaxUses: cfg.db.pool.maxUses, + poolIdleTimeoutMs: cfg.db.pool.idleTimeoutMs, + }) + return new AppContext({ + cfg, + db, + ...overrides, + }) + } +} diff --git a/bskylink/src/db/index.ts b/bskylink/src/db/index.ts new file mode 100644 index 00000000..5f201cc0 --- /dev/null +++ b/bskylink/src/db/index.ts @@ -0,0 +1,174 @@ +import assert from 'assert' +import { + Kysely, + KyselyPlugin, + Migrator, + PluginTransformQueryArgs, + PluginTransformResultArgs, + PostgresDialect, + QueryResult, + RootOperationNode, + UnknownRow, +} from 'kysely' +import {default as Pg} from 'pg' + +import {dbLogger as log} from '../logger.js' +import {default as migrations} from './migrations/index.js' +import {DbMigrationProvider} from './migrations/provider.js' +import {DbSchema} from './schema.js' + +export class Database { + migrator: Migrator + destroyed = false + + constructor(public db: Kysely, public cfg: PgConfig) { + this.migrator = new Migrator({ + db, + migrationTableSchema: cfg.schema, + provider: new DbMigrationProvider(migrations), + }) + } + + static postgres(opts: PgOptions): Database { + const {schema, url, txLockNonce} = opts + const pool = + opts.pool ?? + new Pg.Pool({ + connectionString: url, + max: opts.poolSize, + maxUses: opts.poolMaxUses, + idleTimeoutMillis: opts.poolIdleTimeoutMs, + }) + + // Select count(*) and other pg bigints as js integer + Pg.types.setTypeParser(Pg.types.builtins.INT8, n => parseInt(n, 10)) + + // Setup schema usage, primarily for test parallelism (each test suite runs in its own pg schema) + if (schema && !/^[a-z_]+$/i.test(schema)) { + throw new Error(`Postgres schema must only contain [A-Za-z_]: ${schema}`) + } + + pool.on('error', onPoolError) + + const db = new Kysely({ + dialect: new PostgresDialect({pool}), + }) + + return new Database(db, { + pool, + schema, + url, + txLockNonce, + }) + } + + async transaction(fn: (db: Database) => Promise): Promise { + const leakyTxPlugin = new LeakyTxPlugin() + return this.db + .withPlugin(leakyTxPlugin) + .transaction() + .execute(txn => { + const dbTxn = new Database(txn, this.cfg) + return fn(dbTxn) + .catch(async err => { + leakyTxPlugin.endTx() + // ensure that all in-flight queries are flushed & the connection is open + await dbTxn.db.getExecutor().provideConnection(async () => {}) + throw err + }) + .finally(() => leakyTxPlugin.endTx()) + }) + } + + get schema(): string | undefined { + return this.cfg.schema + } + + get isTransaction() { + return this.db.isTransaction + } + + assertTransaction() { + assert(this.isTransaction, 'Transaction required') + } + + assertNotTransaction() { + assert(!this.isTransaction, 'Cannot be in a transaction') + } + + async close(): Promise { + if (this.destroyed) return + await this.db.destroy() + this.destroyed = true + } + + async migrateToOrThrow(migration: string) { + if (this.schema) { + await this.db.schema.createSchema(this.schema).ifNotExists().execute() + } + const {error, results} = await this.migrator.migrateTo(migration) + if (error) { + throw error + } + if (!results) { + throw new Error('An unknown failure occurred while migrating') + } + return results + } + + async migrateToLatestOrThrow() { + if (this.schema) { + await this.db.schema.createSchema(this.schema).ifNotExists().execute() + } + const {error, results} = await this.migrator.migrateToLatest() + if (error) { + throw error + } + if (!results) { + throw new Error('An unknown failure occurred while migrating') + } + return results + } +} + +export default Database + +export type PgConfig = { + pool: Pg.Pool + url: string + schema?: string + txLockNonce?: string +} + +type PgOptions = { + url: string + pool?: Pg.Pool + schema?: string + poolSize?: number + poolMaxUses?: number + poolIdleTimeoutMs?: number + txLockNonce?: string +} + +class LeakyTxPlugin implements KyselyPlugin { + private txOver = false + + endTx() { + this.txOver = true + } + + transformQuery(args: PluginTransformQueryArgs): RootOperationNode { + if (this.txOver) { + throw new Error('tx already failed') + } + return args.node + } + + async transformResult( + args: PluginTransformResultArgs, + ): Promise> { + return args.result + } +} + +const onPoolError = (err: Error) => log.error({err}, 'db pool error') diff --git a/bskylink/src/db/migrations/001-init.ts b/bskylink/src/db/migrations/001-init.ts new file mode 100644 index 00000000..fe3bcf18 --- /dev/null +++ b/bskylink/src/db/migrations/001-init.ts @@ -0,0 +1,15 @@ +import {Kysely} from 'kysely' + +export async function up(db: Kysely): Promise { + await db.schema + .createTable('link') + .addColumn('id', 'varchar', col => col.primaryKey()) + .addColumn('type', 'smallint', col => col.notNull()) // integer enum: 1->starterpack + .addColumn('path', 'varchar', col => col.notNull()) + .addUniqueConstraint('link_path_unique', ['path']) + .execute() +} + +export async function down(db: Kysely): Promise { + await db.schema.dropTable('link').execute() +} diff --git a/bskylink/src/db/migrations/index.ts b/bskylink/src/db/migrations/index.ts new file mode 100644 index 00000000..05e4de93 --- /dev/null +++ b/bskylink/src/db/migrations/index.ts @@ -0,0 +1,5 @@ +import * as init from './001-init.js' + +export default { + '001': init, +} diff --git a/bskylink/src/db/migrations/provider.ts b/bskylink/src/db/migrations/provider.ts new file mode 100644 index 00000000..bef93a48 --- /dev/null +++ b/bskylink/src/db/migrations/provider.ts @@ -0,0 +1,8 @@ +import {Migration, MigrationProvider} from 'kysely' + +export class DbMigrationProvider implements MigrationProvider { + constructor(private migrations: Record) {} + async getMigrations(): Promise> { + return this.migrations + } +} diff --git a/bskylink/src/db/schema.ts b/bskylink/src/db/schema.ts new file mode 100644 index 00000000..8d97f580 --- /dev/null +++ b/bskylink/src/db/schema.ts @@ -0,0 +1,17 @@ +import {Selectable} from 'kysely' + +export type DbSchema = { + link: Link +} + +export interface Link { + id: string + type: LinkType + path: string +} + +export enum LinkType { + StarterPack = 1, +} + +export type LinkEntry = Selectable diff --git a/bskylink/src/index.ts b/bskylink/src/index.ts new file mode 100644 index 00000000..ca425eee --- /dev/null +++ b/bskylink/src/index.ts @@ -0,0 +1,45 @@ +import events from 'node:events' +import http from 'node:http' + +import cors from 'cors' +import express from 'express' +import {createHttpTerminator, HttpTerminator} from 'http-terminator' + +import {Config} from './config.js' +import {AppContext} from './context.js' +import {default as routes, errorHandler} from './routes/index.js' + +export * from './config.js' +export * from './db/index.js' +export * from './logger.js' + +export class LinkService { + public server?: http.Server + private terminator?: HttpTerminator + + constructor(public app: express.Application, public ctx: AppContext) {} + + static async create(cfg: Config): Promise { + let app = express() + app.use(cors()) + + const ctx = await AppContext.fromConfig(cfg) + app = routes(ctx, app) + app.use(errorHandler) + + return new LinkService(app, ctx) + } + + async start() { + this.server = this.app.listen(this.ctx.cfg.service.port) + this.server.keepAliveTimeout = 90000 + this.terminator = createHttpTerminator({server: this.server}) + await events.once(this.server, 'listening') + } + + async destroy() { + this.ctx.abortController.abort() + await this.terminator?.terminate() + await this.ctx.db.close() + } +} diff --git a/bskylink/src/logger.ts b/bskylink/src/logger.ts new file mode 100644 index 00000000..25bb590a --- /dev/null +++ b/bskylink/src/logger.ts @@ -0,0 +1,4 @@ +import {subsystemLogger} from '@atproto/common' + +export const httpLogger = subsystemLogger('bskylink') +export const dbLogger = subsystemLogger('bskylink:db') diff --git a/bskylink/src/routes/create.ts b/bskylink/src/routes/create.ts new file mode 100644 index 00000000..db7c3f80 --- /dev/null +++ b/bskylink/src/routes/create.ts @@ -0,0 +1,111 @@ +import assert from 'node:assert' + +import bodyParser from 'body-parser' +import {Express, Request} from 'express' + +import {AppContext} from '../context.js' +import {LinkType} from '../db/schema.js' +import {randomId} from '../util.js' +import {handler} from './util.js' + +export default function (ctx: AppContext, app: Express) { + return app.post( + '/link', + bodyParser.json(), + handler(async (req, res) => { + let path: string + if (typeof req.body?.path === 'string') { + path = req.body.path + } else { + return res.status(400).json({ + error: 'InvalidPath', + message: '"path" parameter is missing or not a string', + }) + } + if (!path.startsWith('/')) { + return res.status(400).json({ + error: 'InvalidPath', + message: + '"path" parameter must be formatted as a path, starting with a "/"', + }) + } + const parts = getPathParts(path) + if (parts.length === 3 && parts[0] === 'start') { + // link pattern: /start/{did}/{rkey} + if (!parts[1].startsWith('did:')) { + // enforce strong links + return res.status(400).json({ + error: 'InvalidPath', + message: + '"path" parameter for starter pack must contain the actor\'s DID', + }) + } + const id = await ensureLink(ctx, LinkType.StarterPack, parts) + return res.json({url: getUrl(ctx, req, id)}) + } + return res.status(400).json({ + error: 'InvalidPath', + message: '"path" parameter does not have a known format', + }) + }), + ) +} + +const ensureLink = async (ctx: AppContext, type: LinkType, parts: string[]) => { + const normalizedPath = normalizedPathFromParts(parts) + const created = await ctx.db.db + .insertInto('link') + .values({ + id: randomId(), + type, + path: normalizedPath, + }) + .onConflict(oc => oc.column('path').doNothing()) + .returningAll() + .executeTakeFirst() + if (created) { + return created.id + } + const found = await ctx.db.db + .selectFrom('link') + .selectAll() + .where('path', '=', normalizedPath) + .executeTakeFirstOrThrow() + return found.id +} + +const getUrl = (ctx: AppContext, req: Request, id: string) => { + if (!ctx.cfg.service.hostnames.length) { + assert(req.headers.host, 'request must be made with host header') + const baseUrl = + req.protocol === 'http' && req.headers.host.startsWith('localhost:') + ? `http://${req.headers.host}` + : `https://${req.headers.host}` + return `${baseUrl}/${id}` + } + const baseUrl = ctx.cfg.service.hostnames.includes(req.headers.host) + ? `https://${req.headers.host}` + : `https://${ctx.cfg.service.hostnames[0]}` + return `${baseUrl}/${id}` +} + +const normalizedPathFromParts = (parts: string[]): string => { + return ( + '/' + + parts + .map(encodeURIComponent) + .map(part => part.replaceAll('%3A', ':')) // preserve colons + .join('/') + ) +} + +const getPathParts = (path: string): string[] => { + if (path === '/') return [] + if (path.endsWith('/')) { + path = path.slice(0, -1) // ignore trailing slash + } + return path + .slice(1) // remove leading slash + .split('/') + .map(decodeURIComponent) +} diff --git a/bskylink/src/routes/health.ts b/bskylink/src/routes/health.ts new file mode 100644 index 00000000..c8a30c59 --- /dev/null +++ b/bskylink/src/routes/health.ts @@ -0,0 +1,20 @@ +import {Express} from 'express' +import {sql} from 'kysely' + +import {AppContext} from '../context.js' +import {handler} from './util.js' + +export default function (ctx: AppContext, app: Express) { + return app.get( + '/_health', + handler(async (_req, res) => { + const {version} = ctx.cfg.service + try { + await sql`select 1`.execute(ctx.db.db) + return res.send({version}) + } catch (err) { + return res.status(503).send({version, error: 'Service Unavailable'}) + } + }), + ) +} diff --git a/bskylink/src/routes/index.ts b/bskylink/src/routes/index.ts new file mode 100644 index 00000000..f60b99bc --- /dev/null +++ b/bskylink/src/routes/index.ts @@ -0,0 +1,17 @@ +import {Express} from 'express' + +import {AppContext} from '../context.js' +import {default as create} from './create.js' +import {default as health} from './health.js' +import {default as redirect} from './redirect.js' +import {default as siteAssociation} from './siteAssociation.js' + +export * from './util.js' + +export default function (ctx: AppContext, app: Express) { + app = health(ctx, app) // GET /_health + app = siteAssociation(ctx, app) // GET /.well-known/apple-app-site-association + app = create(ctx, app) // POST /link + app = redirect(ctx, app) // GET /:linkId (should go last due to permissive matching) + return app +} diff --git a/bskylink/src/routes/redirect.ts b/bskylink/src/routes/redirect.ts new file mode 100644 index 00000000..7791ea81 --- /dev/null +++ b/bskylink/src/routes/redirect.ts @@ -0,0 +1,40 @@ +import assert from 'node:assert' + +import {DAY, SECOND} from '@atproto/common' +import {Express} from 'express' + +import {AppContext} from '../context.js' +import {handler} from './util.js' + +export default function (ctx: AppContext, app: Express) { + return app.get( + '/:linkId', + handler(async (req, res) => { + const linkId = req.params.linkId + assert( + typeof linkId === 'string', + 'express guarantees id parameter is a string', + ) + const found = await ctx.db.db + .selectFrom('link') + .selectAll() + .where('id', '=', linkId) + .executeTakeFirst() + if (!found) { + // potentially broken or mistyped link— send user to the app + res.setHeader('Location', `https://${ctx.cfg.service.appHostname}`) + res.setHeader('Cache-Control', 'no-store') + return res.status(302).end() + } + // build url from original url in order to preserve query params + const url = new URL( + req.originalUrl, + `https://${ctx.cfg.service.appHostname}`, + ) + url.pathname = found.path + res.setHeader('Location', url.href) + res.setHeader('Cache-Control', `max-age=${(7 * DAY) / SECOND}`) + return res.status(301).end() + }), + ) +} diff --git a/bskylink/src/routes/siteAssociation.ts b/bskylink/src/routes/siteAssociation.ts new file mode 100644 index 00000000..ae3b42e3 --- /dev/null +++ b/bskylink/src/routes/siteAssociation.ts @@ -0,0 +1,13 @@ +import {Express} from 'express' + +import {AppContext} from '../context.js' + +export default function (ctx: AppContext, app: Express) { + return app.get('/.well-known/apple-app-site-association', (req, res) => { + res.json({ + appclips: { + apps: ['B3LX46C5HS.xyz.blueskyweb.app.AppClip'], + }, + }) + }) +} diff --git a/bskylink/src/routes/util.ts b/bskylink/src/routes/util.ts new file mode 100644 index 00000000..bcac64b0 --- /dev/null +++ b/bskylink/src/routes/util.ts @@ -0,0 +1,23 @@ +import {ErrorRequestHandler, Request, RequestHandler, Response} from 'express' + +import {httpLogger} from '../logger.js' + +export type Handler = (req: Request, res: Response) => Awaited + +export const handler = (runHandler: Handler): RequestHandler => { + return async (req, res, next) => { + try { + await runHandler(req, res) + } catch (err) { + next(err) + } + } +} + +export const errorHandler: ErrorRequestHandler = (err, _req, res, next) => { + httpLogger.error({err}, 'request error') + if (res.headersSent) { + return next(err) + } + return res.status(500).end('server error') +} diff --git a/bskylink/src/util.ts b/bskylink/src/util.ts new file mode 100644 index 00000000..0b57dd5c --- /dev/null +++ b/bskylink/src/util.ts @@ -0,0 +1,8 @@ +import {randomBytes} from 'node:crypto' + +import {toString} from 'uint8arrays' + +// 40bit random id of 5-7 characters +export const randomId = () => { + return toString(randomBytes(5), 'base58btc') +} diff --git a/bskylink/tests/index.ts b/bskylink/tests/index.ts new file mode 100644 index 00000000..51449c21 --- /dev/null +++ b/bskylink/tests/index.ts @@ -0,0 +1,84 @@ +import assert from 'node:assert' +import {AddressInfo} from 'node:net' +import {after, before, describe, it} from 'node:test' + +import {Database, envToCfg, LinkService, readEnv} from '../src/index.js' + +describe('link service', async () => { + let linkService: LinkService + let baseUrl: string + before(async () => { + const env = readEnv() + const cfg = envToCfg({ + ...env, + hostnames: ['test.bsky.link'], + appHostname: 'test.bsky.app', + dbPostgresSchema: 'link_test', + dbPostgresUrl: process.env.DB_POSTGRES_URL, + }) + const migrateDb = Database.postgres({ + url: cfg.db.url, + schema: cfg.db.schema, + }) + await migrateDb.migrateToLatestOrThrow() + await migrateDb.close() + linkService = await LinkService.create(cfg) + await linkService.start() + const {port} = linkService.server?.address() as AddressInfo + baseUrl = `http://localhost:${port}` + }) + + after(async () => { + await linkService?.destroy() + }) + + it('creates a starter pack link', async () => { + const link = await getLink('/start/did:example:alice/xxx') + const url = new URL(link) + assert.strictEqual(url.origin, 'https://test.bsky.link') + assert.match(url.pathname, /^\/[a-z0-9]+$/i) + }) + + it('normalizes input paths and provides same link each time.', async () => { + const link1 = await getLink('/start/did%3Aexample%3Abob/yyy') + const link2 = await getLink('/start/did:example:bob/yyy/') + assert.strictEqual(link1, link2) + }) + + it('serves permanent redirect, preserving query params.', async () => { + const link = await getLink('/start/did:example:carol/zzz/') + const [status, location] = await getRedirect(`${link}?a=b`) + assert.strictEqual(status, 301) + const locationUrl = new URL(location) + assert.strictEqual( + locationUrl.pathname + locationUrl.search, + '/start/did:example:carol/zzz?a=b', + ) + }) + + async function getRedirect(link: string): Promise<[number, string]> { + const url = new URL(link) + const base = new URL(baseUrl) + url.protocol = base.protocol + url.host = base.host + const res = await fetch(url, {redirect: 'manual'}) + await res.arrayBuffer() // drain + assert( + res.status === 301 || res.status === 303, + 'response was not a redirect', + ) + return [res.status, res.headers.get('location') ?? ''] + } + + async function getLink(path: string): Promise { + const res = await fetch(new URL('/link', baseUrl), { + method: 'post', + headers: {'content-type': 'application/json'}, + body: JSON.stringify({path}), + }) + assert.strictEqual(res.status, 200) + const payload = await res.json() + assert(typeof payload.url === 'string') + return payload.url + } +}) diff --git a/bskylink/tests/infra/_common.sh b/bskylink/tests/infra/_common.sh new file mode 100755 index 00000000..1587f5c7 --- /dev/null +++ b/bskylink/tests/infra/_common.sh @@ -0,0 +1,157 @@ +#!/usr/bin/env sh + +# Exit if any command fails +set -e + +get_container_id() { + local compose_file=$1 + local service=$2 + if [ -z "${compose_file}" ] || [ -z "${service}" ]; then + echo "usage: get_container_id " + exit 1 + fi + + # first line of jq normalizes for docker compose breaking change, see docker/compose#10958 + docker compose --file $compose_file ps --format json --status running \ + | jq -sc '.[] | if type=="array" then .[] else . end' | jq -s \ + | jq -r '.[]? | select(.Service == "'${service}'") | .ID' +} + +# Exports all environment variables +export_env() { + export_pg_env +} + +# Exports postgres environment variables +export_pg_env() { + # Based on creds in compose.yaml + export PGPORT=5433 + export PGHOST=localhost + export PGUSER=pg + export PGPASSWORD=password + export PGDATABASE=postgres + export DB_POSTGRES_URL="postgresql://pg:password@127.0.0.1:5433/postgres" +} + + +pg_clear() { + local pg_uri=$1 + + for schema_name in `psql "${pg_uri}" -c "SELECT schema_name FROM information_schema.schemata WHERE schema_name NOT LIKE 'pg_%' AND schema_name NOT LIKE 'information_schema';" -t`; do + psql "${pg_uri}" -c "DROP SCHEMA \"${schema_name}\" CASCADE;" + done +} + +pg_init() { + local pg_uri=$1 + + psql "${pg_uri}" -c "CREATE SCHEMA IF NOT EXISTS \"public\";" +} + +main_native() { + local services=${SERVICES} + local postgres_url_env_var=`[[ $services == *"db_test"* ]] && echo "DB_TEST_POSTGRES_URL" || echo "DB_POSTGRES_URL"` + + postgres_url="${!postgres_url_env_var}" + + if [ -n "${postgres_url}" ]; then + echo "Using ${postgres_url_env_var} (${postgres_url}) to connect to postgres." + pg_init "${postgres_url}" + else + echo "Postgres connection string missing did you set ${postgres_url_env_var}?" + exit 1 + fi + + cleanup() { + local services=$@ + + if [ -n "${postgres_url}" ] && [[ $services == *"db_test"* ]]; then + pg_clear "${postgres_url}" &> /dev/null + fi + } + + # trap SIGINT and performs cleanup + trap "on_sigint ${services}" INT + on_sigint() { + cleanup $@ + exit $? + } + + # Run the arguments as a command + DB_POSTGRES_URL="${postgres_url}" \ + "$@" + code=$? + + cleanup ${services} + + exit ${code} +} + +main_docker() { + # Expect a SERVICES env var to be set with the docker service names + local services=${SERVICES} + + dir=$(dirname $0) + compose_file="${dir}/docker-compose.yaml" + + # whether this particular script started the container(s) + started_container=false + + # performs cleanup as necessary, i.e. taking down containers + # if this script started them + cleanup() { + local services=$@ + echo # newline + if $started_container; then + docker compose --file $compose_file rm --force --stop --volumes ${services} + fi + } + + # trap SIGINT and performs cleanup + trap "on_sigint ${services}" INT + on_sigint() { + cleanup $@ + exit $? + } + + # check if all services are running already + not_running=false + for service in $services; do + container_id=$(get_container_id $compose_file $service) + if [ -z $container_id ]; then + not_running=true + break + fi + done + + # if any are missing, recreate all services + if $not_running; then + started_container=true + docker compose --file $compose_file up --wait --force-recreate ${services} + else + echo "all services ${services} are already running" + fi + + # do not exit when following commands fail, so we can intercept exit code & tear down docker + set +e + + # setup environment variables and run args + export_env + "$@" + # save return code for later + code=$? + + # performs cleanup as necessary + cleanup ${services} + exit ${code} +} + +# Main entry point +main() { + if ! docker ps >/dev/null 2>&1; then + echo "Docker unavailable. Running on host." + main_native $@ + else + main_docker $@ + fi +} diff --git a/bskylink/tests/infra/docker-compose.yaml b/bskylink/tests/infra/docker-compose.yaml new file mode 100644 index 00000000..4bc939e0 --- /dev/null +++ b/bskylink/tests/infra/docker-compose.yaml @@ -0,0 +1,27 @@ +version: '3.8' +services: + # An ephermerally-stored postgres database for single-use test runs + db_test: &db_test + image: postgres:14.11-alpine + environment: + - POSTGRES_USER=pg + - POSTGRES_PASSWORD=password + ports: + - '5433:5432' + # Healthcheck ensures db is queryable when `docker-compose up --wait` completes + healthcheck: + test: 'pg_isready -U pg' + interval: 500ms + timeout: 10s + retries: 20 + # A persistently-stored postgres database + db: + <<: *db_test + ports: + - '5432:5432' + healthcheck: + disable: true + volumes: + - link_db:/var/lib/postgresql/data +volumes: + link_db: diff --git a/bskylink/tests/infra/with-test-db.sh b/bskylink/tests/infra/with-test-db.sh new file mode 100755 index 00000000..cc083491 --- /dev/null +++ b/bskylink/tests/infra/with-test-db.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env sh + +# Example usage: +# ./with-test-db.sh psql postgresql://pg:password@localhost:5433/postgres -c 'select 1;' + +dir=$(dirname $0) +. ${dir}/_common.sh + +SERVICES="db_test" main "$@" diff --git a/bskylink/tsconfig.json b/bskylink/tsconfig.json new file mode 100644 index 00000000..3c382acc --- /dev/null +++ b/bskylink/tsconfig.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "module": "NodeNext", + "esModuleInterop": true, + "moduleResolution": "NodeNext", + "outDir": "dist", + "lib": ["ES2021.String"] + }, + "include": ["./src/index.ts", "./src/bin.ts"] + } diff --git a/bskylink/yarn.lock b/bskylink/yarn.lock new file mode 100644 index 00000000..d2fa3145 --- /dev/null +++ b/bskylink/yarn.lock @@ -0,0 +1,1027 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@atproto/common-web@^0.3.0": + version "0.3.0" + resolved "https://registry.yarnpkg.com/@atproto/common-web/-/common-web-0.3.0.tgz#36da8c2c31d8cf8a140c3c8f03223319bf4430bb" + integrity sha512-67VnV6JJyX+ZWyjV7xFQMypAgDmjVaR9ZCuU/QW+mqlqI7fex2uL4Fv+7/jHadgzhuJHVd6OHOvNn0wR5WZYtA== + dependencies: + graphemer "^1.4.0" + multiformats "^9.9.0" + uint8arrays "3.0.0" + zod "^3.21.4" + +"@atproto/common@^0.4.0": + version "0.4.0" + resolved "https://registry.yarnpkg.com/@atproto/common/-/common-0.4.0.tgz#d77696c7eb545426df727837d9ee333b429fe7ef" + integrity sha512-yOXuPlCjT/OK9j+neIGYn9wkxx/AlxQSucysAF0xgwu0Ji8jAtKBf9Jv6R5ObYAjAD/kVUvEYumle+Yq/R9/7g== + dependencies: + "@atproto/common-web" "^0.3.0" + "@ipld/dag-cbor" "^7.0.3" + cbor-x "^1.5.1" + iso-datestring-validator "^2.2.2" + multiformats "^9.9.0" + pino "^8.15.0" + +"@cbor-extract/cbor-extract-darwin-arm64@2.2.0": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@cbor-extract/cbor-extract-darwin-arm64/-/cbor-extract-darwin-arm64-2.2.0.tgz#8d65cb861a99622e1b4a268e2d522d2ec6137338" + integrity sha512-P7swiOAdF7aSi0H+tHtHtr6zrpF3aAq/W9FXx5HektRvLTM2O89xCyXF3pk7pLc7QpaY7AoaE8UowVf9QBdh3w== + +"@cbor-extract/cbor-extract-darwin-x64@2.2.0": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@cbor-extract/cbor-extract-darwin-x64/-/cbor-extract-darwin-x64-2.2.0.tgz#9fbec199c888c5ec485a1839f4fad0485ab6c40a" + integrity sha512-1liF6fgowph0JxBbYnAS7ZlqNYLf000Qnj4KjqPNW4GViKrEql2MgZnAsExhY9LSy8dnvA4C0qHEBgPrll0z0w== + +"@cbor-extract/cbor-extract-linux-arm64@2.2.0": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@cbor-extract/cbor-extract-linux-arm64/-/cbor-extract-linux-arm64-2.2.0.tgz#bf77e0db4a1d2200a5aa072e02210d5043e953ae" + integrity sha512-rQvhNmDuhjTVXSPFLolmQ47/ydGOFXtbR7+wgkSY0bdOxCFept1hvg59uiLPT2fVDuJFuEy16EImo5tE2x3RsQ== + +"@cbor-extract/cbor-extract-linux-arm@2.2.0": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@cbor-extract/cbor-extract-linux-arm/-/cbor-extract-linux-arm-2.2.0.tgz#491335037eb8533ed8e21b139c59f6df04e39709" + integrity sha512-QeBcBXk964zOytiedMPQNZr7sg0TNavZeuUCD6ON4vEOU/25+pLhNN6EDIKJ9VLTKaZ7K7EaAriyYQ1NQ05s/Q== + +"@cbor-extract/cbor-extract-linux-x64@2.2.0": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@cbor-extract/cbor-extract-linux-x64/-/cbor-extract-linux-x64-2.2.0.tgz#672574485ccd24759bf8fb8eab9dbca517d35b97" + integrity sha512-cWLAWtT3kNLHSvP4RKDzSTX9o0wvQEEAj4SKvhWuOVZxiDAeQazr9A+PSiRILK1VYMLeDml89ohxCnUNQNQNCw== + +"@cbor-extract/cbor-extract-win32-x64@2.2.0": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@cbor-extract/cbor-extract-win32-x64/-/cbor-extract-win32-x64-2.2.0.tgz#4b3f07af047f984c082de34b116e765cb9af975f" + integrity sha512-l2M+Z8DO2vbvADOBNLbbh9y5ST1RY5sqkWOg/58GkUPBYou/cuNZ68SGQ644f1CvZ8kcOxyZtw06+dxWHIoN/w== + +"@ipld/dag-cbor@^7.0.3": + version "7.0.3" + resolved "https://registry.yarnpkg.com/@ipld/dag-cbor/-/dag-cbor-7.0.3.tgz#aa31b28afb11a807c3d627828a344e5521ac4a1e" + integrity sha512-1VVh2huHsuohdXC1bGJNE8WR72slZ9XE2T3wbBBq31dm7ZBatmKLLxrB+XAqafxfRFjv08RZmj/W/ZqaM13AuA== + dependencies: + cborg "^1.6.0" + multiformats "^9.5.4" + +"@types/cors@^2.8.17": + version "2.8.17" + resolved "https://registry.yarnpkg.com/@types/cors/-/cors-2.8.17.tgz#5d718a5e494a8166f569d986794e49c48b216b2b" + integrity sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA== + dependencies: + "@types/node" "*" + +"@types/node@*": + version "20.14.2" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.14.2.tgz#a5f4d2bcb4b6a87bffcaa717718c5a0f208f4a18" + integrity sha512-xyu6WAMVwv6AKFLB+e/7ySZVr/0zLCzOa7rSpq6jNwpqOrUbcACDWC+53d4n2QHOnDou0fbIsg8wZu/sxrnI4Q== + dependencies: + undici-types "~5.26.4" + +"@types/pg@^8.11.6": + version "8.11.6" + resolved "https://registry.yarnpkg.com/@types/pg/-/pg-8.11.6.tgz#a2d0fb0a14b53951a17df5197401569fb9c0c54b" + integrity sha512-/2WmmBXHLsfRqzfHW7BNZ8SbYzE8OSk7i3WjFYvfgRHj7S1xj+16Je5fUKv3lVdVzk/zn9TXOqf+avFCFIE0yQ== + dependencies: + "@types/node" "*" + pg-protocol "*" + pg-types "^4.0.1" + +abort-controller@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/abort-controller/-/abort-controller-3.0.0.tgz#eaf54d53b62bae4138e809ca225c8439a6efb392" + integrity sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg== + dependencies: + event-target-shim "^5.0.0" + +accepts@~1.3.8: + version "1.3.8" + resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e" + integrity sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw== + dependencies: + mime-types "~2.1.34" + negotiator "0.6.3" + +array-flatten@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" + integrity sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg== + +atomic-sleep@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/atomic-sleep/-/atomic-sleep-1.0.0.tgz#eb85b77a601fc932cfe432c5acd364a9e2c9075b" + integrity sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ== + +base64-js@^1.3.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" + integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== + +body-parser@1.20.2, body-parser@^1.20.2: + version "1.20.2" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.2.tgz#6feb0e21c4724d06de7ff38da36dad4f57a747fd" + integrity sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA== + dependencies: + bytes "3.1.2" + content-type "~1.0.5" + debug "2.6.9" + depd "2.0.0" + destroy "1.2.0" + http-errors "2.0.0" + iconv-lite "0.4.24" + on-finished "2.4.1" + qs "6.11.0" + raw-body "2.5.2" + type-is "~1.6.18" + unpipe "1.0.0" + +boolean@^3.1.4: + version "3.2.0" + resolved "https://registry.yarnpkg.com/boolean/-/boolean-3.2.0.tgz#9e5294af4e98314494cbb17979fa54ca159f116b" + integrity sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw== + +buffer@^6.0.3: + version "6.0.3" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-6.0.3.tgz#2ace578459cc8fbe2a70aaa8f52ee63b6a74c6c6" + integrity sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA== + dependencies: + base64-js "^1.3.1" + ieee754 "^1.2.1" + +bytes@3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" + integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== + +call-bind@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.7.tgz#06016599c40c56498c18769d2730be242b6fa3b9" + integrity sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w== + dependencies: + es-define-property "^1.0.0" + es-errors "^1.3.0" + function-bind "^1.1.2" + get-intrinsic "^1.2.4" + set-function-length "^1.2.1" + +cbor-extract@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/cbor-extract/-/cbor-extract-2.2.0.tgz#cee78e630cbeae3918d1e2e58e0cebaf3a3be840" + integrity sha512-Ig1zM66BjLfTXpNgKpvBePq271BPOvu8MR0Jl080yG7Jsl+wAZunfrwiwA+9ruzm/WEdIV5QF/bjDZTqyAIVHA== + dependencies: + node-gyp-build-optional-packages "5.1.1" + optionalDependencies: + "@cbor-extract/cbor-extract-darwin-arm64" "2.2.0" + "@cbor-extract/cbor-extract-darwin-x64" "2.2.0" + "@cbor-extract/cbor-extract-linux-arm" "2.2.0" + "@cbor-extract/cbor-extract-linux-arm64" "2.2.0" + "@cbor-extract/cbor-extract-linux-x64" "2.2.0" + "@cbor-extract/cbor-extract-win32-x64" "2.2.0" + +cbor-x@^1.5.1: + version "1.5.9" + resolved "https://registry.yarnpkg.com/cbor-x/-/cbor-x-1.5.9.tgz#ed6b2afcd7884bdd697674bfb7332c1473a13ecf" + integrity sha512-OEI5rEu3MeR0WWNUXuIGkxmbXVhABP+VtgAXzm48c9ulkrsvxshjjk94XSOGphyAKeNGLPfAxxzEtgQ6rEVpYQ== + optionalDependencies: + cbor-extract "^2.2.0" + +cborg@^1.6.0: + version "1.10.2" + resolved "https://registry.yarnpkg.com/cborg/-/cborg-1.10.2.tgz#83cd581b55b3574c816f82696307c7512db759a1" + integrity sha512-b3tFPA9pUr2zCUiCfRd2+wok2/LBSNUMKOuRRok+WlvvAgEt/PlbgPTsZUcwCOs53IJvLgTp0eotwtosE6njug== + +content-disposition@0.5.4: + version "0.5.4" + resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.4.tgz#8b82b4efac82512a02bb0b1dcec9d2c5e8eb5bfe" + integrity sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ== + dependencies: + safe-buffer "5.2.1" + +content-type@~1.0.4, content-type@~1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.5.tgz#8b773162656d1d1086784c8f23a54ce6d73d7918" + integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA== + +cookie-signature@1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" + integrity sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ== + +cookie@0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.6.0.tgz#2798b04b071b0ecbff0dbb62a505a8efa4e19051" + integrity sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw== + +cors@^2.8.5: + version "2.8.5" + resolved "https://registry.yarnpkg.com/cors/-/cors-2.8.5.tgz#eac11da51592dd86b9f06f6e7ac293b3df875d29" + integrity sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g== + dependencies: + object-assign "^4" + vary "^1" + +debug@2.6.9: + version "2.6.9" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" + integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== + dependencies: + ms "2.0.0" + +define-data-property@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/define-data-property/-/define-data-property-1.1.4.tgz#894dc141bb7d3060ae4366f6a0107e68fbe48c5e" + integrity sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A== + dependencies: + es-define-property "^1.0.0" + es-errors "^1.3.0" + gopd "^1.0.1" + +delay@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/delay/-/delay-5.0.0.tgz#137045ef1b96e5071060dd5be60bf9334436bd1d" + integrity sha512-ReEBKkIfe4ya47wlPYf/gu5ib6yUG0/Aez0JQZQz94kiWtRQvZIQbTiehsnwHvLSWJnQdhVeqYue7Id1dKr0qw== + +depd@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" + integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== + +destroy@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015" + integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg== + +detect-libc@^2.0.1: + version "2.0.3" + resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.0.3.tgz#f0cd503b40f9939b894697d19ad50895e30cf700" + integrity sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw== + +ee-first@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" + integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow== + +encodeurl@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" + integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w== + +es-define-property@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.0.tgz#c7faefbdff8b2696cf5f46921edfb77cc4ba3845" + integrity sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ== + dependencies: + get-intrinsic "^1.2.4" + +es-errors@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f" + integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== + +escape-html@~1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" + integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow== + +etag@~1.8.1: + version "1.8.1" + resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" + integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg== + +event-target-shim@^5.0.0: + version "5.0.1" + resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789" + integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ== + +events@^3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" + integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== + +express@^4.19.2: + version "4.19.2" + resolved "https://registry.yarnpkg.com/express/-/express-4.19.2.tgz#e25437827a3aa7f2a827bc8171bbbb664a356465" + integrity sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q== + dependencies: + accepts "~1.3.8" + array-flatten "1.1.1" + body-parser "1.20.2" + content-disposition "0.5.4" + content-type "~1.0.4" + cookie "0.6.0" + cookie-signature "1.0.6" + debug "2.6.9" + depd "2.0.0" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + finalhandler "1.2.0" + fresh "0.5.2" + http-errors "2.0.0" + merge-descriptors "1.0.1" + methods "~1.1.2" + on-finished "2.4.1" + parseurl "~1.3.3" + path-to-regexp "0.1.7" + proxy-addr "~2.0.7" + qs "6.11.0" + range-parser "~1.2.1" + safe-buffer "5.2.1" + send "0.18.0" + serve-static "1.15.0" + setprototypeof "1.2.0" + statuses "2.0.1" + type-is "~1.6.18" + utils-merge "1.0.1" + vary "~1.1.2" + +fast-printf@^1.6.9: + version "1.6.9" + resolved "https://registry.yarnpkg.com/fast-printf/-/fast-printf-1.6.9.tgz#212f56570d2dc8ccdd057ee93d50dd414d07d676" + integrity sha512-FChq8hbz65WMj4rstcQsFB0O7Cy++nmbNfLYnD9cYv2cRn8EG6k/MGn9kO/tjO66t09DLDugj3yL+V2o6Qftrg== + dependencies: + boolean "^3.1.4" + +fast-redact@^3.1.1: + version "3.5.0" + resolved "https://registry.yarnpkg.com/fast-redact/-/fast-redact-3.5.0.tgz#e9ea02f7e57d0cd8438180083e93077e496285e4" + integrity sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A== + +finalhandler@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.2.0.tgz#7d23fe5731b207b4640e4fcd00aec1f9207a7b32" + integrity sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg== + dependencies: + debug "2.6.9" + encodeurl "~1.0.2" + escape-html "~1.0.3" + on-finished "2.4.1" + parseurl "~1.3.3" + statuses "2.0.1" + unpipe "~1.0.0" + +forwarded@0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" + integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow== + +fresh@0.5.2: + version "0.5.2" + resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" + integrity sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q== + +function-bind@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" + integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== + +get-intrinsic@^1.1.3, get-intrinsic@^1.2.4: + version "1.2.4" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.4.tgz#e385f5a4b5227d449c3eabbad05494ef0abbeadd" + integrity sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ== + dependencies: + es-errors "^1.3.0" + function-bind "^1.1.2" + has-proto "^1.0.1" + has-symbols "^1.0.3" + hasown "^2.0.0" + +gopd@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.0.1.tgz#29ff76de69dac7489b7c0918a5788e56477c332c" + integrity sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA== + dependencies: + get-intrinsic "^1.1.3" + +graphemer@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/graphemer/-/graphemer-1.4.0.tgz#fb2f1d55e0e3a1849aeffc90c4fa0dd53a0e66c6" + integrity sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag== + +has-property-descriptors@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz#963ed7d071dc7bf5f084c5bfbe0d1b6222586854" + integrity sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg== + dependencies: + es-define-property "^1.0.0" + +has-proto@^1.0.1: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has-proto/-/has-proto-1.0.3.tgz#b31ddfe9b0e6e9914536a6ab286426d0214f77fd" + integrity sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q== + +has-symbols@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" + integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== + +hasown@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003" + integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ== + dependencies: + function-bind "^1.1.2" + +http-errors@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.0.tgz#b7774a1486ef73cf7667ac9ae0858c012c57b9d3" + integrity sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ== + dependencies: + depd "2.0.0" + inherits "2.0.4" + setprototypeof "1.2.0" + statuses "2.0.1" + toidentifier "1.0.1" + +http-terminator@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/http-terminator/-/http-terminator-3.2.0.tgz#bc158d2694b733ca4fbf22a35065a81a609fb3e9" + integrity sha512-JLjck1EzPaWjsmIf8bziM3p9fgR1Y3JoUKAkyYEbZmFrIvJM6I8vVJfBGWlEtV9IWOvzNnaTtjuwZeBY2kwB4g== + dependencies: + delay "^5.0.0" + p-wait-for "^3.2.0" + roarr "^7.0.4" + type-fest "^2.3.3" + +iconv-lite@0.4.24: + version "0.4.24" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" + integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== + dependencies: + safer-buffer ">= 2.1.2 < 3" + +ieee754@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" + integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== + +inherits@2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +ipaddr.js@1.9.1: + version "1.9.1" + resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" + integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== + +iso-datestring-validator@^2.2.2: + version "2.2.2" + resolved "https://registry.yarnpkg.com/iso-datestring-validator/-/iso-datestring-validator-2.2.2.tgz#2daa80d2900b7a954f9f731d42f96ee0c19a6895" + integrity sha512-yLEMkBbLZTlVQqOnQ4FiMujR6T4DEcCb1xizmvXS+OxuhwcbtynoosRzdMA69zZCShCNAbi+gJ71FxZBBXx1SA== + +kysely@^0.27.3: + version "0.27.3" + resolved "https://registry.yarnpkg.com/kysely/-/kysely-0.27.3.tgz#6cc6c757040500b43c4ac596cdbb12be400ee276" + integrity sha512-lG03Ru+XyOJFsjH3OMY6R/9U38IjDPfnOfDgO3ynhbDr+Dz8fak+X6L62vqu3iybQnj+lG84OttBuU9KY3L9kA== + +media-typer@0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" + integrity sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ== + +merge-descriptors@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" + integrity sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w== + +methods@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" + integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w== + +mime-db@1.52.0: + version "1.52.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" + integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== + +mime-types@~2.1.24, mime-types@~2.1.34: + version "2.1.35" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" + integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== + dependencies: + mime-db "1.52.0" + +mime@1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" + integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== + +ms@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" + integrity sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A== + +ms@2.1.3: + version "2.1.3" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + +multiformats@^13.0.0: + version "13.1.1" + resolved "https://registry.yarnpkg.com/multiformats/-/multiformats-13.1.1.tgz#b22ce4df26330d2cf0d69f5bdcbc9a787095a6e5" + integrity sha512-JiptvwMmlxlzIlLLwhCi/srf/nk409UL0eUBr0kioRJq15hqqKyg68iftrBvhCRjR6Rw4fkNnSc4ZJXJDuta/Q== + +multiformats@^9.4.2, multiformats@^9.5.4, multiformats@^9.9.0: + version "9.9.0" + resolved "https://registry.yarnpkg.com/multiformats/-/multiformats-9.9.0.tgz#c68354e7d21037a8f1f8833c8ccd68618e8f1d37" + integrity sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg== + +negotiator@0.6.3: + version "0.6.3" + resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd" + integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg== + +node-gyp-build-optional-packages@5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.1.1.tgz#52b143b9dd77b7669073cbfe39e3f4118bfc603c" + integrity sha512-+P72GAjVAbTxjjwUmwjVrqrdZROD4nf8KgpBoDxqXXTiYZZt/ud60dE5yvCSr9lRO8e8yv6kgJIC0K0PfZFVQw== + dependencies: + detect-libc "^2.0.1" + +object-assign@^4: + version "4.1.1" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" + integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== + +object-inspect@^1.13.1: + version "1.13.1" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.1.tgz#b96c6109324ccfef6b12216a956ca4dc2ff94bc2" + integrity sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ== + +obuf@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/obuf/-/obuf-1.1.2.tgz#09bea3343d41859ebd446292d11c9d4db619084e" + integrity sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg== + +on-exit-leak-free@^2.1.0: + version "2.1.2" + resolved "https://registry.yarnpkg.com/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz#fed195c9ebddb7d9e4c3842f93f281ac8dadd3b8" + integrity sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA== + +on-finished@2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f" + integrity sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg== + dependencies: + ee-first "1.1.1" + +p-finally@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae" + integrity sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow== + +p-timeout@^3.0.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/p-timeout/-/p-timeout-3.2.0.tgz#c7e17abc971d2a7962ef83626b35d635acf23dfe" + integrity sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg== + dependencies: + p-finally "^1.0.0" + +p-wait-for@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/p-wait-for/-/p-wait-for-3.2.0.tgz#640429bcabf3b0dd9f492c31539c5718cb6a3f1f" + integrity sha512-wpgERjNkLrBiFmkMEjuZJEWKKDrNfHCKA1OhyN1wg1FrLkULbviEy6py1AyJUgZ72YWFbZ38FIpnqvVqAlDUwA== + dependencies: + p-timeout "^3.0.0" + +parseurl@~1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" + integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== + +path-to-regexp@0.1.7: + version "0.1.7" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" + integrity sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ== + +pg-cloudflare@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/pg-cloudflare/-/pg-cloudflare-1.1.1.tgz#e6d5833015b170e23ae819e8c5d7eaedb472ca98" + integrity sha512-xWPagP/4B6BgFO+EKz3JONXv3YDgvkbVrGw2mTo3D6tVDQRh1e7cqVGvyR3BE+eQgAvx1XhW/iEASj4/jCWl3Q== + +pg-connection-string@^2.6.4: + version "2.6.4" + resolved "https://registry.yarnpkg.com/pg-connection-string/-/pg-connection-string-2.6.4.tgz#f543862adfa49fa4e14bc8a8892d2a84d754246d" + integrity sha512-v+Z7W/0EO707aNMaAEfiGnGL9sxxumwLl2fJvCQtMn9Fxsg+lPpPkdcyBSv/KFgpGdYkMfn+EI1Or2EHjpgLCA== + +pg-int8@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/pg-int8/-/pg-int8-1.0.1.tgz#943bd463bf5b71b4170115f80f8efc9a0c0eb78c" + integrity sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw== + +pg-numeric@1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/pg-numeric/-/pg-numeric-1.0.2.tgz#816d9a44026086ae8ae74839acd6a09b0636aa3a" + integrity sha512-BM/Thnrw5jm2kKLE5uJkXqqExRUY/toLHda65XgFTBTFYZyopbKjBe29Ii3RbkvlsMoFwD+tHeGaCjjv0gHlyw== + +pg-pool@^3.6.2: + version "3.6.2" + resolved "https://registry.yarnpkg.com/pg-pool/-/pg-pool-3.6.2.tgz#3a592370b8ae3f02a7c8130d245bc02fa2c5f3f2" + integrity sha512-Htjbg8BlwXqSBQ9V8Vjtc+vzf/6fVUuak/3/XXKA9oxZprwW3IMDQTGHP+KDmVL7rtd+R1QjbnCFPuTHm3G4hg== + +pg-protocol@*, pg-protocol@^1.6.1: + version "1.6.1" + resolved "https://registry.yarnpkg.com/pg-protocol/-/pg-protocol-1.6.1.tgz#21333e6d83b01faaebfe7a33a7ad6bfd9ed38cb3" + integrity sha512-jPIlvgoD63hrEuihvIg+tJhoGjUsLPn6poJY9N5CnlPd91c2T18T/9zBtLxZSb1EhYxBRoZJtzScCaWlYLtktg== + +pg-types@^2.1.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/pg-types/-/pg-types-2.2.0.tgz#2d0250d636454f7cfa3b6ae0382fdfa8063254a3" + integrity sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA== + dependencies: + pg-int8 "1.0.1" + postgres-array "~2.0.0" + postgres-bytea "~1.0.0" + postgres-date "~1.0.4" + postgres-interval "^1.1.0" + +pg-types@^4.0.1: + version "4.0.2" + resolved "https://registry.yarnpkg.com/pg-types/-/pg-types-4.0.2.tgz#399209a57c326f162461faa870145bb0f918b76d" + integrity sha512-cRL3JpS3lKMGsKaWndugWQoLOCoP+Cic8oseVcbr0qhPzYD5DWXK+RZ9LY9wxRf7RQia4SCwQlXk0q6FCPrVng== + dependencies: + pg-int8 "1.0.1" + pg-numeric "1.0.2" + postgres-array "~3.0.1" + postgres-bytea "~3.0.0" + postgres-date "~2.1.0" + postgres-interval "^3.0.0" + postgres-range "^1.1.1" + +pg@^8.12.0: + version "8.12.0" + resolved "https://registry.yarnpkg.com/pg/-/pg-8.12.0.tgz#9341724db571022490b657908f65aee8db91df79" + integrity sha512-A+LHUSnwnxrnL/tZ+OLfqR1SxLN3c/pgDztZ47Rpbsd4jUytsTtwQo/TLPRzPJMp/1pbhYVhH9cuSZLAajNfjQ== + dependencies: + pg-connection-string "^2.6.4" + pg-pool "^3.6.2" + pg-protocol "^1.6.1" + pg-types "^2.1.0" + pgpass "1.x" + optionalDependencies: + pg-cloudflare "^1.1.1" + +pgpass@1.x: + version "1.0.5" + resolved "https://registry.yarnpkg.com/pgpass/-/pgpass-1.0.5.tgz#9b873e4a564bb10fa7a7dbd55312728d422a223d" + integrity sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug== + dependencies: + split2 "^4.1.0" + +pino-abstract-transport@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/pino-abstract-transport/-/pino-abstract-transport-1.2.0.tgz#97f9f2631931e242da531b5c66d3079c12c9d1b5" + integrity sha512-Guhh8EZfPCfH+PMXAb6rKOjGQEoy0xlAIn+irODG5kgfYV+BQ0rGYYWTIel3P5mmyXqkYkPmdIkywsn6QKUR1Q== + dependencies: + readable-stream "^4.0.0" + split2 "^4.0.0" + +pino-std-serializers@^6.0.0: + version "6.2.2" + resolved "https://registry.yarnpkg.com/pino-std-serializers/-/pino-std-serializers-6.2.2.tgz#d9a9b5f2b9a402486a5fc4db0a737570a860aab3" + integrity sha512-cHjPPsE+vhj/tnhCy/wiMh3M3z3h/j15zHQX+S9GkTBgqJuTuJzYJ4gUyACLhDaJ7kk9ba9iRDmbH2tJU03OiA== + +pino-std-serializers@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/pino-std-serializers/-/pino-std-serializers-7.0.0.tgz#7c625038b13718dbbd84ab446bd673dc52259e3b" + integrity sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA== + +pino@^8.15.0: + version "8.21.0" + resolved "https://registry.yarnpkg.com/pino/-/pino-8.21.0.tgz#e1207f3675a2722940d62da79a7a55a98409f00d" + integrity sha512-ip4qdzjkAyDDZklUaZkcRFb2iA118H9SgRh8yzTkSQK8HilsOJF7rSY8HoW5+I0M46AZgX/pxbprf2vvzQCE0Q== + dependencies: + atomic-sleep "^1.0.0" + fast-redact "^3.1.1" + on-exit-leak-free "^2.1.0" + pino-abstract-transport "^1.2.0" + pino-std-serializers "^6.0.0" + process-warning "^3.0.0" + quick-format-unescaped "^4.0.3" + real-require "^0.2.0" + safe-stable-stringify "^2.3.1" + sonic-boom "^3.7.0" + thread-stream "^2.6.0" + +pino@^9.2.0: + version "9.2.0" + resolved "https://registry.yarnpkg.com/pino/-/pino-9.2.0.tgz#e77a9516f3a3e5550d9b76d9f65ac6118ef02bdd" + integrity sha512-g3/hpwfujK5a4oVbaefoJxezLzsDgLcNJeITvC6yrfwYeT9la+edCK42j5QpEQSQCZgTKapXvnQIdgZwvRaZug== + dependencies: + atomic-sleep "^1.0.0" + fast-redact "^3.1.1" + on-exit-leak-free "^2.1.0" + pino-abstract-transport "^1.2.0" + pino-std-serializers "^7.0.0" + process-warning "^3.0.0" + quick-format-unescaped "^4.0.3" + real-require "^0.2.0" + safe-stable-stringify "^2.3.1" + sonic-boom "^4.0.1" + thread-stream "^3.0.0" + +postgres-array@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/postgres-array/-/postgres-array-2.0.0.tgz#48f8fce054fbc69671999329b8834b772652d82e" + integrity sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA== + +postgres-array@~3.0.1: + version "3.0.2" + resolved "https://registry.yarnpkg.com/postgres-array/-/postgres-array-3.0.2.tgz#68d6182cb0f7f152a7e60dc6a6889ed74b0a5f98" + integrity sha512-6faShkdFugNQCLwucjPcY5ARoW1SlbnrZjmGl0IrrqewpvxvhSLHimCVzqeuULCbG0fQv7Dtk1yDbG3xv7Veog== + +postgres-bytea@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/postgres-bytea/-/postgres-bytea-1.0.0.tgz#027b533c0aa890e26d172d47cf9ccecc521acd35" + integrity sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w== + +postgres-bytea@~3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/postgres-bytea/-/postgres-bytea-3.0.0.tgz#9048dc461ac7ba70a6a42d109221619ecd1cb089" + integrity sha512-CNd4jim9RFPkObHSjVHlVrxoVQXz7quwNFpz7RY1okNNme49+sVyiTvTRobiLV548Hx/hb1BG+iE7h9493WzFw== + dependencies: + obuf "~1.1.2" + +postgres-date@~1.0.4: + version "1.0.7" + resolved "https://registry.yarnpkg.com/postgres-date/-/postgres-date-1.0.7.tgz#51bc086006005e5061c591cee727f2531bf641a8" + integrity sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q== + +postgres-date@~2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/postgres-date/-/postgres-date-2.1.0.tgz#b85d3c1fb6fb3c6c8db1e9942a13a3bf625189d0" + integrity sha512-K7Juri8gtgXVcDfZttFKVmhglp7epKb1K4pgrkLxehjqkrgPhfG6OO8LHLkfaqkbpjNRnra018XwAr1yQFWGcA== + +postgres-interval@^1.1.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/postgres-interval/-/postgres-interval-1.2.0.tgz#b460c82cb1587507788819a06aa0fffdb3544695" + integrity sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ== + dependencies: + xtend "^4.0.0" + +postgres-interval@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/postgres-interval/-/postgres-interval-3.0.0.tgz#baf7a8b3ebab19b7f38f07566c7aab0962f0c86a" + integrity sha512-BSNDnbyZCXSxgA+1f5UU2GmwhoI0aU5yMxRGO8CdFEcY2BQF9xm/7MqKnYoM1nJDk8nONNWDk9WeSmePFhQdlw== + +postgres-range@^1.1.1: + version "1.1.4" + resolved "https://registry.yarnpkg.com/postgres-range/-/postgres-range-1.1.4.tgz#a59c5f9520909bcec5e63e8cf913a92e4c952863" + integrity sha512-i/hbxIE9803Alj/6ytL7UHQxRvZkI9O4Sy+J3HGc4F4oo/2eQAjTSNJ0bfxyse3bH0nuVesCk+3IRLaMtG3H6w== + +process-warning@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/process-warning/-/process-warning-3.0.0.tgz#96e5b88884187a1dce6f5c3166d611132058710b" + integrity sha512-mqn0kFRl0EoqhnL0GQ0veqFHyIN1yig9RHh/InzORTUiZHFRAur+aMtRkELNwGs9aNwKS6tg/An4NYBPGwvtzQ== + +process@^0.11.10: + version "0.11.10" + resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" + integrity sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A== + +proxy-addr@~2.0.7: + version "2.0.7" + resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025" + integrity sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg== + dependencies: + forwarded "0.2.0" + ipaddr.js "1.9.1" + +qs@6.11.0: + version "6.11.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.0.tgz#fd0d963446f7a65e1367e01abd85429453f0c37a" + integrity sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q== + dependencies: + side-channel "^1.0.4" + +quick-format-unescaped@^4.0.3: + version "4.0.4" + resolved "https://registry.yarnpkg.com/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz#93ef6dd8d3453cbc7970dd614fad4c5954d6b5a7" + integrity sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg== + +range-parser@~1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" + integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== + +raw-body@2.5.2: + version "2.5.2" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.2.tgz#99febd83b90e08975087e8f1f9419a149366b68a" + integrity sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA== + dependencies: + bytes "3.1.2" + http-errors "2.0.0" + iconv-lite "0.4.24" + unpipe "1.0.0" + +readable-stream@^4.0.0: + version "4.5.2" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-4.5.2.tgz#9e7fc4c45099baeed934bff6eb97ba6cf2729e09" + integrity sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g== + dependencies: + abort-controller "^3.0.0" + buffer "^6.0.3" + events "^3.3.0" + process "^0.11.10" + string_decoder "^1.3.0" + +real-require@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/real-require/-/real-require-0.2.0.tgz#209632dea1810be2ae063a6ac084fee7e33fba78" + integrity sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg== + +roarr@^7.0.4: + version "7.21.1" + resolved "https://registry.yarnpkg.com/roarr/-/roarr-7.21.1.tgz#fd6452ca822a65f736c35e5372f04ee9f2ca3851" + integrity sha512-3niqt5bXFY1InKU8HKWqqYTYjtrBaxBMnXELXCXUYgtNYGUtZM5rB46HIC430AyacL95iEniGf7RgqsesykLmQ== + dependencies: + fast-printf "^1.6.9" + safe-stable-stringify "^2.4.3" + semver-compare "^1.0.0" + +safe-buffer@5.2.1, safe-buffer@~5.2.0: + version "5.2.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== + +safe-stable-stringify@^2.3.1, safe-stable-stringify@^2.4.3: + version "2.4.3" + resolved "https://registry.yarnpkg.com/safe-stable-stringify/-/safe-stable-stringify-2.4.3.tgz#138c84b6f6edb3db5f8ef3ef7115b8f55ccbf886" + integrity sha512-e2bDA2WJT0wxseVd4lsDP4+3ONX6HpMXQa1ZhFQ7SU+GjvORCmShbCMltrtIDfkYhVHrOcPtj+KhmDBdPdZD1g== + +"safer-buffer@>= 2.1.2 < 3": + version "2.1.2" + resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" + integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== + +semver-compare@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/semver-compare/-/semver-compare-1.0.0.tgz#0dee216a1c941ab37e9efb1788f6afc5ff5537fc" + integrity sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow== + +send@0.18.0: + version "0.18.0" + resolved "https://registry.yarnpkg.com/send/-/send-0.18.0.tgz#670167cc654b05f5aa4a767f9113bb371bc706be" + integrity sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg== + dependencies: + debug "2.6.9" + depd "2.0.0" + destroy "1.2.0" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + fresh "0.5.2" + http-errors "2.0.0" + mime "1.6.0" + ms "2.1.3" + on-finished "2.4.1" + range-parser "~1.2.1" + statuses "2.0.1" + +serve-static@1.15.0: + version "1.15.0" + resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.15.0.tgz#faaef08cffe0a1a62f60cad0c4e513cff0ac9540" + integrity sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g== + dependencies: + encodeurl "~1.0.2" + escape-html "~1.0.3" + parseurl "~1.3.3" + send "0.18.0" + +set-function-length@^1.2.1: + version "1.2.2" + resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.2.2.tgz#aac72314198eaed975cf77b2c3b6b880695e5449" + integrity sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg== + dependencies: + define-data-property "^1.1.4" + es-errors "^1.3.0" + function-bind "^1.1.2" + get-intrinsic "^1.2.4" + gopd "^1.0.1" + has-property-descriptors "^1.0.2" + +setprototypeof@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" + integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== + +side-channel@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.6.tgz#abd25fb7cd24baf45466406b1096b7831c9215f2" + integrity sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA== + dependencies: + call-bind "^1.0.7" + es-errors "^1.3.0" + get-intrinsic "^1.2.4" + object-inspect "^1.13.1" + +sonic-boom@^3.7.0: + version "3.8.1" + resolved "https://registry.yarnpkg.com/sonic-boom/-/sonic-boom-3.8.1.tgz#d5ba8c4e26d6176c9a1d14d549d9ff579a163422" + integrity sha512-y4Z8LCDBuum+PBP3lSV7RHrXscqksve/bi0as7mhwVnBW+/wUqKT/2Kb7um8yqcFy0duYbbPxzt89Zy2nOCaxg== + dependencies: + atomic-sleep "^1.0.0" + +sonic-boom@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/sonic-boom/-/sonic-boom-4.0.1.tgz#515b7cef2c9290cb362c4536388ddeece07aed30" + integrity sha512-hTSD/6JMLyT4r9zeof6UtuBDpjJ9sO08/nmS5djaA9eozT9oOlNdpXSnzcgj4FTqpk3nkLrs61l4gip9r1HCrQ== + dependencies: + atomic-sleep "^1.0.0" + +split2@^4.0.0, split2@^4.1.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/split2/-/split2-4.2.0.tgz#c9c5920904d148bab0b9f67145f245a86aadbfa4" + integrity sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg== + +statuses@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63" + integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== + +string_decoder@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" + integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== + dependencies: + safe-buffer "~5.2.0" + +thread-stream@^2.6.0: + version "2.7.0" + resolved "https://registry.yarnpkg.com/thread-stream/-/thread-stream-2.7.0.tgz#d8a8e1b3fd538a6cca8ce69dbe5d3d097b601e11" + integrity sha512-qQiRWsU/wvNolI6tbbCKd9iKaTnCXsTwVxhhKM6nctPdujTyztjlbUkUTUymidWcMnZ5pWR0ej4a0tjsW021vw== + dependencies: + real-require "^0.2.0" + +thread-stream@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/thread-stream/-/thread-stream-3.1.0.tgz#4b2ef252a7c215064507d4ef70c05a5e2d34c4f1" + integrity sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A== + dependencies: + real-require "^0.2.0" + +toidentifier@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35" + integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== + +type-fest@^2.3.3: + version "2.19.0" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-2.19.0.tgz#88068015bb33036a598b952e55e9311a60fd3a9b" + integrity sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA== + +type-is@~1.6.18: + version "1.6.18" + resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" + integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== + dependencies: + media-typer "0.3.0" + mime-types "~2.1.24" + +typescript@^5.4.5: + version "5.4.5" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.4.5.tgz#42ccef2c571fdbd0f6718b1d1f5e6e5ef006f611" + integrity sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ== + +uint8arrays@3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/uint8arrays/-/uint8arrays-3.0.0.tgz#260869efb8422418b6f04e3fac73a3908175c63b" + integrity sha512-HRCx0q6O9Bfbp+HHSfQQKD7wU70+lydKVt4EghkdOvlK/NlrF90z+eXV34mUd48rNvVJXwkrMSPpCATkct8fJA== + dependencies: + multiformats "^9.4.2" + +uint8arrays@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/uint8arrays/-/uint8arrays-5.1.0.tgz#14047c9bdf825d025b7391299436e5e50e7270f1" + integrity sha512-vA6nFepEmlSKkMBnLBaUMVvAC4G3CTmO58C12y4sq6WPDOR7mOFYOi7GlrQ4djeSbP6JG9Pv9tJDM97PedRSww== + dependencies: + multiformats "^13.0.0" + +undici-types@~5.26.4: + version "5.26.5" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" + integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== + +unpipe@1.0.0, unpipe@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" + integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ== + +utils-merge@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" + integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA== + +vary@^1, vary@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" + integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg== + +xtend@^4.0.0: + version "4.0.2" + resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" + integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== + +zod@^3.21.4: + version "3.23.8" + resolved "https://registry.yarnpkg.com/zod/-/zod-3.23.8.tgz#e37b957b5d52079769fb8097099b592f0ef4067d" + integrity sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g== diff --git a/bskyweb/cmd/bskyweb/main.go b/bskyweb/cmd/bskyweb/main.go index 5185ff57..49629e3f 100644 --- a/bskyweb/cmd/bskyweb/main.go +++ b/bskyweb/cmd/bskyweb/main.go @@ -35,7 +35,7 @@ func run(args []string) { Flags: []cli.Flag{ &cli.StringFlag{ Name: "appview-host", - Usage: "method, hostname, and port of PDS instance", + Usage: "scheme, hostname, and port of PDS instance", Value: "http://localhost:2584", // retain old PDS env var for easy transition EnvVars: []string{"ATP_APPVIEW_HOST", "ATP_PDS_HOST"}, @@ -47,6 +47,13 @@ func run(args []string) { Value: ":8100", EnvVars: []string{"HTTP_ADDRESS"}, }, + &cli.StringFlag{ + Name: "link-host", + Usage: "scheme, hostname, and port of link service", + Required: false, + Value: "", + EnvVars: []string{"LINK_HOST"}, + }, &cli.BoolFlag{ Name: "debug", Usage: "Enable debug mode", diff --git a/bskyweb/cmd/bskyweb/server.go b/bskyweb/cmd/bskyweb/server.go index bb81e780..6d32e0e2 100644 --- a/bskyweb/cmd/bskyweb/server.go +++ b/bskyweb/cmd/bskyweb/server.go @@ -6,6 +6,7 @@ import ( "fmt" "io/fs" "net/http" + "net/url" "os" "os/signal" "strings" @@ -36,6 +37,7 @@ func serve(cctx *cli.Context) error { debug := cctx.Bool("debug") httpAddress := cctx.String("http-address") appviewHost := cctx.String("appview-host") + linkHost := cctx.String("link-host") // Echo e := echo.New() @@ -221,6 +223,14 @@ func serve(cctx *cli.Context) error { e.GET("/profile/:handleOrDID/post/:rkey/liked-by", server.WebGeneric) e.GET("/profile/:handleOrDID/post/:rkey/reposted-by", server.WebGeneric) + if linkHost != "" { + linkUrl, err := url.Parse(linkHost) + if err != nil { + return err + } + e.Group("/:linkId", server.LinkProxyMiddleware(linkUrl)) + } + // Start the server. log.Infof("starting server address=%s", httpAddress) go func() { @@ -292,6 +302,30 @@ func (srv *Server) Download(c echo.Context) error { return c.Redirect(http.StatusFound, "/") } +// Handler for proxying top-level paths to link service, which ends up serving a redirect +func (srv *Server) LinkProxyMiddleware(url *url.URL) echo.MiddlewareFunc { + return middleware.ProxyWithConfig( + middleware.ProxyConfig{ + Balancer: middleware.NewRoundRobinBalancer( + []*middleware.ProxyTarget{{URL: url}}, + ), + Skipper: func(c echo.Context) bool { + req := c.Request() + if req.Method == "GET" && + strings.LastIndex(strings.TrimRight(req.URL.Path, "/"), "/") == 0 && // top-level path + !strings.HasPrefix(req.URL.Path, "/_") { // e.g. /_health endpoint + return false + } + return true + }, + RetryCount: 2, + ErrorHandler: func(c echo.Context, err error) error { + return c.Redirect(302, "/") + }, + }, + ) +} + // handler for endpoint that have no specific server-side handling func (srv *Server) WebGeneric(c echo.Context) error { data := pongo2.Context{}