Bluesky app fork with some witchin' additions 💫 witchsky.app
bluesky fork

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 <me@haileyok.com>

authored by devin ivy Hailey and committed by GitHub 55812b03 ba21fddd

+1
.github/workflows/build-and-push-bskyweb-aws.yaml
··· 4 4 push: 5 5 branches: 6 6 - main 7 + - divy/bskylink 7 8 8 9 env: 9 10 REGISTRY: ${{ secrets.AWS_ECR_REGISTRY_USEAST2_PACKAGES_REGISTRY }}
+26
bskylink/package.json
··· 1 + { 2 + "name": "bskylink", 3 + "version": "0.0.0", 4 + "type": "module", 5 + "main": "index.ts", 6 + "scripts": { 7 + "test": "./tests/infra/with-test-db.sh node --loader ts-node/esm --test ./tests/index.ts", 8 + "build": "tsc" 9 + }, 10 + "dependencies": { 11 + "@atproto/common": "^0.4.0", 12 + "body-parser": "^1.20.2", 13 + "cors": "^2.8.5", 14 + "express": "^4.19.2", 15 + "http-terminator": "^3.2.0", 16 + "kysely": "^0.27.3", 17 + "pg": "^8.12.0", 18 + "pino": "^9.2.0", 19 + "uint8arrays": "^5.1.0" 20 + }, 21 + "devDependencies": { 22 + "@types/cors": "^2.8.17", 23 + "@types/pg": "^8.11.6", 24 + "typescript": "^5.4.5" 25 + } 26 + }
+24
bskylink/src/bin.ts
··· 1 + import {Database, envToCfg, httpLogger, LinkService, readEnv} from './index.js' 2 + 3 + async function main() { 4 + const env = readEnv() 5 + const cfg = envToCfg(env) 6 + if (cfg.db.migrationUrl) { 7 + const migrateDb = Database.postgres({ 8 + url: cfg.db.migrationUrl, 9 + schema: cfg.db.schema, 10 + }) 11 + await migrateDb.migrateToLatestOrThrow() 12 + await migrateDb.close() 13 + } 14 + const link = await LinkService.create(cfg) 15 + await link.start() 16 + httpLogger.info('link service is running') 17 + process.on('SIGTERM', async () => { 18 + httpLogger.info('link service is stopping') 19 + await link.destroy() 20 + httpLogger.info('link service is stopped') 21 + }) 22 + } 23 + 24 + main()
+82
bskylink/src/config.ts
··· 1 + import {envInt, envList, envStr} from '@atproto/common' 2 + 3 + export type Config = { 4 + service: ServiceConfig 5 + db: DbConfig 6 + } 7 + 8 + export type ServiceConfig = { 9 + port: number 10 + version?: string 11 + hostnames: string[] 12 + appHostname: string 13 + } 14 + 15 + export type DbConfig = { 16 + url: string 17 + migrationUrl?: string 18 + pool: DbPoolConfig 19 + schema?: string 20 + } 21 + 22 + export type DbPoolConfig = { 23 + size: number 24 + maxUses: number 25 + idleTimeoutMs: number 26 + } 27 + 28 + export type Environment = { 29 + port?: number 30 + version?: string 31 + hostnames: string[] 32 + appHostname?: string 33 + dbPostgresUrl?: string 34 + dbPostgresMigrationUrl?: string 35 + dbPostgresSchema?: string 36 + dbPostgresPoolSize?: number 37 + dbPostgresPoolMaxUses?: number 38 + dbPostgresPoolIdleTimeoutMs?: number 39 + } 40 + 41 + export const readEnv = (): Environment => { 42 + return { 43 + port: envInt('LINK_PORT'), 44 + version: envStr('LINK_VERSION'), 45 + hostnames: envList('LINK_HOSTNAMES'), 46 + appHostname: envStr('LINK_APP_HOSTNAME'), 47 + dbPostgresUrl: envStr('LINK_DB_POSTGRES_URL'), 48 + dbPostgresMigrationUrl: envStr('LINK_DB_POSTGRES_MIGRATION_URL'), 49 + dbPostgresSchema: envStr('LINK_DB_POSTGRES_SCHEMA'), 50 + dbPostgresPoolSize: envInt('LINK_DB_POSTGRES_POOL_SIZE'), 51 + dbPostgresPoolMaxUses: envInt('LINK_DB_POSTGRES_POOL_MAX_USES'), 52 + dbPostgresPoolIdleTimeoutMs: envInt( 53 + 'LINK_DB_POSTGRES_POOL_IDLE_TIMEOUT_MS', 54 + ), 55 + } 56 + } 57 + 58 + export const envToCfg = (env: Environment): Config => { 59 + const serviceCfg: ServiceConfig = { 60 + port: env.port ?? 3000, 61 + version: env.version, 62 + hostnames: env.hostnames, 63 + appHostname: env.appHostname || 'bsky.app', 64 + } 65 + if (!env.dbPostgresUrl) { 66 + throw new Error('Must configure postgres url (LINK_DB_POSTGRES_URL)') 67 + } 68 + const dbCfg: DbConfig = { 69 + url: env.dbPostgresUrl, 70 + migrationUrl: env.dbPostgresMigrationUrl, 71 + schema: env.dbPostgresSchema, 72 + pool: { 73 + idleTimeoutMs: env.dbPostgresPoolIdleTimeoutMs ?? 10000, 74 + maxUses: env.dbPostgresPoolMaxUses ?? Infinity, 75 + size: env.dbPostgresPoolSize ?? 10, 76 + }, 77 + } 78 + return { 79 + service: serviceCfg, 80 + db: dbCfg, 81 + } 82 + }
+33
bskylink/src/context.ts
··· 1 + import {Config} from './config.js' 2 + import Database from './db/index.js' 3 + 4 + export type AppContextOptions = { 5 + cfg: Config 6 + db: Database 7 + } 8 + 9 + export class AppContext { 10 + cfg: Config 11 + db: Database 12 + abortController = new AbortController() 13 + 14 + constructor(private opts: AppContextOptions) { 15 + this.cfg = this.opts.cfg 16 + this.db = this.opts.db 17 + } 18 + 19 + static async fromConfig(cfg: Config, overrides?: Partial<AppContextOptions>) { 20 + const db = Database.postgres({ 21 + url: cfg.db.url, 22 + schema: cfg.db.schema, 23 + poolSize: cfg.db.pool.size, 24 + poolMaxUses: cfg.db.pool.maxUses, 25 + poolIdleTimeoutMs: cfg.db.pool.idleTimeoutMs, 26 + }) 27 + return new AppContext({ 28 + cfg, 29 + db, 30 + ...overrides, 31 + }) 32 + } 33 + }
+174
bskylink/src/db/index.ts
··· 1 + import assert from 'assert' 2 + import { 3 + Kysely, 4 + KyselyPlugin, 5 + Migrator, 6 + PluginTransformQueryArgs, 7 + PluginTransformResultArgs, 8 + PostgresDialect, 9 + QueryResult, 10 + RootOperationNode, 11 + UnknownRow, 12 + } from 'kysely' 13 + import {default as Pg} from 'pg' 14 + 15 + import {dbLogger as log} from '../logger.js' 16 + import {default as migrations} from './migrations/index.js' 17 + import {DbMigrationProvider} from './migrations/provider.js' 18 + import {DbSchema} from './schema.js' 19 + 20 + export class Database { 21 + migrator: Migrator 22 + destroyed = false 23 + 24 + constructor(public db: Kysely<DbSchema>, public cfg: PgConfig) { 25 + this.migrator = new Migrator({ 26 + db, 27 + migrationTableSchema: cfg.schema, 28 + provider: new DbMigrationProvider(migrations), 29 + }) 30 + } 31 + 32 + static postgres(opts: PgOptions): Database { 33 + const {schema, url, txLockNonce} = opts 34 + const pool = 35 + opts.pool ?? 36 + new Pg.Pool({ 37 + connectionString: url, 38 + max: opts.poolSize, 39 + maxUses: opts.poolMaxUses, 40 + idleTimeoutMillis: opts.poolIdleTimeoutMs, 41 + }) 42 + 43 + // Select count(*) and other pg bigints as js integer 44 + Pg.types.setTypeParser(Pg.types.builtins.INT8, n => parseInt(n, 10)) 45 + 46 + // Setup schema usage, primarily for test parallelism (each test suite runs in its own pg schema) 47 + if (schema && !/^[a-z_]+$/i.test(schema)) { 48 + throw new Error(`Postgres schema must only contain [A-Za-z_]: ${schema}`) 49 + } 50 + 51 + pool.on('error', onPoolError) 52 + 53 + const db = new Kysely<DbSchema>({ 54 + dialect: new PostgresDialect({pool}), 55 + }) 56 + 57 + return new Database(db, { 58 + pool, 59 + schema, 60 + url, 61 + txLockNonce, 62 + }) 63 + } 64 + 65 + async transaction<T>(fn: (db: Database) => Promise<T>): Promise<T> { 66 + const leakyTxPlugin = new LeakyTxPlugin() 67 + return this.db 68 + .withPlugin(leakyTxPlugin) 69 + .transaction() 70 + .execute(txn => { 71 + const dbTxn = new Database(txn, this.cfg) 72 + return fn(dbTxn) 73 + .catch(async err => { 74 + leakyTxPlugin.endTx() 75 + // ensure that all in-flight queries are flushed & the connection is open 76 + await dbTxn.db.getExecutor().provideConnection(async () => {}) 77 + throw err 78 + }) 79 + .finally(() => leakyTxPlugin.endTx()) 80 + }) 81 + } 82 + 83 + get schema(): string | undefined { 84 + return this.cfg.schema 85 + } 86 + 87 + get isTransaction() { 88 + return this.db.isTransaction 89 + } 90 + 91 + assertTransaction() { 92 + assert(this.isTransaction, 'Transaction required') 93 + } 94 + 95 + assertNotTransaction() { 96 + assert(!this.isTransaction, 'Cannot be in a transaction') 97 + } 98 + 99 + async close(): Promise<void> { 100 + if (this.destroyed) return 101 + await this.db.destroy() 102 + this.destroyed = true 103 + } 104 + 105 + async migrateToOrThrow(migration: string) { 106 + if (this.schema) { 107 + await this.db.schema.createSchema(this.schema).ifNotExists().execute() 108 + } 109 + const {error, results} = await this.migrator.migrateTo(migration) 110 + if (error) { 111 + throw error 112 + } 113 + if (!results) { 114 + throw new Error('An unknown failure occurred while migrating') 115 + } 116 + return results 117 + } 118 + 119 + async migrateToLatestOrThrow() { 120 + if (this.schema) { 121 + await this.db.schema.createSchema(this.schema).ifNotExists().execute() 122 + } 123 + const {error, results} = await this.migrator.migrateToLatest() 124 + if (error) { 125 + throw error 126 + } 127 + if (!results) { 128 + throw new Error('An unknown failure occurred while migrating') 129 + } 130 + return results 131 + } 132 + } 133 + 134 + export default Database 135 + 136 + export type PgConfig = { 137 + pool: Pg.Pool 138 + url: string 139 + schema?: string 140 + txLockNonce?: string 141 + } 142 + 143 + type PgOptions = { 144 + url: string 145 + pool?: Pg.Pool 146 + schema?: string 147 + poolSize?: number 148 + poolMaxUses?: number 149 + poolIdleTimeoutMs?: number 150 + txLockNonce?: string 151 + } 152 + 153 + class LeakyTxPlugin implements KyselyPlugin { 154 + private txOver = false 155 + 156 + endTx() { 157 + this.txOver = true 158 + } 159 + 160 + transformQuery(args: PluginTransformQueryArgs): RootOperationNode { 161 + if (this.txOver) { 162 + throw new Error('tx already failed') 163 + } 164 + return args.node 165 + } 166 + 167 + async transformResult( 168 + args: PluginTransformResultArgs, 169 + ): Promise<QueryResult<UnknownRow>> { 170 + return args.result 171 + } 172 + } 173 + 174 + const onPoolError = (err: Error) => log.error({err}, 'db pool error')
+15
bskylink/src/db/migrations/001-init.ts
··· 1 + import {Kysely} from 'kysely' 2 + 3 + export async function up(db: Kysely<unknown>): Promise<void> { 4 + await db.schema 5 + .createTable('link') 6 + .addColumn('id', 'varchar', col => col.primaryKey()) 7 + .addColumn('type', 'smallint', col => col.notNull()) // integer enum: 1->starterpack 8 + .addColumn('path', 'varchar', col => col.notNull()) 9 + .addUniqueConstraint('link_path_unique', ['path']) 10 + .execute() 11 + } 12 + 13 + export async function down(db: Kysely<unknown>): Promise<void> { 14 + await db.schema.dropTable('link').execute() 15 + }
+5
bskylink/src/db/migrations/index.ts
··· 1 + import * as init from './001-init.js' 2 + 3 + export default { 4 + '001': init, 5 + }
+8
bskylink/src/db/migrations/provider.ts
··· 1 + import {Migration, MigrationProvider} from 'kysely' 2 + 3 + export class DbMigrationProvider implements MigrationProvider { 4 + constructor(private migrations: Record<string, Migration>) {} 5 + async getMigrations(): Promise<Record<string, Migration>> { 6 + return this.migrations 7 + } 8 + }
+17
bskylink/src/db/schema.ts
··· 1 + import {Selectable} from 'kysely' 2 + 3 + export type DbSchema = { 4 + link: Link 5 + } 6 + 7 + export interface Link { 8 + id: string 9 + type: LinkType 10 + path: string 11 + } 12 + 13 + export enum LinkType { 14 + StarterPack = 1, 15 + } 16 + 17 + export type LinkEntry = Selectable<Link>
+45
bskylink/src/index.ts
··· 1 + import events from 'node:events' 2 + import http from 'node:http' 3 + 4 + import cors from 'cors' 5 + import express from 'express' 6 + import {createHttpTerminator, HttpTerminator} from 'http-terminator' 7 + 8 + import {Config} from './config.js' 9 + import {AppContext} from './context.js' 10 + import {default as routes, errorHandler} from './routes/index.js' 11 + 12 + export * from './config.js' 13 + export * from './db/index.js' 14 + export * from './logger.js' 15 + 16 + export class LinkService { 17 + public server?: http.Server 18 + private terminator?: HttpTerminator 19 + 20 + constructor(public app: express.Application, public ctx: AppContext) {} 21 + 22 + static async create(cfg: Config): Promise<LinkService> { 23 + let app = express() 24 + app.use(cors()) 25 + 26 + const ctx = await AppContext.fromConfig(cfg) 27 + app = routes(ctx, app) 28 + app.use(errorHandler) 29 + 30 + return new LinkService(app, ctx) 31 + } 32 + 33 + async start() { 34 + this.server = this.app.listen(this.ctx.cfg.service.port) 35 + this.server.keepAliveTimeout = 90000 36 + this.terminator = createHttpTerminator({server: this.server}) 37 + await events.once(this.server, 'listening') 38 + } 39 + 40 + async destroy() { 41 + this.ctx.abortController.abort() 42 + await this.terminator?.terminate() 43 + await this.ctx.db.close() 44 + } 45 + }
+4
bskylink/src/logger.ts
··· 1 + import {subsystemLogger} from '@atproto/common' 2 + 3 + export const httpLogger = subsystemLogger('bskylink') 4 + export const dbLogger = subsystemLogger('bskylink:db')
+111
bskylink/src/routes/create.ts
··· 1 + import assert from 'node:assert' 2 + 3 + import bodyParser from 'body-parser' 4 + import {Express, Request} from 'express' 5 + 6 + import {AppContext} from '../context.js' 7 + import {LinkType} from '../db/schema.js' 8 + import {randomId} from '../util.js' 9 + import {handler} from './util.js' 10 + 11 + export default function (ctx: AppContext, app: Express) { 12 + return app.post( 13 + '/link', 14 + bodyParser.json(), 15 + handler(async (req, res) => { 16 + let path: string 17 + if (typeof req.body?.path === 'string') { 18 + path = req.body.path 19 + } else { 20 + return res.status(400).json({ 21 + error: 'InvalidPath', 22 + message: '"path" parameter is missing or not a string', 23 + }) 24 + } 25 + if (!path.startsWith('/')) { 26 + return res.status(400).json({ 27 + error: 'InvalidPath', 28 + message: 29 + '"path" parameter must be formatted as a path, starting with a "/"', 30 + }) 31 + } 32 + const parts = getPathParts(path) 33 + if (parts.length === 3 && parts[0] === 'start') { 34 + // link pattern: /start/{did}/{rkey} 35 + if (!parts[1].startsWith('did:')) { 36 + // enforce strong links 37 + return res.status(400).json({ 38 + error: 'InvalidPath', 39 + message: 40 + '"path" parameter for starter pack must contain the actor\'s DID', 41 + }) 42 + } 43 + const id = await ensureLink(ctx, LinkType.StarterPack, parts) 44 + return res.json({url: getUrl(ctx, req, id)}) 45 + } 46 + return res.status(400).json({ 47 + error: 'InvalidPath', 48 + message: '"path" parameter does not have a known format', 49 + }) 50 + }), 51 + ) 52 + } 53 + 54 + const ensureLink = async (ctx: AppContext, type: LinkType, parts: string[]) => { 55 + const normalizedPath = normalizedPathFromParts(parts) 56 + const created = await ctx.db.db 57 + .insertInto('link') 58 + .values({ 59 + id: randomId(), 60 + type, 61 + path: normalizedPath, 62 + }) 63 + .onConflict(oc => oc.column('path').doNothing()) 64 + .returningAll() 65 + .executeTakeFirst() 66 + if (created) { 67 + return created.id 68 + } 69 + const found = await ctx.db.db 70 + .selectFrom('link') 71 + .selectAll() 72 + .where('path', '=', normalizedPath) 73 + .executeTakeFirstOrThrow() 74 + return found.id 75 + } 76 + 77 + const getUrl = (ctx: AppContext, req: Request, id: string) => { 78 + if (!ctx.cfg.service.hostnames.length) { 79 + assert(req.headers.host, 'request must be made with host header') 80 + const baseUrl = 81 + req.protocol === 'http' && req.headers.host.startsWith('localhost:') 82 + ? `http://${req.headers.host}` 83 + : `https://${req.headers.host}` 84 + return `${baseUrl}/${id}` 85 + } 86 + const baseUrl = ctx.cfg.service.hostnames.includes(req.headers.host) 87 + ? `https://${req.headers.host}` 88 + : `https://${ctx.cfg.service.hostnames[0]}` 89 + return `${baseUrl}/${id}` 90 + } 91 + 92 + const normalizedPathFromParts = (parts: string[]): string => { 93 + return ( 94 + '/' + 95 + parts 96 + .map(encodeURIComponent) 97 + .map(part => part.replaceAll('%3A', ':')) // preserve colons 98 + .join('/') 99 + ) 100 + } 101 + 102 + const getPathParts = (path: string): string[] => { 103 + if (path === '/') return [] 104 + if (path.endsWith('/')) { 105 + path = path.slice(0, -1) // ignore trailing slash 106 + } 107 + return path 108 + .slice(1) // remove leading slash 109 + .split('/') 110 + .map(decodeURIComponent) 111 + }
+20
bskylink/src/routes/health.ts
··· 1 + import {Express} from 'express' 2 + import {sql} from 'kysely' 3 + 4 + import {AppContext} from '../context.js' 5 + import {handler} from './util.js' 6 + 7 + export default function (ctx: AppContext, app: Express) { 8 + return app.get( 9 + '/_health', 10 + handler(async (_req, res) => { 11 + const {version} = ctx.cfg.service 12 + try { 13 + await sql`select 1`.execute(ctx.db.db) 14 + return res.send({version}) 15 + } catch (err) { 16 + return res.status(503).send({version, error: 'Service Unavailable'}) 17 + } 18 + }), 19 + ) 20 + }
+17
bskylink/src/routes/index.ts
··· 1 + import {Express} from 'express' 2 + 3 + import {AppContext} from '../context.js' 4 + import {default as create} from './create.js' 5 + import {default as health} from './health.js' 6 + import {default as redirect} from './redirect.js' 7 + import {default as siteAssociation} from './siteAssociation.js' 8 + 9 + export * from './util.js' 10 + 11 + export default function (ctx: AppContext, app: Express) { 12 + app = health(ctx, app) // GET /_health 13 + app = siteAssociation(ctx, app) // GET /.well-known/apple-app-site-association 14 + app = create(ctx, app) // POST /link 15 + app = redirect(ctx, app) // GET /:linkId (should go last due to permissive matching) 16 + return app 17 + }
+40
bskylink/src/routes/redirect.ts
··· 1 + import assert from 'node:assert' 2 + 3 + import {DAY, SECOND} from '@atproto/common' 4 + import {Express} from 'express' 5 + 6 + import {AppContext} from '../context.js' 7 + import {handler} from './util.js' 8 + 9 + export default function (ctx: AppContext, app: Express) { 10 + return app.get( 11 + '/:linkId', 12 + handler(async (req, res) => { 13 + const linkId = req.params.linkId 14 + assert( 15 + typeof linkId === 'string', 16 + 'express guarantees id parameter is a string', 17 + ) 18 + const found = await ctx.db.db 19 + .selectFrom('link') 20 + .selectAll() 21 + .where('id', '=', linkId) 22 + .executeTakeFirst() 23 + if (!found) { 24 + // potentially broken or mistyped link— send user to the app 25 + res.setHeader('Location', `https://${ctx.cfg.service.appHostname}`) 26 + res.setHeader('Cache-Control', 'no-store') 27 + return res.status(302).end() 28 + } 29 + // build url from original url in order to preserve query params 30 + const url = new URL( 31 + req.originalUrl, 32 + `https://${ctx.cfg.service.appHostname}`, 33 + ) 34 + url.pathname = found.path 35 + res.setHeader('Location', url.href) 36 + res.setHeader('Cache-Control', `max-age=${(7 * DAY) / SECOND}`) 37 + return res.status(301).end() 38 + }), 39 + ) 40 + }
+13
bskylink/src/routes/siteAssociation.ts
··· 1 + import {Express} from 'express' 2 + 3 + import {AppContext} from '../context.js' 4 + 5 + export default function (ctx: AppContext, app: Express) { 6 + return app.get('/.well-known/apple-app-site-association', (req, res) => { 7 + res.json({ 8 + appclips: { 9 + apps: ['B3LX46C5HS.xyz.blueskyweb.app.AppClip'], 10 + }, 11 + }) 12 + }) 13 + }
+23
bskylink/src/routes/util.ts
··· 1 + import {ErrorRequestHandler, Request, RequestHandler, Response} from 'express' 2 + 3 + import {httpLogger} from '../logger.js' 4 + 5 + export type Handler = (req: Request, res: Response) => Awaited<void> 6 + 7 + export const handler = (runHandler: Handler): RequestHandler => { 8 + return async (req, res, next) => { 9 + try { 10 + await runHandler(req, res) 11 + } catch (err) { 12 + next(err) 13 + } 14 + } 15 + } 16 + 17 + export const errorHandler: ErrorRequestHandler = (err, _req, res, next) => { 18 + httpLogger.error({err}, 'request error') 19 + if (res.headersSent) { 20 + return next(err) 21 + } 22 + return res.status(500).end('server error') 23 + }
+8
bskylink/src/util.ts
··· 1 + import {randomBytes} from 'node:crypto' 2 + 3 + import {toString} from 'uint8arrays' 4 + 5 + // 40bit random id of 5-7 characters 6 + export const randomId = () => { 7 + return toString(randomBytes(5), 'base58btc') 8 + }
+84
bskylink/tests/index.ts
··· 1 + import assert from 'node:assert' 2 + import {AddressInfo} from 'node:net' 3 + import {after, before, describe, it} from 'node:test' 4 + 5 + import {Database, envToCfg, LinkService, readEnv} from '../src/index.js' 6 + 7 + describe('link service', async () => { 8 + let linkService: LinkService 9 + let baseUrl: string 10 + before(async () => { 11 + const env = readEnv() 12 + const cfg = envToCfg({ 13 + ...env, 14 + hostnames: ['test.bsky.link'], 15 + appHostname: 'test.bsky.app', 16 + dbPostgresSchema: 'link_test', 17 + dbPostgresUrl: process.env.DB_POSTGRES_URL, 18 + }) 19 + const migrateDb = Database.postgres({ 20 + url: cfg.db.url, 21 + schema: cfg.db.schema, 22 + }) 23 + await migrateDb.migrateToLatestOrThrow() 24 + await migrateDb.close() 25 + linkService = await LinkService.create(cfg) 26 + await linkService.start() 27 + const {port} = linkService.server?.address() as AddressInfo 28 + baseUrl = `http://localhost:${port}` 29 + }) 30 + 31 + after(async () => { 32 + await linkService?.destroy() 33 + }) 34 + 35 + it('creates a starter pack link', async () => { 36 + const link = await getLink('/start/did:example:alice/xxx') 37 + const url = new URL(link) 38 + assert.strictEqual(url.origin, 'https://test.bsky.link') 39 + assert.match(url.pathname, /^\/[a-z0-9]+$/i) 40 + }) 41 + 42 + it('normalizes input paths and provides same link each time.', async () => { 43 + const link1 = await getLink('/start/did%3Aexample%3Abob/yyy') 44 + const link2 = await getLink('/start/did:example:bob/yyy/') 45 + assert.strictEqual(link1, link2) 46 + }) 47 + 48 + it('serves permanent redirect, preserving query params.', async () => { 49 + const link = await getLink('/start/did:example:carol/zzz/') 50 + const [status, location] = await getRedirect(`${link}?a=b`) 51 + assert.strictEqual(status, 301) 52 + const locationUrl = new URL(location) 53 + assert.strictEqual( 54 + locationUrl.pathname + locationUrl.search, 55 + '/start/did:example:carol/zzz?a=b', 56 + ) 57 + }) 58 + 59 + async function getRedirect(link: string): Promise<[number, string]> { 60 + const url = new URL(link) 61 + const base = new URL(baseUrl) 62 + url.protocol = base.protocol 63 + url.host = base.host 64 + const res = await fetch(url, {redirect: 'manual'}) 65 + await res.arrayBuffer() // drain 66 + assert( 67 + res.status === 301 || res.status === 303, 68 + 'response was not a redirect', 69 + ) 70 + return [res.status, res.headers.get('location') ?? ''] 71 + } 72 + 73 + async function getLink(path: string): Promise<string> { 74 + const res = await fetch(new URL('/link', baseUrl), { 75 + method: 'post', 76 + headers: {'content-type': 'application/json'}, 77 + body: JSON.stringify({path}), 78 + }) 79 + assert.strictEqual(res.status, 200) 80 + const payload = await res.json() 81 + assert(typeof payload.url === 'string') 82 + return payload.url 83 + } 84 + })
+157
bskylink/tests/infra/_common.sh
··· 1 + #!/usr/bin/env sh 2 + 3 + # Exit if any command fails 4 + set -e 5 + 6 + get_container_id() { 7 + local compose_file=$1 8 + local service=$2 9 + if [ -z "${compose_file}" ] || [ -z "${service}" ]; then 10 + echo "usage: get_container_id <compose_file> <service>" 11 + exit 1 12 + fi 13 + 14 + # first line of jq normalizes for docker compose breaking change, see docker/compose#10958 15 + docker compose --file $compose_file ps --format json --status running \ 16 + | jq -sc '.[] | if type=="array" then .[] else . end' | jq -s \ 17 + | jq -r '.[]? | select(.Service == "'${service}'") | .ID' 18 + } 19 + 20 + # Exports all environment variables 21 + export_env() { 22 + export_pg_env 23 + } 24 + 25 + # Exports postgres environment variables 26 + export_pg_env() { 27 + # Based on creds in compose.yaml 28 + export PGPORT=5433 29 + export PGHOST=localhost 30 + export PGUSER=pg 31 + export PGPASSWORD=password 32 + export PGDATABASE=postgres 33 + export DB_POSTGRES_URL="postgresql://pg:password@127.0.0.1:5433/postgres" 34 + } 35 + 36 + 37 + pg_clear() { 38 + local pg_uri=$1 39 + 40 + 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 41 + psql "${pg_uri}" -c "DROP SCHEMA \"${schema_name}\" CASCADE;" 42 + done 43 + } 44 + 45 + pg_init() { 46 + local pg_uri=$1 47 + 48 + psql "${pg_uri}" -c "CREATE SCHEMA IF NOT EXISTS \"public\";" 49 + } 50 + 51 + main_native() { 52 + local services=${SERVICES} 53 + local postgres_url_env_var=`[[ $services == *"db_test"* ]] && echo "DB_TEST_POSTGRES_URL" || echo "DB_POSTGRES_URL"` 54 + 55 + postgres_url="${!postgres_url_env_var}" 56 + 57 + if [ -n "${postgres_url}" ]; then 58 + echo "Using ${postgres_url_env_var} (${postgres_url}) to connect to postgres." 59 + pg_init "${postgres_url}" 60 + else 61 + echo "Postgres connection string missing did you set ${postgres_url_env_var}?" 62 + exit 1 63 + fi 64 + 65 + cleanup() { 66 + local services=$@ 67 + 68 + if [ -n "${postgres_url}" ] && [[ $services == *"db_test"* ]]; then 69 + pg_clear "${postgres_url}" &> /dev/null 70 + fi 71 + } 72 + 73 + # trap SIGINT and performs cleanup 74 + trap "on_sigint ${services}" INT 75 + on_sigint() { 76 + cleanup $@ 77 + exit $? 78 + } 79 + 80 + # Run the arguments as a command 81 + DB_POSTGRES_URL="${postgres_url}" \ 82 + "$@" 83 + code=$? 84 + 85 + cleanup ${services} 86 + 87 + exit ${code} 88 + } 89 + 90 + main_docker() { 91 + # Expect a SERVICES env var to be set with the docker service names 92 + local services=${SERVICES} 93 + 94 + dir=$(dirname $0) 95 + compose_file="${dir}/docker-compose.yaml" 96 + 97 + # whether this particular script started the container(s) 98 + started_container=false 99 + 100 + # performs cleanup as necessary, i.e. taking down containers 101 + # if this script started them 102 + cleanup() { 103 + local services=$@ 104 + echo # newline 105 + if $started_container; then 106 + docker compose --file $compose_file rm --force --stop --volumes ${services} 107 + fi 108 + } 109 + 110 + # trap SIGINT and performs cleanup 111 + trap "on_sigint ${services}" INT 112 + on_sigint() { 113 + cleanup $@ 114 + exit $? 115 + } 116 + 117 + # check if all services are running already 118 + not_running=false 119 + for service in $services; do 120 + container_id=$(get_container_id $compose_file $service) 121 + if [ -z $container_id ]; then 122 + not_running=true 123 + break 124 + fi 125 + done 126 + 127 + # if any are missing, recreate all services 128 + if $not_running; then 129 + started_container=true 130 + docker compose --file $compose_file up --wait --force-recreate ${services} 131 + else 132 + echo "all services ${services} are already running" 133 + fi 134 + 135 + # do not exit when following commands fail, so we can intercept exit code & tear down docker 136 + set +e 137 + 138 + # setup environment variables and run args 139 + export_env 140 + "$@" 141 + # save return code for later 142 + code=$? 143 + 144 + # performs cleanup as necessary 145 + cleanup ${services} 146 + exit ${code} 147 + } 148 + 149 + # Main entry point 150 + main() { 151 + if ! docker ps >/dev/null 2>&1; then 152 + echo "Docker unavailable. Running on host." 153 + main_native $@ 154 + else 155 + main_docker $@ 156 + fi 157 + }
+27
bskylink/tests/infra/docker-compose.yaml
··· 1 + version: '3.8' 2 + services: 3 + # An ephermerally-stored postgres database for single-use test runs 4 + db_test: &db_test 5 + image: postgres:14.11-alpine 6 + environment: 7 + - POSTGRES_USER=pg 8 + - POSTGRES_PASSWORD=password 9 + ports: 10 + - '5433:5432' 11 + # Healthcheck ensures db is queryable when `docker-compose up --wait` completes 12 + healthcheck: 13 + test: 'pg_isready -U pg' 14 + interval: 500ms 15 + timeout: 10s 16 + retries: 20 17 + # A persistently-stored postgres database 18 + db: 19 + <<: *db_test 20 + ports: 21 + - '5432:5432' 22 + healthcheck: 23 + disable: true 24 + volumes: 25 + - link_db:/var/lib/postgresql/data 26 + volumes: 27 + link_db:
+9
bskylink/tests/infra/with-test-db.sh
··· 1 + #!/usr/bin/env sh 2 + 3 + # Example usage: 4 + # ./with-test-db.sh psql postgresql://pg:password@localhost:5433/postgres -c 'select 1;' 5 + 6 + dir=$(dirname $0) 7 + . ${dir}/_common.sh 8 + 9 + SERVICES="db_test" main "$@"
+10
bskylink/tsconfig.json
··· 1 + { 2 + "compilerOptions": { 3 + "module": "NodeNext", 4 + "esModuleInterop": true, 5 + "moduleResolution": "NodeNext", 6 + "outDir": "dist", 7 + "lib": ["ES2021.String"] 8 + }, 9 + "include": ["./src/index.ts", "./src/bin.ts"] 10 + }
+1027
bskylink/yarn.lock
··· 1 + # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 + # yarn lockfile v1 3 + 4 + 5 + "@atproto/common-web@^0.3.0": 6 + version "0.3.0" 7 + resolved "https://registry.yarnpkg.com/@atproto/common-web/-/common-web-0.3.0.tgz#36da8c2c31d8cf8a140c3c8f03223319bf4430bb" 8 + integrity sha512-67VnV6JJyX+ZWyjV7xFQMypAgDmjVaR9ZCuU/QW+mqlqI7fex2uL4Fv+7/jHadgzhuJHVd6OHOvNn0wR5WZYtA== 9 + dependencies: 10 + graphemer "^1.4.0" 11 + multiformats "^9.9.0" 12 + uint8arrays "3.0.0" 13 + zod "^3.21.4" 14 + 15 + "@atproto/common@^0.4.0": 16 + version "0.4.0" 17 + resolved "https://registry.yarnpkg.com/@atproto/common/-/common-0.4.0.tgz#d77696c7eb545426df727837d9ee333b429fe7ef" 18 + integrity sha512-yOXuPlCjT/OK9j+neIGYn9wkxx/AlxQSucysAF0xgwu0Ji8jAtKBf9Jv6R5ObYAjAD/kVUvEYumle+Yq/R9/7g== 19 + dependencies: 20 + "@atproto/common-web" "^0.3.0" 21 + "@ipld/dag-cbor" "^7.0.3" 22 + cbor-x "^1.5.1" 23 + iso-datestring-validator "^2.2.2" 24 + multiformats "^9.9.0" 25 + pino "^8.15.0" 26 + 27 + "@cbor-extract/cbor-extract-darwin-arm64@2.2.0": 28 + version "2.2.0" 29 + resolved "https://registry.yarnpkg.com/@cbor-extract/cbor-extract-darwin-arm64/-/cbor-extract-darwin-arm64-2.2.0.tgz#8d65cb861a99622e1b4a268e2d522d2ec6137338" 30 + integrity sha512-P7swiOAdF7aSi0H+tHtHtr6zrpF3aAq/W9FXx5HektRvLTM2O89xCyXF3pk7pLc7QpaY7AoaE8UowVf9QBdh3w== 31 + 32 + "@cbor-extract/cbor-extract-darwin-x64@2.2.0": 33 + version "2.2.0" 34 + resolved "https://registry.yarnpkg.com/@cbor-extract/cbor-extract-darwin-x64/-/cbor-extract-darwin-x64-2.2.0.tgz#9fbec199c888c5ec485a1839f4fad0485ab6c40a" 35 + integrity sha512-1liF6fgowph0JxBbYnAS7ZlqNYLf000Qnj4KjqPNW4GViKrEql2MgZnAsExhY9LSy8dnvA4C0qHEBgPrll0z0w== 36 + 37 + "@cbor-extract/cbor-extract-linux-arm64@2.2.0": 38 + version "2.2.0" 39 + resolved "https://registry.yarnpkg.com/@cbor-extract/cbor-extract-linux-arm64/-/cbor-extract-linux-arm64-2.2.0.tgz#bf77e0db4a1d2200a5aa072e02210d5043e953ae" 40 + integrity sha512-rQvhNmDuhjTVXSPFLolmQ47/ydGOFXtbR7+wgkSY0bdOxCFept1hvg59uiLPT2fVDuJFuEy16EImo5tE2x3RsQ== 41 + 42 + "@cbor-extract/cbor-extract-linux-arm@2.2.0": 43 + version "2.2.0" 44 + resolved "https://registry.yarnpkg.com/@cbor-extract/cbor-extract-linux-arm/-/cbor-extract-linux-arm-2.2.0.tgz#491335037eb8533ed8e21b139c59f6df04e39709" 45 + integrity sha512-QeBcBXk964zOytiedMPQNZr7sg0TNavZeuUCD6ON4vEOU/25+pLhNN6EDIKJ9VLTKaZ7K7EaAriyYQ1NQ05s/Q== 46 + 47 + "@cbor-extract/cbor-extract-linux-x64@2.2.0": 48 + version "2.2.0" 49 + resolved "https://registry.yarnpkg.com/@cbor-extract/cbor-extract-linux-x64/-/cbor-extract-linux-x64-2.2.0.tgz#672574485ccd24759bf8fb8eab9dbca517d35b97" 50 + integrity sha512-cWLAWtT3kNLHSvP4RKDzSTX9o0wvQEEAj4SKvhWuOVZxiDAeQazr9A+PSiRILK1VYMLeDml89ohxCnUNQNQNCw== 51 + 52 + "@cbor-extract/cbor-extract-win32-x64@2.2.0": 53 + version "2.2.0" 54 + resolved "https://registry.yarnpkg.com/@cbor-extract/cbor-extract-win32-x64/-/cbor-extract-win32-x64-2.2.0.tgz#4b3f07af047f984c082de34b116e765cb9af975f" 55 + integrity sha512-l2M+Z8DO2vbvADOBNLbbh9y5ST1RY5sqkWOg/58GkUPBYou/cuNZ68SGQ644f1CvZ8kcOxyZtw06+dxWHIoN/w== 56 + 57 + "@ipld/dag-cbor@^7.0.3": 58 + version "7.0.3" 59 + resolved "https://registry.yarnpkg.com/@ipld/dag-cbor/-/dag-cbor-7.0.3.tgz#aa31b28afb11a807c3d627828a344e5521ac4a1e" 60 + integrity sha512-1VVh2huHsuohdXC1bGJNE8WR72slZ9XE2T3wbBBq31dm7ZBatmKLLxrB+XAqafxfRFjv08RZmj/W/ZqaM13AuA== 61 + dependencies: 62 + cborg "^1.6.0" 63 + multiformats "^9.5.4" 64 + 65 + "@types/cors@^2.8.17": 66 + version "2.8.17" 67 + resolved "https://registry.yarnpkg.com/@types/cors/-/cors-2.8.17.tgz#5d718a5e494a8166f569d986794e49c48b216b2b" 68 + integrity sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA== 69 + dependencies: 70 + "@types/node" "*" 71 + 72 + "@types/node@*": 73 + version "20.14.2" 74 + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.14.2.tgz#a5f4d2bcb4b6a87bffcaa717718c5a0f208f4a18" 75 + integrity sha512-xyu6WAMVwv6AKFLB+e/7ySZVr/0zLCzOa7rSpq6jNwpqOrUbcACDWC+53d4n2QHOnDou0fbIsg8wZu/sxrnI4Q== 76 + dependencies: 77 + undici-types "~5.26.4" 78 + 79 + "@types/pg@^8.11.6": 80 + version "8.11.6" 81 + resolved "https://registry.yarnpkg.com/@types/pg/-/pg-8.11.6.tgz#a2d0fb0a14b53951a17df5197401569fb9c0c54b" 82 + integrity sha512-/2WmmBXHLsfRqzfHW7BNZ8SbYzE8OSk7i3WjFYvfgRHj7S1xj+16Je5fUKv3lVdVzk/zn9TXOqf+avFCFIE0yQ== 83 + dependencies: 84 + "@types/node" "*" 85 + pg-protocol "*" 86 + pg-types "^4.0.1" 87 + 88 + abort-controller@^3.0.0: 89 + version "3.0.0" 90 + resolved "https://registry.yarnpkg.com/abort-controller/-/abort-controller-3.0.0.tgz#eaf54d53b62bae4138e809ca225c8439a6efb392" 91 + integrity sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg== 92 + dependencies: 93 + event-target-shim "^5.0.0" 94 + 95 + accepts@~1.3.8: 96 + version "1.3.8" 97 + resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e" 98 + integrity sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw== 99 + dependencies: 100 + mime-types "~2.1.34" 101 + negotiator "0.6.3" 102 + 103 + array-flatten@1.1.1: 104 + version "1.1.1" 105 + resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" 106 + integrity sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg== 107 + 108 + atomic-sleep@^1.0.0: 109 + version "1.0.0" 110 + resolved "https://registry.yarnpkg.com/atomic-sleep/-/atomic-sleep-1.0.0.tgz#eb85b77a601fc932cfe432c5acd364a9e2c9075b" 111 + integrity sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ== 112 + 113 + base64-js@^1.3.1: 114 + version "1.5.1" 115 + resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" 116 + integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== 117 + 118 + body-parser@1.20.2, body-parser@^1.20.2: 119 + version "1.20.2" 120 + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.2.tgz#6feb0e21c4724d06de7ff38da36dad4f57a747fd" 121 + integrity sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA== 122 + dependencies: 123 + bytes "3.1.2" 124 + content-type "~1.0.5" 125 + debug "2.6.9" 126 + depd "2.0.0" 127 + destroy "1.2.0" 128 + http-errors "2.0.0" 129 + iconv-lite "0.4.24" 130 + on-finished "2.4.1" 131 + qs "6.11.0" 132 + raw-body "2.5.2" 133 + type-is "~1.6.18" 134 + unpipe "1.0.0" 135 + 136 + boolean@^3.1.4: 137 + version "3.2.0" 138 + resolved "https://registry.yarnpkg.com/boolean/-/boolean-3.2.0.tgz#9e5294af4e98314494cbb17979fa54ca159f116b" 139 + integrity sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw== 140 + 141 + buffer@^6.0.3: 142 + version "6.0.3" 143 + resolved "https://registry.yarnpkg.com/buffer/-/buffer-6.0.3.tgz#2ace578459cc8fbe2a70aaa8f52ee63b6a74c6c6" 144 + integrity sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA== 145 + dependencies: 146 + base64-js "^1.3.1" 147 + ieee754 "^1.2.1" 148 + 149 + bytes@3.1.2: 150 + version "3.1.2" 151 + resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" 152 + integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== 153 + 154 + call-bind@^1.0.7: 155 + version "1.0.7" 156 + resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.7.tgz#06016599c40c56498c18769d2730be242b6fa3b9" 157 + integrity sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w== 158 + dependencies: 159 + es-define-property "^1.0.0" 160 + es-errors "^1.3.0" 161 + function-bind "^1.1.2" 162 + get-intrinsic "^1.2.4" 163 + set-function-length "^1.2.1" 164 + 165 + cbor-extract@^2.2.0: 166 + version "2.2.0" 167 + resolved "https://registry.yarnpkg.com/cbor-extract/-/cbor-extract-2.2.0.tgz#cee78e630cbeae3918d1e2e58e0cebaf3a3be840" 168 + integrity sha512-Ig1zM66BjLfTXpNgKpvBePq271BPOvu8MR0Jl080yG7Jsl+wAZunfrwiwA+9ruzm/WEdIV5QF/bjDZTqyAIVHA== 169 + dependencies: 170 + node-gyp-build-optional-packages "5.1.1" 171 + optionalDependencies: 172 + "@cbor-extract/cbor-extract-darwin-arm64" "2.2.0" 173 + "@cbor-extract/cbor-extract-darwin-x64" "2.2.0" 174 + "@cbor-extract/cbor-extract-linux-arm" "2.2.0" 175 + "@cbor-extract/cbor-extract-linux-arm64" "2.2.0" 176 + "@cbor-extract/cbor-extract-linux-x64" "2.2.0" 177 + "@cbor-extract/cbor-extract-win32-x64" "2.2.0" 178 + 179 + cbor-x@^1.5.1: 180 + version "1.5.9" 181 + resolved "https://registry.yarnpkg.com/cbor-x/-/cbor-x-1.5.9.tgz#ed6b2afcd7884bdd697674bfb7332c1473a13ecf" 182 + integrity sha512-OEI5rEu3MeR0WWNUXuIGkxmbXVhABP+VtgAXzm48c9ulkrsvxshjjk94XSOGphyAKeNGLPfAxxzEtgQ6rEVpYQ== 183 + optionalDependencies: 184 + cbor-extract "^2.2.0" 185 + 186 + cborg@^1.6.0: 187 + version "1.10.2" 188 + resolved "https://registry.yarnpkg.com/cborg/-/cborg-1.10.2.tgz#83cd581b55b3574c816f82696307c7512db759a1" 189 + integrity sha512-b3tFPA9pUr2zCUiCfRd2+wok2/LBSNUMKOuRRok+WlvvAgEt/PlbgPTsZUcwCOs53IJvLgTp0eotwtosE6njug== 190 + 191 + content-disposition@0.5.4: 192 + version "0.5.4" 193 + resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.4.tgz#8b82b4efac82512a02bb0b1dcec9d2c5e8eb5bfe" 194 + integrity sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ== 195 + dependencies: 196 + safe-buffer "5.2.1" 197 + 198 + content-type@~1.0.4, content-type@~1.0.5: 199 + version "1.0.5" 200 + resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.5.tgz#8b773162656d1d1086784c8f23a54ce6d73d7918" 201 + integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA== 202 + 203 + cookie-signature@1.0.6: 204 + version "1.0.6" 205 + resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" 206 + integrity sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ== 207 + 208 + cookie@0.6.0: 209 + version "0.6.0" 210 + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.6.0.tgz#2798b04b071b0ecbff0dbb62a505a8efa4e19051" 211 + integrity sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw== 212 + 213 + cors@^2.8.5: 214 + version "2.8.5" 215 + resolved "https://registry.yarnpkg.com/cors/-/cors-2.8.5.tgz#eac11da51592dd86b9f06f6e7ac293b3df875d29" 216 + integrity sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g== 217 + dependencies: 218 + object-assign "^4" 219 + vary "^1" 220 + 221 + debug@2.6.9: 222 + version "2.6.9" 223 + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" 224 + integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== 225 + dependencies: 226 + ms "2.0.0" 227 + 228 + define-data-property@^1.1.4: 229 + version "1.1.4" 230 + resolved "https://registry.yarnpkg.com/define-data-property/-/define-data-property-1.1.4.tgz#894dc141bb7d3060ae4366f6a0107e68fbe48c5e" 231 + integrity sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A== 232 + dependencies: 233 + es-define-property "^1.0.0" 234 + es-errors "^1.3.0" 235 + gopd "^1.0.1" 236 + 237 + delay@^5.0.0: 238 + version "5.0.0" 239 + resolved "https://registry.yarnpkg.com/delay/-/delay-5.0.0.tgz#137045ef1b96e5071060dd5be60bf9334436bd1d" 240 + integrity sha512-ReEBKkIfe4ya47wlPYf/gu5ib6yUG0/Aez0JQZQz94kiWtRQvZIQbTiehsnwHvLSWJnQdhVeqYue7Id1dKr0qw== 241 + 242 + depd@2.0.0: 243 + version "2.0.0" 244 + resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" 245 + integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== 246 + 247 + destroy@1.2.0: 248 + version "1.2.0" 249 + resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015" 250 + integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg== 251 + 252 + detect-libc@^2.0.1: 253 + version "2.0.3" 254 + resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.0.3.tgz#f0cd503b40f9939b894697d19ad50895e30cf700" 255 + integrity sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw== 256 + 257 + ee-first@1.1.1: 258 + version "1.1.1" 259 + resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" 260 + integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow== 261 + 262 + encodeurl@~1.0.2: 263 + version "1.0.2" 264 + resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" 265 + integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w== 266 + 267 + es-define-property@^1.0.0: 268 + version "1.0.0" 269 + resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.0.tgz#c7faefbdff8b2696cf5f46921edfb77cc4ba3845" 270 + integrity sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ== 271 + dependencies: 272 + get-intrinsic "^1.2.4" 273 + 274 + es-errors@^1.3.0: 275 + version "1.3.0" 276 + resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f" 277 + integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== 278 + 279 + escape-html@~1.0.3: 280 + version "1.0.3" 281 + resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" 282 + integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow== 283 + 284 + etag@~1.8.1: 285 + version "1.8.1" 286 + resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" 287 + integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg== 288 + 289 + event-target-shim@^5.0.0: 290 + version "5.0.1" 291 + resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789" 292 + integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ== 293 + 294 + events@^3.3.0: 295 + version "3.3.0" 296 + resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" 297 + integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== 298 + 299 + express@^4.19.2: 300 + version "4.19.2" 301 + resolved "https://registry.yarnpkg.com/express/-/express-4.19.2.tgz#e25437827a3aa7f2a827bc8171bbbb664a356465" 302 + integrity sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q== 303 + dependencies: 304 + accepts "~1.3.8" 305 + array-flatten "1.1.1" 306 + body-parser "1.20.2" 307 + content-disposition "0.5.4" 308 + content-type "~1.0.4" 309 + cookie "0.6.0" 310 + cookie-signature "1.0.6" 311 + debug "2.6.9" 312 + depd "2.0.0" 313 + encodeurl "~1.0.2" 314 + escape-html "~1.0.3" 315 + etag "~1.8.1" 316 + finalhandler "1.2.0" 317 + fresh "0.5.2" 318 + http-errors "2.0.0" 319 + merge-descriptors "1.0.1" 320 + methods "~1.1.2" 321 + on-finished "2.4.1" 322 + parseurl "~1.3.3" 323 + path-to-regexp "0.1.7" 324 + proxy-addr "~2.0.7" 325 + qs "6.11.0" 326 + range-parser "~1.2.1" 327 + safe-buffer "5.2.1" 328 + send "0.18.0" 329 + serve-static "1.15.0" 330 + setprototypeof "1.2.0" 331 + statuses "2.0.1" 332 + type-is "~1.6.18" 333 + utils-merge "1.0.1" 334 + vary "~1.1.2" 335 + 336 + fast-printf@^1.6.9: 337 + version "1.6.9" 338 + resolved "https://registry.yarnpkg.com/fast-printf/-/fast-printf-1.6.9.tgz#212f56570d2dc8ccdd057ee93d50dd414d07d676" 339 + integrity sha512-FChq8hbz65WMj4rstcQsFB0O7Cy++nmbNfLYnD9cYv2cRn8EG6k/MGn9kO/tjO66t09DLDugj3yL+V2o6Qftrg== 340 + dependencies: 341 + boolean "^3.1.4" 342 + 343 + fast-redact@^3.1.1: 344 + version "3.5.0" 345 + resolved "https://registry.yarnpkg.com/fast-redact/-/fast-redact-3.5.0.tgz#e9ea02f7e57d0cd8438180083e93077e496285e4" 346 + integrity sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A== 347 + 348 + finalhandler@1.2.0: 349 + version "1.2.0" 350 + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.2.0.tgz#7d23fe5731b207b4640e4fcd00aec1f9207a7b32" 351 + integrity sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg== 352 + dependencies: 353 + debug "2.6.9" 354 + encodeurl "~1.0.2" 355 + escape-html "~1.0.3" 356 + on-finished "2.4.1" 357 + parseurl "~1.3.3" 358 + statuses "2.0.1" 359 + unpipe "~1.0.0" 360 + 361 + forwarded@0.2.0: 362 + version "0.2.0" 363 + resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" 364 + integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow== 365 + 366 + fresh@0.5.2: 367 + version "0.5.2" 368 + resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" 369 + integrity sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q== 370 + 371 + function-bind@^1.1.2: 372 + version "1.1.2" 373 + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" 374 + integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== 375 + 376 + get-intrinsic@^1.1.3, get-intrinsic@^1.2.4: 377 + version "1.2.4" 378 + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.4.tgz#e385f5a4b5227d449c3eabbad05494ef0abbeadd" 379 + integrity sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ== 380 + dependencies: 381 + es-errors "^1.3.0" 382 + function-bind "^1.1.2" 383 + has-proto "^1.0.1" 384 + has-symbols "^1.0.3" 385 + hasown "^2.0.0" 386 + 387 + gopd@^1.0.1: 388 + version "1.0.1" 389 + resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.0.1.tgz#29ff76de69dac7489b7c0918a5788e56477c332c" 390 + integrity sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA== 391 + dependencies: 392 + get-intrinsic "^1.1.3" 393 + 394 + graphemer@^1.4.0: 395 + version "1.4.0" 396 + resolved "https://registry.yarnpkg.com/graphemer/-/graphemer-1.4.0.tgz#fb2f1d55e0e3a1849aeffc90c4fa0dd53a0e66c6" 397 + integrity sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag== 398 + 399 + has-property-descriptors@^1.0.2: 400 + version "1.0.2" 401 + resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz#963ed7d071dc7bf5f084c5bfbe0d1b6222586854" 402 + integrity sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg== 403 + dependencies: 404 + es-define-property "^1.0.0" 405 + 406 + has-proto@^1.0.1: 407 + version "1.0.3" 408 + resolved "https://registry.yarnpkg.com/has-proto/-/has-proto-1.0.3.tgz#b31ddfe9b0e6e9914536a6ab286426d0214f77fd" 409 + integrity sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q== 410 + 411 + has-symbols@^1.0.3: 412 + version "1.0.3" 413 + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" 414 + integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== 415 + 416 + hasown@^2.0.0: 417 + version "2.0.2" 418 + resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003" 419 + integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ== 420 + dependencies: 421 + function-bind "^1.1.2" 422 + 423 + http-errors@2.0.0: 424 + version "2.0.0" 425 + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.0.tgz#b7774a1486ef73cf7667ac9ae0858c012c57b9d3" 426 + integrity sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ== 427 + dependencies: 428 + depd "2.0.0" 429 + inherits "2.0.4" 430 + setprototypeof "1.2.0" 431 + statuses "2.0.1" 432 + toidentifier "1.0.1" 433 + 434 + http-terminator@^3.2.0: 435 + version "3.2.0" 436 + resolved "https://registry.yarnpkg.com/http-terminator/-/http-terminator-3.2.0.tgz#bc158d2694b733ca4fbf22a35065a81a609fb3e9" 437 + integrity sha512-JLjck1EzPaWjsmIf8bziM3p9fgR1Y3JoUKAkyYEbZmFrIvJM6I8vVJfBGWlEtV9IWOvzNnaTtjuwZeBY2kwB4g== 438 + dependencies: 439 + delay "^5.0.0" 440 + p-wait-for "^3.2.0" 441 + roarr "^7.0.4" 442 + type-fest "^2.3.3" 443 + 444 + iconv-lite@0.4.24: 445 + version "0.4.24" 446 + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" 447 + integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== 448 + dependencies: 449 + safer-buffer ">= 2.1.2 < 3" 450 + 451 + ieee754@^1.2.1: 452 + version "1.2.1" 453 + resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" 454 + integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== 455 + 456 + inherits@2.0.4: 457 + version "2.0.4" 458 + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" 459 + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== 460 + 461 + ipaddr.js@1.9.1: 462 + version "1.9.1" 463 + resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" 464 + integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== 465 + 466 + iso-datestring-validator@^2.2.2: 467 + version "2.2.2" 468 + resolved "https://registry.yarnpkg.com/iso-datestring-validator/-/iso-datestring-validator-2.2.2.tgz#2daa80d2900b7a954f9f731d42f96ee0c19a6895" 469 + integrity sha512-yLEMkBbLZTlVQqOnQ4FiMujR6T4DEcCb1xizmvXS+OxuhwcbtynoosRzdMA69zZCShCNAbi+gJ71FxZBBXx1SA== 470 + 471 + kysely@^0.27.3: 472 + version "0.27.3" 473 + resolved "https://registry.yarnpkg.com/kysely/-/kysely-0.27.3.tgz#6cc6c757040500b43c4ac596cdbb12be400ee276" 474 + integrity sha512-lG03Ru+XyOJFsjH3OMY6R/9U38IjDPfnOfDgO3ynhbDr+Dz8fak+X6L62vqu3iybQnj+lG84OttBuU9KY3L9kA== 475 + 476 + media-typer@0.3.0: 477 + version "0.3.0" 478 + resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" 479 + integrity sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ== 480 + 481 + merge-descriptors@1.0.1: 482 + version "1.0.1" 483 + resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" 484 + integrity sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w== 485 + 486 + methods@~1.1.2: 487 + version "1.1.2" 488 + resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" 489 + integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w== 490 + 491 + mime-db@1.52.0: 492 + version "1.52.0" 493 + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" 494 + integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== 495 + 496 + mime-types@~2.1.24, mime-types@~2.1.34: 497 + version "2.1.35" 498 + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" 499 + integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== 500 + dependencies: 501 + mime-db "1.52.0" 502 + 503 + mime@1.6.0: 504 + version "1.6.0" 505 + resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" 506 + integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== 507 + 508 + ms@2.0.0: 509 + version "2.0.0" 510 + resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" 511 + integrity sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A== 512 + 513 + ms@2.1.3: 514 + version "2.1.3" 515 + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" 516 + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== 517 + 518 + multiformats@^13.0.0: 519 + version "13.1.1" 520 + resolved "https://registry.yarnpkg.com/multiformats/-/multiformats-13.1.1.tgz#b22ce4df26330d2cf0d69f5bdcbc9a787095a6e5" 521 + integrity sha512-JiptvwMmlxlzIlLLwhCi/srf/nk409UL0eUBr0kioRJq15hqqKyg68iftrBvhCRjR6Rw4fkNnSc4ZJXJDuta/Q== 522 + 523 + multiformats@^9.4.2, multiformats@^9.5.4, multiformats@^9.9.0: 524 + version "9.9.0" 525 + resolved "https://registry.yarnpkg.com/multiformats/-/multiformats-9.9.0.tgz#c68354e7d21037a8f1f8833c8ccd68618e8f1d37" 526 + integrity sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg== 527 + 528 + negotiator@0.6.3: 529 + version "0.6.3" 530 + resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd" 531 + integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg== 532 + 533 + node-gyp-build-optional-packages@5.1.1: 534 + version "5.1.1" 535 + resolved "https://registry.yarnpkg.com/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.1.1.tgz#52b143b9dd77b7669073cbfe39e3f4118bfc603c" 536 + integrity sha512-+P72GAjVAbTxjjwUmwjVrqrdZROD4nf8KgpBoDxqXXTiYZZt/ud60dE5yvCSr9lRO8e8yv6kgJIC0K0PfZFVQw== 537 + dependencies: 538 + detect-libc "^2.0.1" 539 + 540 + object-assign@^4: 541 + version "4.1.1" 542 + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" 543 + integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== 544 + 545 + object-inspect@^1.13.1: 546 + version "1.13.1" 547 + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.1.tgz#b96c6109324ccfef6b12216a956ca4dc2ff94bc2" 548 + integrity sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ== 549 + 550 + obuf@~1.1.2: 551 + version "1.1.2" 552 + resolved "https://registry.yarnpkg.com/obuf/-/obuf-1.1.2.tgz#09bea3343d41859ebd446292d11c9d4db619084e" 553 + integrity sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg== 554 + 555 + on-exit-leak-free@^2.1.0: 556 + version "2.1.2" 557 + resolved "https://registry.yarnpkg.com/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz#fed195c9ebddb7d9e4c3842f93f281ac8dadd3b8" 558 + integrity sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA== 559 + 560 + on-finished@2.4.1: 561 + version "2.4.1" 562 + resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f" 563 + integrity sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg== 564 + dependencies: 565 + ee-first "1.1.1" 566 + 567 + p-finally@^1.0.0: 568 + version "1.0.0" 569 + resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae" 570 + integrity sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow== 571 + 572 + p-timeout@^3.0.0: 573 + version "3.2.0" 574 + resolved "https://registry.yarnpkg.com/p-timeout/-/p-timeout-3.2.0.tgz#c7e17abc971d2a7962ef83626b35d635acf23dfe" 575 + integrity sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg== 576 + dependencies: 577 + p-finally "^1.0.0" 578 + 579 + p-wait-for@^3.2.0: 580 + version "3.2.0" 581 + resolved "https://registry.yarnpkg.com/p-wait-for/-/p-wait-for-3.2.0.tgz#640429bcabf3b0dd9f492c31539c5718cb6a3f1f" 582 + integrity sha512-wpgERjNkLrBiFmkMEjuZJEWKKDrNfHCKA1OhyN1wg1FrLkULbviEy6py1AyJUgZ72YWFbZ38FIpnqvVqAlDUwA== 583 + dependencies: 584 + p-timeout "^3.0.0" 585 + 586 + parseurl@~1.3.3: 587 + version "1.3.3" 588 + resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" 589 + integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== 590 + 591 + path-to-regexp@0.1.7: 592 + version "0.1.7" 593 + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" 594 + integrity sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ== 595 + 596 + pg-cloudflare@^1.1.1: 597 + version "1.1.1" 598 + resolved "https://registry.yarnpkg.com/pg-cloudflare/-/pg-cloudflare-1.1.1.tgz#e6d5833015b170e23ae819e8c5d7eaedb472ca98" 599 + integrity sha512-xWPagP/4B6BgFO+EKz3JONXv3YDgvkbVrGw2mTo3D6tVDQRh1e7cqVGvyR3BE+eQgAvx1XhW/iEASj4/jCWl3Q== 600 + 601 + pg-connection-string@^2.6.4: 602 + version "2.6.4" 603 + resolved "https://registry.yarnpkg.com/pg-connection-string/-/pg-connection-string-2.6.4.tgz#f543862adfa49fa4e14bc8a8892d2a84d754246d" 604 + integrity sha512-v+Z7W/0EO707aNMaAEfiGnGL9sxxumwLl2fJvCQtMn9Fxsg+lPpPkdcyBSv/KFgpGdYkMfn+EI1Or2EHjpgLCA== 605 + 606 + pg-int8@1.0.1: 607 + version "1.0.1" 608 + resolved "https://registry.yarnpkg.com/pg-int8/-/pg-int8-1.0.1.tgz#943bd463bf5b71b4170115f80f8efc9a0c0eb78c" 609 + integrity sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw== 610 + 611 + pg-numeric@1.0.2: 612 + version "1.0.2" 613 + resolved "https://registry.yarnpkg.com/pg-numeric/-/pg-numeric-1.0.2.tgz#816d9a44026086ae8ae74839acd6a09b0636aa3a" 614 + integrity sha512-BM/Thnrw5jm2kKLE5uJkXqqExRUY/toLHda65XgFTBTFYZyopbKjBe29Ii3RbkvlsMoFwD+tHeGaCjjv0gHlyw== 615 + 616 + pg-pool@^3.6.2: 617 + version "3.6.2" 618 + resolved "https://registry.yarnpkg.com/pg-pool/-/pg-pool-3.6.2.tgz#3a592370b8ae3f02a7c8130d245bc02fa2c5f3f2" 619 + integrity sha512-Htjbg8BlwXqSBQ9V8Vjtc+vzf/6fVUuak/3/XXKA9oxZprwW3IMDQTGHP+KDmVL7rtd+R1QjbnCFPuTHm3G4hg== 620 + 621 + pg-protocol@*, pg-protocol@^1.6.1: 622 + version "1.6.1" 623 + resolved "https://registry.yarnpkg.com/pg-protocol/-/pg-protocol-1.6.1.tgz#21333e6d83b01faaebfe7a33a7ad6bfd9ed38cb3" 624 + integrity sha512-jPIlvgoD63hrEuihvIg+tJhoGjUsLPn6poJY9N5CnlPd91c2T18T/9zBtLxZSb1EhYxBRoZJtzScCaWlYLtktg== 625 + 626 + pg-types@^2.1.0: 627 + version "2.2.0" 628 + resolved "https://registry.yarnpkg.com/pg-types/-/pg-types-2.2.0.tgz#2d0250d636454f7cfa3b6ae0382fdfa8063254a3" 629 + integrity sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA== 630 + dependencies: 631 + pg-int8 "1.0.1" 632 + postgres-array "~2.0.0" 633 + postgres-bytea "~1.0.0" 634 + postgres-date "~1.0.4" 635 + postgres-interval "^1.1.0" 636 + 637 + pg-types@^4.0.1: 638 + version "4.0.2" 639 + resolved "https://registry.yarnpkg.com/pg-types/-/pg-types-4.0.2.tgz#399209a57c326f162461faa870145bb0f918b76d" 640 + integrity sha512-cRL3JpS3lKMGsKaWndugWQoLOCoP+Cic8oseVcbr0qhPzYD5DWXK+RZ9LY9wxRf7RQia4SCwQlXk0q6FCPrVng== 641 + dependencies: 642 + pg-int8 "1.0.1" 643 + pg-numeric "1.0.2" 644 + postgres-array "~3.0.1" 645 + postgres-bytea "~3.0.0" 646 + postgres-date "~2.1.0" 647 + postgres-interval "^3.0.0" 648 + postgres-range "^1.1.1" 649 + 650 + pg@^8.12.0: 651 + version "8.12.0" 652 + resolved "https://registry.yarnpkg.com/pg/-/pg-8.12.0.tgz#9341724db571022490b657908f65aee8db91df79" 653 + integrity sha512-A+LHUSnwnxrnL/tZ+OLfqR1SxLN3c/pgDztZ47Rpbsd4jUytsTtwQo/TLPRzPJMp/1pbhYVhH9cuSZLAajNfjQ== 654 + dependencies: 655 + pg-connection-string "^2.6.4" 656 + pg-pool "^3.6.2" 657 + pg-protocol "^1.6.1" 658 + pg-types "^2.1.0" 659 + pgpass "1.x" 660 + optionalDependencies: 661 + pg-cloudflare "^1.1.1" 662 + 663 + pgpass@1.x: 664 + version "1.0.5" 665 + resolved "https://registry.yarnpkg.com/pgpass/-/pgpass-1.0.5.tgz#9b873e4a564bb10fa7a7dbd55312728d422a223d" 666 + integrity sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug== 667 + dependencies: 668 + split2 "^4.1.0" 669 + 670 + pino-abstract-transport@^1.2.0: 671 + version "1.2.0" 672 + resolved "https://registry.yarnpkg.com/pino-abstract-transport/-/pino-abstract-transport-1.2.0.tgz#97f9f2631931e242da531b5c66d3079c12c9d1b5" 673 + integrity sha512-Guhh8EZfPCfH+PMXAb6rKOjGQEoy0xlAIn+irODG5kgfYV+BQ0rGYYWTIel3P5mmyXqkYkPmdIkywsn6QKUR1Q== 674 + dependencies: 675 + readable-stream "^4.0.0" 676 + split2 "^4.0.0" 677 + 678 + pino-std-serializers@^6.0.0: 679 + version "6.2.2" 680 + resolved "https://registry.yarnpkg.com/pino-std-serializers/-/pino-std-serializers-6.2.2.tgz#d9a9b5f2b9a402486a5fc4db0a737570a860aab3" 681 + integrity sha512-cHjPPsE+vhj/tnhCy/wiMh3M3z3h/j15zHQX+S9GkTBgqJuTuJzYJ4gUyACLhDaJ7kk9ba9iRDmbH2tJU03OiA== 682 + 683 + pino-std-serializers@^7.0.0: 684 + version "7.0.0" 685 + resolved "https://registry.yarnpkg.com/pino-std-serializers/-/pino-std-serializers-7.0.0.tgz#7c625038b13718dbbd84ab446bd673dc52259e3b" 686 + integrity sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA== 687 + 688 + pino@^8.15.0: 689 + version "8.21.0" 690 + resolved "https://registry.yarnpkg.com/pino/-/pino-8.21.0.tgz#e1207f3675a2722940d62da79a7a55a98409f00d" 691 + integrity sha512-ip4qdzjkAyDDZklUaZkcRFb2iA118H9SgRh8yzTkSQK8HilsOJF7rSY8HoW5+I0M46AZgX/pxbprf2vvzQCE0Q== 692 + dependencies: 693 + atomic-sleep "^1.0.0" 694 + fast-redact "^3.1.1" 695 + on-exit-leak-free "^2.1.0" 696 + pino-abstract-transport "^1.2.0" 697 + pino-std-serializers "^6.0.0" 698 + process-warning "^3.0.0" 699 + quick-format-unescaped "^4.0.3" 700 + real-require "^0.2.0" 701 + safe-stable-stringify "^2.3.1" 702 + sonic-boom "^3.7.0" 703 + thread-stream "^2.6.0" 704 + 705 + pino@^9.2.0: 706 + version "9.2.0" 707 + resolved "https://registry.yarnpkg.com/pino/-/pino-9.2.0.tgz#e77a9516f3a3e5550d9b76d9f65ac6118ef02bdd" 708 + integrity sha512-g3/hpwfujK5a4oVbaefoJxezLzsDgLcNJeITvC6yrfwYeT9la+edCK42j5QpEQSQCZgTKapXvnQIdgZwvRaZug== 709 + dependencies: 710 + atomic-sleep "^1.0.0" 711 + fast-redact "^3.1.1" 712 + on-exit-leak-free "^2.1.0" 713 + pino-abstract-transport "^1.2.0" 714 + pino-std-serializers "^7.0.0" 715 + process-warning "^3.0.0" 716 + quick-format-unescaped "^4.0.3" 717 + real-require "^0.2.0" 718 + safe-stable-stringify "^2.3.1" 719 + sonic-boom "^4.0.1" 720 + thread-stream "^3.0.0" 721 + 722 + postgres-array@~2.0.0: 723 + version "2.0.0" 724 + resolved "https://registry.yarnpkg.com/postgres-array/-/postgres-array-2.0.0.tgz#48f8fce054fbc69671999329b8834b772652d82e" 725 + integrity sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA== 726 + 727 + postgres-array@~3.0.1: 728 + version "3.0.2" 729 + resolved "https://registry.yarnpkg.com/postgres-array/-/postgres-array-3.0.2.tgz#68d6182cb0f7f152a7e60dc6a6889ed74b0a5f98" 730 + integrity sha512-6faShkdFugNQCLwucjPcY5ARoW1SlbnrZjmGl0IrrqewpvxvhSLHimCVzqeuULCbG0fQv7Dtk1yDbG3xv7Veog== 731 + 732 + postgres-bytea@~1.0.0: 733 + version "1.0.0" 734 + resolved "https://registry.yarnpkg.com/postgres-bytea/-/postgres-bytea-1.0.0.tgz#027b533c0aa890e26d172d47cf9ccecc521acd35" 735 + integrity sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w== 736 + 737 + postgres-bytea@~3.0.0: 738 + version "3.0.0" 739 + resolved "https://registry.yarnpkg.com/postgres-bytea/-/postgres-bytea-3.0.0.tgz#9048dc461ac7ba70a6a42d109221619ecd1cb089" 740 + integrity sha512-CNd4jim9RFPkObHSjVHlVrxoVQXz7quwNFpz7RY1okNNme49+sVyiTvTRobiLV548Hx/hb1BG+iE7h9493WzFw== 741 + dependencies: 742 + obuf "~1.1.2" 743 + 744 + postgres-date@~1.0.4: 745 + version "1.0.7" 746 + resolved "https://registry.yarnpkg.com/postgres-date/-/postgres-date-1.0.7.tgz#51bc086006005e5061c591cee727f2531bf641a8" 747 + integrity sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q== 748 + 749 + postgres-date@~2.1.0: 750 + version "2.1.0" 751 + resolved "https://registry.yarnpkg.com/postgres-date/-/postgres-date-2.1.0.tgz#b85d3c1fb6fb3c6c8db1e9942a13a3bf625189d0" 752 + integrity sha512-K7Juri8gtgXVcDfZttFKVmhglp7epKb1K4pgrkLxehjqkrgPhfG6OO8LHLkfaqkbpjNRnra018XwAr1yQFWGcA== 753 + 754 + postgres-interval@^1.1.0: 755 + version "1.2.0" 756 + resolved "https://registry.yarnpkg.com/postgres-interval/-/postgres-interval-1.2.0.tgz#b460c82cb1587507788819a06aa0fffdb3544695" 757 + integrity sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ== 758 + dependencies: 759 + xtend "^4.0.0" 760 + 761 + postgres-interval@^3.0.0: 762 + version "3.0.0" 763 + resolved "https://registry.yarnpkg.com/postgres-interval/-/postgres-interval-3.0.0.tgz#baf7a8b3ebab19b7f38f07566c7aab0962f0c86a" 764 + integrity sha512-BSNDnbyZCXSxgA+1f5UU2GmwhoI0aU5yMxRGO8CdFEcY2BQF9xm/7MqKnYoM1nJDk8nONNWDk9WeSmePFhQdlw== 765 + 766 + postgres-range@^1.1.1: 767 + version "1.1.4" 768 + resolved "https://registry.yarnpkg.com/postgres-range/-/postgres-range-1.1.4.tgz#a59c5f9520909bcec5e63e8cf913a92e4c952863" 769 + integrity sha512-i/hbxIE9803Alj/6ytL7UHQxRvZkI9O4Sy+J3HGc4F4oo/2eQAjTSNJ0bfxyse3bH0nuVesCk+3IRLaMtG3H6w== 770 + 771 + process-warning@^3.0.0: 772 + version "3.0.0" 773 + resolved "https://registry.yarnpkg.com/process-warning/-/process-warning-3.0.0.tgz#96e5b88884187a1dce6f5c3166d611132058710b" 774 + integrity sha512-mqn0kFRl0EoqhnL0GQ0veqFHyIN1yig9RHh/InzORTUiZHFRAur+aMtRkELNwGs9aNwKS6tg/An4NYBPGwvtzQ== 775 + 776 + process@^0.11.10: 777 + version "0.11.10" 778 + resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" 779 + integrity sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A== 780 + 781 + proxy-addr@~2.0.7: 782 + version "2.0.7" 783 + resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025" 784 + integrity sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg== 785 + dependencies: 786 + forwarded "0.2.0" 787 + ipaddr.js "1.9.1" 788 + 789 + qs@6.11.0: 790 + version "6.11.0" 791 + resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.0.tgz#fd0d963446f7a65e1367e01abd85429453f0c37a" 792 + integrity sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q== 793 + dependencies: 794 + side-channel "^1.0.4" 795 + 796 + quick-format-unescaped@^4.0.3: 797 + version "4.0.4" 798 + resolved "https://registry.yarnpkg.com/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz#93ef6dd8d3453cbc7970dd614fad4c5954d6b5a7" 799 + integrity sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg== 800 + 801 + range-parser@~1.2.1: 802 + version "1.2.1" 803 + resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" 804 + integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== 805 + 806 + raw-body@2.5.2: 807 + version "2.5.2" 808 + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.2.tgz#99febd83b90e08975087e8f1f9419a149366b68a" 809 + integrity sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA== 810 + dependencies: 811 + bytes "3.1.2" 812 + http-errors "2.0.0" 813 + iconv-lite "0.4.24" 814 + unpipe "1.0.0" 815 + 816 + readable-stream@^4.0.0: 817 + version "4.5.2" 818 + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-4.5.2.tgz#9e7fc4c45099baeed934bff6eb97ba6cf2729e09" 819 + integrity sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g== 820 + dependencies: 821 + abort-controller "^3.0.0" 822 + buffer "^6.0.3" 823 + events "^3.3.0" 824 + process "^0.11.10" 825 + string_decoder "^1.3.0" 826 + 827 + real-require@^0.2.0: 828 + version "0.2.0" 829 + resolved "https://registry.yarnpkg.com/real-require/-/real-require-0.2.0.tgz#209632dea1810be2ae063a6ac084fee7e33fba78" 830 + integrity sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg== 831 + 832 + roarr@^7.0.4: 833 + version "7.21.1" 834 + resolved "https://registry.yarnpkg.com/roarr/-/roarr-7.21.1.tgz#fd6452ca822a65f736c35e5372f04ee9f2ca3851" 835 + integrity sha512-3niqt5bXFY1InKU8HKWqqYTYjtrBaxBMnXELXCXUYgtNYGUtZM5rB46HIC430AyacL95iEniGf7RgqsesykLmQ== 836 + dependencies: 837 + fast-printf "^1.6.9" 838 + safe-stable-stringify "^2.4.3" 839 + semver-compare "^1.0.0" 840 + 841 + safe-buffer@5.2.1, safe-buffer@~5.2.0: 842 + version "5.2.1" 843 + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" 844 + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== 845 + 846 + safe-stable-stringify@^2.3.1, safe-stable-stringify@^2.4.3: 847 + version "2.4.3" 848 + resolved "https://registry.yarnpkg.com/safe-stable-stringify/-/safe-stable-stringify-2.4.3.tgz#138c84b6f6edb3db5f8ef3ef7115b8f55ccbf886" 849 + integrity sha512-e2bDA2WJT0wxseVd4lsDP4+3ONX6HpMXQa1ZhFQ7SU+GjvORCmShbCMltrtIDfkYhVHrOcPtj+KhmDBdPdZD1g== 850 + 851 + "safer-buffer@>= 2.1.2 < 3": 852 + version "2.1.2" 853 + resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" 854 + integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== 855 + 856 + semver-compare@^1.0.0: 857 + version "1.0.0" 858 + resolved "https://registry.yarnpkg.com/semver-compare/-/semver-compare-1.0.0.tgz#0dee216a1c941ab37e9efb1788f6afc5ff5537fc" 859 + integrity sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow== 860 + 861 + send@0.18.0: 862 + version "0.18.0" 863 + resolved "https://registry.yarnpkg.com/send/-/send-0.18.0.tgz#670167cc654b05f5aa4a767f9113bb371bc706be" 864 + integrity sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg== 865 + dependencies: 866 + debug "2.6.9" 867 + depd "2.0.0" 868 + destroy "1.2.0" 869 + encodeurl "~1.0.2" 870 + escape-html "~1.0.3" 871 + etag "~1.8.1" 872 + fresh "0.5.2" 873 + http-errors "2.0.0" 874 + mime "1.6.0" 875 + ms "2.1.3" 876 + on-finished "2.4.1" 877 + range-parser "~1.2.1" 878 + statuses "2.0.1" 879 + 880 + serve-static@1.15.0: 881 + version "1.15.0" 882 + resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.15.0.tgz#faaef08cffe0a1a62f60cad0c4e513cff0ac9540" 883 + integrity sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g== 884 + dependencies: 885 + encodeurl "~1.0.2" 886 + escape-html "~1.0.3" 887 + parseurl "~1.3.3" 888 + send "0.18.0" 889 + 890 + set-function-length@^1.2.1: 891 + version "1.2.2" 892 + resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.2.2.tgz#aac72314198eaed975cf77b2c3b6b880695e5449" 893 + integrity sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg== 894 + dependencies: 895 + define-data-property "^1.1.4" 896 + es-errors "^1.3.0" 897 + function-bind "^1.1.2" 898 + get-intrinsic "^1.2.4" 899 + gopd "^1.0.1" 900 + has-property-descriptors "^1.0.2" 901 + 902 + setprototypeof@1.2.0: 903 + version "1.2.0" 904 + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" 905 + integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== 906 + 907 + side-channel@^1.0.4: 908 + version "1.0.6" 909 + resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.6.tgz#abd25fb7cd24baf45466406b1096b7831c9215f2" 910 + integrity sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA== 911 + dependencies: 912 + call-bind "^1.0.7" 913 + es-errors "^1.3.0" 914 + get-intrinsic "^1.2.4" 915 + object-inspect "^1.13.1" 916 + 917 + sonic-boom@^3.7.0: 918 + version "3.8.1" 919 + resolved "https://registry.yarnpkg.com/sonic-boom/-/sonic-boom-3.8.1.tgz#d5ba8c4e26d6176c9a1d14d549d9ff579a163422" 920 + integrity sha512-y4Z8LCDBuum+PBP3lSV7RHrXscqksve/bi0as7mhwVnBW+/wUqKT/2Kb7um8yqcFy0duYbbPxzt89Zy2nOCaxg== 921 + dependencies: 922 + atomic-sleep "^1.0.0" 923 + 924 + sonic-boom@^4.0.1: 925 + version "4.0.1" 926 + resolved "https://registry.yarnpkg.com/sonic-boom/-/sonic-boom-4.0.1.tgz#515b7cef2c9290cb362c4536388ddeece07aed30" 927 + integrity sha512-hTSD/6JMLyT4r9zeof6UtuBDpjJ9sO08/nmS5djaA9eozT9oOlNdpXSnzcgj4FTqpk3nkLrs61l4gip9r1HCrQ== 928 + dependencies: 929 + atomic-sleep "^1.0.0" 930 + 931 + split2@^4.0.0, split2@^4.1.0: 932 + version "4.2.0" 933 + resolved "https://registry.yarnpkg.com/split2/-/split2-4.2.0.tgz#c9c5920904d148bab0b9f67145f245a86aadbfa4" 934 + integrity sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg== 935 + 936 + statuses@2.0.1: 937 + version "2.0.1" 938 + resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63" 939 + integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== 940 + 941 + string_decoder@^1.3.0: 942 + version "1.3.0" 943 + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" 944 + integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== 945 + dependencies: 946 + safe-buffer "~5.2.0" 947 + 948 + thread-stream@^2.6.0: 949 + version "2.7.0" 950 + resolved "https://registry.yarnpkg.com/thread-stream/-/thread-stream-2.7.0.tgz#d8a8e1b3fd538a6cca8ce69dbe5d3d097b601e11" 951 + integrity sha512-qQiRWsU/wvNolI6tbbCKd9iKaTnCXsTwVxhhKM6nctPdujTyztjlbUkUTUymidWcMnZ5pWR0ej4a0tjsW021vw== 952 + dependencies: 953 + real-require "^0.2.0" 954 + 955 + thread-stream@^3.0.0: 956 + version "3.1.0" 957 + resolved "https://registry.yarnpkg.com/thread-stream/-/thread-stream-3.1.0.tgz#4b2ef252a7c215064507d4ef70c05a5e2d34c4f1" 958 + integrity sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A== 959 + dependencies: 960 + real-require "^0.2.0" 961 + 962 + toidentifier@1.0.1: 963 + version "1.0.1" 964 + resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35" 965 + integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== 966 + 967 + type-fest@^2.3.3: 968 + version "2.19.0" 969 + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-2.19.0.tgz#88068015bb33036a598b952e55e9311a60fd3a9b" 970 + integrity sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA== 971 + 972 + type-is@~1.6.18: 973 + version "1.6.18" 974 + resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" 975 + integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== 976 + dependencies: 977 + media-typer "0.3.0" 978 + mime-types "~2.1.24" 979 + 980 + typescript@^5.4.5: 981 + version "5.4.5" 982 + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.4.5.tgz#42ccef2c571fdbd0f6718b1d1f5e6e5ef006f611" 983 + integrity sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ== 984 + 985 + uint8arrays@3.0.0: 986 + version "3.0.0" 987 + resolved "https://registry.yarnpkg.com/uint8arrays/-/uint8arrays-3.0.0.tgz#260869efb8422418b6f04e3fac73a3908175c63b" 988 + integrity sha512-HRCx0q6O9Bfbp+HHSfQQKD7wU70+lydKVt4EghkdOvlK/NlrF90z+eXV34mUd48rNvVJXwkrMSPpCATkct8fJA== 989 + dependencies: 990 + multiformats "^9.4.2" 991 + 992 + uint8arrays@^5.1.0: 993 + version "5.1.0" 994 + resolved "https://registry.yarnpkg.com/uint8arrays/-/uint8arrays-5.1.0.tgz#14047c9bdf825d025b7391299436e5e50e7270f1" 995 + integrity sha512-vA6nFepEmlSKkMBnLBaUMVvAC4G3CTmO58C12y4sq6WPDOR7mOFYOi7GlrQ4djeSbP6JG9Pv9tJDM97PedRSww== 996 + dependencies: 997 + multiformats "^13.0.0" 998 + 999 + undici-types@~5.26.4: 1000 + version "5.26.5" 1001 + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" 1002 + integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== 1003 + 1004 + unpipe@1.0.0, unpipe@~1.0.0: 1005 + version "1.0.0" 1006 + resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" 1007 + integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ== 1008 + 1009 + utils-merge@1.0.1: 1010 + version "1.0.1" 1011 + resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" 1012 + integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA== 1013 + 1014 + vary@^1, vary@~1.1.2: 1015 + version "1.1.2" 1016 + resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" 1017 + integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg== 1018 + 1019 + xtend@^4.0.0: 1020 + version "4.0.2" 1021 + resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" 1022 + integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== 1023 + 1024 + zod@^3.21.4: 1025 + version "3.23.8" 1026 + resolved "https://registry.yarnpkg.com/zod/-/zod-3.23.8.tgz#e37b957b5d52079769fb8097099b592f0ef4067d" 1027 + integrity sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==
+8 -1
bskyweb/cmd/bskyweb/main.go
··· 35 35 Flags: []cli.Flag{ 36 36 &cli.StringFlag{ 37 37 Name: "appview-host", 38 - Usage: "method, hostname, and port of PDS instance", 38 + Usage: "scheme, hostname, and port of PDS instance", 39 39 Value: "http://localhost:2584", 40 40 // retain old PDS env var for easy transition 41 41 EnvVars: []string{"ATP_APPVIEW_HOST", "ATP_PDS_HOST"}, ··· 46 46 Required: false, 47 47 Value: ":8100", 48 48 EnvVars: []string{"HTTP_ADDRESS"}, 49 + }, 50 + &cli.StringFlag{ 51 + Name: "link-host", 52 + Usage: "scheme, hostname, and port of link service", 53 + Required: false, 54 + Value: "", 55 + EnvVars: []string{"LINK_HOST"}, 49 56 }, 50 57 &cli.BoolFlag{ 51 58 Name: "debug",
+34
bskyweb/cmd/bskyweb/server.go
··· 6 6 "fmt" 7 7 "io/fs" 8 8 "net/http" 9 + "net/url" 9 10 "os" 10 11 "os/signal" 11 12 "strings" ··· 36 37 debug := cctx.Bool("debug") 37 38 httpAddress := cctx.String("http-address") 38 39 appviewHost := cctx.String("appview-host") 40 + linkHost := cctx.String("link-host") 39 41 40 42 // Echo 41 43 e := echo.New() ··· 221 223 e.GET("/profile/:handleOrDID/post/:rkey/liked-by", server.WebGeneric) 222 224 e.GET("/profile/:handleOrDID/post/:rkey/reposted-by", server.WebGeneric) 223 225 226 + if linkHost != "" { 227 + linkUrl, err := url.Parse(linkHost) 228 + if err != nil { 229 + return err 230 + } 231 + e.Group("/:linkId", server.LinkProxyMiddleware(linkUrl)) 232 + } 233 + 224 234 // Start the server. 225 235 log.Infof("starting server address=%s", httpAddress) 226 236 go func() { ··· 290 300 } 291 301 292 302 return c.Redirect(http.StatusFound, "/") 303 + } 304 + 305 + // Handler for proxying top-level paths to link service, which ends up serving a redirect 306 + func (srv *Server) LinkProxyMiddleware(url *url.URL) echo.MiddlewareFunc { 307 + return middleware.ProxyWithConfig( 308 + middleware.ProxyConfig{ 309 + Balancer: middleware.NewRoundRobinBalancer( 310 + []*middleware.ProxyTarget{{URL: url}}, 311 + ), 312 + Skipper: func(c echo.Context) bool { 313 + req := c.Request() 314 + if req.Method == "GET" && 315 + strings.LastIndex(strings.TrimRight(req.URL.Path, "/"), "/") == 0 && // top-level path 316 + !strings.HasPrefix(req.URL.Path, "/_") { // e.g. /_health endpoint 317 + return false 318 + } 319 + return true 320 + }, 321 + RetryCount: 2, 322 + ErrorHandler: func(c echo.Context, err error) error { 323 + return c.Redirect(302, "/") 324 + }, 325 + }, 326 + ) 293 327 } 294 328 295 329 // handler for endpoint that have no specific server-side handling