@@ -79,14 +84,14 @@ export function Post({thread}: Props) {
)}
{!!post.repostCount && (
@@ -118,16 +123,26 @@ function PostContent({record}: {record: AppBskyFeedPost.Record | null}) {
let counter = 0
for (const segment of rt.segments()) {
- if (segment.isLink() && segment.link) {
+ if (
+ segment.link &&
+ AppBskyRichtextFacet.validateLink(segment.link).success
+ ) {
richText.push(
+ className="text-blue-400 hover:underline"
+ disableTracking={
+ !segment.link.uri.startsWith('https://bsky.app') &&
+ !segment.link.uri.startsWith('https://go.bsky.app')
+ }>
{segment.text}
,
)
- } else if (segment.isMention() && segment.mention) {
+ } else if (
+ segment.mention &&
+ AppBskyRichtextFacet.validateMention(segment.mention).success
+ ) {
richText.push(
,
)
- } else if (segment.isTag() && segment.tag) {
+ } else if (
+ segment.tag &&
+ AppBskyRichtextFacet.validateTag(segment.tag).success
+ ) {
richText.push(
@@ -125,7 +125,7 @@ function LandingPage() {
placeholder={DEFAULT_POST}
/>
-
diff --git a/bskyembed/src/screens/post.tsx b/bskyembed/src/screens/post.tsx
index 365227cd47..6ccf10a791 100644
--- a/bskyembed/src/screens/post.tsx
+++ b/bskyembed/src/screens/post.tsx
@@ -1,6 +1,6 @@
import '../index.css'
-import {AppBskyFeedDefs, BskyAgent} from '@atproto/api'
+import {AppBskyFeedDefs, AtpAgent} from '@atproto/api'
import {h, render} from 'preact'
import logo from '../../assets/logo.svg'
@@ -12,7 +12,7 @@ import {getRkey} from '../utils'
const root = document.getElementById('app')
if (!root) throw new Error('No root element')
-const agent = new BskyAgent({
+const agent = new AtpAgent({
service: 'https://public.api.bsky.app',
})
@@ -52,7 +52,7 @@ function PwiOptOut({thread}: {thread: AppBskyFeedDefs.ThreadViewPost}) {
@@ -75,7 +75,7 @@ function ErrorMessage() {
-
+
Post not found, it may have been deleted.
diff --git a/bskyembed/yarn.lock b/bskyembed/yarn.lock
index 60efe36845..ca52dc0747 100644
--- a/bskyembed/yarn.lock
+++ b/bskyembed/yarn.lock
@@ -20,15 +20,16 @@
"@jridgewell/gen-mapping" "^0.3.5"
"@jridgewell/trace-mapping" "^0.3.24"
-"@atproto/api@^0.12.2":
- version "0.12.2"
- resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.12.2.tgz#5df6d4f60dea0395c84fdebd9e81a7e853edf130"
- integrity sha512-UVzCiDZH2j0wrr/O8nb1edD5cYLVqB5iujueXUCbHS3rAwIxgmyLtA3Hzm2QYsGPo/+xsIg1fNvpq9rNT6KWUA==
+"@atproto/api@0.13.1":
+ version "0.13.1"
+ resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.13.1.tgz#fbf4306e4465d5467aaf031308c1b47dcc8039d0"
+ integrity sha512-DL3iBfavn8Nnl48FmnAreQB0k0cIkW531DJ5JAHUCQZo10Nq0ZLk2/WFxcs0KuBG5wuLnGUdo+Y6/GQPVq8dYw==
dependencies:
"@atproto/common-web" "^0.3.0"
- "@atproto/lexicon" "^0.4.0"
+ "@atproto/lexicon" "^0.4.1"
"@atproto/syntax" "^0.3.0"
- "@atproto/xrpc" "^0.5.0"
+ "@atproto/xrpc" "^0.6.0"
+ await-lock "^2.2.2"
multiformats "^9.9.0"
tlds "^1.234.0"
@@ -42,29 +43,29 @@
uint8arrays "3.0.0"
zod "^3.21.4"
-"@atproto/lexicon@^0.4.0":
- version "0.4.0"
- resolved "https://registry.yarnpkg.com/@atproto/lexicon/-/lexicon-0.4.0.tgz#63e8829945d80c25524882caa8ed27b1151cc576"
- integrity sha512-RvCBKdSI4M8qWm5uTNz1z3R2yIvIhmOsMuleOj8YR6BwRD+QbtUBy3l+xQ7iXf4M5fdfJFxaUNa6Ty0iRwdKqQ==
+"@atproto/lexicon@^0.4.1":
+ version "0.4.1"
+ resolved "https://registry.yarnpkg.com/@atproto/lexicon/-/lexicon-0.4.1.tgz#19155210570a2fafbcc7d4f655d9b813948e72a0"
+ integrity sha512-bzyr+/VHXLQWbumViX5L7h1NKQObfs8Z+XZJl43OUK8nYFUI4e/sW1IZKRNfw7Wvi5YVNK+J+yP3DWIBZhkCYA==
dependencies:
"@atproto/common-web" "^0.3.0"
"@atproto/syntax" "^0.3.0"
iso-datestring-validator "^2.2.2"
multiformats "^9.9.0"
- zod "^3.21.4"
+ zod "^3.23.8"
"@atproto/syntax@^0.3.0":
version "0.3.0"
resolved "https://registry.yarnpkg.com/@atproto/syntax/-/syntax-0.3.0.tgz#fafa2dbea9add37253005cb663e7373e05e618b3"
integrity sha512-Weq0ZBxffGHDXHl9U7BQc2BFJi/e23AL+k+i5+D9hUq/bzT4yjGsrCejkjq0xt82xXDjmhhvQSZ0LqxyZ5woxA==
-"@atproto/xrpc@^0.5.0":
- version "0.5.0"
- resolved "https://registry.yarnpkg.com/@atproto/xrpc/-/xrpc-0.5.0.tgz#dacbfd8f7b13f0ab5bd56f8fdd4b460e132a6032"
- integrity sha512-swu+wyOLvYW4l3n+VAuJbHcPcES+tin2Lsrp8Bw5aIXIICiuFn1YMFlwK9JwVUzTH21Py1s1nHEjr4CJeElJog==
+"@atproto/xrpc@^0.6.0":
+ version "0.6.0"
+ resolved "https://registry.yarnpkg.com/@atproto/xrpc/-/xrpc-0.6.0.tgz#668c3262e67e2afa65951ea79a03bfe3720ddf5c"
+ integrity sha512-5BbhBTv5j6MC3iIQ4+vYxQE7nLy2dDGQ+LYJrH8PptOCUdq0Pwg6aRccQ3y52kUZlhE/mzOTZ8Ngiy9pSAyfVQ==
dependencies:
- "@atproto/lexicon" "^0.4.0"
- zod "^3.21.4"
+ "@atproto/lexicon" "^0.4.1"
+ zod "^3.23.8"
"@babel/code-frame@^7.23.5", "@babel/code-frame@^7.24.1", "@babel/code-frame@^7.24.2":
version "7.24.2"
@@ -1710,6 +1711,11 @@ available-typed-arrays@^1.0.7:
dependencies:
possible-typed-array-names "^1.0.0"
+await-lock@^2.2.2:
+ version "2.2.2"
+ resolved "https://registry.yarnpkg.com/await-lock/-/await-lock-2.2.2.tgz#a95a9b269bfd2f69d22b17a321686f551152bcef"
+ integrity sha512-aDczADvlvTGajTDjcjpJMqRkOF6Qdz3YbPZm/PyW6tKPkx2hlYBzxMhEywM/tU72HrVZjgl5VCdRuMlA7pZ8Gw==
+
babel-plugin-polyfill-corejs2@^0.4.10:
version "0.4.10"
resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.10.tgz#276f41710b03a64f6467433cab72cbc2653c38b1"
@@ -3730,8 +3736,16 @@ stack-trace@^1.0.0-pre2:
resolved "https://registry.yarnpkg.com/stack-trace/-/stack-trace-1.0.0-pre2.tgz#46a83a79f1b287807e9aaafc6a5dd8bcde626f9c"
integrity sha512-2ztBJRek8IVofG9DBJqdy2N5kulaacX30Nz7xmkYF6ale9WBVmIy6mFBchvGX7Vx/MyjBhx+Rcxqrj+dbOnQ6A==
-"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0:
- name string-width-cjs
+"string-width-cjs@npm:string-width@^4.2.0":
+ version "4.2.3"
+ resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
+ integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
+ dependencies:
+ emoji-regex "^8.0.0"
+ is-fullwidth-code-point "^3.0.0"
+ strip-ansi "^6.0.1"
+
+string-width@^4.1.0:
version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
@@ -3795,7 +3809,14 @@ string.prototype.trimstart@^1.0.8:
define-properties "^1.2.1"
es-object-atoms "^1.0.0"
-"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1:
+"strip-ansi-cjs@npm:strip-ansi@^6.0.1":
+ version "6.0.1"
+ resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
+ integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
+ dependencies:
+ ansi-regex "^5.0.1"
+
+strip-ansi@^6.0.0, strip-ansi@^6.0.1:
version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
@@ -4192,3 +4213,8 @@ zod@^3.21.4:
version "3.22.4"
resolved "https://registry.yarnpkg.com/zod/-/zod-3.22.4.tgz#f31c3a9386f61b1f228af56faa9255e845cf3fff"
integrity sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==
+
+zod@^3.23.8:
+ 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/bskylink/package.json b/bskylink/package.json
new file mode 100644
index 0000000000..5fdee206b4
--- /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 0000000000..17f068841b
--- /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 0000000000..ce409cccca
--- /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 0000000000..7e6f2f34e8
--- /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 0000000000..5f201cc07d
--- /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 0000000000..fe3bcf1867
--- /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 0000000000..05e4de9377
--- /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 0000000000..bef93a48fd
--- /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 0000000000..8d97f58005
--- /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 0000000000..ca425eee8c
--- /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 0000000000..25bb590a1d
--- /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 0000000000..db7c3f8090
--- /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 0000000000..c8a30c59ea
--- /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 0000000000..f60b99bcb7
--- /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 0000000000..276aae1ca1
--- /dev/null
+++ b/bskylink/src/routes/redirect.ts
@@ -0,0 +1,54 @@
+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
+ const contentType = req.accepts(['html', 'json'])
+ 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
+ res.setHeader('Cache-Control', 'no-store')
+ if (contentType === 'json') {
+ return res
+ .status(404)
+ .json({
+ error: 'NotFound',
+ message: 'Link not found',
+ })
+ .end()
+ }
+ // send the user to the app
+ res.setHeader('Location', `https://${ctx.cfg.service.appHostname}`)
+ 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('Cache-Control', `max-age=${(7 * DAY) / SECOND}`)
+ if (contentType === 'json') {
+ return res.json({url: url.href}).end()
+ }
+ res.setHeader('Location', url.href)
+ return res.status(301).end()
+ }),
+ )
+}
diff --git a/bskylink/src/routes/siteAssociation.ts b/bskylink/src/routes/siteAssociation.ts
new file mode 100644
index 0000000000..7b71ccdd57
--- /dev/null
+++ b/bskylink/src/routes/siteAssociation.ts
@@ -0,0 +1,22 @@
+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({
+ applinks: {
+ apps: [],
+ details: [
+ {
+ appID: 'B3LX46C5HS.xyz.blueskyweb.app',
+ paths: ['*'],
+ },
+ ],
+ },
+ 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 0000000000..bcac64b01d
--- /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 0000000000..0b57dd5c5b
--- /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 0000000000..c5604c7a12
--- /dev/null
+++ b/bskylink/tests/index.ts
@@ -0,0 +1,123 @@
+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',
+ )
+ })
+
+ it('returns json object with url when requested', async () => {
+ const link = await getLink('/start/did:example:carol/zzz/')
+ const [status, json] = await getJsonRedirect(link)
+ assert.strictEqual(status, 200)
+ assert(json.url)
+ const url = new URL(json.url)
+ assert.strictEqual(url.pathname, '/start/did:example:carol/zzz')
+ })
+
+ it('returns 404 for unknown link when requesting json', async () => {
+ const [status, json] = await getJsonRedirect(
+ 'https://test.bsky.link/unknown',
+ )
+ assert(json.error)
+ assert(json.message)
+ assert.strictEqual(status, 404)
+ assert.strictEqual(json.error, 'NotFound')
+ assert.strictEqual(json.message, 'Link not found')
+ })
+
+ 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 getJsonRedirect(
+ link: string,
+ ): Promise<[number, {url?: string; error?: string; message?: 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',
+ headers: {accept: 'application/json,text/html'},
+ })
+ assert(
+ res.headers.get('content-type')?.startsWith('application/json'),
+ 'content type was not json',
+ )
+ const json = await res.json()
+ return [res.status, json]
+ }
+
+ 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 0000000000..1587f5c70a
--- /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 0000000000..4bc939e01c
--- /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 0000000000..cc083491a5
--- /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 0000000000..3c382acc41
--- /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 0000000000..d2fa31456b
--- /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/bskyogcard/package.json b/bskyogcard/package.json
new file mode 100644
index 0000000000..176f9d8c49
--- /dev/null
+++ b/bskyogcard/package.json
@@ -0,0 +1,28 @@
+{
+ "name": "bskyogcard",
+ "version": "0.0.0",
+ "type": "module",
+ "main": "src/index.ts",
+ "scripts": {
+ "start": "node --loader ts-node/esm ./src/bin.ts",
+ "dev": "node --watch-path ./src --loader ts-node/esm ./src/bin.ts",
+ "build": "tsc && cp -r src/assets dist/",
+ "install-fonts": "node --loader ts-node/esm scripts/install-fonts.ts"
+ },
+ "dependencies": {
+ "@atproto/api": "0.12.19-next.0",
+ "@atproto/common": "^0.4.0",
+ "@resvg/resvg-js": "^2.6.2",
+ "express": "^4.19.2",
+ "http-terminator": "^3.2.0",
+ "pino": "^9.2.0",
+ "react": "^18.3.1",
+ "satori": "^0.10.13",
+ "twemoji": "^14.0.2"
+ },
+ "devDependencies": {
+ "@types/node": "^20.14.3",
+ "ts-node": "^10.9.2",
+ "typescript": "^5.4.5"
+ }
+}
diff --git a/bskyogcard/scripts/install-fonts.ts b/bskyogcard/scripts/install-fonts.ts
new file mode 100644
index 0000000000..5c58fb7b9e
--- /dev/null
+++ b/bskyogcard/scripts/install-fonts.ts
@@ -0,0 +1,40 @@
+import {writeFile} from 'node:fs/promises'
+import * as path from 'node:path'
+import {fileURLToPath} from 'node:url'
+
+const __DIRNAME = path.dirname(fileURLToPath(import.meta.url))
+
+const FONTS = [
+ 'https://cdn.jsdelivr.net/fontsource/fonts/noto-sans-jp@5.0/japanese-700-normal.ttf',
+ 'https://cdn.jsdelivr.net/fontsource/fonts/noto-sans-tc@5.0/chinese-traditional-700-normal.ttf',
+ 'https://cdn.jsdelivr.net/fontsource/fonts/noto-sans-sc@5.0/chinese-simplified-700-normal.ttf',
+ 'https://cdn.jsdelivr.net/fontsource/fonts/noto-sans-hk@5.0/chinese-hongkong-700-normal.ttf',
+ 'https://cdn.jsdelivr.net/fontsource/fonts/noto-sans-kr@5.0/korean-700-normal.ttf',
+ 'https://cdn.jsdelivr.net/fontsource/fonts/noto-sans-thai@5.0/thai-700-normal.ttf',
+ 'https://cdn.jsdelivr.net/fontsource/fonts/noto-sans-arabic@5.0/arabic-700-normal.ttf',
+ 'https://cdn.jsdelivr.net/fontsource/fonts/noto-sans-hebrew@5.0/hebrew-700-normal.ttf',
+]
+
+async function main() {
+ await Promise.all(
+ FONTS.map(async urlStr => {
+ const url = new URL(urlStr)
+ const res = await fetch(url)
+ const font = await res.arrayBuffer()
+ const filename = url.pathname
+ .split('/')
+ .slice(-2)
+ .join('/')
+ .replace(/@[\d.]+\//, '-')
+ if (!res.ok) {
+ throw new Error(`HTTP ${res.status}: fetching failed for ${filename}`)
+ }
+ await writeFile(
+ path.join(__DIRNAME, '..', 'src', 'assets', 'fonts', filename),
+ Buffer.from(font),
+ )
+ }),
+ )
+}
+
+main()
diff --git a/bskyogcard/src/assets/fonts/Inter-Bold.ttf b/bskyogcard/src/assets/fonts/Inter-Bold.ttf
new file mode 100644
index 0000000000..fe23eeb9c9
Binary files /dev/null and b/bskyogcard/src/assets/fonts/Inter-Bold.ttf differ
diff --git a/bskyogcard/src/bin.ts b/bskyogcard/src/bin.ts
new file mode 100644
index 0000000000..ff550809db
--- /dev/null
+++ b/bskyogcard/src/bin.ts
@@ -0,0 +1,48 @@
+import cluster, {Worker} from 'node:cluster'
+
+import {envInt} from '@atproto/common'
+
+import {CardService, envToCfg, httpLogger, readEnv} from './index.js'
+
+async function main() {
+ const env = readEnv()
+ const cfg = envToCfg(env)
+ const card = await CardService.create(cfg)
+ await card.start()
+ httpLogger.info('card service is running')
+ process.on('SIGTERM', async () => {
+ httpLogger.info('card service is stopping')
+ await card.destroy()
+ httpLogger.info('card service is stopped')
+ if (cluster.isWorker) process.exit(0)
+ })
+}
+
+const workerCount = envInt('CARD_CLUSTER_WORKER_COUNT')
+
+if (workerCount) {
+ if (cluster.isPrimary) {
+ httpLogger.info(`primary ${process.pid} is running`)
+ const workers = new Set()
+ for (let i = 0; i < workerCount; ++i) {
+ workers.add(cluster.fork())
+ }
+ let teardown = false
+ cluster.on('exit', worker => {
+ workers.delete(worker)
+ if (!teardown) {
+ workers.add(cluster.fork()) // restart on crash
+ }
+ })
+ process.on('SIGTERM', () => {
+ teardown = true
+ httpLogger.info('disconnecting workers')
+ workers.forEach(w => w.kill('SIGTERM'))
+ })
+ } else {
+ httpLogger.info(`worker ${process.pid} is running`)
+ main()
+ }
+} else {
+ main() // non-clustering
+}
diff --git a/bskyogcard/src/components/Butterfly.tsx b/bskyogcard/src/components/Butterfly.tsx
new file mode 100644
index 0000000000..5a4124975c
--- /dev/null
+++ b/bskyogcard/src/components/Butterfly.tsx
@@ -0,0 +1,16 @@
+import React from 'react'
+
+export function Butterfly(props: React.SVGAttributes) {
+ return (
+
+ )
+}
diff --git a/bskyogcard/src/components/Img.tsx b/bskyogcard/src/components/Img.tsx
new file mode 100644
index 0000000000..dac223180c
--- /dev/null
+++ b/bskyogcard/src/components/Img.tsx
@@ -0,0 +1,10 @@
+import React from 'react'
+
+export function Img(
+ props: Omit, 'src'> & {src: Buffer},
+) {
+ const {src, ...others} = props
+ return (
+
+ )
+}
diff --git a/bskyogcard/src/components/StarterPack.tsx b/bskyogcard/src/components/StarterPack.tsx
new file mode 100644
index 0000000000..29bb8f32ab
--- /dev/null
+++ b/bskyogcard/src/components/StarterPack.tsx
@@ -0,0 +1,152 @@
+/* eslint-disable bsky-internal/avoid-unwrapped-text */
+import React from 'react'
+import {AppBskyGraphDefs, AppBskyGraphStarterpack} from '@atproto/api'
+
+import {Butterfly} from './Butterfly.js'
+import {Img} from './Img.js'
+
+export const STARTERPACK_HEIGHT = 630
+export const STARTERPACK_WIDTH = 1200
+export const TILE_SIZE = STARTERPACK_HEIGHT / 3
+
+const GRADIENT_TOP = '#0A7AFF'
+const GRADIENT_BOTTOM = '#59B9FF'
+const IMAGE_STROKE = '#359CFF'
+
+export function StarterPack(props: {
+ starterPack: AppBskyGraphDefs.StarterPackView
+ images: Map
+}) {
+ const {starterPack, images} = props
+ const record = AppBskyGraphStarterpack.isRecord(starterPack.record)
+ ? starterPack.record
+ : null
+ const imagesArray = [...images.values()]
+ const imageOfCreator = images.get(starterPack.creator.did)
+ const imagesExceptCreator = [...images.entries()]
+ .filter(([did]) => did !== starterPack.creator.did)
+ .map(([, image]) => image)
+ const imagesAcross: Buffer[] = []
+ if (imageOfCreator) {
+ if (imagesExceptCreator.length >= 6) {
+ imagesAcross.push(...imagesExceptCreator.slice(0, 3))
+ imagesAcross.push(imageOfCreator)
+ imagesAcross.push(...imagesExceptCreator.slice(3, 6))
+ } else {
+ const firstHalf = Math.floor(imagesExceptCreator.length / 2)
+ imagesAcross.push(...imagesExceptCreator.slice(0, firstHalf))
+ imagesAcross.push(imageOfCreator)
+ imagesAcross.push(
+ ...imagesExceptCreator.slice(firstHalf, imagesExceptCreator.length),
+ )
+ }
+ } else {
+ imagesAcross.push(...imagesExceptCreator.slice(0, 7))
+ }
+ const isLongTitle = record ? record.name.length > 30 : false
+ return (
+
+ {/* image tiles */}
+
+ {[...Array(18)].map((_, i) => {
+ const image = imagesArray.at(i % imagesArray.length)
+ return (
+
+ {image &&

}
+
+ )
+ })}
+ {/* background overlay */}
+
+
+ {/* foreground text & images */}
+
+
+ JOIN THE CONVERSATION
+
+
+ {imagesAcross.map((image, i) => {
+ return (
+
+

+
+ )
+ })}
+
+
+ {record?.name || 'Starter Pack'}
+
+
+ on Bluesky
+
+
+
+ )
+}
diff --git a/bskyogcard/src/config.ts b/bskyogcard/src/config.ts
new file mode 100644
index 0000000000..fafa18e743
--- /dev/null
+++ b/bskyogcard/src/config.ts
@@ -0,0 +1,40 @@
+import {envInt, envStr} from '@atproto/common'
+
+export type Config = {
+ service: ServiceConfig
+}
+
+export type ServiceConfig = {
+ port: number
+ version?: string
+ appviewUrl: string
+ originVerify?: string
+}
+
+export type Environment = {
+ port?: number
+ version?: string
+ appviewUrl?: string
+ originVerify?: string
+}
+
+export const readEnv = (): Environment => {
+ return {
+ port: envInt('CARD_PORT'),
+ version: envStr('CARD_VERSION'),
+ appviewUrl: envStr('CARD_APPVIEW_URL'),
+ originVerify: envStr('CARD_ORIGIN_VERIFY'),
+ }
+}
+
+export const envToCfg = (env: Environment): Config => {
+ const serviceCfg: ServiceConfig = {
+ port: env.port ?? 3000,
+ version: env.version,
+ appviewUrl: env.appviewUrl ?? 'https://api.bsky.app',
+ originVerify: env.originVerify,
+ }
+ return {
+ service: serviceCfg,
+ }
+}
diff --git a/bskyogcard/src/context.ts b/bskyogcard/src/context.ts
new file mode 100644
index 0000000000..0c972c94de
--- /dev/null
+++ b/bskyogcard/src/context.ts
@@ -0,0 +1,46 @@
+import {readdirSync, readFileSync} from 'node:fs'
+import * as path from 'node:path'
+import {fileURLToPath} from 'node:url'
+
+import {AtpAgent} from '@atproto/api'
+
+import {Config} from './config.js'
+
+const __DIRNAME = path.dirname(fileURLToPath(import.meta.url))
+
+export type AppContextOptions = {
+ cfg: Config
+ appviewAgent: AtpAgent
+ fonts: {name: string; data: Buffer}[]
+}
+
+export class AppContext {
+ cfg: Config
+ appviewAgent: AtpAgent
+ fonts: {name: string; data: Buffer}[]
+ abortController = new AbortController()
+
+ constructor(private opts: AppContextOptions) {
+ this.cfg = this.opts.cfg
+ this.appviewAgent = this.opts.appviewAgent
+ this.fonts = this.opts.fonts
+ }
+
+ static async fromConfig(cfg: Config, overrides?: Partial) {
+ const appviewAgent = new AtpAgent({service: cfg.service.appviewUrl})
+ const fontDirectory = path.join(__DIRNAME, 'assets', 'fonts')
+ const fontFiles = readdirSync(fontDirectory)
+ const fonts = fontFiles.map(file => {
+ return {
+ name: path.basename(file, path.extname(file)),
+ data: readFileSync(path.join(fontDirectory, file)),
+ }
+ })
+ return new AppContext({
+ cfg,
+ appviewAgent,
+ fonts,
+ ...overrides,
+ })
+ }
+}
diff --git a/bskyogcard/src/index.ts b/bskyogcard/src/index.ts
new file mode 100644
index 0000000000..ef8d48494d
--- /dev/null
+++ b/bskyogcard/src/index.ts
@@ -0,0 +1,41 @@
+import events from 'node:events'
+import http from 'node:http'
+
+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 './logger.js'
+
+export class CardService {
+ public server?: http.Server
+ private terminator?: HttpTerminator
+
+ constructor(public app: express.Application, public ctx: AppContext) {}
+
+ static async create(cfg: Config): Promise {
+ let app = express()
+
+ const ctx = await AppContext.fromConfig(cfg)
+ app = routes(ctx, app)
+ app.use(errorHandler)
+
+ return new CardService(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()
+ }
+}
diff --git a/bskyogcard/src/logger.ts b/bskyogcard/src/logger.ts
new file mode 100644
index 0000000000..3202065134
--- /dev/null
+++ b/bskyogcard/src/logger.ts
@@ -0,0 +1,4 @@
+import {subsystemLogger} from '@atproto/common'
+
+export const httpLogger = subsystemLogger('bskyogcard')
+export const renderLogger = subsystemLogger('bskyogcard:render')
diff --git a/bskyogcard/src/routes/health.ts b/bskyogcard/src/routes/health.ts
new file mode 100644
index 0000000000..0cc69515eb
--- /dev/null
+++ b/bskyogcard/src/routes/health.ts
@@ -0,0 +1,14 @@
+import {Express} from 'express'
+
+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
+ return res.send({version})
+ }),
+ )
+}
diff --git a/bskyogcard/src/routes/index.ts b/bskyogcard/src/routes/index.ts
new file mode 100644
index 0000000000..0c40f89d3b
--- /dev/null
+++ b/bskyogcard/src/routes/index.ts
@@ -0,0 +1,13 @@
+import {Express} from 'express'
+
+import {AppContext} from '../context.js'
+import {default as health} from './health.js'
+import {default as starterPack} from './starter-pack.js'
+
+export * from './util.js'
+
+export default function (ctx: AppContext, app: Express) {
+ app = health(ctx, app) // GET /_health
+ app = starterPack(ctx, app) // GET /start/:actor/:rkey
+ return app
+}
diff --git a/bskyogcard/src/routes/starter-pack.tsx b/bskyogcard/src/routes/starter-pack.tsx
new file mode 100644
index 0000000000..06cd6977c0
--- /dev/null
+++ b/bskyogcard/src/routes/starter-pack.tsx
@@ -0,0 +1,108 @@
+import assert from 'node:assert'
+
+import React from 'react'
+import {AppBskyGraphDefs, AtUri} from '@atproto/api'
+import resvg from '@resvg/resvg-js'
+import {Express} from 'express'
+import satori from 'satori'
+
+import {
+ StarterPack,
+ STARTERPACK_HEIGHT,
+ STARTERPACK_WIDTH,
+} from '../components/StarterPack.js'
+import {AppContext} from '../context.js'
+import {httpLogger} from '../logger.js'
+import {loadEmojiAsSvg} from '../util.js'
+import {handler, originVerifyMiddleware} from './util.js'
+
+export default function (ctx: AppContext, app: Express) {
+ return app.get(
+ '/start/:actor/:rkey',
+ originVerifyMiddleware(ctx),
+ handler(async (req, res) => {
+ const {actor, rkey} = req.params
+ const uri = AtUri.make(actor, 'app.bsky.graph.starterpack', rkey)
+ let starterPack: AppBskyGraphDefs.StarterPackView
+ try {
+ const result = await ctx.appviewAgent.api.app.bsky.graph.getStarterPack(
+ {starterPack: uri.toString()},
+ )
+ starterPack = result.data.starterPack
+ } catch (err) {
+ httpLogger.warn(
+ {err, uri: uri.toString()},
+ 'could not fetch starter pack',
+ )
+ return res.status(404).end('not found')
+ }
+ const imageEntries = await Promise.all(
+ [starterPack.creator]
+ .concat(starterPack.listItemsSample.map(li => li.subject))
+ // has avatar
+ .filter(p => p.avatar)
+ // no sensitive labels
+ .filter(p => !p.labels.some(l => hideAvatarLabels.has(l.val)))
+ .map(async p => {
+ try {
+ assert(p.avatar)
+ const image = await getImage(p.avatar)
+ return [p.did, image] as const
+ } catch (err) {
+ httpLogger.warn(
+ {err, uri: uri.toString(), did: p.did},
+ 'could not fetch image',
+ )
+ return [p.did, null] as const
+ }
+ }),
+ )
+ const images = new Map(
+ imageEntries.filter(([_, image]) => image !== null).slice(0, 7),
+ )
+ const svg = await satori(
+ ,
+ {
+ fonts: ctx.fonts,
+ height: STARTERPACK_HEIGHT,
+ width: STARTERPACK_WIDTH,
+ loadAdditionalAsset: async (code, text) => {
+ if (code === 'emoji') {
+ return await loadEmojiAsSvg(text)
+ }
+ },
+ },
+ )
+ const output = await resvg.renderAsync(svg)
+ res.statusCode = 200
+ res.setHeader('content-type', 'image/png')
+ res.setHeader('cdn-tag', [...images.keys()].join(','))
+ return res.end(output.asPng())
+ }),
+ )
+}
+
+async function getImage(url: string) {
+ const response = await fetch(url)
+ const arrayBuf = await response.arrayBuffer() // must drain body even if it will be discarded
+ if (response.status !== 200) return null
+ return Buffer.from(arrayBuf)
+}
+
+const hideAvatarLabels = new Set([
+ '!hide',
+ '!warn',
+ 'porn',
+ 'sexual',
+ 'nudity',
+ 'sexual-figurative',
+ 'graphic-media',
+ 'self-harm',
+ 'sensitive',
+ 'security',
+ 'impersonation',
+ 'scam',
+ 'spam',
+ 'misleading',
+ 'inauthentic',
+])
diff --git a/bskyogcard/src/routes/util.ts b/bskyogcard/src/routes/util.ts
new file mode 100644
index 0000000000..718ed592a1
--- /dev/null
+++ b/bskyogcard/src/routes/util.ts
@@ -0,0 +1,36 @@
+import {ErrorRequestHandler, Request, RequestHandler, Response} from 'express'
+
+import {AppContext} from '../context.js'
+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 function originVerifyMiddleware(ctx: AppContext): RequestHandler {
+ const {originVerify} = ctx.cfg.service
+ if (!originVerify) return (_req, _res, next) => next()
+ return (req, res, next) => {
+ const verifyHeader = req.headers['x-origin-verify']
+ if (verifyHeader !== originVerify) {
+ return res.status(404).end('not found')
+ }
+ next()
+ }
+}
+
+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/bskyogcard/src/util.ts b/bskyogcard/src/util.ts
new file mode 100644
index 0000000000..2b86ded066
--- /dev/null
+++ b/bskyogcard/src/util.ts
@@ -0,0 +1,37 @@
+import twemoji from 'twemoji'
+
+import {renderLogger} from './logger.js'
+
+const U200D = String.fromCharCode(0x200d)
+const UFE0F_REGEXP = /\uFE0F/g
+
+export async function loadEmojiAsSvg(chars: string) {
+ const cached = emojiCache.get(chars)
+ if (cached) return cached
+ const iconCode = twemoji.convert.toCodePoint(
+ chars.indexOf(U200D) < 0 ? chars.replace(UFE0F_REGEXP, '') : chars,
+ )
+ const res = await fetch(getEmojiUrl(iconCode))
+ const body = await res.arrayBuffer()
+ if (!res.ok) {
+ renderLogger.warn(
+ {status: res.status, err: Buffer.from(body).toString()},
+ 'could not fetch emoji',
+ )
+ return
+ }
+ const svg =
+ 'data:image/svg+xml;base64,' + Buffer.from(body).toString('base64')
+ emojiCache.set(chars, svg)
+ return svg
+}
+
+const emojiCache = new Map()
+
+function getEmojiUrl(code: string) {
+ return (
+ 'https://cdnjs.cloudflare.com/ajax/libs/twemoji/14.0.2/svg/' +
+ code.toLowerCase() +
+ '.svg'
+ )
+}
diff --git a/bskyogcard/tsconfig.json b/bskyogcard/tsconfig.json
new file mode 100644
index 0000000000..a5c3beecb1
--- /dev/null
+++ b/bskyogcard/tsconfig.json
@@ -0,0 +1,11 @@
+{
+ "compilerOptions": {
+ "target": "ESNext",
+ "module": "NodeNext",
+ "esModuleInterop": true,
+ "moduleResolution": "NodeNext",
+ "jsx": "react-jsx",
+ "outDir": "dist"
+ },
+ "include": ["./src/index.ts", "./src/bin.ts"]
+}
diff --git a/bskyogcard/yarn.lock b/bskyogcard/yarn.lock
new file mode 100644
index 0000000000..484aee9ee8
--- /dev/null
+++ b/bskyogcard/yarn.lock
@@ -0,0 +1,1269 @@
+# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
+# yarn lockfile v1
+
+
+"@atproto/api@0.12.19-next.0":
+ version "0.12.19-next.0"
+ resolved "https://registry.npmjs.org/@atproto/api/-/api-0.12.19-next.0.tgz"
+ integrity sha512-wyWr4uIabTgDTBY99y3QyrFxcIx1Mh4DkURgSv8sd/b+w0lfrZAJh0Gg9BXdg/iIjcf/M2lCTL04r0vASfkMVg==
+ dependencies:
+ "@atproto/common-web" "^0.3.0"
+ "@atproto/lexicon" "^0.4.0"
+ "@atproto/syntax" "^0.3.0"
+ "@atproto/xrpc" "^0.5.0"
+ multiformats "^9.9.0"
+ tlds "^1.234.0"
+
+"@atproto/common-web@^0.3.0":
+ version "0.3.0"
+ resolved "https://registry.npmjs.org/@atproto/common-web/-/common-web-0.3.0.tgz"
+ 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.npmjs.org/@atproto/common/-/common-0.4.0.tgz"
+ 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"
+
+"@atproto/lexicon@^0.4.0":
+ version "0.4.0"
+ resolved "https://registry.npmjs.org/@atproto/lexicon/-/lexicon-0.4.0.tgz"
+ integrity sha512-RvCBKdSI4M8qWm5uTNz1z3R2yIvIhmOsMuleOj8YR6BwRD+QbtUBy3l+xQ7iXf4M5fdfJFxaUNa6Ty0iRwdKqQ==
+ dependencies:
+ "@atproto/common-web" "^0.3.0"
+ "@atproto/syntax" "^0.3.0"
+ iso-datestring-validator "^2.2.2"
+ multiformats "^9.9.0"
+ zod "^3.21.4"
+
+"@atproto/syntax@^0.3.0":
+ version "0.3.0"
+ resolved "https://registry.npmjs.org/@atproto/syntax/-/syntax-0.3.0.tgz"
+ integrity sha512-Weq0ZBxffGHDXHl9U7BQc2BFJi/e23AL+k+i5+D9hUq/bzT4yjGsrCejkjq0xt82xXDjmhhvQSZ0LqxyZ5woxA==
+
+"@atproto/xrpc@^0.5.0":
+ version "0.5.0"
+ resolved "https://registry.npmjs.org/@atproto/xrpc/-/xrpc-0.5.0.tgz"
+ integrity sha512-swu+wyOLvYW4l3n+VAuJbHcPcES+tin2Lsrp8Bw5aIXIICiuFn1YMFlwK9JwVUzTH21Py1s1nHEjr4CJeElJog==
+ dependencies:
+ "@atproto/lexicon" "^0.4.0"
+ zod "^3.21.4"
+
+"@cbor-extract/cbor-extract-darwin-arm64@2.2.0":
+ version "2.2.0"
+ resolved "https://registry.npmjs.org/@cbor-extract/cbor-extract-darwin-arm64/-/cbor-extract-darwin-arm64-2.2.0.tgz"
+ 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==
+
+"@cspotcode/source-map-support@^0.8.0":
+ version "0.8.1"
+ resolved "https://registry.yarnpkg.com/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz#00629c35a688e05a88b1cda684fb9d5e73f000a1"
+ integrity sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==
+ dependencies:
+ "@jridgewell/trace-mapping" "0.3.9"
+
+"@ipld/dag-cbor@^7.0.3":
+ version "7.0.3"
+ resolved "https://registry.npmjs.org/@ipld/dag-cbor/-/dag-cbor-7.0.3.tgz"
+ integrity sha512-1VVh2huHsuohdXC1bGJNE8WR72slZ9XE2T3wbBBq31dm7ZBatmKLLxrB+XAqafxfRFjv08RZmj/W/ZqaM13AuA==
+ dependencies:
+ cborg "^1.6.0"
+ multiformats "^9.5.4"
+
+"@jridgewell/resolve-uri@^3.0.3":
+ version "3.1.2"
+ resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz#7a0ee601f60f99a20c7c7c5ff0c80388c1189bd6"
+ integrity sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==
+
+"@jridgewell/sourcemap-codec@^1.4.10":
+ version "1.4.15"
+ resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32"
+ integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==
+
+"@jridgewell/trace-mapping@0.3.9":
+ version "0.3.9"
+ resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz#6534fd5933a53ba7cbf3a17615e273a0d1273ff9"
+ integrity sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==
+ dependencies:
+ "@jridgewell/resolve-uri" "^3.0.3"
+ "@jridgewell/sourcemap-codec" "^1.4.10"
+
+"@resvg/resvg-js-android-arm-eabi@2.6.2":
+ version "2.6.2"
+ resolved "https://registry.yarnpkg.com/@resvg/resvg-js-android-arm-eabi/-/resvg-js-android-arm-eabi-2.6.2.tgz#e761e0b688127db64879f455178c92468a9aeabe"
+ integrity sha512-FrJibrAk6v29eabIPgcTUMPXiEz8ssrAk7TXxsiZzww9UTQ1Z5KAbFJs+Z0Ez+VZTYgnE5IQJqBcoSiMebtPHA==
+
+"@resvg/resvg-js-android-arm64@2.6.2":
+ version "2.6.2"
+ resolved "https://registry.yarnpkg.com/@resvg/resvg-js-android-arm64/-/resvg-js-android-arm64-2.6.2.tgz#b8cb564d7f6b3f37d9b43129f5dc5fe171e249e4"
+ integrity sha512-VcOKezEhm2VqzXpcIJoITuvUS/fcjIw5NA/w3tjzWyzmvoCdd+QXIqy3FBGulWdClvp4g+IfUemigrkLThSjAQ==
+
+"@resvg/resvg-js-darwin-arm64@2.6.2":
+ version "2.6.2"
+ resolved "https://registry.npmjs.org/@resvg/resvg-js-darwin-arm64/-/resvg-js-darwin-arm64-2.6.2.tgz"
+ integrity sha512-nmok2LnAd6nLUKI16aEB9ydMC6Lidiiq2m1nEBDR1LaaP7FGs4AJ90qDraxX+CWlVuRlvNjyYJTNv8qFjtL9+A==
+
+"@resvg/resvg-js-darwin-x64@2.6.2":
+ version "2.6.2"
+ resolved "https://registry.yarnpkg.com/@resvg/resvg-js-darwin-x64/-/resvg-js-darwin-x64-2.6.2.tgz#e1344173aa27bfb4d880ab576d1acf1c1648faca"
+ integrity sha512-GInyZLjgWDfsVT6+SHxQVRwNzV0AuA1uqGsOAW+0th56J7Nh6bHHKXHBWzUrihxMetcFDmQMAX1tZ1fZDYSRsw==
+
+"@resvg/resvg-js-linux-arm-gnueabihf@2.6.2":
+ version "2.6.2"
+ resolved "https://registry.yarnpkg.com/@resvg/resvg-js-linux-arm-gnueabihf/-/resvg-js-linux-arm-gnueabihf-2.6.2.tgz#34c445eba45efd68f6130b2ab426d76a7424253d"
+ integrity sha512-YIV3u/R9zJbpqTTNwTZM5/ocWetDKGsro0SWp70eGEM9eV2MerWyBRZnQIgzU3YBnSBQ1RcxRZvY/UxwESfZIw==
+
+"@resvg/resvg-js-linux-arm64-gnu@2.6.2":
+ version "2.6.2"
+ resolved "https://registry.yarnpkg.com/@resvg/resvg-js-linux-arm64-gnu/-/resvg-js-linux-arm64-gnu-2.6.2.tgz#30da47087dd8153182198b94fe9f8d994890dae5"
+ integrity sha512-zc2BlJSim7YR4FZDQ8OUoJg5holYzdiYMeobb9pJuGDidGL9KZUv7SbiD4E8oZogtYY42UZEap7dqkkYuA91pg==
+
+"@resvg/resvg-js-linux-arm64-musl@2.6.2":
+ version "2.6.2"
+ resolved "https://registry.yarnpkg.com/@resvg/resvg-js-linux-arm64-musl/-/resvg-js-linux-arm64-musl-2.6.2.tgz#5d75b8ff5c83103729c1ca3779987302753c50d4"
+ integrity sha512-3h3dLPWNgSsD4lQBJPb4f+kvdOSJHa5PjTYVsWHxLUzH4IFTJUAnmuWpw4KqyQ3NA5QCyhw4TWgxk3jRkQxEKg==
+
+"@resvg/resvg-js-linux-x64-gnu@2.6.2":
+ version "2.6.2"
+ resolved "https://registry.yarnpkg.com/@resvg/resvg-js-linux-x64-gnu/-/resvg-js-linux-x64-gnu-2.6.2.tgz#411abedfaee5edc57cbb7701736cecba522e26f3"
+ integrity sha512-IVUe+ckIerA7xMZ50duAZzwf1U7khQe2E0QpUxu5MBJNao5RqC0zwV/Zm965vw6D3gGFUl7j4m+oJjubBVoftw==
+
+"@resvg/resvg-js-linux-x64-musl@2.6.2":
+ version "2.6.2"
+ resolved "https://registry.yarnpkg.com/@resvg/resvg-js-linux-x64-musl/-/resvg-js-linux-x64-musl-2.6.2.tgz#fe4984038f0372f279e3ff570b72934dd7eb2a5c"
+ integrity sha512-UOf83vqTzoYQO9SZ0fPl2ZIFtNIz/Rr/y+7X8XRX1ZnBYsQ/tTb+cj9TE+KHOdmlTFBxhYzVkP2lRByCzqi4jQ==
+
+"@resvg/resvg-js-win32-arm64-msvc@2.6.2":
+ version "2.6.2"
+ resolved "https://registry.yarnpkg.com/@resvg/resvg-js-win32-arm64-msvc/-/resvg-js-win32-arm64-msvc-2.6.2.tgz#d3a053cf7ff687087a2106330c0fdaae706254d1"
+ integrity sha512-7C/RSgCa+7vqZ7qAbItfiaAWhyRSoD4l4BQAbVDqRRsRgY+S+hgS3in0Rxr7IorKUpGE69X48q6/nOAuTJQxeQ==
+
+"@resvg/resvg-js-win32-ia32-msvc@2.6.2":
+ version "2.6.2"
+ resolved "https://registry.yarnpkg.com/@resvg/resvg-js-win32-ia32-msvc/-/resvg-js-win32-ia32-msvc-2.6.2.tgz#7cdda1ce29ef7209e28191d917fa5bef0624a4ad"
+ integrity sha512-har4aPAlvjnLcil40AC77YDIk6loMawuJwFINEM7n0pZviwMkMvjb2W5ZirsNOZY4aDbo5tLx0wNMREp5Brk+w==
+
+"@resvg/resvg-js-win32-x64-msvc@2.6.2":
+ version "2.6.2"
+ resolved "https://registry.yarnpkg.com/@resvg/resvg-js-win32-x64-msvc/-/resvg-js-win32-x64-msvc-2.6.2.tgz#cb0ad04525d65f3def4c8d346157a57976d5b388"
+ integrity sha512-ZXtYhtUr5SSaBrUDq7DiyjOFJqBVL/dOBN7N/qmi/pO0IgiWW/f/ue3nbvu9joWE5aAKDoIzy/CxsY0suwGosQ==
+
+"@resvg/resvg-js@^2.6.2":
+ version "2.6.2"
+ resolved "https://registry.npmjs.org/@resvg/resvg-js/-/resvg-js-2.6.2.tgz"
+ integrity sha512-xBaJish5OeGmniDj9cW5PRa/PtmuVU3ziqrbr5xJj901ZDN4TosrVaNZpEiLZAxdfnhAe7uQ7QFWfjPe9d9K2Q==
+ optionalDependencies:
+ "@resvg/resvg-js-android-arm-eabi" "2.6.2"
+ "@resvg/resvg-js-android-arm64" "2.6.2"
+ "@resvg/resvg-js-darwin-arm64" "2.6.2"
+ "@resvg/resvg-js-darwin-x64" "2.6.2"
+ "@resvg/resvg-js-linux-arm-gnueabihf" "2.6.2"
+ "@resvg/resvg-js-linux-arm64-gnu" "2.6.2"
+ "@resvg/resvg-js-linux-arm64-musl" "2.6.2"
+ "@resvg/resvg-js-linux-x64-gnu" "2.6.2"
+ "@resvg/resvg-js-linux-x64-musl" "2.6.2"
+ "@resvg/resvg-js-win32-arm64-msvc" "2.6.2"
+ "@resvg/resvg-js-win32-ia32-msvc" "2.6.2"
+ "@resvg/resvg-js-win32-x64-msvc" "2.6.2"
+
+"@shuding/opentype.js@1.4.0-beta.0":
+ version "1.4.0-beta.0"
+ resolved "https://registry.npmjs.org/@shuding/opentype.js/-/opentype.js-1.4.0-beta.0.tgz"
+ integrity sha512-3NgmNyH3l/Hv6EvsWJbsvpcpUba6R8IREQ83nH83cyakCw7uM1arZKNfHwv1Wz6jgqrF/j4x5ELvR6PnK9nTcA==
+ dependencies:
+ fflate "^0.7.3"
+ string.prototype.codepointat "^0.2.1"
+
+"@tsconfig/node10@^1.0.7":
+ version "1.0.11"
+ resolved "https://registry.yarnpkg.com/@tsconfig/node10/-/node10-1.0.11.tgz#6ee46400685f130e278128c7b38b7e031ff5b2f2"
+ integrity sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==
+
+"@tsconfig/node12@^1.0.7":
+ version "1.0.11"
+ resolved "https://registry.yarnpkg.com/@tsconfig/node12/-/node12-1.0.11.tgz#ee3def1f27d9ed66dac6e46a295cffb0152e058d"
+ integrity sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==
+
+"@tsconfig/node14@^1.0.0":
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/@tsconfig/node14/-/node14-1.0.3.tgz#e4386316284f00b98435bf40f72f75a09dabf6c1"
+ integrity sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==
+
+"@tsconfig/node16@^1.0.2":
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.4.tgz#0b92dcc0cc1c81f6f306a381f28e31b1a56536e9"
+ integrity sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==
+
+"@types/node@^20.14.3":
+ version "20.14.3"
+ resolved "https://registry.npmjs.org/@types/node/-/node-20.14.3.tgz"
+ integrity sha512-Nuzqa6WAxeGnve6SXqiPAM9rA++VQs+iLZ1DDd56y0gdvygSZlQvZuvdFPR3yLqkVxPu4WrO02iDEyH1g+wazw==
+ dependencies:
+ undici-types "~5.26.4"
+
+abort-controller@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz"
+ integrity sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==
+ dependencies:
+ event-target-shim "^5.0.0"
+
+accepts@~1.3.8:
+ version "1.3.8"
+ resolved "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz"
+ integrity sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==
+ dependencies:
+ mime-types "~2.1.34"
+ negotiator "0.6.3"
+
+acorn-walk@^8.1.1:
+ version "8.3.3"
+ resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.3.3.tgz#9caeac29eefaa0c41e3d4c65137de4d6f34df43e"
+ integrity sha512-MxXdReSRhGO7VlFe1bRG/oI7/mdLV9B9JJT0N8vZOhF7gFRR5l3M8W9G8JxmKV+JC5mGqJ0QvqfSOLsCPa4nUw==
+ dependencies:
+ acorn "^8.11.0"
+
+acorn@^8.11.0, acorn@^8.4.1:
+ version "8.12.0"
+ resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.12.0.tgz#1627bfa2e058148036133b8d9b51a700663c294c"
+ integrity sha512-RTvkC4w+KNXrM39/lWCUaG0IbRkWdCv7W/IOW9oU6SawyxulvkQy5HQPVTKxEjczcUvapcrw3cFx/60VN/NRNw==
+
+arg@^4.1.0:
+ version "4.1.3"
+ resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089"
+ integrity sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==
+
+array-flatten@1.1.1:
+ version "1.1.1"
+ resolved "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz"
+ integrity sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==
+
+atomic-sleep@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz"
+ integrity sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==
+
+base64-js@0.0.8:
+ version "0.0.8"
+ resolved "https://registry.npmjs.org/base64-js/-/base64-js-0.0.8.tgz"
+ integrity sha512-3XSA2cR/h/73EzlXXdU6YNycmYI7+kicTxks4eJg2g39biHR84slg2+des+p7iHYhbRg/udIS4TD53WabcOUkw==
+
+base64-js@^1.3.1:
+ version "1.5.1"
+ resolved "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz"
+ integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==
+
+body-parser@1.20.2:
+ version "1.20.2"
+ resolved "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz"
+ 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.npmjs.org/boolean/-/boolean-3.2.0.tgz"
+ integrity sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==
+
+buffer@^6.0.3:
+ version "6.0.3"
+ resolved "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz"
+ 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.npmjs.org/bytes/-/bytes-3.1.2.tgz"
+ integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==
+
+call-bind@^1.0.7:
+ version "1.0.7"
+ resolved "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz"
+ 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"
+
+camelize@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.npmjs.org/camelize/-/camelize-1.0.1.tgz"
+ integrity sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==
+
+cbor-extract@^2.2.0:
+ version "2.2.0"
+ resolved "https://registry.npmjs.org/cbor-extract/-/cbor-extract-2.2.0.tgz"
+ 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.npmjs.org/cbor-x/-/cbor-x-1.5.9.tgz"
+ integrity sha512-OEI5rEu3MeR0WWNUXuIGkxmbXVhABP+VtgAXzm48c9ulkrsvxshjjk94XSOGphyAKeNGLPfAxxzEtgQ6rEVpYQ==
+ optionalDependencies:
+ cbor-extract "^2.2.0"
+
+cborg@^1.6.0:
+ version "1.10.2"
+ resolved "https://registry.npmjs.org/cborg/-/cborg-1.10.2.tgz"
+ integrity sha512-b3tFPA9pUr2zCUiCfRd2+wok2/LBSNUMKOuRRok+WlvvAgEt/PlbgPTsZUcwCOs53IJvLgTp0eotwtosE6njug==
+
+color-name@^1.1.4:
+ version "1.1.4"
+ resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz"
+ integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
+
+content-disposition@0.5.4:
+ version "0.5.4"
+ resolved "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz"
+ 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.npmjs.org/content-type/-/content-type-1.0.5.tgz"
+ integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==
+
+cookie-signature@1.0.6:
+ version "1.0.6"
+ resolved "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz"
+ integrity sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==
+
+cookie@0.6.0:
+ version "0.6.0"
+ resolved "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz"
+ integrity sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==
+
+create-require@^1.1.0:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333"
+ integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==
+
+css-background-parser@^0.1.0:
+ version "0.1.0"
+ resolved "https://registry.npmjs.org/css-background-parser/-/css-background-parser-0.1.0.tgz"
+ integrity sha512-2EZLisiZQ+7m4wwur/qiYJRniHX4K5Tc9w93MT3AS0WS1u5kaZ4FKXlOTBhOjc+CgEgPiGY+fX1yWD8UwpEqUA==
+
+css-box-shadow@1.0.0-3:
+ version "1.0.0-3"
+ resolved "https://registry.npmjs.org/css-box-shadow/-/css-box-shadow-1.0.0-3.tgz"
+ integrity sha512-9jaqR6e7Ohds+aWwmhe6wILJ99xYQbfmK9QQB9CcMjDbTxPZjwEmUQpU91OG05Xgm8BahT5fW+svbsQGjS/zPg==
+
+css-color-keywords@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz"
+ integrity sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==
+
+css-to-react-native@^3.0.0:
+ version "3.2.0"
+ resolved "https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-3.2.0.tgz"
+ integrity sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ==
+ dependencies:
+ camelize "^1.0.0"
+ css-color-keywords "^1.0.0"
+ postcss-value-parser "^4.0.2"
+
+debug@2.6.9:
+ version "2.6.9"
+ resolved "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz"
+ integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==
+ dependencies:
+ ms "2.0.0"
+
+define-data-property@^1.1.4:
+ version "1.1.4"
+ resolved "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz"
+ 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.npmjs.org/delay/-/delay-5.0.0.tgz"
+ integrity sha512-ReEBKkIfe4ya47wlPYf/gu5ib6yUG0/Aez0JQZQz94kiWtRQvZIQbTiehsnwHvLSWJnQdhVeqYue7Id1dKr0qw==
+
+depd@2.0.0:
+ version "2.0.0"
+ resolved "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz"
+ integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==
+
+destroy@1.2.0:
+ version "1.2.0"
+ resolved "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz"
+ integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==
+
+detect-libc@^2.0.1:
+ version "2.0.3"
+ resolved "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz"
+ integrity sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==
+
+diff@^4.0.1:
+ version "4.0.2"
+ resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d"
+ integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==
+
+ee-first@1.1.1:
+ version "1.1.1"
+ resolved "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz"
+ integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==
+
+emoji-regex@^10.2.1:
+ version "10.3.0"
+ resolved "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.3.0.tgz"
+ integrity sha512-QpLs9D9v9kArv4lfDEgg1X/gN5XLnf/A6l9cs8SPZLRZR3ZkY9+kwIQTxm+fsSej5UMYGE8fdoaZVIBlqG0XTw==
+
+encodeurl@~1.0.2:
+ version "1.0.2"
+ resolved "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz"
+ integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==
+
+es-define-property@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz"
+ integrity sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==
+ dependencies:
+ get-intrinsic "^1.2.4"
+
+es-errors@^1.3.0:
+ version "1.3.0"
+ resolved "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz"
+ integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==
+
+escape-html@^1.0.3, escape-html@~1.0.3:
+ version "1.0.3"
+ resolved "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz"
+ integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==
+
+etag@~1.8.1:
+ version "1.8.1"
+ resolved "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz"
+ integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==
+
+event-target-shim@^5.0.0:
+ version "5.0.1"
+ resolved "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz"
+ integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==
+
+events@^3.3.0:
+ version "3.3.0"
+ resolved "https://registry.npmjs.org/events/-/events-3.3.0.tgz"
+ integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==
+
+express@^4.19.2:
+ version "4.19.2"
+ resolved "https://registry.npmjs.org/express/-/express-4.19.2.tgz"
+ 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.npmjs.org/fast-printf/-/fast-printf-1.6.9.tgz"
+ integrity sha512-FChq8hbz65WMj4rstcQsFB0O7Cy++nmbNfLYnD9cYv2cRn8EG6k/MGn9kO/tjO66t09DLDugj3yL+V2o6Qftrg==
+ dependencies:
+ boolean "^3.1.4"
+
+fast-redact@^3.1.1:
+ version "3.5.0"
+ resolved "https://registry.npmjs.org/fast-redact/-/fast-redact-3.5.0.tgz"
+ integrity sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A==
+
+fflate@^0.7.3:
+ version "0.7.4"
+ resolved "https://registry.npmjs.org/fflate/-/fflate-0.7.4.tgz"
+ integrity sha512-5u2V/CDW15QM1XbbgS+0DfPxVB+jUKhWEKuuFuHncbk3tEEqzmoXL+2KyOFuKGqOnmdIy0/davWF1CkuwtibCw==
+
+finalhandler@1.2.0:
+ version "1.2.0"
+ resolved "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz"
+ 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.npmjs.org/forwarded/-/forwarded-0.2.0.tgz"
+ integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==
+
+fresh@0.5.2:
+ version "0.5.2"
+ resolved "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz"
+ integrity sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==
+
+fs-extra@^8.0.1:
+ version "8.1.0"
+ resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-8.1.0.tgz#49d43c45a88cd9677668cb7be1b46efdb8d2e1c0"
+ integrity sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==
+ dependencies:
+ graceful-fs "^4.2.0"
+ jsonfile "^4.0.0"
+ universalify "^0.1.0"
+
+function-bind@^1.1.2:
+ version "1.1.2"
+ resolved "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz"
+ integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==
+
+get-intrinsic@^1.1.3, get-intrinsic@^1.2.4:
+ version "1.2.4"
+ resolved "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz"
+ 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.npmjs.org/gopd/-/gopd-1.0.1.tgz"
+ integrity sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==
+ dependencies:
+ get-intrinsic "^1.1.3"
+
+graceful-fs@^4.1.6, graceful-fs@^4.2.0:
+ version "4.2.11"
+ resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3"
+ integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==
+
+graphemer@^1.4.0:
+ version "1.4.0"
+ resolved "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz"
+ integrity sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==
+
+has-property-descriptors@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz"
+ integrity sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==
+ dependencies:
+ es-define-property "^1.0.0"
+
+has-proto@^1.0.1:
+ version "1.0.3"
+ resolved "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz"
+ integrity sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==
+
+has-symbols@^1.0.3:
+ version "1.0.3"
+ resolved "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz"
+ integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==
+
+hasown@^2.0.0:
+ version "2.0.2"
+ resolved "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz"
+ integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==
+ dependencies:
+ function-bind "^1.1.2"
+
+hex-rgb@^4.1.0:
+ version "4.3.0"
+ resolved "https://registry.npmjs.org/hex-rgb/-/hex-rgb-4.3.0.tgz"
+ integrity sha512-Ox1pJVrDCyGHMG9CFg1tmrRUMRPRsAWYc/PinY0XzJU4K7y7vjNoLKIQ7BR5UJMCxNN8EM1MNDmHWA/B3aZUuw==
+
+http-errors@2.0.0:
+ version "2.0.0"
+ resolved "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz"
+ 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.npmjs.org/http-terminator/-/http-terminator-3.2.0.tgz"
+ 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.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz"
+ integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==
+ dependencies:
+ safer-buffer ">= 2.1.2 < 3"
+
+ieee754@^1.2.1:
+ version "1.2.1"
+ resolved "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz"
+ integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==
+
+inherits@2.0.4:
+ version "2.0.4"
+ resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz"
+ integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
+
+ipaddr.js@1.9.1:
+ version "1.9.1"
+ resolved "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz"
+ integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==
+
+iso-datestring-validator@^2.2.2:
+ version "2.2.2"
+ resolved "https://registry.npmjs.org/iso-datestring-validator/-/iso-datestring-validator-2.2.2.tgz"
+ integrity sha512-yLEMkBbLZTlVQqOnQ4FiMujR6T4DEcCb1xizmvXS+OxuhwcbtynoosRzdMA69zZCShCNAbi+gJ71FxZBBXx1SA==
+
+"js-tokens@^3.0.0 || ^4.0.0":
+ version "4.0.0"
+ resolved "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz"
+ integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==
+
+jsonfile@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-4.0.0.tgz#8771aae0799b64076b76640fca058f9c10e33ecb"
+ integrity sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==
+ optionalDependencies:
+ graceful-fs "^4.1.6"
+
+jsonfile@^5.0.0:
+ version "5.0.0"
+ resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-5.0.0.tgz#e6b718f73da420d612823996fdf14a03f6ff6922"
+ integrity sha512-NQRZ5CRo74MhMMC3/3r5g2k4fjodJ/wh8MxjFbCViWKFjxrnudWSY5vomh+23ZaXzAS7J3fBZIR2dV6WbmfM0w==
+ dependencies:
+ universalify "^0.1.2"
+ optionalDependencies:
+ graceful-fs "^4.1.6"
+
+linebreak@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.npmjs.org/linebreak/-/linebreak-1.1.0.tgz"
+ integrity sha512-MHp03UImeVhB7XZtjd0E4n6+3xr5Dq/9xI/5FptGk5FrbDR3zagPa2DS6U8ks/3HjbKWG9Q1M2ufOzxV2qLYSQ==
+ dependencies:
+ base64-js "0.0.8"
+ unicode-trie "^2.0.0"
+
+loose-envify@^1.1.0:
+ version "1.4.0"
+ resolved "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz"
+ integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==
+ dependencies:
+ js-tokens "^3.0.0 || ^4.0.0"
+
+make-error@^1.1.1:
+ version "1.3.6"
+ resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2"
+ integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==
+
+media-typer@0.3.0:
+ version "0.3.0"
+ resolved "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz"
+ integrity sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==
+
+merge-descriptors@1.0.1:
+ version "1.0.1"
+ resolved "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz"
+ integrity sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==
+
+methods@~1.1.2:
+ version "1.1.2"
+ resolved "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz"
+ integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==
+
+mime-db@1.52.0:
+ version "1.52.0"
+ resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz"
+ integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==
+
+mime-types@~2.1.24, mime-types@~2.1.34:
+ version "2.1.35"
+ resolved "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz"
+ integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==
+ dependencies:
+ mime-db "1.52.0"
+
+mime@1.6.0:
+ version "1.6.0"
+ resolved "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz"
+ integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==
+
+ms@2.0.0:
+ version "2.0.0"
+ resolved "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz"
+ integrity sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==
+
+ms@2.1.3:
+ version "2.1.3"
+ resolved "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz"
+ integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
+
+multiformats@^9.4.2, multiformats@^9.5.4, multiformats@^9.9.0:
+ version "9.9.0"
+ resolved "https://registry.npmjs.org/multiformats/-/multiformats-9.9.0.tgz"
+ integrity sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg==
+
+negotiator@0.6.3:
+ version "0.6.3"
+ resolved "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz"
+ integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==
+
+node-gyp-build-optional-packages@5.1.1:
+ version "5.1.1"
+ resolved "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.1.1.tgz"
+ integrity sha512-+P72GAjVAbTxjjwUmwjVrqrdZROD4nf8KgpBoDxqXXTiYZZt/ud60dE5yvCSr9lRO8e8yv6kgJIC0K0PfZFVQw==
+ dependencies:
+ detect-libc "^2.0.1"
+
+object-inspect@^1.13.1:
+ version "1.13.1"
+ resolved "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz"
+ integrity sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==
+
+on-exit-leak-free@^2.1.0:
+ version "2.1.2"
+ resolved "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz"
+ integrity sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==
+
+on-finished@2.4.1:
+ version "2.4.1"
+ resolved "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz"
+ 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.npmjs.org/p-finally/-/p-finally-1.0.0.tgz"
+ integrity sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==
+
+p-timeout@^3.0.0:
+ version "3.2.0"
+ resolved "https://registry.npmjs.org/p-timeout/-/p-timeout-3.2.0.tgz"
+ 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.npmjs.org/p-wait-for/-/p-wait-for-3.2.0.tgz"
+ integrity sha512-wpgERjNkLrBiFmkMEjuZJEWKKDrNfHCKA1OhyN1wg1FrLkULbviEy6py1AyJUgZ72YWFbZ38FIpnqvVqAlDUwA==
+ dependencies:
+ p-timeout "^3.0.0"
+
+pako@^0.2.5:
+ version "0.2.9"
+ resolved "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz"
+ integrity sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==
+
+parse-css-color@^0.2.1:
+ version "0.2.1"
+ resolved "https://registry.npmjs.org/parse-css-color/-/parse-css-color-0.2.1.tgz"
+ integrity sha512-bwS/GGIFV3b6KS4uwpzCFj4w297Yl3uqnSgIPsoQkx7GMLROXfMnWvxfNkL0oh8HVhZA4hvJoEoEIqonfJ3BWg==
+ dependencies:
+ color-name "^1.1.4"
+ hex-rgb "^4.1.0"
+
+parseurl@~1.3.3:
+ version "1.3.3"
+ resolved "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz"
+ integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==
+
+path-to-regexp@0.1.7:
+ version "0.1.7"
+ resolved "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz"
+ integrity sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==
+
+pino-abstract-transport@^1.2.0:
+ version "1.2.0"
+ resolved "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-1.2.0.tgz"
+ 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.npmjs.org/pino-std-serializers/-/pino-std-serializers-6.2.2.tgz"
+ integrity sha512-cHjPPsE+vhj/tnhCy/wiMh3M3z3h/j15zHQX+S9GkTBgqJuTuJzYJ4gUyACLhDaJ7kk9ba9iRDmbH2tJU03OiA==
+
+pino-std-serializers@^7.0.0:
+ version "7.0.0"
+ resolved "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.0.0.tgz"
+ integrity sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA==
+
+pino@^8.15.0:
+ version "8.21.0"
+ resolved "https://registry.npmjs.org/pino/-/pino-8.21.0.tgz"
+ 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.npmjs.org/pino/-/pino-9.2.0.tgz"
+ 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"
+
+postcss-value-parser@^4.0.2, postcss-value-parser@^4.2.0:
+ version "4.2.0"
+ resolved "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz"
+ integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==
+
+process-warning@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.npmjs.org/process-warning/-/process-warning-3.0.0.tgz"
+ integrity sha512-mqn0kFRl0EoqhnL0GQ0veqFHyIN1yig9RHh/InzORTUiZHFRAur+aMtRkELNwGs9aNwKS6tg/An4NYBPGwvtzQ==
+
+process@^0.11.10:
+ version "0.11.10"
+ resolved "https://registry.npmjs.org/process/-/process-0.11.10.tgz"
+ integrity sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==
+
+proxy-addr@~2.0.7:
+ version "2.0.7"
+ resolved "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz"
+ 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.npmjs.org/qs/-/qs-6.11.0.tgz"
+ integrity sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==
+ dependencies:
+ side-channel "^1.0.4"
+
+quick-format-unescaped@^4.0.3:
+ version "4.0.4"
+ resolved "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz"
+ integrity sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==
+
+range-parser@~1.2.1:
+ version "1.2.1"
+ resolved "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz"
+ integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==
+
+raw-body@2.5.2:
+ version "2.5.2"
+ resolved "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz"
+ 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"
+
+react@^18.3.1:
+ version "18.3.1"
+ resolved "https://registry.npmjs.org/react/-/react-18.3.1.tgz"
+ integrity sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==
+ dependencies:
+ loose-envify "^1.1.0"
+
+readable-stream@^4.0.0:
+ version "4.5.2"
+ resolved "https://registry.npmjs.org/readable-stream/-/readable-stream-4.5.2.tgz"
+ 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.npmjs.org/real-require/-/real-require-0.2.0.tgz"
+ integrity sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==
+
+roarr@^7.0.4:
+ version "7.21.1"
+ resolved "https://registry.npmjs.org/roarr/-/roarr-7.21.1.tgz"
+ 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.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz"
+ 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.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.4.3.tgz"
+ integrity sha512-e2bDA2WJT0wxseVd4lsDP4+3ONX6HpMXQa1ZhFQ7SU+GjvORCmShbCMltrtIDfkYhVHrOcPtj+KhmDBdPdZD1g==
+
+"safer-buffer@>= 2.1.2 < 3":
+ version "2.1.2"
+ resolved "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz"
+ integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
+
+satori@^0.10.13:
+ version "0.10.13"
+ resolved "https://registry.npmjs.org/satori/-/satori-0.10.13.tgz"
+ integrity sha512-klCwkVYMQ/ZN5inJLHzrUmGwoRfsdP7idB5hfpJ1jfiJk1ErDitK8Hkc6Kll1+Ox2WtqEuGecSZLnmup3CGzvQ==
+ dependencies:
+ "@shuding/opentype.js" "1.4.0-beta.0"
+ css-background-parser "^0.1.0"
+ css-box-shadow "1.0.0-3"
+ css-to-react-native "^3.0.0"
+ emoji-regex "^10.2.1"
+ escape-html "^1.0.3"
+ linebreak "^1.1.0"
+ parse-css-color "^0.2.1"
+ postcss-value-parser "^4.2.0"
+ yoga-wasm-web "^0.3.3"
+
+semver-compare@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz"
+ integrity sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==
+
+send@0.18.0:
+ version "0.18.0"
+ resolved "https://registry.npmjs.org/send/-/send-0.18.0.tgz"
+ 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.npmjs.org/serve-static/-/serve-static-1.15.0.tgz"
+ 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.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz"
+ 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.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz"
+ integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==
+
+side-channel@^1.0.4:
+ version "1.0.6"
+ resolved "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz"
+ 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.npmjs.org/sonic-boom/-/sonic-boom-3.8.1.tgz"
+ 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.npmjs.org/sonic-boom/-/sonic-boom-4.0.1.tgz"
+ integrity sha512-hTSD/6JMLyT4r9zeof6UtuBDpjJ9sO08/nmS5djaA9eozT9oOlNdpXSnzcgj4FTqpk3nkLrs61l4gip9r1HCrQ==
+ dependencies:
+ atomic-sleep "^1.0.0"
+
+split2@^4.0.0:
+ version "4.2.0"
+ resolved "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz"
+ integrity sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==
+
+statuses@2.0.1:
+ version "2.0.1"
+ resolved "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz"
+ integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==
+
+string.prototype.codepointat@^0.2.1:
+ version "0.2.1"
+ resolved "https://registry.npmjs.org/string.prototype.codepointat/-/string.prototype.codepointat-0.2.1.tgz"
+ integrity sha512-2cBVCj6I4IOvEnjgO/hWqXjqBGsY+zwPmHl12Srk9IXSZ56Jwwmy+66XO5Iut/oQVR7t5ihYdLB0GMa4alEUcg==
+
+string_decoder@^1.3.0:
+ version "1.3.0"
+ resolved "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz"
+ 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.npmjs.org/thread-stream/-/thread-stream-2.7.0.tgz"
+ integrity sha512-qQiRWsU/wvNolI6tbbCKd9iKaTnCXsTwVxhhKM6nctPdujTyztjlbUkUTUymidWcMnZ5pWR0ej4a0tjsW021vw==
+ dependencies:
+ real-require "^0.2.0"
+
+thread-stream@^3.0.0:
+ version "3.1.0"
+ resolved "https://registry.npmjs.org/thread-stream/-/thread-stream-3.1.0.tgz"
+ integrity sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==
+ dependencies:
+ real-require "^0.2.0"
+
+tiny-inflate@^1.0.0:
+ version "1.0.3"
+ resolved "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz"
+ integrity sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==
+
+tlds@^1.234.0:
+ version "1.252.0"
+ resolved "https://registry.npmjs.org/tlds/-/tlds-1.252.0.tgz"
+ integrity sha512-GA16+8HXvqtfEnw/DTcwB0UU354QE1n3+wh08oFjr6Znl7ZLAeUgYzCcK+/CCrOyE0vnHR8/pu3XXG3vDijXpQ==
+
+toidentifier@1.0.1:
+ version "1.0.1"
+ resolved "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz"
+ integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==
+
+ts-node@^10.9.2:
+ version "10.9.2"
+ resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.9.2.tgz#70f021c9e185bccdca820e26dc413805c101c71f"
+ integrity sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==
+ dependencies:
+ "@cspotcode/source-map-support" "^0.8.0"
+ "@tsconfig/node10" "^1.0.7"
+ "@tsconfig/node12" "^1.0.7"
+ "@tsconfig/node14" "^1.0.0"
+ "@tsconfig/node16" "^1.0.2"
+ acorn "^8.4.1"
+ acorn-walk "^8.1.1"
+ arg "^4.1.0"
+ create-require "^1.1.0"
+ diff "^4.0.1"
+ make-error "^1.1.1"
+ v8-compile-cache-lib "^3.0.1"
+ yn "3.1.1"
+
+twemoji-parser@14.0.0:
+ version "14.0.0"
+ resolved "https://registry.yarnpkg.com/twemoji-parser/-/twemoji-parser-14.0.0.tgz#13dabcb6d3a261d9efbf58a1666b182033bf2b62"
+ integrity sha512-9DUOTGLOWs0pFWnh1p6NF+C3CkQ96PWmEFwhOVmT3WbecRC+68AIqpsnJXygfkFcp4aXbOp8Dwbhh/HQgvoRxA==
+
+twemoji@^14.0.2:
+ version "14.0.2"
+ resolved "https://registry.yarnpkg.com/twemoji/-/twemoji-14.0.2.tgz#c53adb01dab22bf4870f648ca8cc347ce99ee37e"
+ integrity sha512-BzOoXIe1QVdmsUmZ54xbEH+8AgtOKUiG53zO5vVP2iUu6h5u9lN15NcuS6te4OY96qx0H7JK9vjjl9WQbkTRuA==
+ dependencies:
+ fs-extra "^8.0.1"
+ jsonfile "^5.0.0"
+ twemoji-parser "14.0.0"
+ universalify "^0.1.2"
+
+type-fest@^2.3.3:
+ version "2.19.0"
+ resolved "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz"
+ integrity sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==
+
+type-is@~1.6.18:
+ version "1.6.18"
+ resolved "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz"
+ 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.npmjs.org/typescript/-/typescript-5.4.5.tgz"
+ integrity sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==
+
+uint8arrays@3.0.0:
+ version "3.0.0"
+ resolved "https://registry.npmjs.org/uint8arrays/-/uint8arrays-3.0.0.tgz"
+ integrity sha512-HRCx0q6O9Bfbp+HHSfQQKD7wU70+lydKVt4EghkdOvlK/NlrF90z+eXV34mUd48rNvVJXwkrMSPpCATkct8fJA==
+ dependencies:
+ multiformats "^9.4.2"
+
+undici-types@~5.26.4:
+ version "5.26.5"
+ resolved "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz"
+ integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==
+
+unicode-trie@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.npmjs.org/unicode-trie/-/unicode-trie-2.0.0.tgz"
+ integrity sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ==
+ dependencies:
+ pako "^0.2.5"
+ tiny-inflate "^1.0.0"
+
+universalify@^0.1.0, universalify@^0.1.2:
+ version "0.1.2"
+ resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66"
+ integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==
+
+unpipe@1.0.0, unpipe@~1.0.0:
+ version "1.0.0"
+ resolved "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz"
+ integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==
+
+utils-merge@1.0.1:
+ version "1.0.1"
+ resolved "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz"
+ integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==
+
+v8-compile-cache-lib@^3.0.1:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz#6336e8d71965cb3d35a1bbb7868445a7c05264bf"
+ integrity sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==
+
+vary@~1.1.2:
+ version "1.1.2"
+ resolved "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz"
+ integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==
+
+yn@3.1.1:
+ version "3.1.1"
+ resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50"
+ integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==
+
+yoga-wasm-web@^0.3.3:
+ version "0.3.3"
+ resolved "https://registry.npmjs.org/yoga-wasm-web/-/yoga-wasm-web-0.3.3.tgz"
+ integrity sha512-N+d4UJSJbt/R3wqY7Coqs5pcV0aUj2j9IaQ3rNj9bVCLld8tTGKRa2USARjnvZJWVx1NDmQev8EknoczaOQDOA==
+
+zod@^3.21.4:
+ version "3.23.8"
+ resolved "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz"
+ integrity sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==
diff --git a/bskyweb/README.md b/bskyweb/README.md
index 640c30f4ad..4717c45e0c 100644
--- a/bskyweb/README.md
+++ b/bskyweb/README.md
@@ -24,7 +24,7 @@ Then build and copy over the big 'ol `bundle.web.js` file:
### Golang Daemon
-Install golang. We are generally using v1.21+.
+Install golang. We are generally using v1.22+.
In this directory (`bskyweb/`):
diff --git a/bskyweb/cmd/bskyweb/main.go b/bskyweb/cmd/bskyweb/main.go
index 5185ff573a..d9235afdee 100644
--- a/bskyweb/cmd/bskyweb/main.go
+++ b/bskyweb/cmd/bskyweb/main.go
@@ -35,11 +35,17 @@ 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"},
},
+ &cli.StringFlag{
+ Name: "ogcard-host",
+ Usage: "scheme, hostname, and port of ogcard service",
+ Required: false,
+ EnvVars: []string{"OGCARD_HOST"},
+ },
&cli.StringFlag{
Name: "http-address",
Usage: "Specify the local IP/port to bind to",
@@ -47,6 +53,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",
@@ -54,6 +67,13 @@ func run(args []string) {
Required: false,
EnvVars: []string{"DEBUG"},
},
+ &cli.StringFlag{
+ Name: "basic-auth-password",
+ Usage: "optional password to restrict access to web interface",
+ Required: false,
+ Value: "",
+ EnvVars: []string{"BASIC_AUTH_PASSWORD"},
+ },
},
},
}
diff --git a/bskyweb/cmd/bskyweb/server.go b/bskyweb/cmd/bskyweb/server.go
index 47fa2dbc7a..2d8153eb70 100644
--- a/bskyweb/cmd/bskyweb/server.go
+++ b/bskyweb/cmd/bskyweb/server.go
@@ -2,10 +2,12 @@ package main
import (
"context"
+ "crypto/subtle"
"errors"
"fmt"
"io/fs"
"net/http"
+ "net/url"
"os"
"os/signal"
"strings"
@@ -30,12 +32,24 @@ type Server struct {
echo *echo.Echo
httpd *http.Server
xrpcc *xrpc.Client
+ cfg *Config
+}
+
+type Config struct {
+ debug bool
+ httpAddress string
+ appviewHost string
+ ogcardHost string
+ linkHost string
}
func serve(cctx *cli.Context) error {
debug := cctx.Bool("debug")
httpAddress := cctx.String("http-address")
appviewHost := cctx.String("appview-host")
+ ogcardHost := cctx.String("ogcard-host")
+ linkHost := cctx.String("link-host")
+ basicAuthPassword := cctx.String("basic-auth-password")
// Echo
e := echo.New()
@@ -71,6 +85,13 @@ func serve(cctx *cli.Context) error {
server := &Server{
echo: e,
xrpcc: xrpcc,
+ cfg: &Config{
+ debug: debug,
+ httpAddress: httpAddress,
+ appviewHost: appviewHost,
+ ogcardHost: ogcardHost,
+ linkHost: linkHost,
+ },
}
// Create the HTTP server.
@@ -121,6 +142,18 @@ func serve(cctx *cli.Context) error {
},
}))
+ // optional password gating of entire web interface
+ if basicAuthPassword != "" {
+ e.Use(middleware.BasicAuth(func(username, password string, c echo.Context) (bool, error) {
+ // Be careful to use constant time comparison to prevent timing attacks
+ if subtle.ConstantTimeCompare([]byte(username), []byte("admin")) == 1 &&
+ subtle.ConstantTimeCompare([]byte(password), []byte(basicAuthPassword)) == 1 {
+ return true, nil
+ }
+ return false, nil
+ }))
+ }
+
// redirect trailing slash to non-trailing slash.
// all of our current endpoints have no trailing slash.
e.Use(middleware.RemoveTrailingSlashWithConfig(middleware.TrailingSlashConfig{
@@ -179,6 +212,7 @@ func serve(cctx *cli.Context) error {
e.GET("/search", server.WebGeneric)
e.GET("/feeds", server.WebGeneric)
e.GET("/notifications", server.WebGeneric)
+ e.GET("/notifications/settings", server.WebGeneric)
e.GET("/lists", server.WebGeneric)
e.GET("/moderation", server.WebGeneric)
e.GET("/moderation/modlists", server.WebGeneric)
@@ -192,6 +226,7 @@ func serve(cctx *cli.Context) error {
e.GET("/settings/threads", server.WebGeneric)
e.GET("/settings/external-embeds", server.WebGeneric)
e.GET("/settings/accessibility", server.WebGeneric)
+ e.GET("/settings/appearance", server.WebGeneric)
e.GET("/sys/debug", server.WebGeneric)
e.GET("/sys/debug-mod", server.WebGeneric)
e.GET("/sys/log", server.WebGeneric)
@@ -208,6 +243,7 @@ func serve(cctx *cli.Context) error {
e.GET("/profile/:handleOrDID", server.WebProfile)
e.GET("/profile/:handleOrDID/follows", server.WebGeneric)
e.GET("/profile/:handleOrDID/followers", server.WebGeneric)
+ e.GET("/profile/:handleOrDID/known-followers", server.WebGeneric)
e.GET("/profile/:handleOrDID/lists/:rkey", server.WebGeneric)
e.GET("/profile/:handleOrDID/feed/:rkey", server.WebGeneric)
e.GET("/profile/:handleOrDID/feed/:rkey/liked-by", server.WebGeneric)
@@ -221,6 +257,21 @@ 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)
+ // video download
+ e.GET("/video-download", server.WebGeneric)
+
+ // starter packs
+ e.GET("/starter-pack/:handleOrDID/:rkey", server.WebStarterPack)
+ e.GET("/start/:handleOrDID/:rkey", server.WebStarterPack)
+
+ 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 +343,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{}
@@ -377,6 +452,45 @@ func (srv *Server) WebPost(c echo.Context) error {
return c.Render(http.StatusOK, "post.html", data)
}
+func (srv *Server) WebStarterPack(c echo.Context) error {
+ req := c.Request()
+ ctx := req.Context()
+ data := pongo2.Context{}
+ data["requestURI"] = fmt.Sprintf("https://%s%s", req.Host, req.URL.Path)
+ // sanity check arguments. don't 4xx, just let app handle if not expected format
+ rkeyParam := c.Param("rkey")
+ rkey, err := syntax.ParseRecordKey(rkeyParam)
+ if err != nil {
+ log.Errorf("bad rkey: %v", err)
+ return c.Render(http.StatusOK, "starterpack.html", data)
+ }
+ handleOrDIDParam := c.Param("handleOrDID")
+ handleOrDID, err := syntax.ParseAtIdentifier(handleOrDIDParam)
+ if err != nil {
+ log.Errorf("bad identifier: %v", err)
+ return c.Render(http.StatusOK, "starterpack.html", data)
+ }
+ identifier := handleOrDID.Normalize().String()
+ starterPackURI := fmt.Sprintf("at://%s/app.bsky.graph.starterpack/%s", identifier, rkey)
+ spv, err := appbsky.GraphGetStarterPack(ctx, srv.xrpcc, starterPackURI)
+ if err != nil {
+ log.Errorf("failed to fetch starter pack view for: %s\t%v", starterPackURI, err)
+ return c.Render(http.StatusOK, "starterpack.html", data)
+ }
+ if spv.StarterPack == nil || spv.StarterPack.Record == nil {
+ return c.Render(http.StatusOK, "starterpack.html", data)
+ }
+ rec, ok := spv.StarterPack.Record.Val.(*appbsky.GraphStarterpack)
+ if !ok {
+ return c.Render(http.StatusOK, "starterpack.html", data)
+ }
+ data["title"] = rec.Name
+ if srv.cfg.ogcardHost != "" {
+ data["imgThumbUrl"] = fmt.Sprintf("%s/start/%s/%s", srv.cfg.ogcardHost, identifier, rkey)
+ }
+ return c.Render(http.StatusOK, "starterpack.html", data)
+}
+
func (srv *Server) WebProfile(c echo.Context) error {
ctx := c.Request().Context()
data := pongo2.Context{}
diff --git a/bskyweb/go.mod b/bskyweb/go.mod
index 0989217cac..d07bb7f89b 100644
--- a/bskyweb/go.mod
+++ b/bskyweb/go.mod
@@ -1,9 +1,11 @@
module github.com/bluesky-social/social-app/bskyweb
-go 1.21
+go 1.22
+
+toolchain go1.22.4
require (
- github.com/bluesky-social/indigo v0.0.0-20231216010655-ad730a7da4f5
+ github.com/bluesky-social/indigo v0.0.0-20240624223826-fd66f93ce141
github.com/flosch/pongo2/v6 v6.0.0
github.com/ipfs/go-log v1.0.5
github.com/joho/godotenv v1.5.1
@@ -19,7 +21,7 @@ require (
github.com/cpuguy83/go-md2man/v2 v2.0.3 // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
- github.com/go-logr/logr v1.3.0 // indirect
+ github.com/go-logr/logr v1.4.1 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
@@ -48,7 +50,7 @@ require (
github.com/jbenet/goprocess v0.1.4 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
- github.com/klauspost/cpuid/v2 v2.2.6 // indirect
+ github.com/klauspost/cpuid/v2 v2.2.7 // indirect
github.com/labstack/gommon v0.4.1 // indirect
github.com/lestrrat-go/blackmagic v1.0.1 // indirect
github.com/lestrrat-go/httpcc v1.0.1 // indirect
@@ -58,10 +60,9 @@ require (
github.com/lestrrat-go/option v1.0.1 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
- github.com/mattn/go-sqlite3 v1.14.18 // indirect
+ github.com/mattn/go-sqlite3 v1.14.22 // indirect
github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect
github.com/minio/sha256-simd v1.0.1 // indirect
- github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/mr-tron/base58 v1.2.0 // indirect
github.com/multiformats/go-base32 v0.1.0 // indirect
github.com/multiformats/go-base36 v0.2.0 // indirect
@@ -69,6 +70,7 @@ require (
github.com/multiformats/go-multihash v0.2.3 // indirect
github.com/multiformats/go-varint v0.0.7 // indirect
github.com/opentracing/opentracing-go v1.2.0 // indirect
+ github.com/orandin/slog-gorm v1.3.2 // indirect
github.com/polydawn/refmt v0.89.1-0.20221221234430-40501e09de1f // indirect
github.com/prometheus/client_golang v1.17.0 // indirect
github.com/prometheus/client_model v0.5.0 // indirect
@@ -79,28 +81,28 @@ require (
github.com/spaolacci/murmur3 v1.1.0 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasttemplate v1.2.2 // indirect
- github.com/whyrusleeping/cbor-gen v0.0.0-20230923211252-36a87e1ba72f // indirect
+ github.com/whyrusleeping/cbor-gen v0.1.1-0.20240311221002-68b9f235c302 // indirect
github.com/whyrusleeping/go-did v0.0.0-20230824162731-404d1707d5d6 // indirect
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b // indirect
gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02 // indirect
- go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.42.0 // indirect
+ go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1 // indirect
go.opentelemetry.io/otel v1.21.0 // indirect
go.opentelemetry.io/otel/metric v1.21.0 // indirect
go.opentelemetry.io/otel/trace v1.21.0 // indirect
go.uber.org/atomic v1.11.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.26.0 // indirect
- golang.org/x/crypto v0.15.0 // indirect
- golang.org/x/net v0.18.0 // indirect
+ golang.org/x/crypto v0.21.0 // indirect
+ golang.org/x/net v0.21.0 // indirect
golang.org/x/sync v0.5.0 // indirect
- golang.org/x/sys v0.14.0 // indirect
+ golang.org/x/sys v0.18.0 // indirect
golang.org/x/text v0.14.0 // indirect
golang.org/x/time v0.3.0 // indirect
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect
google.golang.org/protobuf v1.31.0 // indirect
- gorm.io/driver/postgres v1.5.0 // indirect
- gorm.io/driver/sqlite v1.5.4 // indirect
- gorm.io/gorm v1.25.5 // indirect
+ gorm.io/driver/postgres v1.5.7 // indirect
+ gorm.io/driver/sqlite v1.5.5 // indirect
+ gorm.io/gorm v1.25.9 // indirect
lukechampine.com/blake3 v1.2.1 // indirect
)
diff --git a/bskyweb/go.sum b/bskyweb/go.sum
index 59797e35db..57b127b863 100644
--- a/bskyweb/go.sum
+++ b/bskyweb/go.sum
@@ -2,8 +2,8 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
-github.com/bluesky-social/indigo v0.0.0-20231216010655-ad730a7da4f5 h1:Zk1c+mxCYH6G/vLL0+9lO2Eci4OT3AFy73qPWa9auDM=
-github.com/bluesky-social/indigo v0.0.0-20231216010655-ad730a7da4f5/go.mod h1:a8cPbqDkRX+aPwJnXF7kAi3PF26hYiR4w5H8624MB7k=
+github.com/bluesky-social/indigo v0.0.0-20240624223826-fd66f93ce141 h1:F3ZceqS82fFfVV3ychWRLNDRFaFlbOwrPic0qJiUVqw=
+github.com/bluesky-social/indigo v0.0.0-20240624223826-fd66f93ce141/go.mod h1:dBIOGhsiK0rgEETnxiGiuEyrSx6DKxEotHIPbiKD6WU=
github.com/carlmjohnson/versioninfo v0.22.5 h1:O00sjOLUAFxYQjlN/bzYTuZiS0y6fWDQjMRvwtKgwwc=
github.com/carlmjohnson/versioninfo v0.22.5/go.mod h1:QT9mph3wcVfISUKd0i9sZfVrPviHuSF+cUtLjm2WSf8=
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
@@ -11,7 +11,6 @@ github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XL
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/cpuguy83/go-md2man/v2 v2.0.3 h1:qMCsGGgs+MAzDFyp9LpAe1Lqy/fY/qCovCm0qnXZOBM=
github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
-github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -23,8 +22,8 @@ github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSw
github.com/flosch/pongo2/v6 v6.0.0 h1:lsGru8IAzHgIAw6H2m4PCyleO58I40ow6apih0WprMU=
github.com/flosch/pongo2/v6 v6.0.0/go.mod h1:CuDpFm47R0uGGE7z13/tTlt1Y6zdxvr2RLT5LJhsHEU=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
-github.com/go-logr/logr v1.3.0 h1:2y3SDp0ZXuc6/cjLSZ+Q3ir+QB9T/iG5yYRXqsagWSY=
-github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
+github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=
+github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-yaml/yaml v2.1.0+incompatible/go.mod h1:w2MrLa16VYP0jy6N7M5kHaCkaLENm+P+Tv+MfurjSw0=
@@ -35,7 +34,6 @@ github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
-github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
@@ -60,7 +58,6 @@ github.com/ipfs/bbloom v0.0.4 h1:Gi+8EGJ2y5qiD5FbsbpX/TMNcJw8gSqr7eyjHa4Fhvs=
github.com/ipfs/bbloom v0.0.4/go.mod h1:cS9YprKXpoZ9lT0n/Mw/a6/aFV6DTjTLYHeA+gyqMG0=
github.com/ipfs/go-block-format v0.2.0 h1:ZqrkxBA2ICbDRbK8KJs/u0O3dlp6gmAuuXUJNiW1Ycs=
github.com/ipfs/go-block-format v0.2.0/go.mod h1:+jpL11nFx5A/SPpsoBn6Bzkra/zaArfSmsknbPMYgzM=
-github.com/ipfs/go-cid v0.0.6/go.mod h1:6Ux9z5e+HpkQdckYoX1PG/6xqKspzlEIR5SDmgqgC/I=
github.com/ipfs/go-cid v0.4.1 h1:A/T3qGvxi4kpKWWcPC/PgbvDA2bjVLO7n4UeVwnbs/s=
github.com/ipfs/go-cid v0.4.1/go.mod h1:uQHwDeX4c6CtyrFwdqyhpNcxVewur1M7l7fNU7LKwZk=
github.com/ipfs/go-datastore v0.6.0 h1:JKyz+Gvz1QEZw0LsX1IBn+JFCJQH4SJVFtM4uWU0Myk=
@@ -88,10 +85,8 @@ github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsI
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
-github.com/jackc/pgx/v5 v5.3.0/go.mod h1:t3JDKnCBlYIc0ewLF0Q7B8MXmoIaBOZj/ic7iHozM/8=
github.com/jackc/pgx/v5 v5.5.0 h1:NxstgwndsTRy7eq9/kqYc/BZh5w2hHJV86wjvO+1xPw=
github.com/jackc/pgx/v5 v5.5.0/go.mod h1:Ig06C2Vu0t5qXC60W8sqIthScaEnFvojjj9dSljmHRA=
-github.com/jackc/puddle/v2 v2.2.0/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk=
github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/jbenet/go-cienv v0.1.0/go.mod h1:TqNnHUmJgXau0nCzC7kXWeotg3J9W34CUv5Djy1+FlA=
@@ -109,11 +104,9 @@ github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.17.3 h1:qkRjuerhUU1EmXLYGkSH6EZL+vPSxIrYjLNAK4slzwA=
github.com/klauspost/compress v1.17.3/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM=
-github.com/klauspost/cpuid/v2 v2.2.6 h1:ndNyv040zDGIDh8thGkXYjnFtiN02M1PVVF+JE/48xc=
-github.com/klauspost/cpuid/v2 v2.2.6/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
+github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
+github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
-github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
-github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
@@ -143,37 +136,28 @@ github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27k
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
-github.com/mattn/go-sqlite3 v1.14.18 h1:JL0eqdCOq6DJVNPSvArO/bIV9/P7fbGrV00LZHc+5aI=
-github.com/mattn/go-sqlite3 v1.14.18/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
+github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
+github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 h1:jWpvCLoY8Z/e3VKvlsiIGKtc+UG6U5vzxaoagmhXfyg=
github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0/go.mod h1:QUyp042oQthUoa9bqDv0ER0wrtXnBruoNd7aNjkbP+k=
-github.com/minio/blake2b-simd v0.0.0-20160723061019-3f5f724cb5b1/go.mod h1:pD8RvIylQ358TN4wwqatJ8rNavkEINozVn9DtGI3dfQ=
-github.com/minio/sha256-simd v0.1.1-0.20190913151208-6de447530771/go.mod h1:B5e1o+1/KgNmWrSQK08Y6Z1Vb5pwIktudl0J58iy0KM=
github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM=
github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8=
-github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
-github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
-github.com/mr-tron/base58 v1.1.0/go.mod h1:xcD2VGqlgYjBdcBLw+TuYLr8afG+Hj8g2eTVqeSzSU8=
-github.com/mr-tron/base58 v1.1.3/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc=
github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o=
github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc=
-github.com/multiformats/go-base32 v0.0.3/go.mod h1:pLiuGC8y0QR3Ue4Zug5UzK9LjgbkL8NSQj0zQ5Nz/AA=
github.com/multiformats/go-base32 v0.1.0 h1:pVx9xoSPqEIQG8o+UbAe7DNi51oej1NtK+aGkbLYxPE=
github.com/multiformats/go-base32 v0.1.0/go.mod h1:Kj3tFY6zNr+ABYMqeUNeGvkIC/UYgtWibDcT0rExnbI=
-github.com/multiformats/go-base36 v0.1.0/go.mod h1:kFGE83c6s80PklsHO9sRn2NCoffoRdUUOENyW/Vv6sM=
github.com/multiformats/go-base36 v0.2.0 h1:lFsAbNOGeKtuKozrtBsAkSVhv1p9D0/qedU9rQyccr0=
github.com/multiformats/go-base36 v0.2.0/go.mod h1:qvnKE++v+2MWCfePClUEjE78Z7P2a1UV0xHgWc0hkp4=
-github.com/multiformats/go-multibase v0.0.3/go.mod h1:5+1R4eQrT3PkYZ24C3W2Ue2tPwIdYQD509ZjSb5y9Oc=
github.com/multiformats/go-multibase v0.2.0 h1:isdYCVLvksgWlMW9OZRYJEa9pZETFivncJHmHnnd87g=
github.com/multiformats/go-multibase v0.2.0/go.mod h1:bFBZX4lKCA/2lyOFSAoKH5SS6oPyjtnzK/XTFDPkNuk=
-github.com/multiformats/go-multihash v0.0.13/go.mod h1:VdAWLKTwram9oKAatUcLxBNUjdtcVwxObEQBtRfuyjc=
github.com/multiformats/go-multihash v0.2.3 h1:7Lyc8XfX/IY2jWb/gI7JP+o7JEq9hOa7BFvVU9RSh+U=
github.com/multiformats/go-multihash v0.2.3/go.mod h1:dXgKXCXjBzdscBLk9JkjINiEsCKRVch90MdaGiKsvSM=
-github.com/multiformats/go-varint v0.0.5/go.mod h1:3Ls8CIEsrijN6+B7PbrXRPxHRPuXSrVKRY101jdMZYE=
github.com/multiformats/go-varint v0.0.7 h1:sWSGR+f/eu5ABZA2ZpYKBILXTTs9JWpdEM/nEGOHFS8=
github.com/multiformats/go-varint v0.0.7/go.mod h1:r8PUYw/fD/SjBCiKOoDlGF6QawOELpZAu9eioSos/OU=
github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs=
github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc=
+github.com/orandin/slog-gorm v1.3.2 h1:C0lKDQPAx/pF+8K2HL7bdShPwOEJpPM0Bn80zTzxU1g=
+github.com/orandin/slog-gorm v1.3.2/go.mod h1:MoZ51+b7xE9lwGNPYEhxcUtRNrYzjdcKvA8QXQQGEPA=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
@@ -188,7 +172,6 @@ github.com/prometheus/common v0.45.0/go.mod h1:YJmSTw9BoKxJplESWWxlbyttQR4uaEcGy
github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=
github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
-github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
@@ -213,9 +196,9 @@ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
-github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
-github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
+github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
+github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/urfave/cli v1.22.10/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
github.com/urfave/cli/v2 v2.25.7 h1:VAzn5oq403l5pHjc4OhD54+XGO9cdKVL/7lDjF+iKUs=
github.com/urfave/cli/v2 v2.25.7/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ=
@@ -225,8 +208,8 @@ github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQ
github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0 h1:GDDkbFiaK8jsSDJfjId/PEGEShv6ugrt4kYsC5UIDaQ=
github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0/go.mod h1:x6AKhvSSexNrVSrViXSHUEbICjmGXhtgABaHIySUSGw=
-github.com/whyrusleeping/cbor-gen v0.0.0-20230923211252-36a87e1ba72f h1:SBuSxXJL0/ZJMtTxbXZgHZkThl9dNrzyaNhlyaqscRo=
-github.com/whyrusleeping/cbor-gen v0.0.0-20230923211252-36a87e1ba72f/go.mod h1:fgkXqYy7bV2cFeIEOkVTZS/WjXARfBqSH6Q2qHL33hQ=
+github.com/whyrusleeping/cbor-gen v0.1.1-0.20240311221002-68b9f235c302 h1:MhInbXe4SzcImAKktUvWBCWZgcw6MYf5NfumTj1BhAw=
+github.com/whyrusleeping/cbor-gen v0.1.1-0.20240311221002-68b9f235c302/go.mod h1:pM99HXyEbSQHcosHc0iW7YFmwnscr+t9Te4ibko05so=
github.com/whyrusleeping/go-did v0.0.0-20230824162731-404d1707d5d6 h1:yJ9/LwIGIk/c0CdoavpC9RNSGSruIspSZtxG3Nnldic=
github.com/whyrusleeping/go-did v0.0.0-20230824162731-404d1707d5d6/go.mod h1:39U9RRVr4CKbXpXYopWn+FSH5s+vWu6+RmguSPWAq5s=
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU=
@@ -239,8 +222,8 @@ gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b h1:CzigHMRyS
gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b/go.mod h1:/y/V339mxv2sZmYYR64O07VuCpdNZqCTwO8ZcouTMI8=
gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02 h1:qwDnMxjkyLmAFgcfgTnfJrmYKWhHnci3GjDqcZp1M3Q=
gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02/go.mod h1:JTnUj0mpYiAsuZLmKjTx/ex3AtMowcCgnE7YNyCEP0I=
-go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.42.0 h1:pginetY7+onl4qN1vl0xW/V/v6OBZ0vVdH+esuJgvmM=
-go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.42.0/go.mod h1:XiYsayHc36K3EByOO6nbAXnAWbrUxdjUROCEeeROOH8=
+go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1 h1:aFJWCqJMNjENlcleuuOkGAPH82y0yULBScfXcIEdS24=
+go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1/go.mod h1:sEGXWArGqc3tVa+ekntsN65DmVbVeW+7lTKTjZF3/Fo=
go.opentelemetry.io/otel v1.21.0 h1:hzLeKBZEL7Okw2mGzZ0cc4k/A7Fta0uoPgaJCr8fsFc=
go.opentelemetry.io/otel v1.21.0/go.mod h1:QZzNPQPm1zLX4gZK4cMi+71eaorMSGT3A4znnUvNNEo=
go.opentelemetry.io/otel/metric v1.21.0 h1:tlYWfeo+Bocx5kLEloTjbcDwBuELRrIFxwdQ36PlJu4=
@@ -265,14 +248,12 @@ go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo=
go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
-golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
-golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
-golang.org/x/crypto v0.15.0 h1:frVn1TEaCEaZcn3Tmd7Y2b5KKPaZ+I32Q2OA3kYp5TA=
-golang.org/x/crypto v0.15.0/go.mod h1:4ChreQoLWfG3xLDer1WdlH5NdlQ3+mwnQq1YTKY+72g=
+golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA=
+golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
@@ -290,8 +271,8 @@ golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96b
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
-golang.org/x/net v0.18.0 h1:mIYleuAkSbHh0tCv7RvjL3F6ZVbLjq4+R7zbOn3Kokg=
-golang.org/x/net v0.18.0/go.mod h1:/czyP5RqHAH4odGYxBJ1qz0+CE5WZ+2j1YgoEo8F2jQ=
+golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
+golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -316,8 +297,8 @@ golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q=
-golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
+golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
@@ -365,13 +346,12 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-gorm.io/driver/postgres v1.5.0 h1:u2FXTy14l45qc3UeCJ7QaAXZmZfDDv0YrthvmRq1l0U=
-gorm.io/driver/postgres v1.5.0/go.mod h1:FUZXzO+5Uqg5zzwzv4KK49R8lvGIyscBOqYrtI1Ce9A=
-gorm.io/driver/sqlite v1.5.4 h1:IqXwXi8M/ZlPzH/947tn5uik3aYQslP9BVveoax0nV0=
-gorm.io/driver/sqlite v1.5.4/go.mod h1:qxAuCol+2r6PannQDpOP1FP6ag3mKi4esLnB/jHed+4=
-gorm.io/gorm v1.24.7-0.20230306060331-85eaf9eeda11/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
-gorm.io/gorm v1.25.5 h1:zR9lOiiYf09VNh5Q1gphfyia1JpiClIWG9hQaxB/mls=
-gorm.io/gorm v1.25.5/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
+gorm.io/driver/postgres v1.5.7 h1:8ptbNJTDbEmhdr62uReG5BGkdQyeasu/FZHxI0IMGnM=
+gorm.io/driver/postgres v1.5.7/go.mod h1:3e019WlBaYI5o5LIdNV+LyxCMNtLOQETBXL2h4chKpA=
+gorm.io/driver/sqlite v1.5.5 h1:7MDMtUZhV065SilG62E0MquljeArQZNfJnjd9i9gx3E=
+gorm.io/driver/sqlite v1.5.5/go.mod h1:6NgQ7sQWAIFsPrJJl1lSNSu2TABh0ZZ/zm5fosATavE=
+gorm.io/gorm v1.25.9 h1:wct0gxZIELDk8+ZqF/MVnHLkA1rvYlBWUMv2EdsK1g8=
+gorm.io/gorm v1.25.9/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
lukechampine.com/blake3 v1.2.1 h1:YuqqRuaqsGV71BV/nm9xlI0MKUv4QC54jQnBChWbGnI=
lukechampine.com/blake3 v1.2.1/go.mod h1:0OFRp7fBtAylGVCO40o87sbupkyIGgbpv1+M1k1LM6k=
diff --git a/bskyweb/static/.well-known/apple-app-site-association b/bskyweb/static/.well-known/apple-app-site-association
index 232acdf255..f5752d7c15 100644
--- a/bskyweb/static/.well-known/apple-app-site-association
+++ b/bskyweb/static/.well-known/apple-app-site-association
@@ -1,6 +1,5 @@
{
"applinks": {
- "apps": [],
"details": [
{
"appID": "B3LX46C5HS.xyz.blueskyweb.app",
@@ -9,5 +8,8 @@
]
}
]
+ },
+ "appclips": {
+ "apps": ["B3LX46C5HS.xyz.blueskyweb.app.AppClip"]
}
-}
\ No newline at end of file
+}
diff --git a/bskyweb/static/robots.txt b/bskyweb/static/robots.txt
index 4b53a1d996..ed110a32a7 100644
--- a/bskyweb/static/robots.txt
+++ b/bskyweb/static/robots.txt
@@ -7,4 +7,5 @@
# be ok.
User-Agent: *
Allow: /
+Disallow: /video-download
Disallow: /ACKNOWLEDGEMENTS.txt
diff --git a/bskyweb/templates/starterpack.html b/bskyweb/templates/starterpack.html
new file mode 100644
index 0000000000..80cbfc80a6
--- /dev/null
+++ b/bskyweb/templates/starterpack.html
@@ -0,0 +1,26 @@
+{% extends "base.html" %}
+
+{% block html_head_extra -%}
+
+ {%- if requestURI %}
+
+ {% endif -%}
+ {%- if imgThumbUrl %}
+
+
+ {%- else -%}
+
+
+ {% endif -%}
+
+ {%- if title %}
+
+
+ {%- else -%}
+
+
+ {% endif -%}
+
+
+
+{%- endblock %}
diff --git a/docs/build.md b/docs/build.md
index 88733d3b09..dc710686e7 100644
--- a/docs/build.md
+++ b/docs/build.md
@@ -4,7 +4,10 @@
- Set up your environment [using the expo instructions](https://docs.expo.dev/guides/local-app-development/).
- make sure that the JAVA_HOME points to the zulu-17 directory in your `.zshrc` or `.bashrc` file: `export JAVA_HOME=/Library/Java/JavaVirtualMachines/zulu-17.jdk/Contents/Home`. DO NOT use another JDK or you will encounter build errors.
-- If you're running macOS, make sure you are running the correct versions of Ruby and Cocoapods:
+- If you're running macOS, make sure you are running the correct versions of Ruby and Cocoapods:-
+ - If you are using Apple Silicon and this is the first time you are building for RN 0.74+, you may need to run:
+ - `arch -arm64 brew install llvm`
+ - `sudo gem install ffi`
- Check if you've installed Cocoapods through `homebrew`. If you have, remove it:
- `brew info cocoapods`
- If output says `Installed`:
diff --git a/docs/deploy-ota.md b/docs/deploy-ota.md
new file mode 100644
index 0000000000..391a6bf6bf
--- /dev/null
+++ b/docs/deploy-ota.md
@@ -0,0 +1,95 @@
+# OTA Deployments
+
+## Overview
+
+
+
+## Internal Deployments
+
+Internal OTA deployments should be performed automatically upon all merges into main. In cases where the fingerprint
+diff results in incompatible native changes, a new client build will automatically be ran and deployed to TestFlight
+(iOS) or delivered in Slack (Android).
+
+## Production Deployments
+
+### Prerequisites
+
+- Find the latest production build number for both iOS and Android in Slack. These are listed in #client-builds
+ - Production builds always send the Version Number and Build Number in the Slack message. Search for the latest
+ production version number, and you should find the correct information.
+
+ 
+
+- It may also be useful to check the current production clients for these values. This will also help for testing. Note
+that you will need to _fully_ remove the existing internal client build from your device, otherwise the given values in
+the app may differ from the actual production values.
+
+ 
+
+- You should have signed in to EAS locally through npx eas login. You will need to modify the build number in a
+subsequent step.
+- Ensure that the commit the initial client was cut from is properly tagged in git. The tag should be in the format of 1.X.0
+ - Note: If the commit is not properly tagged, then the OTA deployment will simply fail since the GitHub Action will
+ not be able to find a commit to fingerprint and diff against.
+
+### Preparation
+
+- Create a new branch from the git tag that the initial release was cut from if no OTA deployment has been made yet for this
+client. Name this branch `1.X.0-ota-1`
+- If a deployment has been made previously for this release, increment the branch name, i.e. `1.x.0-ota-2`
+- If necessary, cherry-pick the commit(s) that you wish to deploy
+- Ensure that the package.json’s version field is set to the appropriate value. As long as used the correct git tag
+to create your branch from, this should be properly set.
+
+### Deployment
+
+- Update the build number through EAS to match the build numbers of the
+ production iOS/Android apps
+ - Note: This isn’t strictly necessary, but having a step that takes you off of GitHub and into the terminal provides
+ a little “friction” to avoid fat fingering a release. Since there are legitimate reasons to just “click and deploy”
+ for internal builds, I felt it useful to make sure it doesn’t accidentally become a prod deployment.
+ - Set the build numbers to the values found in the prerequisite steps. Again, this should be the
+ build number for the current production release you want to deploy for.
+ - `npx eas build:version:set -p ios`
+ - `npx eas build:version:set -p android`
+ - These steps should spit out what the current build number is, save those values
+ for later too
+- Run the deployment
+ - Navigate to https://github.com/bluesky-social/social-app/actions/workflows/bundle-deploy-eas-update.yml
+ - Select the “Run Workflow” dropdown
+
+ 
+
+ - Select the branch for the deployment you are releasing.
+
+ 
+
+ - Double check the branch selection.
+ - Select the production channel
+ - Enter the version for the client you are releasing to, i.e. 1.80.0
+ - Note: If you do enter an incorrect version here, the deployment will either:
+ - Fail because the action cannot find a commit with your misentered version
+ - Succeed - but with no users receiving the update. This is because the version you entered will not properly
+ correlate to a _build number_ as well, so no clients in the wild will be able to receive the update.
+
+ 
+
+ - Triple check the branch selection.
+ - You selected the correct branch
+ - You selected the "Production" channel
+ - You entered the correct version in the format of `1.X.0`.
+ - Press “Run Workflow”
+
+In about five minutes, the new deployment should be available for download. To test:
+
+- Remove the internal build of the app from your device
+- Download the app from the App Store/Google Play
+- Launch the app once and wait approximately 15 seconds
+- Relaunch the app
+- Check the Settings page and scroll to the bottom. The commit hash should now be the latest commit on your deployed branch.
+
+### Post Deployment
+
+- Reset both platforms build numbers to what they were before the OTA
+ deployment. These values should have been logged by the EAS CLI when you
+ reset them to the production values prior to OTA.
diff --git a/docs/img/app-build-number.png b/docs/img/app-build-number.png
new file mode 100644
index 0000000000..449309bd33
Binary files /dev/null and b/docs/img/app-build-number.png differ
diff --git a/docs/img/branch-selection.png b/docs/img/branch-selection.png
new file mode 100644
index 0000000000..650583687c
Binary files /dev/null and b/docs/img/branch-selection.png differ
diff --git a/docs/img/ota-flow.png b/docs/img/ota-flow.png
new file mode 100644
index 0000000000..f7b72b35df
Binary files /dev/null and b/docs/img/ota-flow.png differ
diff --git a/docs/img/other-ota-options.png b/docs/img/other-ota-options.png
new file mode 100644
index 0000000000..8715c65c1f
Binary files /dev/null and b/docs/img/other-ota-options.png differ
diff --git a/docs/img/run-workflow.png b/docs/img/run-workflow.png
new file mode 100644
index 0000000000..f0aa2fcb1f
Binary files /dev/null and b/docs/img/run-workflow.png differ
diff --git a/docs/img/slack-build-info.png b/docs/img/slack-build-info.png
new file mode 100644
index 0000000000..1f69518ced
Binary files /dev/null and b/docs/img/slack-build-info.png differ
diff --git a/eas.json b/eas.json
index ed647dbb9c..a705c40027 100644
--- a/eas.json
+++ b/eas.json
@@ -46,6 +46,20 @@
"EXPO_PUBLIC_ENV": "production"
}
},
+ "production-apk": {
+ "extends": "base",
+ "distribution": "internal",
+ "ios": {
+ "autoIncrement": false
+ },
+ "android": {
+ "autoIncrement": false
+ },
+ "channel": "production",
+ "env": {
+ "EXPO_PUBLIC_ENV": "production"
+ }
+ },
"testflight": {
"extends": "base",
"ios": {
diff --git a/eslint/index.js b/eslint/index.js
index bb31a942d1..cf5d41225d 100644
--- a/eslint/index.js
+++ b/eslint/index.js
@@ -3,6 +3,7 @@
module.exports = {
rules: {
'avoid-unwrapped-text': require('./avoid-unwrapped-text'),
+ 'use-exact-imports': require('./use-exact-imports'),
'use-typed-gates': require('./use-typed-gates'),
},
}
diff --git a/eslint/use-exact-imports.js b/eslint/use-exact-imports.js
new file mode 100644
index 0000000000..06723043fe
--- /dev/null
+++ b/eslint/use-exact-imports.js
@@ -0,0 +1,22 @@
+/* eslint-disable bsky-internal/use-exact-imports */
+const BANNED_IMPORTS = [
+ '@fortawesome/free-regular-svg-icons',
+ '@fortawesome/free-solid-svg-icons',
+]
+
+exports.create = function create(context) {
+ return {
+ Literal(node) {
+ if (typeof node.value !== 'string') {
+ return
+ }
+ if (BANNED_IMPORTS.includes(node.value)) {
+ context.report({
+ node,
+ message:
+ 'Import the specific thing you want instead of the entire package',
+ })
+ }
+ },
+ }
+}
diff --git a/index.js b/index.js
index 7630d0538a..2f13ce1ea1 100644
--- a/index.js
+++ b/index.js
@@ -1,14 +1,11 @@
import 'react-native-gesture-handler' // must be first
-import {LogBox} from 'react-native'
-
import '#/platform/polyfills'
-import {IS_TEST} from '#/env'
+
+import {LogBox} from 'react-native'
import {registerRootComponent} from 'expo'
-import {doPolyfill} from '#/lib/api/api-polyfill'
import App from '#/App'
-
-doPolyfill()
+import {IS_TEST} from '#/env'
if (IS_TEST) {
LogBox.ignoreAllLogs() // suppress all logs in tests
diff --git a/index.web.js b/index.web.js
index 9623734512..be75bc772e 100644
--- a/index.web.js
+++ b/index.web.js
@@ -1,9 +1,8 @@
import '#/platform/markBundleStartTime'
-
import '#/platform/polyfills'
+
import {registerRootComponent} from 'expo'
-import {doPolyfill} from '#/lib/api/api-polyfill'
+
import App from '#/App'
-doPolyfill()
registerRootComponent(App)
diff --git a/jest/jestSetup.js b/jest/jestSetup.js
index e690e813a9..a68c1dc4bf 100644
--- a/jest/jestSetup.js
+++ b/jest/jestSetup.js
@@ -1,10 +1,10 @@
/* global jest */
-import {configure} from '@testing-library/react-native'
import 'react-native-gesture-handler/jestSetup'
-
// IMPORTANT: this is what's used in the native runtime
import 'react-native-url-polyfill/auto'
+import {configure} from '@testing-library/react-native'
+
configure({asyncUtilTimeout: 20000})
jest.mock('@react-native-async-storage/async-storage', () =>
@@ -90,3 +90,21 @@ jest.mock('sentry-expo', () => ({
}))
jest.mock('crypto', () => ({}))
+
+jest.mock('expo-application', () => ({
+ nativeApplicationVersion: '1.0.0',
+ nativeBuildVersion: '1',
+}))
+
+jest.mock('expo-modules-core', () => ({
+ requireNativeModule: jest.fn().mockImplementation(moduleName => {
+ if (moduleName === 'ExpoPlatformInfo') {
+ return {
+ getIsReducedMotionEnabled: () => false,
+ }
+ }
+ }),
+ requireNativeViewManager: jest.fn().mockImplementation(moduleName => {
+ return () => null
+ }),
+}))
diff --git a/jest/test-pds.ts b/jest/test-pds.ts
index 2fe623ca98..bfcc970c2f 100644
--- a/jest/test-pds.ts
+++ b/jest/test-pds.ts
@@ -156,7 +156,7 @@ class Mocker {
}
async createUser(name: string) {
- const agent = new BskyAgent({service: this.agent.service})
+ const agent = new BskyAgent({service: this.service})
const inviteRes = await agent.api.com.atproto.server.createInviteCode(
{useCount: 1},
@@ -332,7 +332,7 @@ class Mocker {
}
async createInvite(forAccount: string) {
- const agent = new BskyAgent({service: this.agent.service})
+ const agent = new BskyAgent({service: this.service})
await agent.api.com.atproto.server.createInviteCode(
{useCount: 1, forAccount},
{
diff --git a/lib/react-compiler-runtime/index.js b/lib/react-compiler-runtime/index.js
new file mode 100644
index 0000000000..44f80e0c98
--- /dev/null
+++ b/lib/react-compiler-runtime/index.js
@@ -0,0 +1,21 @@
+const React = require('react')
+const $empty = Symbol.for('react.memo_cache_sentinel')
+/**
+ * DANGER: this hook is NEVER meant to be called directly!
+ *
+ * Note that this is a temporary userspace implementation of this function
+ * from React 19. It is not as efficient and may invalidate more frequently
+ * than the official API. Please upgrade to React 19 as soon as you can.
+ **/
+export function c(size) {
+ // eslint-disable-next-line react-hooks/rules-of-hooks
+ return React.useState(() => {
+ const $ = new Array(size)
+ for (let ii = 0; ii < size; ii++) {
+ $[ii] = $empty
+ }
+ // @ts-ignore
+ $[$empty] = true
+ return $
+ })[0]
+}
diff --git a/lib/react-compiler-runtime/package.json b/lib/react-compiler-runtime/package.json
new file mode 100644
index 0000000000..cb8f78bd55
--- /dev/null
+++ b/lib/react-compiler-runtime/package.json
@@ -0,0 +1,9 @@
+{
+ "name": "react-compiler-runtime",
+ "version": "0.0.1",
+ "license": "MIT",
+ "main": "index.js",
+ "peerDependencies": {
+ "react": "^18.2.0"
+ }
+}
\ No newline at end of file
diff --git a/modules/BlueskyClip/AppDelegate.swift b/modules/BlueskyClip/AppDelegate.swift
new file mode 100644
index 0000000000..684194953c
--- /dev/null
+++ b/modules/BlueskyClip/AppDelegate.swift
@@ -0,0 +1,32 @@
+import UIKit
+
+@main
+class AppDelegate: UIResponder, UIApplicationDelegate {
+ var window: UIWindow?
+ var controller: ViewController?
+
+ func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
+ let window = UIWindow()
+ self.window = UIWindow()
+
+ let controller = ViewController(window: window)
+ self.controller = controller
+
+ window.rootViewController = self.controller
+ window.makeKeyAndVisible()
+
+ return true
+ }
+
+ func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool {
+ self.controller?.handleURL(url: url)
+ return true
+ }
+
+ func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
+ if let incomingURL = userActivity.webpageURL {
+ self.controller?.handleURL(url: incomingURL)
+ }
+ return true
+ }
+}
diff --git a/modules/BlueskyClip/Images.xcassets/AppIcon.appiconset/App-Icon-1024x1024@1x.png b/modules/BlueskyClip/Images.xcassets/AppIcon.appiconset/App-Icon-1024x1024@1x.png
new file mode 100644
index 0000000000..75ce4b813a
Binary files /dev/null and b/modules/BlueskyClip/Images.xcassets/AppIcon.appiconset/App-Icon-1024x1024@1x.png differ
diff --git a/modules/BlueskyClip/Images.xcassets/AppIcon.appiconset/Contents.json b/modules/BlueskyClip/Images.xcassets/AppIcon.appiconset/Contents.json
new file mode 100644
index 0000000000..c3bb428dbd
--- /dev/null
+++ b/modules/BlueskyClip/Images.xcassets/AppIcon.appiconset/Contents.json
@@ -0,0 +1,14 @@
+{
+ "images" : [
+ {
+ "filename" : "App-Icon-1024x1024@1x.png",
+ "idiom" : "universal",
+ "platform" : "ios",
+ "size" : "1024x1024"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/modules/BlueskyClip/Images.xcassets/Contents.json b/modules/BlueskyClip/Images.xcassets/Contents.json
new file mode 100644
index 0000000000..73c00596a7
--- /dev/null
+++ b/modules/BlueskyClip/Images.xcassets/Contents.json
@@ -0,0 +1,6 @@
+{
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/modules/BlueskyClip/ViewController.swift b/modules/BlueskyClip/ViewController.swift
new file mode 100644
index 0000000000..b178644b8f
--- /dev/null
+++ b/modules/BlueskyClip/ViewController.swift
@@ -0,0 +1,133 @@
+import UIKit
+import WebKit
+import StoreKit
+
+class ViewController: UIViewController, WKScriptMessageHandler, WKNavigationDelegate {
+ let defaults = UserDefaults(suiteName: "group.app.bsky")
+
+ var window: UIWindow
+ var webView: WKWebView?
+
+ var prevUrl: URL?
+ var starterPackUrl: URL?
+
+ init(window: UIWindow) {
+ self.window = window
+ super.init(nibName: nil, bundle: nil)
+ }
+
+ required init?(coder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ override func viewDidLoad() {
+ super.viewDidLoad()
+
+ let contentController = WKUserContentController()
+ contentController.add(self, name: "onMessage")
+ let configuration = WKWebViewConfiguration()
+ configuration.userContentController = contentController
+
+ let webView = WKWebView(frame: self.view.bounds, configuration: configuration)
+ webView.translatesAutoresizingMaskIntoConstraints = false
+ webView.contentMode = .scaleToFill
+ webView.navigationDelegate = self
+ self.view.addSubview(webView)
+ self.webView = webView
+ }
+
+ func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
+ guard let response = message.body as? String,
+ let data = response.data(using: .utf8),
+ let payload = try? JSONDecoder().decode(WebViewActionPayload.self, from: data) else {
+ return
+ }
+
+ switch payload.action {
+ case .present:
+ guard let url = self.starterPackUrl else {
+ return
+ }
+
+ self.presentAppStoreOverlay()
+ defaults?.setValue(url.absoluteString, forKey: "starterPackUri")
+
+ case .store:
+ guard let keyToStoreAs = payload.keyToStoreAs, let jsonToStore = payload.jsonToStore else {
+ return
+ }
+
+ self.defaults?.setValue(jsonToStore, forKey: keyToStoreAs)
+ }
+ }
+
+ func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction) async -> WKNavigationActionPolicy {
+ // Detect when we land on the right URL. This is incase of a short link opening the app clip
+ guard let url = navigationAction.request.url else {
+ return .allow
+ }
+
+ // Store the previous one to compare later, but only set starterPackUrl when we find the right one
+ prevUrl = url
+ // pathComponents starts with "/" as the first component, then each path name. so...
+ // ["/", "start", "name", "rkey"]
+ if url.pathComponents.count == 4,
+ url.pathComponents[1] == "start" {
+ self.starterPackUrl = url
+ }
+
+ return .allow
+ }
+
+ func handleURL(url: URL) {
+ let urlString = "\(url.absoluteString)?clip=true"
+ if let url = URL(string: urlString) {
+ self.webView?.load(URLRequest(url: url))
+ }
+ }
+
+ func presentAppStoreOverlay() {
+ guard let windowScene = self.window.windowScene else {
+ return
+ }
+
+ let configuration = SKOverlay.AppClipConfiguration(position: .bottomRaised)
+ let overlay = SKOverlay(configuration: configuration)
+
+ overlay.present(in: windowScene)
+ }
+
+ func getHost(_ url: URL?) -> String? {
+ if #available(iOS 16.0, *) {
+ return url?.host()
+ } else {
+ return url?.host
+ }
+ }
+
+ func getQuery(_ url: URL?) -> String? {
+ if #available(iOS 16.0, *) {
+ return url?.query()
+ } else {
+ return url?.query
+ }
+ }
+
+ func urlMatchesPrevious(_ url: URL?) -> Bool {
+ if #available(iOS 16.0, *) {
+ return url?.query() == prevUrl?.query() && url?.host() == prevUrl?.host() && url?.query() == prevUrl?.query()
+ } else {
+ return url?.query == prevUrl?.query && url?.host == prevUrl?.host && url?.query == prevUrl?.query
+ }
+ }
+}
+
+struct WebViewActionPayload: Decodable {
+ enum Action: String, Decodable {
+ case present, store
+ }
+
+ let action: Action
+ let keyToStoreAs: String?
+ let jsonToStore: String?
+}
diff --git a/modules/BlueskyNSE/NotificationService.swift b/modules/BlueskyNSE/NotificationService.swift
index c6f391e007..f863eaf223 100644
--- a/modules/BlueskyNSE/NotificationService.swift
+++ b/modules/BlueskyNSE/NotificationService.swift
@@ -1,4 +1,5 @@
import UserNotifications
+import UIKit
let APP_GROUP = "group.app.bsky"
@@ -6,45 +7,49 @@ class NotificationService: UNNotificationServiceExtension {
var prefs = UserDefaults(suiteName: APP_GROUP)
override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {
- guard var bestAttempt = createCopy(request.content),
+ guard let bestAttempt = createCopy(request.content),
let reason = request.content.userInfo["reason"] as? String
else {
contentHandler(request.content)
return
}
-
+
if reason == "chat-message" {
mutateWithChatMessage(bestAttempt)
+ } else {
+ mutateWithBadge(bestAttempt)
}
-
- // The badge should always be incremented when in the background
- mutateWithBadge(bestAttempt)
-
+
contentHandler(bestAttempt)
}
-
+
override func serviceExtensionTimeWillExpire() {
// If for some reason the alloted time expires, we don't actually want to display a notification
}
-
+
func createCopy(_ content: UNNotificationContent) -> UNMutableNotificationContent? {
return content.mutableCopy() as? UNMutableNotificationContent
}
-
+
func mutateWithBadge(_ content: UNMutableNotificationContent) {
- content.badge = 1
+ var count = prefs?.integer(forKey: "badgeCount") ?? 0
+ count += 1
+
+ // Set the new badge number for the notification, then store that value for using later
+ content.badge = NSNumber(value: count)
+ prefs?.setValue(count, forKey: "badgeCount")
}
-
+
func mutateWithChatMessage(_ content: UNMutableNotificationContent) {
if self.prefs?.bool(forKey: "playSoundChat") == true {
mutateWithDmSound(content)
}
}
-
+
func mutateWithDefaultSound(_ content: UNMutableNotificationContent) {
content.sound = UNNotificationSound.default
}
-
+
func mutateWithDmSound(_ content: UNMutableNotificationContent) {
content.sound = UNNotificationSound(named: UNNotificationSoundName(rawValue: "dm.aiff"))
}
diff --git a/modules/Share-with-Bluesky/ShareViewController.swift b/modules/Share-with-Bluesky/ShareViewController.swift
index 4c1d635ce3..c045d578fe 100644
--- a/modules/Share-with-Bluesky/ShareViewController.swift
+++ b/modules/Share-with-Bluesky/ShareViewController.swift
@@ -30,12 +30,11 @@ class ShareViewController: UIViewController {
}
}
- private func handleText(item: NSItemProvider) async -> Void {
+ private func handleText(item: NSItemProvider) async {
do {
if let data = try await item.loadItem(forTypeIdentifier: "public.text") as? String {
if let encoded = data.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed),
- let url = URL(string: "\(self.appScheme)://intent/compose?text=\(encoded)")
- {
+ let url = URL(string: "\(self.appScheme)://intent/compose?text=\(encoded)") {
_ = self.openURL(url)
}
}
@@ -45,12 +44,11 @@ class ShareViewController: UIViewController {
}
}
- private func handleUrl(item: NSItemProvider) async -> Void {
+ private func handleUrl(item: NSItemProvider) async {
do {
if let data = try await item.loadItem(forTypeIdentifier: "public.url") as? URL {
if let encoded = data.absoluteString.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed),
- let url = URL(string: "\(self.appScheme)://intent/compose?text=\(encoded)")
- {
+ let url = URL(string: "\(self.appScheme)://intent/compose?text=\(encoded)") {
_ = self.openURL(url)
}
}
@@ -60,7 +58,7 @@ class ShareViewController: UIViewController {
}
}
- private func handleImages(items: [NSItemProvider]) async -> Void {
+ private func handleImages(items: [NSItemProvider]) async {
let firstFourItems: [NSItemProvider]
if items.count < 4 {
firstFourItems = items
@@ -72,7 +70,7 @@ class ShareViewController: UIViewController {
var imageUris = ""
for (index, item) in firstFourItems.enumerated() {
- var imageUriInfo: String? = nil
+ var imageUriInfo: String?
do {
if let dataUri = try await item.loadItem(forTypeIdentifier: "public.image") as? URL {
@@ -100,8 +98,7 @@ class ShareViewController: UIViewController {
if valid,
let encoded = imageUris.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed),
- let url = URL(string: "\(self.appScheme)://intent/compose?imageUris=\(encoded)")
- {
+ let url = URL(string: "\(self.appScheme)://intent/compose?imageUris=\(encoded)") {
_ = self.openURL(url)
}
@@ -119,13 +116,11 @@ class ShareViewController: UIViewController {
// extension does.
if let dir = FileManager()
.containerURL(
- forSecurityApplicationGroupIdentifier: "group.app.bsky")
- {
+ forSecurityApplicationGroupIdentifier: "group.app.bsky") {
let filePath = "\(dir.absoluteString)\(ProcessInfo.processInfo.globallyUniqueString).jpeg"
if let newUri = URL(string: filePath),
- let jpegData = image.jpegData(compressionQuality: 1)
- {
+ let jpegData = image.jpegData(compressionQuality: 1) {
try jpegData.write(to: newUri)
return "\(newUri.absoluteString)|\(image.size.width)|\(image.size.height)"
}
@@ -136,7 +131,7 @@ class ShareViewController: UIViewController {
}
}
- private func completeRequest() -> Void {
+ private func completeRequest() {
self.extensionContext?.completeRequest(returningItems: nil)
}
diff --git a/modules/expo-background-notification-handler/android/src/main/java/expo/modules/backgroundnotificationhandler/BackgroundNotificationHandler.kt b/modules/expo-background-notification-handler/android/src/main/java/expo/modules/backgroundnotificationhandler/BackgroundNotificationHandler.kt
index 344508523d..7c1494c706 100644
--- a/modules/expo-background-notification-handler/android/src/main/java/expo/modules/backgroundnotificationhandler/BackgroundNotificationHandler.kt
+++ b/modules/expo-background-notification-handler/android/src/main/java/expo/modules/backgroundnotificationhandler/BackgroundNotificationHandler.kt
@@ -5,7 +5,7 @@ import com.google.firebase.messaging.RemoteMessage
class BackgroundNotificationHandler(
private val context: Context,
- private val notifInterface: BackgroundNotificationHandlerInterface
+ private val notifInterface: BackgroundNotificationHandlerInterface,
) {
fun handleMessage(remoteMessage: RemoteMessage) {
if (ExpoBackgroundNotificationHandlerModule.isForegrounded) {
@@ -35,5 +35,8 @@ class BackgroundNotificationHandler(
remoteMessage.data["sound"] = null
}
}
+
+ // TODO - Remove this once we have more backend capability
+ remoteMessage.data["badge"] = null
}
}
diff --git a/modules/expo-background-notification-handler/android/src/main/java/expo/modules/backgroundnotificationhandler/ExpoBackgroundNotificationHandlerModule.kt b/modules/expo-background-notification-handler/android/src/main/java/expo/modules/backgroundnotificationhandler/ExpoBackgroundNotificationHandlerModule.kt
index 083ff1223c..0cc5fa6ab1 100644
--- a/modules/expo-background-notification-handler/android/src/main/java/expo/modules/backgroundnotificationhandler/ExpoBackgroundNotificationHandlerModule.kt
+++ b/modules/expo-background-notification-handler/android/src/main/java/expo/modules/backgroundnotificationhandler/ExpoBackgroundNotificationHandlerModule.kt
@@ -8,63 +8,68 @@ class ExpoBackgroundNotificationHandlerModule : Module() {
var isForegrounded = false
}
- override fun definition() = ModuleDefinition {
- Name("ExpoBackgroundNotificationHandler")
+ override fun definition() =
+ ModuleDefinition {
+ Name("ExpoBackgroundNotificationHandler")
- OnCreate {
- NotificationPrefs(appContext.reactContext).initialize()
- }
+ OnCreate {
+ NotificationPrefs(appContext.reactContext).initialize()
+ }
- OnActivityEntersForeground {
- isForegrounded = true
- }
+ OnActivityEntersForeground {
+ isForegrounded = true
+ }
- OnActivityEntersBackground {
- isForegrounded = false
- }
+ OnActivityEntersBackground {
+ isForegrounded = false
+ }
- AsyncFunction("getAllPrefsAsync") {
- return@AsyncFunction NotificationPrefs(appContext.reactContext).getAllPrefs()
- }
+ AsyncFunction("getAllPrefsAsync") {
+ return@AsyncFunction NotificationPrefs(appContext.reactContext).getAllPrefs()
+ }
- AsyncFunction("getBoolAsync") { forKey: String ->
- return@AsyncFunction NotificationPrefs(appContext.reactContext).getBoolean(forKey)
- }
+ AsyncFunction("getBoolAsync") { forKey: String ->
+ return@AsyncFunction NotificationPrefs(appContext.reactContext).getBoolean(forKey)
+ }
- AsyncFunction("getStringAsync") { forKey: String ->
- return@AsyncFunction NotificationPrefs(appContext.reactContext).getString(forKey)
- }
+ AsyncFunction("getStringAsync") { forKey: String ->
+ return@AsyncFunction NotificationPrefs(appContext.reactContext).getString(forKey)
+ }
- AsyncFunction("getStringArrayAsync") { forKey: String ->
- return@AsyncFunction NotificationPrefs(appContext.reactContext).getStringArray(forKey)
- }
+ AsyncFunction("getStringArrayAsync") { forKey: String ->
+ return@AsyncFunction NotificationPrefs(appContext.reactContext).getStringArray(forKey)
+ }
- AsyncFunction("setBoolAsync") { forKey: String, value: Boolean ->
- NotificationPrefs(appContext.reactContext).setBoolean(forKey, value)
- }
+ AsyncFunction("setBoolAsync") { forKey: String, value: Boolean ->
+ NotificationPrefs(appContext.reactContext).setBoolean(forKey, value)
+ }
- AsyncFunction("setStringAsync") { forKey: String, value: String ->
- NotificationPrefs(appContext.reactContext).setString(forKey, value)
- }
+ AsyncFunction("setStringAsync") { forKey: String, value: String ->
+ NotificationPrefs(appContext.reactContext).setString(forKey, value)
+ }
- AsyncFunction("setStringArrayAsync") { forKey: String, value: Array ->
- NotificationPrefs(appContext.reactContext).setStringArray(forKey, value)
- }
+ AsyncFunction("setStringArrayAsync") { forKey: String, value: Array ->
+ NotificationPrefs(appContext.reactContext).setStringArray(forKey, value)
+ }
- AsyncFunction("addToStringArrayAsync") { forKey: String, string: String ->
- NotificationPrefs(appContext.reactContext).addToStringArray(forKey, string)
- }
+ AsyncFunction("addToStringArrayAsync") { forKey: String, string: String ->
+ NotificationPrefs(appContext.reactContext).addToStringArray(forKey, string)
+ }
- AsyncFunction("removeFromStringArrayAsync") { forKey: String, string: String ->
- NotificationPrefs(appContext.reactContext).removeFromStringArray(forKey, string)
- }
+ AsyncFunction("removeFromStringArrayAsync") { forKey: String, string: String ->
+ NotificationPrefs(appContext.reactContext).removeFromStringArray(forKey, string)
+ }
- AsyncFunction("addManyToStringArrayAsync") { forKey: String, strings: Array ->
- NotificationPrefs(appContext.reactContext).addManyToStringArray(forKey, strings)
- }
+ AsyncFunction("addManyToStringArrayAsync") { forKey: String, strings: Array ->
+ NotificationPrefs(appContext.reactContext).addManyToStringArray(forKey, strings)
+ }
- AsyncFunction("removeManyFromStringArrayAsync") { forKey: String, strings: Array ->
- NotificationPrefs(appContext.reactContext).removeManyFromStringArray(forKey, strings)
+ AsyncFunction("removeManyFromStringArrayAsync") { forKey: String, strings: Array ->
+ NotificationPrefs(appContext.reactContext).removeManyFromStringArray(forKey, strings)
+ }
+
+ AsyncFunction("setBadgeCountAsync") { _: Int ->
+ // This does nothing on Android
+ }
}
- }
}
diff --git a/modules/expo-background-notification-handler/android/src/main/java/expo/modules/backgroundnotificationhandler/NotificationPrefs.kt b/modules/expo-background-notification-handler/android/src/main/java/expo/modules/backgroundnotificationhandler/NotificationPrefs.kt
index 17ef9205ef..97b05b4d73 100644
--- a/modules/expo-background-notification-handler/android/src/main/java/expo/modules/backgroundnotificationhandler/NotificationPrefs.kt
+++ b/modules/expo-background-notification-handler/android/src/main/java/expo/modules/backgroundnotificationhandler/NotificationPrefs.kt
@@ -2,20 +2,24 @@ package expo.modules.backgroundnotificationhandler
import android.content.Context
-val DEFAULTS = mapOf(
- "playSoundChat" to true,
- "playSoundFollow" to false,
- "playSoundLike" to false,
- "playSoundMention" to false,
- "playSoundQuote" to false,
- "playSoundReply" to false,
- "playSoundRepost" to false,
- "mutedThreads" to mapOf>()
-)
+val DEFAULTS =
+ mapOf(
+ "playSoundChat" to true,
+ "playSoundFollow" to false,
+ "playSoundLike" to false,
+ "playSoundMention" to false,
+ "playSoundQuote" to false,
+ "playSoundReply" to false,
+ "playSoundRepost" to false,
+ "mutedThreads" to mapOf>(),
+ )
-class NotificationPrefs (private val context: Context?) {
- private val prefs = context?.getSharedPreferences("xyz.blueskyweb.app", Context.MODE_PRIVATE)
- ?: throw Error("Context is null")
+class NotificationPrefs(
+ private val context: Context?,
+) {
+ private val prefs =
+ context?.getSharedPreferences("xyz.blueskyweb.app", Context.MODE_PRIVATE)
+ ?: throw Error("Context is null")
fun initialize() {
prefs
@@ -41,94 +45,99 @@ class NotificationPrefs (private val context: Context?) {
}
}
}
- }
- .apply()
+ }.apply()
}
- fun getAllPrefs(): MutableMap {
- return prefs.all
- }
+ fun getAllPrefs(): MutableMap = prefs.all
- fun getBoolean(key: String): Boolean {
- return prefs.getBoolean(key, false)
- }
+ fun getBoolean(key: String): Boolean = prefs.getBoolean(key, false)
- fun getString(key: String): String? {
- return prefs.getString(key, null)
- }
+ fun getString(key: String): String? = prefs.getString(key, null)
- fun getStringArray(key: String): Array? {
- return prefs.getStringSet(key, null)?.toTypedArray()
- }
+ fun getStringArray(key: String): Array? = prefs.getStringSet(key, null)?.toTypedArray()
- fun setBoolean(key: String, value: Boolean) {
+ fun setBoolean(
+ key: String,
+ value: Boolean,
+ ) {
prefs
.edit()
.apply {
putBoolean(key, value)
- }
- .apply()
+ }.apply()
}
- fun setString(key: String, value: String) {
+ fun setString(
+ key: String,
+ value: String,
+ ) {
prefs
.edit()
.apply {
putString(key, value)
- }
- .apply()
+ }.apply()
}
- fun setStringArray(key: String, value: Array) {
+ fun setStringArray(
+ key: String,
+ value: Array,
+ ) {
prefs
.edit()
.apply {
putStringSet(key, value.toSet())
- }
- .apply()
+ }.apply()
}
- fun addToStringArray(key: String, string: String) {
+ fun addToStringArray(
+ key: String,
+ string: String,
+ ) {
prefs
.edit()
.apply {
val set = prefs.getStringSet(key, null)?.toMutableSet() ?: mutableSetOf()
set.add(string)
putStringSet(key, set)
- }
- .apply()
+ }.apply()
}
- fun removeFromStringArray(key: String, string: String) {
+ fun removeFromStringArray(
+ key: String,
+ string: String,
+ ) {
prefs
.edit()
.apply {
val set = prefs.getStringSet(key, null)?.toMutableSet() ?: mutableSetOf()
set.remove(string)
putStringSet(key, set)
- }
- .apply()
+ }.apply()
}
- fun addManyToStringArray(key: String, strings: Array) {
+ fun addManyToStringArray(
+ key: String,
+ strings: Array,
+ ) {
prefs
.edit()
.apply {
val set = prefs.getStringSet(key, null)?.toMutableSet() ?: mutableSetOf()
set.addAll(strings.toSet())
putStringSet(key, set)
- }
- .apply()
+ }.apply()
}
- fun removeManyFromStringArray(key: String, strings: Array) {
+ fun removeManyFromStringArray(
+ key: String,
+ strings: Array,
+ ) {
prefs
.edit()
.apply {
val set = prefs.getStringSet(key, null)?.toMutableSet() ?: mutableSetOf()
set.removeAll(strings.toSet())
putStringSet(key, set)
- }
- .apply()
+ }.apply()
}
-}
\ No newline at end of file
+}
diff --git a/modules/expo-background-notification-handler/ios/ExpoBackgroundNotificationHandlerModule.swift b/modules/expo-background-notification-handler/ios/ExpoBackgroundNotificationHandlerModule.swift
index 08972a04c5..3845fe765c 100644
--- a/modules/expo-background-notification-handler/ios/ExpoBackgroundNotificationHandlerModule.swift
+++ b/modules/expo-background-notification-handler/ios/ExpoBackgroundNotificationHandlerModule.swift
@@ -2,15 +2,16 @@ import ExpoModulesCore
let APP_GROUP = "group.app.bsky"
-let DEFAULTS: [String:Any] = [
- "playSoundChat" : true,
+let DEFAULTS: [String: Any] = [
+ "playSoundChat": true,
"playSoundFollow": false,
"playSoundLike": false,
"playSoundMention": false,
"playSoundQuote": false,
"playSoundReply": false,
"playSoundRepost": false,
- "mutedThreads": [:] as! [String:[String]]
+ "mutedThreads": [:] as! [String: [String]],
+ "badgeCount": 0
]
/*
@@ -22,10 +23,10 @@ let DEFAULTS: [String:Any] = [
*/
public class ExpoBackgroundNotificationHandlerModule: Module {
let userDefaults = UserDefaults(suiteName: APP_GROUP)
-
+
public func definition() -> ModuleDefinition {
Name("ExpoBackgroundNotificationHandler")
-
+
OnCreate {
DEFAULTS.forEach { p in
if userDefaults?.value(forKey: p.key) == nil {
@@ -33,57 +34,56 @@ public class ExpoBackgroundNotificationHandlerModule: Module {
}
}
}
-
- AsyncFunction("getAllPrefsAsync") { () -> [String:Any]? in
+
+ AsyncFunction("getAllPrefsAsync") { () -> [String: Any]? in
var keys: [String] = []
DEFAULTS.forEach { p in
keys.append(p.key)
}
return userDefaults?.dictionaryWithValues(forKeys: keys)
}
-
+
AsyncFunction("getBoolAsync") { (forKey: String) -> Bool in
if let pref = userDefaults?.bool(forKey: forKey) {
return pref
}
return false
}
-
+
AsyncFunction("getStringAsync") { (forKey: String) -> String? in
if let pref = userDefaults?.string(forKey: forKey) {
return pref
}
return nil
}
-
+
AsyncFunction("getStringArrayAsync") { (forKey: String) -> [String]? in
if let pref = userDefaults?.stringArray(forKey: forKey) {
return pref
}
return nil
}
-
- AsyncFunction("setBoolAsync") { (forKey: String, value: Bool) -> Void in
+
+ AsyncFunction("setBoolAsync") { (forKey: String, value: Bool) in
userDefaults?.setValue(value, forKey: forKey)
}
-
- AsyncFunction("setStringAsync") { (forKey: String, value: String) -> Void in
+
+ AsyncFunction("setStringAsync") { (forKey: String, value: String) in
userDefaults?.setValue(value, forKey: forKey)
}
-
- AsyncFunction("setStringArrayAsync") { (forKey: String, value: [String]) -> Void in
+
+ AsyncFunction("setStringArrayAsync") { (forKey: String, value: [String]) in
userDefaults?.setValue(value, forKey: forKey)
}
-
+
AsyncFunction("addToStringArrayAsync") { (forKey: String, string: String) in
if var curr = userDefaults?.stringArray(forKey: forKey),
- !curr.contains(string)
- {
+ !curr.contains(string) {
curr.append(string)
userDefaults?.setValue(curr, forKey: forKey)
}
}
-
+
AsyncFunction("removeFromStringArrayAsync") { (forKey: String, string: String) in
if var curr = userDefaults?.stringArray(forKey: forKey) {
curr.removeAll { s in
@@ -92,7 +92,7 @@ public class ExpoBackgroundNotificationHandlerModule: Module {
userDefaults?.setValue(curr, forKey: forKey)
}
}
-
+
AsyncFunction("addManyToStringArrayAsync") { (forKey: String, strings: [String]) in
if var curr = userDefaults?.stringArray(forKey: forKey) {
strings.forEach { s in
@@ -103,7 +103,7 @@ public class ExpoBackgroundNotificationHandlerModule: Module {
userDefaults?.setValue(curr, forKey: forKey)
}
}
-
+
AsyncFunction("removeManyFromStringArrayAsync") { (forKey: String, strings: [String]) in
if var curr = userDefaults?.stringArray(forKey: forKey) {
strings.forEach { s in
@@ -112,5 +112,9 @@ public class ExpoBackgroundNotificationHandlerModule: Module {
userDefaults?.setValue(curr, forKey: forKey)
}
}
+
+ AsyncFunction("setBadgeCountAsync") { (count: Int) in
+ userDefaults?.setValue(count, forKey: "badgeCount")
+ }
}
}
diff --git a/modules/expo-background-notification-handler/src/ExpoBackgroundNotificationHandler.types.ts b/modules/expo-background-notification-handler/src/ExpoBackgroundNotificationHandler.types.ts
index 5fbd302da9..b74148db48 100644
--- a/modules/expo-background-notification-handler/src/ExpoBackgroundNotificationHandler.types.ts
+++ b/modules/expo-background-notification-handler/src/ExpoBackgroundNotificationHandler.types.ts
@@ -31,6 +31,7 @@ export type ExpoBackgroundNotificationHandlerModule = {
forKey: keyof BackgroundNotificationHandlerPreferences,
value: string[],
) => Promise
+ setBadgeCountAsync: (count: number) => Promise
}
// TODO there are more preferences in the native code, however they have not been added here yet.
diff --git a/modules/expo-background-notification-handler/src/ExpoBackgroundNotificationHandlerModule.web.ts b/modules/expo-background-notification-handler/src/ExpoBackgroundNotificationHandlerModule.web.ts
index 29e27fd0fa..893548e183 100644
--- a/modules/expo-background-notification-handler/src/ExpoBackgroundNotificationHandlerModule.web.ts
+++ b/modules/expo-background-notification-handler/src/ExpoBackgroundNotificationHandlerModule.web.ts
@@ -24,4 +24,5 @@ export const BackgroundNotificationHandler = {
removeFromStringArrayAsync: async (_: string, __: string) => {},
addManyToStringArrayAsync: async (_: string, __: string[]) => {},
removeManyFromStringArrayAsync: async (_: string, __: string[]) => {},
+ setBadgeCountAsync: async (_: number) => {},
} as ExpoBackgroundNotificationHandlerModule
diff --git a/modules/expo-bluesky-gif-view/android/src/main/java/expo/modules/blueskygifview/AppCompatImageViewExtended.kt b/modules/expo-bluesky-gif-view/android/src/main/java/expo/modules/blueskygifview/AppCompatImageViewExtended.kt
index 5d20848453..5ba5ccf71a 100644
--- a/modules/expo-bluesky-gif-view/android/src/main/java/expo/modules/blueskygifview/AppCompatImageViewExtended.kt
+++ b/modules/expo-bluesky-gif-view/android/src/main/java/expo/modules/blueskygifview/AppCompatImageViewExtended.kt
@@ -5,7 +5,10 @@ import android.graphics.Canvas
import android.graphics.drawable.Animatable
import androidx.appcompat.widget.AppCompatImageView
-class AppCompatImageViewExtended(context: Context, private val parent: GifView): AppCompatImageView(context) {
+class AppCompatImageViewExtended(
+ context: Context,
+ private val parent: GifView,
+) : AppCompatImageView(context) {
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
@@ -34,4 +37,4 @@ class AppCompatImageViewExtended(context: Context, private val parent: GifView):
drawable.start()
}
}
-}
\ No newline at end of file
+}
diff --git a/modules/expo-bluesky-gif-view/android/src/main/java/expo/modules/blueskygifview/ExpoBlueskyGifViewModule.kt b/modules/expo-bluesky-gif-view/android/src/main/java/expo/modules/blueskygifview/ExpoBlueskyGifViewModule.kt
index 625e1d45f9..b0b3e8eeb7 100644
--- a/modules/expo-bluesky-gif-view/android/src/main/java/expo/modules/blueskygifview/ExpoBlueskyGifViewModule.kt
+++ b/modules/expo-bluesky-gif-view/android/src/main/java/expo/modules/blueskygifview/ExpoBlueskyGifViewModule.kt
@@ -6,49 +6,50 @@ import expo.modules.kotlin.modules.Module
import expo.modules.kotlin.modules.ModuleDefinition
class ExpoBlueskyGifViewModule : Module() {
- override fun definition() = ModuleDefinition {
- Name("ExpoBlueskyGifView")
+ override fun definition() =
+ ModuleDefinition {
+ Name("ExpoBlueskyGifView")
- AsyncFunction("prefetchAsync") { sources: List ->
- val activity = appContext.currentActivity ?: return@AsyncFunction
- val glide = Glide.with(activity)
+ AsyncFunction("prefetchAsync") { sources: List ->
+ val activity = appContext.currentActivity ?: return@AsyncFunction
+ val glide = Glide.with(activity)
- sources.forEach { source ->
- glide
- .download(source)
- .diskCacheStrategy(DiskCacheStrategy.DATA)
- .submit()
+ sources.forEach { source ->
+ glide
+ .download(source)
+ .diskCacheStrategy(DiskCacheStrategy.DATA)
+ .submit()
+ }
+ }
+
+ View(GifView::class) {
+ Events(
+ "onPlayerStateChange",
+ )
+
+ Prop("source") { view: GifView, source: String ->
+ view.source = source
+ }
+
+ Prop("placeholderSource") { view: GifView, source: String ->
+ view.placeholderSource = source
+ }
+
+ Prop("autoplay") { view: GifView, autoplay: Boolean ->
+ view.autoplay = autoplay
+ }
+
+ AsyncFunction("playAsync") { view: GifView ->
+ view.play()
+ }
+
+ AsyncFunction("pauseAsync") { view: GifView ->
+ view.pause()
+ }
+
+ AsyncFunction("toggleAsync") { view: GifView ->
+ view.toggle()
+ }
}
}
-
- View(GifView::class) {
- Events(
- "onPlayerStateChange"
- )
-
- Prop("source") { view: GifView, source: String ->
- view.source = source
- }
-
- Prop("placeholderSource") { view: GifView, source: String ->
- view.placeholderSource = source
- }
-
- Prop("autoplay") { view: GifView, autoplay: Boolean ->
- view.autoplay = autoplay
- }
-
- AsyncFunction("playAsync") { view: GifView ->
- view.play()
- }
-
- AsyncFunction("pauseAsync") { view: GifView ->
- view.pause()
- }
-
- AsyncFunction("toggleAsync") { view: GifView ->
- view.toggle()
- }
- }
- }
}
diff --git a/modules/expo-bluesky-gif-view/android/src/main/java/expo/modules/blueskygifview/GifView.kt b/modules/expo-bluesky-gif-view/android/src/main/java/expo/modules/blueskygifview/GifView.kt
index be5830df7a..5e467a1603 100644
--- a/modules/expo-bluesky-gif-view/android/src/main/java/expo/modules/blueskygifview/GifView.kt
+++ b/modules/expo-bluesky-gif-view/android/src/main/java/expo/modules/blueskygifview/GifView.kt
@@ -1,6 +1,5 @@
package expo.modules.blueskygifview
-
import android.content.Context
import android.graphics.Color
import android.graphics.drawable.Animatable
@@ -15,7 +14,10 @@ import expo.modules.kotlin.exception.Exceptions
import expo.modules.kotlin.viewevent.EventDispatcher
import expo.modules.kotlin.views.ExpoView
-class GifView(context: Context, appContext: AppContext) : ExpoView(context, appContext) {
+class GifView(
+ context: Context,
+ appContext: AppContext,
+) : ExpoView(context, appContext) {
// Events
private val onPlayerStateChange by EventDispatcher()
@@ -44,8 +46,7 @@ class GifView(context: Context, appContext: AppContext) : ExpoView(context, appC
}
}
-
- //
+ //
init {
this.setBackgroundColor(Color.TRANSPARENT)
@@ -70,80 +71,82 @@ class GifView(context: Context, appContext: AppContext) : ExpoView(context, appC
super.onDetachedFromWindow()
}
- //
+ //
- //
+ //
private fun load() {
if (placeholderSource == null || source == null) {
return
}
- this.webpRequest = glide.load(source)
- .diskCacheStrategy(DiskCacheStrategy.DATA)
- .skipMemoryCache(false)
- .listener(object: RequestListener {
- override fun onResourceReady(
- resource: Drawable?,
- model: Any?,
- target: Target?,
- dataSource: com.bumptech.glide.load.DataSource?,
- isFirstResource: Boolean
- ): Boolean {
- if (placeholderRequest != null) {
- glide.clear(placeholderRequest)
- }
- return false
- }
+ this.webpRequest =
+ glide
+ .load(source)
+ .diskCacheStrategy(DiskCacheStrategy.DATA)
+ .skipMemoryCache(false)
+ .listener(
+ object : RequestListener {
+ override fun onResourceReady(
+ resource: Drawable?,
+ model: Any?,
+ target: Target?,
+ dataSource: com.bumptech.glide.load.DataSource?,
+ isFirstResource: Boolean,
+ ): Boolean {
+ if (placeholderRequest != null) {
+ glide.clear(placeholderRequest)
+ }
+ return false
+ }
- override fun onLoadFailed(
- e: GlideException?,
- model: Any?,
- target: Target?,
- isFirstResource: Boolean
- ): Boolean {
- return true
- }
- })
- .into(this.imageView)
+ override fun onLoadFailed(
+ e: GlideException?,
+ model: Any?,
+ target: Target?,
+ isFirstResource: Boolean,
+ ): Boolean = true
+ },
+ ).into(this.imageView)
if (this.imageView.drawable == null || this.imageView.drawable !is Animatable) {
- this.placeholderRequest = glide.load(placeholderSource)
- .diskCacheStrategy(DiskCacheStrategy.DATA)
- // Let's not bloat the memory cache with placeholders
- .skipMemoryCache(true)
- .listener(object: RequestListener {
- override fun onResourceReady(
- resource: Drawable?,
- model: Any?,
- target: Target?,
- dataSource: com.bumptech.glide.load.DataSource?,
- isFirstResource: Boolean
- ): Boolean {
- // Incase this request finishes after the webp, let's just not set
- // the drawable. This shouldn't happen because the request should get cancelled
- if (imageView.drawable == null) {
- imageView.setImageDrawable(resource)
- }
- return true
- }
+ this.placeholderRequest =
+ glide
+ .load(placeholderSource)
+ .diskCacheStrategy(DiskCacheStrategy.DATA)
+ // Let's not bloat the memory cache with placeholders
+ .skipMemoryCache(true)
+ .listener(
+ object : RequestListener {
+ override fun onResourceReady(
+ resource: Drawable?,
+ model: Any?,
+ target: Target?,
+ dataSource: com.bumptech.glide.load.DataSource?,
+ isFirstResource: Boolean,
+ ): Boolean {
+ // Incase this request finishes after the webp, let's just not set
+ // the drawable. This shouldn't happen because the request should get cancelled
+ if (imageView.drawable == null) {
+ imageView.setImageDrawable(resource)
+ }
+ return true
+ }
- override fun onLoadFailed(
- e: GlideException?,
- model: Any?,
- target: Target?,
- isFirstResource: Boolean
- ): Boolean {
- return true
- }
- })
- .submit()
+ override fun onLoadFailed(
+ e: GlideException?,
+ model: Any?,
+ target: Target?,
+ isFirstResource: Boolean,
+ ): Boolean = true
+ },
+ ).submit()
}
}
- //
+ //
- //
+ //
fun play() {
this.imageView.play()
@@ -165,16 +168,18 @@ class GifView(context: Context, appContext: AppContext) : ExpoView(context, appC
}
}
- //
+ //
- //
+ //
fun firePlayerStateChange() {
- onPlayerStateChange(mapOf(
- "isPlaying" to this.isPlaying,
- "isLoaded" to this.isLoaded,
- ))
+ onPlayerStateChange(
+ mapOf(
+ "isPlaying" to this.isPlaying,
+ "isLoaded" to this.isLoaded,
+ ),
+ )
}
- //
+ //
}
diff --git a/modules/expo-bluesky-gif-view/ios/ExpoBlueskyGifView.podspec b/modules/expo-bluesky-gif-view/ios/ExpoBlueskyGifView.podspec
index ddd0877b24..352812640b 100644
--- a/modules/expo-bluesky-gif-view/ios/ExpoBlueskyGifView.podspec
+++ b/modules/expo-bluesky-gif-view/ios/ExpoBlueskyGifView.podspec
@@ -10,8 +10,8 @@ Pod::Spec.new do |s|
s.static_framework = true
s.dependency 'ExpoModulesCore'
- s.dependency 'SDWebImage', '~> 5.17.0'
- s.dependency 'SDWebImageWebPCoder', '~> 0.13.0'
+ s.dependency 'SDWebImage', '~> 5.19.1'
+ s.dependency 'SDWebImageWebPCoder', '~> 0.14.6'
# Swift/Objective-C compatibility
s.pod_target_xcconfig = {
diff --git a/modules/expo-bluesky-gif-view/ios/ExpoBlueskyGifViewModule.swift b/modules/expo-bluesky-gif-view/ios/ExpoBlueskyGifViewModule.swift
index 7c7132290d..9156bd1990 100644
--- a/modules/expo-bluesky-gif-view/ios/ExpoBlueskyGifViewModule.swift
+++ b/modules/expo-bluesky-gif-view/ios/ExpoBlueskyGifViewModule.swift
@@ -5,11 +5,11 @@ import SDWebImageWebPCoder
public class ExpoBlueskyGifViewModule: Module {
public func definition() -> ModuleDefinition {
Name("ExpoBlueskyGifView")
-
+
OnCreate {
SDImageCodersManager.shared.addCoder(SDImageGIFCoder.shared)
}
-
+
AsyncFunction("prefetchAsync") { (sources: [URL]) in
SDWebImagePrefetcher.shared.prefetchURLs(sources, context: Util.createContext(), progress: nil)
}
@@ -18,27 +18,27 @@ public class ExpoBlueskyGifViewModule: Module {
Events(
"onPlayerStateChange"
)
-
+
Prop("source") { (view: GifView, prop: String) in
view.source = prop
}
-
+
Prop("placeholderSource") { (view: GifView, prop: String) in
view.placeholderSource = prop
}
-
+
Prop("autoplay") { (view: GifView, prop: Bool) in
view.autoplay = prop
}
-
+
AsyncFunction("toggleAsync") { (view: GifView) in
view.toggle()
}
-
+
AsyncFunction("playAsync") { (view: GifView) in
view.play()
}
-
+
AsyncFunction("pauseAsync") { (view: GifView) in
view.pause()
}
diff --git a/modules/expo-bluesky-gif-view/ios/GifView.swift b/modules/expo-bluesky-gif-view/ios/GifView.swift
index de722d7a63..b42a4735c6 100644
--- a/modules/expo-bluesky-gif-view/ios/GifView.swift
+++ b/modules/expo-bluesky-gif-view/ios/GifView.swift
@@ -16,14 +16,14 @@ public class GifView: ExpoView, AVPlayerViewControllerDelegate {
)
private var isPlaying = true
private var isLoaded = false
-
+
// Requests
private var webpOperation: SDWebImageCombinedOperation?
private var placeholderOperation: SDWebImageCombinedOperation?
// Props
- var source: String? = nil
- var placeholderSource: String? = nil
+ var source: String?
+ var placeholderSource: String?
var autoplay = true {
didSet {
if !autoplay {
@@ -78,8 +78,7 @@ public class GifView: ExpoView, AVPlayerViewControllerDelegate {
// See:
// https://github.com/SDWebImage/SDWebImage/blob/master/Docs/HowToUse.md#using-asynchronous-image-caching-independently
if !SDImageCache.shared.diskImageDataExists(withKey: source),
- let url = URL(string: placeholderSource)
- {
+ let url = URL(string: placeholderSource) {
self.placeholderOperation = imageManager.loadImage(
with: url,
options: [.retryFailed],
@@ -132,8 +131,7 @@ public class GifView: ExpoView, AVPlayerViewControllerDelegate {
if let placeholderSource = self.placeholderSource,
imageUrl?.absoluteString == placeholderSource,
self.imageView.image == nil,
- let image = image
- {
+ let image = image {
self.setImage(image)
return
}
@@ -142,8 +140,7 @@ public class GifView: ExpoView, AVPlayerViewControllerDelegate {
imageUrl?.absoluteString == source,
// UIImage perf suckssss if the image is animated
let data = data,
- let animatedImage = SDAnimatedImage(data: data)
- {
+ let animatedImage = SDAnimatedImage(data: data) {
self.placeholderOperation?.cancel()
self.isPlaying = self.autoplay
self.isLoaded = true
diff --git a/modules/expo-bluesky-swiss-army/android/build.gradle b/modules/expo-bluesky-swiss-army/android/build.gradle
new file mode 100644
index 0000000000..b031cde57e
--- /dev/null
+++ b/modules/expo-bluesky-swiss-army/android/build.gradle
@@ -0,0 +1,47 @@
+apply plugin: 'com.android.library'
+
+group = 'expo.modules.blueskyswissarmy'
+version = '0.6.0'
+
+def expoModulesCorePlugin = new File(project(":expo-modules-core").projectDir.absolutePath, "ExpoModulesCorePlugin.gradle")
+apply from: expoModulesCorePlugin
+applyKotlinExpoModulesCorePlugin()
+useCoreDependencies()
+useExpoPublishing()
+
+// If you want to use the managed Android SDK versions from expo-modules-core, set this to true.
+// The Android SDK versions will be bumped from time to time in SDK releases and may introduce breaking changes in your module code.
+// Most of the time, you may like to manage the Android SDK versions yourself.
+def useManagedAndroidSdkVersions = false
+if (useManagedAndroidSdkVersions) {
+ useDefaultAndroidSdkVersions()
+} else {
+ buildscript {
+ // Simple helper that allows the root project to override versions declared by this library.
+ ext.safeExtGet = { prop, fallback ->
+ rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
+ }
+ }
+ project.android {
+ compileSdkVersion safeExtGet("compileSdkVersion", 34)
+ defaultConfig {
+ minSdkVersion safeExtGet("minSdkVersion", 21)
+ targetSdkVersion safeExtGet("targetSdkVersion", 34)
+ }
+ }
+}
+
+android {
+ namespace "expo.modules.blueskyswissarmy"
+ defaultConfig {
+ versionCode 1
+ versionName "0.6.0"
+ }
+ lintOptions {
+ abortOnError false
+ }
+}
+
+dependencies {
+ implementation("com.android.installreferrer:installreferrer:2.2")
+}
diff --git a/modules/expo-bluesky-swiss-army/android/src/main/AndroidManifest.xml b/modules/expo-bluesky-swiss-army/android/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..bdae66c8f5
--- /dev/null
+++ b/modules/expo-bluesky-swiss-army/android/src/main/AndroidManifest.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/modules/expo-bluesky-swiss-army/android/src/main/java/expo/modules/blueskyswissarmy/hlsdownload/ExpoHLSDownloadModule.kt b/modules/expo-bluesky-swiss-army/android/src/main/java/expo/modules/blueskyswissarmy/hlsdownload/ExpoHLSDownloadModule.kt
new file mode 100644
index 0000000000..786b84e41c
--- /dev/null
+++ b/modules/expo-bluesky-swiss-army/android/src/main/java/expo/modules/blueskyswissarmy/hlsdownload/ExpoHLSDownloadModule.kt
@@ -0,0 +1,35 @@
+package expo.modules.blueskyswissarmy.hlsdownload
+
+import android.net.Uri
+import expo.modules.kotlin.modules.Module
+import expo.modules.kotlin.modules.ModuleDefinition
+
+class ExpoHLSDownloadModule : Module() {
+ override fun definition() =
+ ModuleDefinition {
+ Name("ExpoHLSDownload")
+
+ Function("isAvailable") {
+ return@Function true
+ }
+
+ View(HLSDownloadView::class) {
+ Events(
+ arrayOf(
+ "onStart",
+ "onError",
+ "onProgress",
+ "onSuccess",
+ ),
+ )
+
+ Prop("downloaderUrl") { view: HLSDownloadView, downloaderUrl: Uri ->
+ view.downloaderUrl = downloaderUrl
+ }
+
+ AsyncFunction("startDownloadAsync") { view: HLSDownloadView, sourceUrl: Uri ->
+ view.startDownload(sourceUrl)
+ }
+ }
+ }
+}
diff --git a/modules/expo-bluesky-swiss-army/android/src/main/java/expo/modules/blueskyswissarmy/hlsdownload/HLSDownloadView.kt b/modules/expo-bluesky-swiss-army/android/src/main/java/expo/modules/blueskyswissarmy/hlsdownload/HLSDownloadView.kt
new file mode 100644
index 0000000000..5f3082a819
--- /dev/null
+++ b/modules/expo-bluesky-swiss-army/android/src/main/java/expo/modules/blueskyswissarmy/hlsdownload/HLSDownloadView.kt
@@ -0,0 +1,141 @@
+package expo.modules.blueskyswissarmy.hlsdownload
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.net.Uri
+import android.util.Base64
+import android.util.Log
+import android.webkit.DownloadListener
+import android.webkit.JavascriptInterface
+import android.webkit.WebView
+import expo.modules.kotlin.AppContext
+import expo.modules.kotlin.viewevent.EventDispatcher
+import expo.modules.kotlin.viewevent.ViewEventCallback
+import expo.modules.kotlin.views.ExpoView
+import org.json.JSONObject
+import java.io.File
+import java.io.FileOutputStream
+import java.net.URI
+import java.util.UUID
+
+class HLSDownloadView(
+ context: Context,
+ appContext: AppContext,
+) : ExpoView(context, appContext),
+ DownloadListener {
+ private val webView = WebView(context)
+
+ var downloaderUrl: Uri? = null
+
+ private val onStart by EventDispatcher()
+ private val onError by EventDispatcher()
+ private val onProgress by EventDispatcher()
+ private val onSuccess by EventDispatcher()
+
+ init {
+ this.setupWebView()
+ this.addView(this.webView, LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT))
+ }
+
+ @SuppressLint("SetJavaScriptEnabled")
+ private fun setupWebView() {
+ val webSettings = this.webView.settings
+ webSettings.javaScriptEnabled = true
+ webSettings.domStorageEnabled = true
+
+ webView.setDownloadListener(this)
+ webView.addJavascriptInterface(WebAppInterface(this.onProgress, this.onError), "AndroidInterface")
+ }
+
+ override fun onDetachedFromWindow() {
+ super.onDetachedFromWindow()
+ this.webView.stopLoading()
+ this.webView.clearHistory()
+ this.webView.removeAllViews()
+ this.webView.destroy()
+ }
+
+ fun startDownload(sourceUrl: Uri) {
+ if (this.downloaderUrl == null) {
+ this.onError(mapOf(ERROR_KEY to "Downloader URL is not set."))
+ return
+ }
+
+ val url = URI("${this.downloaderUrl}?videoUrl=$sourceUrl")
+ this.webView.loadUrl(url.toString())
+ this.onStart(mapOf())
+ }
+
+ override fun onDownloadStart(
+ url: String?,
+ userAgent: String?,
+ contentDisposition: String?,
+ mimeType: String?,
+ contentLength: Long,
+ ) {
+ if (url == null) {
+ this.onError(mapOf(ERROR_KEY to "Failed to retrieve download URL from webview."))
+ return
+ }
+
+ val tempDir = context.cacheDir
+ val fileName = "${UUID.randomUUID()}.mp4"
+ val file = File(tempDir, fileName)
+
+ val base64 = url.split(",")[1]
+ val bytes = Base64.decode(base64, Base64.DEFAULT)
+
+ val fos = FileOutputStream(file)
+ try {
+ fos.write(bytes)
+ } catch (e: Exception) {
+ Log.e("FileDownload", "Error downloading file", e)
+ this.onError(mapOf(ERROR_KEY to e.message.toString()))
+ return
+ } finally {
+ fos.close()
+ }
+
+ val uri = Uri.fromFile(file)
+ this.onSuccess(mapOf("uri" to uri.toString()))
+ }
+
+ companion object {
+ const val ERROR_KEY = "message"
+ }
+}
+
+public class WebAppInterface(
+ val onProgress: ViewEventCallback