Compare commits
24 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b06abba6b2 | |||
| fe1fdce3f7 | |||
| 6f8c233397 | |||
| 2ce5020814 | |||
| a36743ecc1 | |||
| 65fa002e67 | |||
| 96b5c6bf8d | |||
| 3db994ac52 | |||
| f796278329 | |||
| c81ad8efe4 | |||
| a8d62c402d | |||
| c0c844ed84 | |||
| 0b14d1110c | |||
| fb1ee87596 | |||
| 70e9ea9387 | |||
| e8f9886d15 | |||
| 468d32f184 | |||
| bc1573cf47 | |||
| 086256cd9a | |||
| 2daf20874d | |||
| 0a5d9f0452 | |||
| 014c78fa06 | |||
| 3957a6f68e | |||
| 5e8ee142b1 |
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"Potentially Dangerous Link": "Potentially Dangerous Link",
|
||||
"Blocked Link": "Blocked Link",
|
||||
"This link may be malicious. You should proceed at your own risk.": "This link may be malicious. You should proceed at your own risk.",
|
||||
"This link has been identified as malicious and has blocked for your safety.": "This link has been identified as malicious and has blocked for your safety.",
|
||||
"Continue Anyway": "Continue Anyway",
|
||||
"Return to Bluesky": "Return to Bluesky"
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"Potentially Dangerous Link": "Enlace Potencialmente Peligroso",
|
||||
"Blocked Link": "Enlace Bloqueado",
|
||||
"This link may be malicious. You should proceed at your own risk.": "Este enlace puede ser malicioso. Debes proceder bajo tu propio riesgo.",
|
||||
"This link has been identified as malicious and has blocked for your safety.": "Este enlace ha sido identificado como malicioso y ha sido bloqueado por tu seguridad.",
|
||||
"Continue Anyway": "Continuar de Todos Modos",
|
||||
"Return to Bluesky": "Regresar a Bluesky"
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"Potentially Dangerous Link": "Lien Potentiellement Dangereux",
|
||||
"Blocked Link": "Lien Bloqué",
|
||||
"This link may be malicious. You should proceed at your own risk.": "Ce lien peut être malveillant. Vous devriez procéder à vos propres risques.",
|
||||
"This link has been identified as malicious and has blocked for your safety.": "Ce lien a été identifié comme malveillant et a été bloqué pour votre sécurité.",
|
||||
"Continue Anyway": "Continuer Quand Même",
|
||||
"Return to Bluesky": "Retourner à Bluesky"
|
||||
}
|
||||
@@ -8,20 +8,23 @@
|
||||
"build": "tsc"
|
||||
},
|
||||
"dependencies": {
|
||||
"@atproto/common": "^0.4.0",
|
||||
"@atproto/common": "^0.4.11",
|
||||
"@types/escape-html": "^1.0.4",
|
||||
"body-parser": "^1.20.2",
|
||||
"cors": "^2.8.5",
|
||||
"escape-html": "^1.0.3",
|
||||
"express": "^4.19.2",
|
||||
"http-terminator": "^3.2.0",
|
||||
"i18n": "^0.15.1",
|
||||
"kysely": "^0.27.3",
|
||||
"pg": "^8.12.0",
|
||||
"pino": "^9.2.0",
|
||||
"uhtml": "^4.7.1",
|
||||
"uint8arrays": "^5.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/cors": "^2.8.17",
|
||||
"@types/i18n": "^0.13.12",
|
||||
"@types/pg": "^8.11.6",
|
||||
"typescript": "^5.4.5"
|
||||
}
|
||||
|
||||
+6
-1
@@ -1,5 +1,4 @@
|
||||
import {Database, envToCfg, httpLogger, LinkService, readEnv} from './index.js'
|
||||
|
||||
async function main() {
|
||||
const env = readEnv()
|
||||
const cfg = envToCfg(env)
|
||||
@@ -11,7 +10,13 @@ async function main() {
|
||||
await migrateDb.migrateToLatestOrThrow()
|
||||
await migrateDb.close()
|
||||
}
|
||||
|
||||
const link = await LinkService.create(cfg)
|
||||
|
||||
if (cfg.service.safelinkEnabled) {
|
||||
cfg.eventCache.adaptiveFetchAndUpdate()
|
||||
}
|
||||
|
||||
await link.start()
|
||||
httpLogger.info('link service is running')
|
||||
process.on('SIGTERM', async () => {
|
||||
|
||||
Vendored
+242
@@ -0,0 +1,242 @@
|
||||
import {ToolsOzoneSafelinkDefs} from '@atproto/api'
|
||||
|
||||
import {type ServiceConfig} from '../config.js'
|
||||
import {redirectLogger} from '../logger.js'
|
||||
import {OzoneAgent} from './ozoneAgent.js'
|
||||
let cacheCursor: string | undefined
|
||||
|
||||
export class EventCache {
|
||||
private rules = new Map<string, ToolsOzoneSafelinkDefs.Event>()
|
||||
private cfg: ServiceConfig
|
||||
private pollInterval = 1 * 1000 // start at 1 second
|
||||
|
||||
constructor(cfg: ServiceConfig) {
|
||||
this.cfg = cfg
|
||||
}
|
||||
|
||||
async getConfig(): Promise<ServiceConfig | undefined> {
|
||||
return this.cfg
|
||||
}
|
||||
|
||||
insert(key: string, evt: ToolsOzoneSafelinkDefs.Event) {
|
||||
const existing = this.rules.get(key)
|
||||
if (!existing || new Date(evt.createdAt) > new Date(existing.createdAt)) {
|
||||
this.rules.set(key, evt)
|
||||
}
|
||||
}
|
||||
|
||||
smartUpdateDomain(event: ToolsOzoneSafelinkDefs.Event) {
|
||||
let domain: string
|
||||
try {
|
||||
domain = new URL(event.url).hostname
|
||||
} catch (error) {
|
||||
redirectLogger.error(
|
||||
`[EventCache:smartUpdateDomain] Invalid URL: ${event.url}, error: ${error}`,
|
||||
)
|
||||
throw new Error(
|
||||
`[EventCache:smartUpdateDomain] Error parsing domain from URL: ${error}`,
|
||||
)
|
||||
}
|
||||
event.url = domain
|
||||
|
||||
try {
|
||||
redirectLogger.info(
|
||||
`[EventCache] smartUpdateDomain called for domain: ${domain}, action: ${event.action}`,
|
||||
)
|
||||
|
||||
if (event.action) {
|
||||
this.insert(domain, event)
|
||||
redirectLogger.info(
|
||||
`[EventCache] rule updated or inserted for: ${domain}`,
|
||||
)
|
||||
return
|
||||
}
|
||||
} catch (error) {
|
||||
redirectLogger.error(
|
||||
`[EventCache:smartUpdateDomain] Error updating rule for domain: ${domain}, error: ${error}`,
|
||||
)
|
||||
throw new Error(
|
||||
`[EventCache:smartUpdateDomain] Error processing domain event: ${error}`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
smartUpdateUrl(event: ToolsOzoneSafelinkDefs.Event) {
|
||||
let url: string
|
||||
try {
|
||||
url = new URL(event.url).toString()
|
||||
} catch (error) {
|
||||
redirectLogger.error(
|
||||
`[EventCache:smartUpdateUrl] Invalid URL: ${event.url}, error: ${error}`,
|
||||
)
|
||||
throw new Error(`[EventCache:smartUpdateUrl] Error parsing URL: ${error}`)
|
||||
}
|
||||
event.url = url
|
||||
|
||||
try {
|
||||
redirectLogger.info(
|
||||
`[EventCache] smartUpdateUrl called for url: ${url}, action: ${event.action}`,
|
||||
)
|
||||
|
||||
if (event.action) {
|
||||
this.insert(url, event)
|
||||
redirectLogger.info(
|
||||
`[EventCache] rule updated or inserted for url: ${url}`,
|
||||
)
|
||||
return
|
||||
}
|
||||
} catch (error) {
|
||||
redirectLogger.error(
|
||||
`[EventCache:smartUpdateUrl] Error updating rule for url: ${url}, error: ${error}`,
|
||||
)
|
||||
throw new Error(
|
||||
`[EventCache:smartUpdateUrl] Error processing url event: ${error}`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
smartUpdate(event: ToolsOzoneSafelinkDefs.Event) {
|
||||
if (event.pattern === ToolsOzoneSafelinkDefs.DOMAIN) {
|
||||
redirectLogger.info(
|
||||
`[EventCache] smartUpdate called for domain event: ${event.url}, performing ${event.action}`,
|
||||
)
|
||||
return this.smartUpdateDomain(event)
|
||||
}
|
||||
if (event.pattern === ToolsOzoneSafelinkDefs.URL) {
|
||||
redirectLogger.info(
|
||||
`[EventCache] smartUpdate called for url event: ${event.url}`,
|
||||
)
|
||||
return this.smartUpdateUrl(event)
|
||||
}
|
||||
throw new Error('[EventCache] Unknown event pattern')
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to retrieve an event for the given URL.
|
||||
* Checks in order: domain, domain+path, then full URL.
|
||||
*/
|
||||
smartGet(url: string): ToolsOzoneSafelinkDefs.Event | undefined {
|
||||
const parsedUrl = new URL(url)
|
||||
const domain = parsedUrl.hostname
|
||||
const domainAndPath = domain + parsedUrl.pathname
|
||||
|
||||
const byDomain = this.rules.get(domain)
|
||||
if (byDomain) {
|
||||
return byDomain
|
||||
}
|
||||
|
||||
const byDomainAndPath = this.rules.get(domainAndPath)
|
||||
if (byDomainAndPath) {
|
||||
return byDomainAndPath
|
||||
}
|
||||
|
||||
return this.rules.get(url)
|
||||
}
|
||||
|
||||
delete(event: ToolsOzoneSafelinkDefs.Event) {
|
||||
this.rules.delete(event.url)
|
||||
}
|
||||
|
||||
get(url: string): ToolsOzoneSafelinkDefs.Event | undefined {
|
||||
const event = this.rules.get(url)
|
||||
return event
|
||||
}
|
||||
|
||||
list(): ToolsOzoneSafelinkDefs.Event[] {
|
||||
return Array.from(this.rules.values())
|
||||
}
|
||||
|
||||
// Adaptive polling: slow down if no new events, speed up if updates found
|
||||
async adaptiveFetchAndUpdate() {
|
||||
const prevCursor = cacheCursor
|
||||
const eventConfig = await this.getConfig()
|
||||
if (eventConfig === undefined) {
|
||||
redirectLogger.info(
|
||||
`[adaptiveFetchAndUpdate] No Configuration found, skipping fetch.`,
|
||||
)
|
||||
} else {
|
||||
await this.fetchAndUpdateEvents(eventConfig)
|
||||
}
|
||||
if (cacheCursor === prevCursor) {
|
||||
this.pollInterval = Math.min(this.pollInterval * 2, 10 * 60 * 1000)
|
||||
redirectLogger.info(
|
||||
`[adaptiveFetchAndUpdate] No new events, backing off. Next poll in ${
|
||||
this.pollInterval / 1000
|
||||
}s`,
|
||||
)
|
||||
} else {
|
||||
this.pollInterval = 5 * 1000
|
||||
redirectLogger.info(
|
||||
`[adaptiveFetchAndUpdate] New events found, resetting poll interval to ${
|
||||
this.pollInterval / 1000
|
||||
}s`,
|
||||
)
|
||||
}
|
||||
setTimeout(() => this.adaptiveFetchAndUpdate(), this.pollInterval)
|
||||
}
|
||||
|
||||
// Fetch and update events from the server
|
||||
async fetchAndUpdateEvents(cfg: ServiceConfig) {
|
||||
if (!cfg || !cfg.ozoneUrl || !cfg.ozoneAgentHandle || !cfg.ozoneAgentPass) {
|
||||
console.error(
|
||||
'[eventCache:fetchAndUpdateEvents] No active config, skipping actions',
|
||||
)
|
||||
return
|
||||
}
|
||||
redirectLogger.info(
|
||||
`[eventCache] Fetching events with cursor: ${cacheCursor}`,
|
||||
)
|
||||
const ozoneAgent = new OzoneAgent(cfg)
|
||||
const ozoneSession = await ozoneAgent.getSession?.()
|
||||
if (!ozoneSession) {
|
||||
console.error(
|
||||
'[eventCache:fetchAndUpdateEvents] No active session found.',
|
||||
)
|
||||
return
|
||||
}
|
||||
const ozoneDid = ozoneSession.did
|
||||
if (!ozoneDid || ozoneDid === 'did:plc:invalid') {
|
||||
console.error(
|
||||
'[eventCache:fetchAndUpdateEvents] Invalid or missing session DID.',
|
||||
)
|
||||
return
|
||||
}
|
||||
ozoneAgent.agent.setHeader?.('atproto-proxy', `${ozoneDid}#atproto_labeler`)
|
||||
|
||||
const res = await ozoneAgent.agent.tools?.ozone?.safelink?.queryEvents?.({
|
||||
cursor: cacheCursor,
|
||||
limit: 100,
|
||||
})
|
||||
|
||||
if (res?.data.cursor === cacheCursor) {
|
||||
redirectLogger.info(
|
||||
'[eventCache:fetchAndUpdateEvents] No new events to update.',
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
redirectLogger.info(
|
||||
`[eventCache:fetchAndUpdateEvents] Received response:`,
|
||||
{
|
||||
...res,
|
||||
data: {
|
||||
...res?.data,
|
||||
rules: Array.isArray(res?.data?.events)
|
||||
? res.data.events.map(event => JSON.stringify(event))
|
||||
: res?.data?.events,
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
for (const event of res.data.events) {
|
||||
this.smartUpdate(event)
|
||||
}
|
||||
|
||||
cacheCursor = res.data?.cursor
|
||||
|
||||
redirectLogger.info(
|
||||
'[eventCache:fetchAndUpdateEvents] Current cache contents:',
|
||||
)
|
||||
redirectLogger.info(this.list())
|
||||
}
|
||||
}
|
||||
Vendored
+49
@@ -0,0 +1,49 @@
|
||||
import {AtpAgent, CredentialSession} from '@atproto/api'
|
||||
|
||||
import {type ServiceConfig} from '../config'
|
||||
|
||||
export class OzoneAgent {
|
||||
public session: CredentialSession
|
||||
public agent: AtpAgent
|
||||
private cfg: ServiceConfig
|
||||
|
||||
constructor(cfg: ServiceConfig) {
|
||||
this.cfg = cfg
|
||||
this.session = new CredentialSession(
|
||||
new URL(cfg.ozoneUrl || 'http://localhost:2583'),
|
||||
)
|
||||
this.agent = new AtpAgent(this.session)
|
||||
}
|
||||
|
||||
public async getSession(): Promise<CredentialSession> {
|
||||
if (!this.session.hasSession) {
|
||||
await this.getAgent()
|
||||
}
|
||||
return this.session
|
||||
}
|
||||
|
||||
public async getAgent(): Promise<AtpAgent> {
|
||||
if (!this.cfg.ozoneAgentHandle && !this.cfg.ozoneAgentPass) {
|
||||
throw new Error(
|
||||
'OZONE_AGENT_HANDLE and OZONE_AGENT_PASS environment variables must be set',
|
||||
)
|
||||
}
|
||||
|
||||
const identifier = this.cfg.ozoneAgentHandle || 'did:plc:invalid'
|
||||
const password = this.cfg.ozoneAgentPass || 'invalid'
|
||||
|
||||
if (!this.session.hasSession) {
|
||||
await this.session.login({identifier, password})
|
||||
}
|
||||
|
||||
try {
|
||||
await this.agent.com.atproto.server.getSession()
|
||||
} catch (err) {
|
||||
if ((err as any).status === 401) {
|
||||
await this.session.login({identifier, password})
|
||||
}
|
||||
}
|
||||
|
||||
return this.agent
|
||||
}
|
||||
}
|
||||
Vendored
+29
@@ -0,0 +1,29 @@
|
||||
export type RuleEventType = '#addRule' | '#updateRule' | '#removeRule'
|
||||
export type RulePatternType = '#domain' | '#url'
|
||||
export type RuleActionType = '#block' | '#warn' | '#whitelist'
|
||||
export type RuleReasonType = '#csam' | '#spam' | '#phishing' | '#none'
|
||||
|
||||
export interface Rule {
|
||||
id: number
|
||||
eventType: RuleEventType
|
||||
url: string
|
||||
pattern: RulePatternType
|
||||
action: RuleActionType
|
||||
reason: RuleReasonType
|
||||
createdBy: string // DID format
|
||||
createdAt: string // ISO datetime string
|
||||
comment?: string
|
||||
}
|
||||
|
||||
// Example Rule object
|
||||
export const exampleRule: Rule = {
|
||||
id: 1,
|
||||
eventType: '#addRule',
|
||||
url: 'https://malicious.example.com/phishing',
|
||||
pattern: '#domain',
|
||||
action: '#block',
|
||||
reason: '#phishing',
|
||||
createdBy: 'did:plc:adminozonetools',
|
||||
createdAt: '2024-06-01T12:00:00Z',
|
||||
comment: 'Known phishing domain detected by automated scan.',
|
||||
}
|
||||
+26
-2
@@ -1,7 +1,11 @@
|
||||
import {envInt, envList, envStr} from '@atproto/common'
|
||||
import {envBool, envInt, envList, envStr} from '@atproto/common'
|
||||
|
||||
// import { type EventCache, eventCache } from '../cache/cache.js'
|
||||
import {EventCache} from './cache/cache.js'
|
||||
|
||||
export type Config = {
|
||||
service: ServiceConfig
|
||||
eventCache: EventCache
|
||||
db: DbConfig
|
||||
}
|
||||
|
||||
@@ -10,6 +14,10 @@ export type ServiceConfig = {
|
||||
version?: string
|
||||
hostnames: string[]
|
||||
appHostname: string
|
||||
safelinkEnabled: boolean
|
||||
ozoneUrl?: string
|
||||
ozoneAgentHandle?: string
|
||||
ozoneAgentPass?: string
|
||||
}
|
||||
|
||||
export type DbConfig = {
|
||||
@@ -36,6 +44,10 @@ export type Environment = {
|
||||
dbPostgresPoolSize?: number
|
||||
dbPostgresPoolMaxUses?: number
|
||||
dbPostgresPoolIdleTimeoutMs?: number
|
||||
safelinkEnabled?: boolean
|
||||
ozoneUrl?: string
|
||||
ozoneAgentHandle?: string
|
||||
ozoneAgentPass?: string
|
||||
}
|
||||
|
||||
export const readEnv = (): Environment => {
|
||||
@@ -52,6 +64,10 @@ export const readEnv = (): Environment => {
|
||||
dbPostgresPoolIdleTimeoutMs: envInt(
|
||||
'LINK_DB_POSTGRES_POOL_IDLE_TIMEOUT_MS',
|
||||
),
|
||||
safelinkEnabled: envBool('SAFELINK_ENABLED'),
|
||||
ozoneUrl: envStr('OZONE_URL'),
|
||||
ozoneAgentHandle: envStr('OZONE_AGENT_HANDLE'),
|
||||
ozoneAgentPass: envStr('OZONE_AGENT_PASS'),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,7 +76,11 @@ export const envToCfg = (env: Environment): Config => {
|
||||
port: env.port ?? 3000,
|
||||
version: env.version,
|
||||
hostnames: env.hostnames,
|
||||
appHostname: env.appHostname || 'bsky.app',
|
||||
appHostname: env.appHostname ?? 'bsky.app',
|
||||
safelinkEnabled: env.safelinkEnabled ?? false,
|
||||
ozoneUrl: env.ozoneUrl,
|
||||
ozoneAgentHandle: env.ozoneAgentHandle,
|
||||
ozoneAgentPass: env.ozoneAgentPass,
|
||||
}
|
||||
if (!env.dbPostgresUrl) {
|
||||
throw new Error('Must configure postgres url (LINK_DB_POSTGRES_URL)')
|
||||
@@ -75,8 +95,12 @@ export const envToCfg = (env: Environment): Config => {
|
||||
size: env.dbPostgresPoolSize ?? 10,
|
||||
},
|
||||
}
|
||||
|
||||
const eventCache = new EventCache(serviceCfg)
|
||||
|
||||
return {
|
||||
service: serviceCfg,
|
||||
eventCache: eventCache,
|
||||
db: dbCfg,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import escapeHTML from 'escape-html'
|
||||
import {type Hole, html} from 'uhtml'
|
||||
|
||||
export function linkRedirectContents(link: string): Hole {
|
||||
return html`
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="refresh" content="0; URL='${escapeHTML(link)}'" />
|
||||
<meta
|
||||
http-equiv="Cache-Control"
|
||||
content="no-store, no-cache, must-revalidate, max-age=0" />
|
||||
<meta http-equiv="Pragma" content="no-cache" />
|
||||
<meta http-equiv="Expires" content="0" />
|
||||
<style>
|
||||
:root {
|
||||
color-scheme: light dark;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
</html>
|
||||
`
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import escapeHTML from 'escape-html'
|
||||
import {type Request} from 'express'
|
||||
import {type Hole, html} from 'uhtml'
|
||||
|
||||
export function linkWarningContents(
|
||||
req: Request,
|
||||
opts: {
|
||||
type: 'warn' | 'block'
|
||||
link: string
|
||||
},
|
||||
): Hole {
|
||||
return html`
|
||||
<div class="warning-icon">⚠️</div>
|
||||
<h1>
|
||||
${opts.type === 'warn'
|
||||
? req.__('Potentially Dangerous Link')
|
||||
: req.__('Blocked Link')}
|
||||
</h1>
|
||||
<p class="warning-text">
|
||||
${opts.type === 'warn'
|
||||
? req.__(
|
||||
'This link may be malicious. You should proceed at your own risk.',
|
||||
)
|
||||
: req.__(
|
||||
'This link has been identified as malicious and has blocked for your safety.',
|
||||
)}
|
||||
</p>
|
||||
<div class="blocked-site">
|
||||
<p class="site-url">${escapeHTML(opts.link)}</p>
|
||||
</div>
|
||||
<div class="button-group">
|
||||
${opts.type === 'warn'
|
||||
? html`<a class="button secondary" href="${escapeHTML(opts.link)}"
|
||||
>${req.__('Continue Anyway')}</a
|
||||
>`
|
||||
: null}
|
||||
<a class="button primary" href="https://bsky.app"
|
||||
>${req.__('Return to Bluesky')}</a
|
||||
>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
import escapeHTML from 'escape-html'
|
||||
import {type Hole, html} from 'uhtml'
|
||||
|
||||
export function linkWarningLayout(
|
||||
title: string,
|
||||
containerContents: Hole,
|
||||
): Hole {
|
||||
return html`
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta
|
||||
http-equiv="Cache-Control"
|
||||
content="no-store, no-cache, must-revalidate, max-age=0" />
|
||||
<meta http-equiv="Pragma" content="no-cache" />
|
||||
<meta http-equiv="Expires" content="0" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>${escapeHTML(title)}</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family:
|
||||
-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Arial,
|
||||
sans-serif;
|
||||
background-color: #ffffff;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.container {
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.warning-icon {
|
||||
font-size: 48px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 12px;
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
.warning-text {
|
||||
font-size: 15px;
|
||||
color: #536471;
|
||||
line-height: 1.4;
|
||||
margin-bottom: 24px;
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
.blocked-site {
|
||||
background-color: #f7f9fa;
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
margin-bottom: 24px;
|
||||
text-align: left;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.site-name {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: #000000;
|
||||
margin-bottom: 4px;
|
||||
word-break: break-word;
|
||||
display: block;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.site-url {
|
||||
font-size: 14px;
|
||||
color: #536471;
|
||||
word-break: break-all;
|
||||
display: block;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.button {
|
||||
border: none;
|
||||
border-radius: 24px;
|
||||
padding: 12px 32px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
width: 100%;
|
||||
max-width: 280px;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.primary {
|
||||
background-color: #1d9bf0;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.secondary {
|
||||
}
|
||||
|
||||
.back-button:hover {
|
||||
background-color: #1a8cd8;
|
||||
}
|
||||
|
||||
.back-button:active {
|
||||
background-color: #1681c4;
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.warning-text {
|
||||
padding: 0 10px;
|
||||
}
|
||||
.blocked-site {
|
||||
padding: 8px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">${containerContents}</div>
|
||||
</body>
|
||||
</html>
|
||||
`
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import path from 'node:path'
|
||||
import {fileURLToPath} from 'node:url'
|
||||
|
||||
import i18n from 'i18n'
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url)
|
||||
const __dirname = path.dirname(__filename)
|
||||
|
||||
i18n.configure({
|
||||
locales: ['en', 'es', 'fr'],
|
||||
defaultLocale: 'en',
|
||||
directory: path.join(__dirname, '../locales'),
|
||||
})
|
||||
|
||||
export default i18n
|
||||
@@ -7,6 +7,7 @@ import {createHttpTerminator, type HttpTerminator} from 'http-terminator'
|
||||
|
||||
import {type Config} from './config.js'
|
||||
import {AppContext} from './context.js'
|
||||
import i18n from './i18n.js'
|
||||
import {default as routes, errorHandler} from './routes/index.js'
|
||||
|
||||
export * from './config.js'
|
||||
@@ -25,6 +26,7 @@ export class LinkService {
|
||||
static async create(cfg: Config): Promise<LinkService> {
|
||||
let app = express()
|
||||
app.use(cors())
|
||||
app.use(i18n.init)
|
||||
|
||||
const ctx = await AppContext.fromConfig(cfg)
|
||||
app = routes(ctx, app)
|
||||
|
||||
+13
-2
@@ -1,4 +1,15 @@
|
||||
import {subsystemLogger} from '@atproto/common'
|
||||
import {type Logger} from 'pino'
|
||||
|
||||
export const httpLogger = subsystemLogger('bskylink')
|
||||
export const dbLogger = subsystemLogger('bskylink:db')
|
||||
export const httpLogger: Logger = subsystemLogger('bskylink')
|
||||
export const dbLogger: Logger = subsystemLogger('bskylink:db')
|
||||
export const redirectLogger: Logger = subsystemLogger('bskylink:redirect')
|
||||
|
||||
redirectLogger.info = (
|
||||
orig =>
|
||||
(...args: any[]) => {
|
||||
const [msg, ...rest] = args
|
||||
orig.apply(redirectLogger, [String(msg), ...rest])
|
||||
console.log('[bskylink:redirect]', ...args)
|
||||
}
|
||||
)(redirectLogger.info) as typeof redirectLogger.info
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import assert from 'node:assert'
|
||||
|
||||
import bodyParser from 'body-parser'
|
||||
import {Express, Request} from 'express'
|
||||
import {type Express, type Request} from 'express'
|
||||
|
||||
import {AppContext} from '../context.js'
|
||||
import {type AppContext} from '../context.js'
|
||||
import {LinkType} from '../db/schema.js'
|
||||
import {randomId} from '../util.js'
|
||||
import {handler} from './util.js'
|
||||
@@ -83,8 +83,9 @@ const getUrl = (ctx: AppContext, req: Request, id: string) => {
|
||||
: `https://${req.headers.host}`
|
||||
return `${baseUrl}/${id}`
|
||||
}
|
||||
const baseUrl = ctx.cfg.service.hostnames.includes(req.headers.host)
|
||||
? `https://${req.headers.host}`
|
||||
const host = req.headers.host ?? ''
|
||||
const baseUrl = ctx.cfg.service.hostnames.includes(host)
|
||||
? `https://${host}`
|
||||
: `https://${ctx.cfg.service.hostnames[0]}`
|
||||
return `${baseUrl}/${id}`
|
||||
}
|
||||
@@ -94,7 +95,7 @@ const normalizedPathFromParts = (parts: string[]): string => {
|
||||
'/' +
|
||||
parts
|
||||
.map(encodeURIComponent)
|
||||
.map(part => part.replaceAll('%3A', ':')) // preserve colons
|
||||
.map(part => part.replace(/%3A/g, ':')) // preserve colons
|
||||
.join('/')
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
import assert from 'node:assert'
|
||||
|
||||
import {ToolsOzoneSafelinkDefs} from '@atproto/api'
|
||||
import {DAY, SECOND} from '@atproto/common'
|
||||
import escapeHTML from 'escape-html'
|
||||
import {type Express} from 'express'
|
||||
import {type Hole} from 'uhtml'
|
||||
|
||||
import {type AppContext} from '../context.js'
|
||||
import {linkRedirectContents} from '../html/linkRedirectContents.js'
|
||||
import {linkWarningContents} from '../html/linkWarningContents.js'
|
||||
import {linkWarningLayout} from '../html/linkWarningLayout.js'
|
||||
import {redirectLogger} from '../logger.js'
|
||||
import {handler} from './util.js'
|
||||
|
||||
const INTERNAL_IP_REGEX = new RegExp(
|
||||
@@ -39,14 +44,65 @@ export default function (ctx: AppContext, app: Express) {
|
||||
return res.status(302).end()
|
||||
}
|
||||
|
||||
// Default to a max age header
|
||||
res.setHeader('Cache-Control', `max-age=${(7 * DAY) / SECOND}`)
|
||||
res.type('html')
|
||||
res.status(200)
|
||||
res.type('html')
|
||||
|
||||
const escaped = escapeHTML(url.href)
|
||||
return res.send(
|
||||
`<html><head><meta http-equiv="refresh" content="0; URL='${escaped}'" /><style>:root { color-scheme: light dark; }</style></head></html>`,
|
||||
)
|
||||
let hole: Hole | undefined
|
||||
|
||||
if (ctx.cfg.service.safelinkEnabled) {
|
||||
const rulePresent: ToolsOzoneSafelinkDefs.Event | undefined =
|
||||
ctx.cfg.eventCache.smartGet(link)
|
||||
|
||||
if (rulePresent) {
|
||||
switch (rulePresent.action) {
|
||||
case ToolsOzoneSafelinkDefs.WHITELIST:
|
||||
redirectLogger.info(
|
||||
`Whitelist rule matched for ${rulePresent.url}`,
|
||||
)
|
||||
break
|
||||
case ToolsOzoneSafelinkDefs.REMOVERULE:
|
||||
redirectLogger.info(`Remove rule matched for ${rulePresent.url}`)
|
||||
break
|
||||
case ToolsOzoneSafelinkDefs.BLOCK:
|
||||
hole = linkWarningLayout(
|
||||
'Blocked Link Warning',
|
||||
linkWarningContents(req, {
|
||||
type: 'block',
|
||||
link: url.href,
|
||||
}),
|
||||
)
|
||||
res.setHeader('Cache-Control', 'no-store')
|
||||
redirectLogger.info(`Block rule matched for ${rulePresent.url}`)
|
||||
break
|
||||
case ToolsOzoneSafelinkDefs.WARN:
|
||||
hole = linkWarningLayout(
|
||||
'Malicious Link Warning',
|
||||
linkWarningContents(req, {
|
||||
type: 'warn',
|
||||
link: url.href,
|
||||
}),
|
||||
)
|
||||
res.setHeader('Cache-Control', 'no-store')
|
||||
redirectLogger.info(`Warn rule matched for ${rulePresent.url}`)
|
||||
break
|
||||
default:
|
||||
redirectLogger.warn(
|
||||
`${rulePresent.action} rule (an unknown rule) matched for ${rulePresent.url}`,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
redirectLogger.info(`No rule present for ${rulePresent.url}`)
|
||||
}
|
||||
}
|
||||
|
||||
// If there is no hole defined yet, we will create a redirect hole
|
||||
if (!hole) {
|
||||
hole = linkRedirectContents(url.href)
|
||||
}
|
||||
|
||||
return res.end(String(hole))
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
+239
-2
@@ -1,7 +1,9 @@
|
||||
import assert from 'node:assert'
|
||||
import {AddressInfo} from 'node:net'
|
||||
import {type AddressInfo} from 'node:net'
|
||||
import {after, before, describe, it} from 'node:test'
|
||||
|
||||
import {ToolsOzoneSafelinkDefs} from '@atproto/api'
|
||||
|
||||
import {Database, envToCfg, LinkService, readEnv} from '../src/index.js'
|
||||
|
||||
describe('link service', async () => {
|
||||
@@ -15,6 +17,10 @@ describe('link service', async () => {
|
||||
appHostname: 'test.bsky.app',
|
||||
dbPostgresSchema: 'link_test',
|
||||
dbPostgresUrl: process.env.DB_POSTGRES_URL,
|
||||
safelinkEnabled: true,
|
||||
ozoneUrl: 'http://localhost:2583',
|
||||
ozoneAgentHandle: 'mod-authority.test',
|
||||
ozoneAgentPass: 'hunter2',
|
||||
})
|
||||
const migrateDb = Database.postgres({
|
||||
url: cfg.db.url,
|
||||
@@ -26,8 +32,85 @@ describe('link service', async () => {
|
||||
await linkService.start()
|
||||
const {port} = linkService.server?.address() as AddressInfo
|
||||
baseUrl = `http://localhost:${port}`
|
||||
})
|
||||
|
||||
// Ensure blocklist, whitelist, and safelink rules are set up
|
||||
const now = new Date().toISOString()
|
||||
linkService.ctx.cfg.eventCache.smartUpdate({
|
||||
$type: 'tools.ozone.safelink.defs#event',
|
||||
id: 1,
|
||||
eventType: ToolsOzoneSafelinkDefs.ADDRULE,
|
||||
url: 'https://en.wikipedia.org/wiki/Fight_Club',
|
||||
pattern: ToolsOzoneSafelinkDefs.URL,
|
||||
action: ToolsOzoneSafelinkDefs.WARN,
|
||||
reason: ToolsOzoneSafelinkDefs.SPAM,
|
||||
createdBy: 'did:example:admin',
|
||||
createdAt: now,
|
||||
comment: 'Do not talk about Fight Club',
|
||||
})
|
||||
linkService.ctx.cfg.eventCache.smartUpdate({
|
||||
$type: 'tools.ozone.safelink.defs#event',
|
||||
id: 2,
|
||||
eventType: ToolsOzoneSafelinkDefs.ADDRULE,
|
||||
url: 'https://gist.github.com/MattIPv4/045239bc27b16b2bcf7a3a9a4648c08a',
|
||||
pattern: ToolsOzoneSafelinkDefs.URL,
|
||||
action: ToolsOzoneSafelinkDefs.BLOCK,
|
||||
reason: ToolsOzoneSafelinkDefs.SPAM,
|
||||
createdBy: 'did:example:admin',
|
||||
createdAt: now,
|
||||
comment: 'All Bs',
|
||||
})
|
||||
linkService.ctx.cfg.eventCache.smartUpdate({
|
||||
$type: 'tools.ozone.safelink.defs#event',
|
||||
id: 3,
|
||||
eventType: ToolsOzoneSafelinkDefs.ADDRULE,
|
||||
url: 'https://en.wikipedia.org',
|
||||
pattern: ToolsOzoneSafelinkDefs.DOMAIN,
|
||||
action: ToolsOzoneSafelinkDefs.WHITELIST,
|
||||
reason: ToolsOzoneSafelinkDefs.NONE,
|
||||
createdBy: 'did:example:admin',
|
||||
createdAt: now,
|
||||
comment: 'Whitelisting the knowledge base of the internet',
|
||||
})
|
||||
linkService.ctx.cfg.eventCache.smartUpdate({
|
||||
$type: 'tools.ozone.safelink.defs#event',
|
||||
id: 4,
|
||||
eventType: ToolsOzoneSafelinkDefs.ADDRULE,
|
||||
url: 'https://www.instagram.com/teamseshbones/?hl=en',
|
||||
pattern: ToolsOzoneSafelinkDefs.URL,
|
||||
action: ToolsOzoneSafelinkDefs.BLOCK,
|
||||
reason: ToolsOzoneSafelinkDefs.SPAM,
|
||||
createdBy: 'did:example:admin',
|
||||
createdAt: now,
|
||||
comment: 'BONES has been erroneously blocked for the sake of this test',
|
||||
})
|
||||
const later = new Date(Date.now() + 1000).toISOString()
|
||||
linkService.ctx.cfg.eventCache.smartUpdate({
|
||||
$type: 'tools.ozone.safelink.defs#event',
|
||||
id: 5,
|
||||
eventType: ToolsOzoneSafelinkDefs.REMOVERULE,
|
||||
url: 'https://www.instagram.com/teamseshbones/?hl=en',
|
||||
pattern: ToolsOzoneSafelinkDefs.URL,
|
||||
action: ToolsOzoneSafelinkDefs.REMOVERULE,
|
||||
reason: ToolsOzoneSafelinkDefs.NONE,
|
||||
createdBy: 'did:example:admin',
|
||||
createdAt: later,
|
||||
comment:
|
||||
'BONES has been resurrected to bring good music to the world once again',
|
||||
})
|
||||
linkService.ctx.cfg.eventCache.smartUpdate({
|
||||
$type: 'tools.ozone.safelink.defs#event',
|
||||
id: 6,
|
||||
eventType: ToolsOzoneSafelinkDefs.ADDRULE,
|
||||
url: 'https://www.leagueoflegends.com/en-us/',
|
||||
pattern: ToolsOzoneSafelinkDefs.URL,
|
||||
action: ToolsOzoneSafelinkDefs.WARN,
|
||||
reason: ToolsOzoneSafelinkDefs.SPAM,
|
||||
createdBy: 'did:example:admin',
|
||||
createdAt: now,
|
||||
comment:
|
||||
'Could be quite the mistake to get into this addicting game, but we will warn instead of block',
|
||||
})
|
||||
})
|
||||
after(async () => {
|
||||
await linkService?.destroy()
|
||||
})
|
||||
@@ -76,6 +159,80 @@ describe('link service', async () => {
|
||||
assert.strictEqual(json.message, 'Link not found')
|
||||
})
|
||||
|
||||
it('League of Legends warned', async () => {
|
||||
const urlToRedirect = 'https://www.leagueoflegends.com/en-us/'
|
||||
const url = new URL(`${baseUrl}/redirect`)
|
||||
url.searchParams.set('u', urlToRedirect)
|
||||
const res = await fetch(url, {redirect: 'manual'})
|
||||
assert.strictEqual(res.status, 200)
|
||||
const html = await res.text()
|
||||
assert.match(
|
||||
html,
|
||||
new RegExp(urlToRedirect.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')),
|
||||
)
|
||||
// League of Legends is set to WARN, not BLOCK, so expect a warning (blocked-site div present)
|
||||
assert.match(
|
||||
html,
|
||||
/Warning: Malicious Link/,
|
||||
'Expected warning not found in HTML',
|
||||
)
|
||||
})
|
||||
|
||||
it('Wikipedia whitelisted, url restricted. Redirect safely since wikipedia is whitelisted', async () => {
|
||||
const urlToRedirect = 'https://en.wikipedia.org/wiki/Fight_Club'
|
||||
const url = new URL(`${baseUrl}/redirect`)
|
||||
url.searchParams.set('u', urlToRedirect)
|
||||
const res = await fetch(url, {redirect: 'manual'})
|
||||
assert.strictEqual(res.status, 200)
|
||||
const html = await res.text()
|
||||
assert.match(html, /meta http-equiv="refresh"/)
|
||||
assert.match(
|
||||
html,
|
||||
new RegExp(urlToRedirect.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')),
|
||||
)
|
||||
// Wikipedia domain is whitelisted, so no blocked-site div should be present
|
||||
assert.doesNotMatch(html, /"blocked-site"/)
|
||||
})
|
||||
|
||||
it('Unsafe redirect with block rule, due to the content of webpage.', async () => {
|
||||
const urlToRedirect =
|
||||
'https://gist.github.com/MattIPv4/045239bc27b16b2bcf7a3a9a4648c08a'
|
||||
const url = new URL(`${baseUrl}/redirect`)
|
||||
url.searchParams.set('u', urlToRedirect)
|
||||
const res = await fetch(url, {redirect: 'manual'})
|
||||
assert.strictEqual(res.status, 200)
|
||||
const html = await res.text()
|
||||
assert.match(
|
||||
html,
|
||||
new RegExp(urlToRedirect.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')),
|
||||
)
|
||||
assert.match(
|
||||
html,
|
||||
/"blocked-site"/,
|
||||
'Expected blocked-site div not found in HTML',
|
||||
)
|
||||
})
|
||||
|
||||
it('Rule adjustment, safe redirect, 200 response for Instagram Account of teamsesh Bones', async () => {
|
||||
// Retrieve the latest event after all updates
|
||||
const result = linkService.ctx.cfg.eventCache.smartGet(
|
||||
'https://www.instagram.com/teamseshbones/?hl=en',
|
||||
)
|
||||
assert(result, 'Expected event not found in eventCache')
|
||||
assert.strictEqual(result.eventType, ToolsOzoneSafelinkDefs.REMOVERULE)
|
||||
const urlToRedirect = 'https://www.instagram.com/teamseshbones/?hl=en'
|
||||
const url = new URL(`${baseUrl}/redirect`)
|
||||
url.searchParams.set('u', urlToRedirect)
|
||||
const res = await fetch(url, {redirect: 'manual'})
|
||||
assert.strictEqual(res.status, 200)
|
||||
const html = await res.text()
|
||||
assert.match(html, /meta http-equiv="refresh"/)
|
||||
assert.match(
|
||||
html,
|
||||
new RegExp(urlToRedirect.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')),
|
||||
)
|
||||
})
|
||||
|
||||
async function getRedirect(link: string): Promise<[number, string]> {
|
||||
const url = new URL(link)
|
||||
const base = new URL(baseUrl)
|
||||
@@ -121,3 +278,83 @@ describe('link service', async () => {
|
||||
return payload.url
|
||||
}
|
||||
})
|
||||
|
||||
describe('link service no safelink', 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,
|
||||
safelinkEnabled: false,
|
||||
ozoneUrl: 'http://localhost:2583',
|
||||
ozoneAgentHandle: 'mod-authority.test',
|
||||
ozoneAgentPass: 'hunter2',
|
||||
})
|
||||
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('Wikipedia whitelisted, url restricted. Safelink is disabled, so redirect is always safe', async () => {
|
||||
const urlToRedirect = 'https://en.wikipedia.org/wiki/Fight_Club'
|
||||
const url = new URL(`${baseUrl}/redirect`)
|
||||
url.searchParams.set('u', urlToRedirect)
|
||||
const res = await fetch(url, {redirect: 'manual'})
|
||||
assert.strictEqual(res.status, 200)
|
||||
const html = await res.text()
|
||||
assert.match(html, /meta http-equiv="refresh"/)
|
||||
assert.match(
|
||||
html,
|
||||
new RegExp(urlToRedirect.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')),
|
||||
)
|
||||
// No blocked-site div, always safe
|
||||
assert.doesNotMatch(html, /"blocked-site"/)
|
||||
})
|
||||
|
||||
it('Unsafe redirect with block rule, but safelink is disabled so redirect is always safe', async () => {
|
||||
const urlToRedirect =
|
||||
'https://gist.github.com/MattIPv4/045239bc27b16b2bcf7a3a9a4648c08a'
|
||||
const url = new URL(`${baseUrl}/redirect`)
|
||||
url.searchParams.set('u', urlToRedirect)
|
||||
const res = await fetch(url, {redirect: 'manual'})
|
||||
assert.strictEqual(res.status, 200)
|
||||
const html = await res.text()
|
||||
assert.match(html, /meta http-equiv="refresh"/)
|
||||
assert.match(
|
||||
html,
|
||||
new RegExp(urlToRedirect.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')),
|
||||
)
|
||||
// No blocked-site div, always safe
|
||||
assert.doesNotMatch(html, /"blocked-site"/)
|
||||
})
|
||||
|
||||
it('Rule adjustment, safe redirect, safelink is disabled so always safe', async () => {
|
||||
const urlToRedirect = 'https://www.instagram.com/teamseshbones/?hl=en'
|
||||
const url = new URL(`${baseUrl}/redirect`)
|
||||
url.searchParams.set('u', urlToRedirect)
|
||||
const res = await fetch(url, {redirect: 'manual'})
|
||||
assert.strictEqual(res.status, 200)
|
||||
const html = await res.text()
|
||||
assert.match(html, /meta http-equiv="refresh"/)
|
||||
assert.match(
|
||||
html,
|
||||
new RegExp(urlToRedirect.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')),
|
||||
)
|
||||
// No blocked-site div, always safe
|
||||
assert.doesNotMatch(html, /"blocked-site"/)
|
||||
})
|
||||
})
|
||||
|
||||
+18
-9
@@ -1,10 +1,19 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "NodeNext",
|
||||
"esModuleInterop": true,
|
||||
"moduleResolution": "NodeNext",
|
||||
"outDir": "dist",
|
||||
"lib": ["ES2021.String"]
|
||||
},
|
||||
"include": ["./src/index.ts", "./src/bin.ts"]
|
||||
}
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"sourceMap": true
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
|
||||
|
||||
+205
-17
@@ -2,27 +2,27 @@
|
||||
# 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==
|
||||
"@atproto/common-web@^0.4.2":
|
||||
version "0.4.2"
|
||||
resolved "https://registry.yarnpkg.com/@atproto/common-web/-/common-web-0.4.2.tgz#6e3add6939da93d3dfbc8f87e26dc4f57fad7259"
|
||||
integrity sha512-vrXwGNoFGogodjQvJDxAeP3QbGtawgZute2ed1XdRO0wMixLk3qewtikZm06H259QDJVu6voKC5mubml+WgQUw==
|
||||
dependencies:
|
||||
graphemer "^1.4.0"
|
||||
multiformats "^9.9.0"
|
||||
uint8arrays "3.0.0"
|
||||
zod "^3.21.4"
|
||||
zod "^3.23.8"
|
||||
|
||||
"@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==
|
||||
"@atproto/common@^0.4.11":
|
||||
version "0.4.11"
|
||||
resolved "https://registry.yarnpkg.com/@atproto/common/-/common-0.4.11.tgz#9291b7c26f8b3507e280f7ecbdf1695ab5ea62f6"
|
||||
integrity sha512-Knv0viYXNMfCdIE7jLUiWJKnnMfEwg+vz2epJQi8WOjqtqCFb3W/3Jn72ZiuovIfpdm13MaOiny6w2NErUQC6g==
|
||||
dependencies:
|
||||
"@atproto/common-web" "^0.3.0"
|
||||
"@atproto/common-web" "^0.4.2"
|
||||
"@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"
|
||||
pino "^8.21.0"
|
||||
|
||||
"@cbor-extract/cbor-extract-darwin-arm64@2.2.0":
|
||||
version "2.2.0"
|
||||
@@ -62,6 +62,47 @@
|
||||
cborg "^1.6.0"
|
||||
multiformats "^9.5.4"
|
||||
|
||||
"@messageformat/core@^3.0.0":
|
||||
version "3.4.0"
|
||||
resolved "https://registry.yarnpkg.com/@messageformat/core/-/core-3.4.0.tgz#2814c23383dec7bddf535d54f2a03e410165ca9f"
|
||||
integrity sha512-NgCFubFFIdMWJGN5WuQhHCNmzk7QgiVfrViFxcS99j7F5dDS5EP6raR54I+2ydhe4+5/XTn/YIEppFaqqVWHsw==
|
||||
dependencies:
|
||||
"@messageformat/date-skeleton" "^1.0.0"
|
||||
"@messageformat/number-skeleton" "^1.0.0"
|
||||
"@messageformat/parser" "^5.1.0"
|
||||
"@messageformat/runtime" "^3.0.1"
|
||||
make-plural "^7.0.0"
|
||||
safe-identifier "^0.4.1"
|
||||
|
||||
"@messageformat/date-skeleton@^1.0.0":
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@messageformat/date-skeleton/-/date-skeleton-1.1.0.tgz#3bad068cbf5873d14592cfc7a73dd4d8615e2739"
|
||||
integrity sha512-rmGAfB1tIPER+gh3p/RgA+PVeRE/gxuQ2w4snFWPF5xtb5mbWR7Cbw7wCOftcUypbD6HVoxrVdyyghPm3WzP5A==
|
||||
|
||||
"@messageformat/number-skeleton@^1.0.0":
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/@messageformat/number-skeleton/-/number-skeleton-1.2.0.tgz#e7c245c41a1b2722bc59dad68f4d454f761bc9b4"
|
||||
integrity sha512-xsgwcL7J7WhlHJ3RNbaVgssaIwcEyFkBqxHdcdaiJzwTZAWEOD8BuUFxnxV9k5S0qHN3v/KzUpq0IUpjH1seRg==
|
||||
|
||||
"@messageformat/parser@^5.1.0":
|
||||
version "5.1.1"
|
||||
resolved "https://registry.yarnpkg.com/@messageformat/parser/-/parser-5.1.1.tgz#ca7d6c18e9f3f6b6bc984a465dac16da00106055"
|
||||
integrity sha512-3p0YRGCcTUCYvBKLIxtDDyrJ0YijGIwrTRu1DT8gIviIDZru8H23+FkY6MJBzM1n9n20CiM4VeDYuBsrrwnLjg==
|
||||
dependencies:
|
||||
moo "^0.5.1"
|
||||
|
||||
"@messageformat/runtime@^3.0.1":
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@messageformat/runtime/-/runtime-3.0.1.tgz#94d1f6c43265c28ef7aed98ecfcc0968c6c849ac"
|
||||
integrity sha512-6RU5ol2lDtO8bD9Yxe6CZkl0DArdv0qkuoZC+ZwowU+cdRlVE1157wjCmlA5Rsf1Xc/brACnsZa5PZpEDfTFFg==
|
||||
dependencies:
|
||||
make-plural "^7.0.0"
|
||||
|
||||
"@preact/signals-core@^1.8.0":
|
||||
version "1.10.0"
|
||||
resolved "https://registry.yarnpkg.com/@preact/signals-core/-/signals-core-1.10.0.tgz#765eb7045998b98c437e1ad1a660e5ff96a40136"
|
||||
integrity sha512-qlKeXlfqtlC+sjxCPHt6Sk0/dXBrKZVcPlianqjNc/vW263YBFiP5mRrgKpHoO0q222Thm1TdYQWfCKpbbgvwA==
|
||||
|
||||
"@types/cors@^2.8.17":
|
||||
version "2.8.17"
|
||||
resolved "https://registry.yarnpkg.com/@types/cors/-/cors-2.8.17.tgz#5d718a5e494a8166f569d986794e49c48b216b2b"
|
||||
@@ -74,6 +115,11 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/escape-html/-/escape-html-1.0.4.tgz#dc7c166b76c7b03b27e32f80edf01d91eb5d9af2"
|
||||
integrity sha512-qZ72SFTgUAZ5a7Tj6kf2SHLetiH5S6f8G5frB2SPQ3EyF02kxdyBFf4Tz4banE3xCgGnKgWLt//a6VuYHKYJTg==
|
||||
|
||||
"@types/i18n@^0.13.12":
|
||||
version "0.13.12"
|
||||
resolved "https://registry.yarnpkg.com/@types/i18n/-/i18n-0.13.12.tgz#b457715766c63d8ffdfc51dd4fc1f72728e9d38f"
|
||||
integrity sha512-iAd2QjKh+0ToBXocmCS3m38GskiaGzmSV1MTQz2GaOraqSqBiLf46J7u3EGINl+st+Uk4lO3OL7QyIjTJlrWIg==
|
||||
|
||||
"@types/node@*":
|
||||
version "20.14.2"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-20.14.2.tgz#a5f4d2bcb4b6a87bffcaa717718c5a0f208f4a18"
|
||||
@@ -90,6 +136,18 @@
|
||||
pg-protocol "*"
|
||||
pg-types "^4.0.1"
|
||||
|
||||
"@webreflection/signal@^2.1.2":
|
||||
version "2.1.2"
|
||||
resolved "https://registry.yarnpkg.com/@webreflection/signal/-/signal-2.1.2.tgz#8adcf99b33f7e8ddfade4742b171c1743dc930d2"
|
||||
integrity sha512-0dW0fstQQkIt588JwhDiPS4xgeeQcQnBHn6MVInrBzmFlnLtzoSJL9G7JqdAlZVVi19tfb8R1QisZIT31cgiug==
|
||||
|
||||
"@webreflection/uparser@^0.4.0":
|
||||
version "0.4.0"
|
||||
resolved "https://registry.yarnpkg.com/@webreflection/uparser/-/uparser-0.4.0.tgz#49c105455e9482f9a7398f96eb398e421e440f26"
|
||||
integrity sha512-kAFWUEw5eool295y01VDr+DOsyog6lURX9l288JCJAD2gxc0tFk34dYaAi6O3BbJyfSoncVEV+nw87bsssdppQ==
|
||||
dependencies:
|
||||
domconstants "^1.1.6"
|
||||
|
||||
abort-controller@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/abort-controller/-/abort-controller-3.0.0.tgz#eaf54d53b62bae4138e809ca225c8439a6efb392"
|
||||
@@ -223,6 +281,11 @@ cors@^2.8.5:
|
||||
object-assign "^4"
|
||||
vary "^1"
|
||||
|
||||
custom-function@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/custom-function/-/custom-function-2.0.0.tgz#e421ce1712fa5f8e240a518080a4c82ae97e8606"
|
||||
integrity sha512-2OPHkZzq3mK1nWpJqWWkGD6Z+0AajNeIxmXl+MRVL8Vysjjf5tf9B5mo713/X2khEwBn/3BKQ7NphpP1vpVKug==
|
||||
|
||||
debug@2.6.9:
|
||||
version "2.6.9"
|
||||
resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
|
||||
@@ -230,6 +293,13 @@ debug@2.6.9:
|
||||
dependencies:
|
||||
ms "2.0.0"
|
||||
|
||||
debug@^4.3.3:
|
||||
version "4.4.1"
|
||||
resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.1.tgz#e5a8bc6cbc4c6cd3e64308b0693a3d4fa550189b"
|
||||
integrity sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==
|
||||
dependencies:
|
||||
ms "^2.1.3"
|
||||
|
||||
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"
|
||||
@@ -259,6 +329,41 @@ detect-libc@^2.0.1:
|
||||
resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.0.3.tgz#f0cd503b40f9939b894697d19ad50895e30cf700"
|
||||
integrity sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==
|
||||
|
||||
dom-serializer@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-2.0.0.tgz#e41b802e1eedf9f6cae183ce5e622d789d7d8e53"
|
||||
integrity sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==
|
||||
dependencies:
|
||||
domelementtype "^2.3.0"
|
||||
domhandler "^5.0.2"
|
||||
entities "^4.2.0"
|
||||
|
||||
domconstants@^1.1.6:
|
||||
version "1.1.6"
|
||||
resolved "https://registry.yarnpkg.com/domconstants/-/domconstants-1.1.6.tgz#c6dd4e181a3ddf641a3d2f55ec79569a409d82d4"
|
||||
integrity sha512-CuaDrThJ4VM+LyZ4ax8n52k0KbLJZtffyGkuj1WhpTRRcSfcy/9DfOBa68jenhX96oNUTunblSJEUNC4baFdmQ==
|
||||
|
||||
domelementtype@^2.3.0:
|
||||
version "2.3.0"
|
||||
resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.3.0.tgz#5c45e8e869952626331d7aab326d01daf65d589d"
|
||||
integrity sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==
|
||||
|
||||
domhandler@^5.0.2, domhandler@^5.0.3:
|
||||
version "5.0.3"
|
||||
resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-5.0.3.tgz#cc385f7f751f1d1fc650c21374804254538c7d31"
|
||||
integrity sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==
|
||||
dependencies:
|
||||
domelementtype "^2.3.0"
|
||||
|
||||
domutils@^3.1.0:
|
||||
version "3.2.2"
|
||||
resolved "https://registry.yarnpkg.com/domutils/-/domutils-3.2.2.tgz#edbfe2b668b0c1d97c24baf0f1062b132221bc78"
|
||||
integrity sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==
|
||||
dependencies:
|
||||
dom-serializer "^2.0.0"
|
||||
domelementtype "^2.3.0"
|
||||
domhandler "^5.0.3"
|
||||
|
||||
ee-first@1.1.1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d"
|
||||
@@ -269,6 +374,11 @@ encodeurl@~1.0.2:
|
||||
resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59"
|
||||
integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==
|
||||
|
||||
entities@^4.2.0, entities@^4.5.0:
|
||||
version "4.5.0"
|
||||
resolved "https://registry.yarnpkg.com/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48"
|
||||
integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==
|
||||
|
||||
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"
|
||||
@@ -378,6 +488,11 @@ function-bind@^1.1.2:
|
||||
resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c"
|
||||
integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==
|
||||
|
||||
gc-hook@^0.4.1:
|
||||
version "0.4.1"
|
||||
resolved "https://registry.yarnpkg.com/gc-hook/-/gc-hook-0.4.1.tgz#61e0ef4c5c2a13ae6f938cc2b0b9a71a94a12e68"
|
||||
integrity sha512-uiF+uUftDVLr+VRdudsdsT3/LQYnv2ntwhRH964O7xXDI57Smrek5olv75Wb8Nnz6U+7iVTRXsBlxKcsaDTJTQ==
|
||||
|
||||
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"
|
||||
@@ -425,6 +540,21 @@ hasown@^2.0.0:
|
||||
dependencies:
|
||||
function-bind "^1.1.2"
|
||||
|
||||
html-escaper@^3.0.3:
|
||||
version "3.0.3"
|
||||
resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-3.0.3.tgz#4d336674652beb1dcbc29ef6b6ba7f6be6fdfed6"
|
||||
integrity sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ==
|
||||
|
||||
htmlparser2@^9.1.0:
|
||||
version "9.1.0"
|
||||
resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-9.1.0.tgz#cdb498d8a75a51f739b61d3f718136c369bc8c23"
|
||||
integrity sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ==
|
||||
dependencies:
|
||||
domelementtype "^2.3.0"
|
||||
domhandler "^5.0.3"
|
||||
domutils "^3.1.0"
|
||||
entities "^4.5.0"
|
||||
|
||||
http-errors@2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.0.tgz#b7774a1486ef73cf7667ac9ae0858c012c57b9d3"
|
||||
@@ -446,6 +576,18 @@ http-terminator@^3.2.0:
|
||||
roarr "^7.0.4"
|
||||
type-fest "^2.3.3"
|
||||
|
||||
i18n@^0.15.1:
|
||||
version "0.15.1"
|
||||
resolved "https://registry.yarnpkg.com/i18n/-/i18n-0.15.1.tgz#68fb8993c461cc440bc2485d82f72019f2b92de8"
|
||||
integrity sha512-yue187t8MqUPMHdKjiZGrX+L+xcUsDClGO0Cz4loaKUOK9WrGw5pgan4bv130utOwX7fHE9w2iUeHFalVQWkXA==
|
||||
dependencies:
|
||||
"@messageformat/core" "^3.0.0"
|
||||
debug "^4.3.3"
|
||||
fast-printf "^1.6.9"
|
||||
make-plural "^7.0.0"
|
||||
math-interval-parser "^2.0.1"
|
||||
mustache "^4.2.0"
|
||||
|
||||
iconv-lite@0.4.24:
|
||||
version "0.4.24"
|
||||
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
|
||||
@@ -478,6 +620,16 @@ kysely@^0.27.3:
|
||||
resolved "https://registry.yarnpkg.com/kysely/-/kysely-0.27.3.tgz#6cc6c757040500b43c4ac596cdbb12be400ee276"
|
||||
integrity sha512-lG03Ru+XyOJFsjH3OMY6R/9U38IjDPfnOfDgO3ynhbDr+Dz8fak+X6L62vqu3iybQnj+lG84OttBuU9KY3L9kA==
|
||||
|
||||
make-plural@^7.0.0:
|
||||
version "7.4.0"
|
||||
resolved "https://registry.yarnpkg.com/make-plural/-/make-plural-7.4.0.tgz#fa6990dd550dea4de6b20163f74e5ed83d8a8d6d"
|
||||
integrity sha512-4/gC9KVNTV6pvYg2gFeQYTW3mWaoJt7WZE5vrp1KnQDgW92JtYZnzmZT81oj/dUTqAIu0ufI2x3dkgu3bB1tYg==
|
||||
|
||||
math-interval-parser@^2.0.1:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/math-interval-parser/-/math-interval-parser-2.0.1.tgz#e22cd6d15a0a7f4c03aec560db76513da615bed4"
|
||||
integrity sha512-VmlAmb0UJwlvMyx8iPhXUDnVW1F9IrGEd9CIOmv+XL8AErCUUuozoDMrgImvnYt2A+53qVX/tPW6YJurMKYsvA==
|
||||
|
||||
media-typer@0.3.0:
|
||||
version "0.3.0"
|
||||
resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"
|
||||
@@ -510,12 +662,17 @@ mime@1.6.0:
|
||||
resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1"
|
||||
integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==
|
||||
|
||||
moo@^0.5.1:
|
||||
version "0.5.2"
|
||||
resolved "https://registry.yarnpkg.com/moo/-/moo-0.5.2.tgz#f9fe82473bc7c184b0d32e2215d3f6e67278733c"
|
||||
integrity sha512-iSAJLHYKnX41mKcJKjqvnAN9sf0LMDTXDEvFv+ffuRR9a1MIuXLjMNL6EsnDHSkKLTWNqQQ5uo61P4EbU4NU+Q==
|
||||
|
||||
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:
|
||||
ms@2.1.3, 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==
|
||||
@@ -530,6 +687,11 @@ multiformats@^9.4.2, multiformats@^9.5.4, multiformats@^9.9.0:
|
||||
resolved "https://registry.yarnpkg.com/multiformats/-/multiformats-9.9.0.tgz#c68354e7d21037a8f1f8833c8ccd68618e8f1d37"
|
||||
integrity sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg==
|
||||
|
||||
mustache@^4.2.0:
|
||||
version "4.2.0"
|
||||
resolved "https://registry.yarnpkg.com/mustache/-/mustache-4.2.0.tgz#e5892324d60a12ec9c2a73359edca52972bf6f64"
|
||||
integrity sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==
|
||||
|
||||
negotiator@0.6.3:
|
||||
version "0.6.3"
|
||||
resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd"
|
||||
@@ -690,7 +852,7 @@ pino-std-serializers@^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:
|
||||
pino@^8.21.0:
|
||||
version "8.21.0"
|
||||
resolved "https://registry.yarnpkg.com/pino/-/pino-8.21.0.tgz#e1207f3675a2722940d62da79a7a55a98409f00d"
|
||||
integrity sha512-ip4qdzjkAyDDZklUaZkcRFb2iA118H9SgRh8yzTkSQK8HilsOJF7rSY8HoW5+I0M46AZgX/pxbprf2vvzQCE0Q==
|
||||
@@ -848,6 +1010,11 @@ safe-buffer@5.2.1, safe-buffer@~5.2.0:
|
||||
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"
|
||||
integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==
|
||||
|
||||
safe-identifier@^0.4.1:
|
||||
version "0.4.2"
|
||||
resolved "https://registry.yarnpkg.com/safe-identifier/-/safe-identifier-0.4.2.tgz#cf6bfca31c2897c588092d1750d30ef501d59fcb"
|
||||
integrity sha512-6pNbSMW6OhAi9j+N8V+U715yBQsaWJ7eyEUaOrawX+isg5ZxhUlV1NipNtgaKHmFGiABwt+ZF04Ii+3Xjkg+8w==
|
||||
|
||||
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"
|
||||
@@ -987,6 +1154,27 @@ typescript@^5.4.5:
|
||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.4.5.tgz#42ccef2c571fdbd0f6718b1d1f5e6e5ef006f611"
|
||||
integrity sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==
|
||||
|
||||
udomdiff@^1.1.2:
|
||||
version "1.1.2"
|
||||
resolved "https://registry.yarnpkg.com/udomdiff/-/udomdiff-1.1.2.tgz#2979769943afddfb1e6f40e8bda41e431bdcd813"
|
||||
integrity sha512-v+Z8Jal+GtmKGtJ34GIQlCJAxrDt9kbjpNsNvYoAXFyr4gNfWlD4uJJuoNNu/0UTVaKvQwHaSU095YDl71lKPw==
|
||||
|
||||
uhtml@^4.7.1:
|
||||
version "4.7.1"
|
||||
resolved "https://registry.yarnpkg.com/uhtml/-/uhtml-4.7.1.tgz#5a0e9c08aefb08c8e9d59e208d7d52f1646561e3"
|
||||
integrity sha512-2Nv8m2WTVBAmep42aYDnMDTRf87yHRWFSif9uEqkCB1fgX85q7rxTXkP6PNINCNUs9/KmQJ+RBgSH1BNZmxEtg==
|
||||
dependencies:
|
||||
"@webreflection/uparser" "^0.4.0"
|
||||
custom-function "^2.0.0"
|
||||
domconstants "^1.1.6"
|
||||
gc-hook "^0.4.1"
|
||||
html-escaper "^3.0.3"
|
||||
htmlparser2 "^9.1.0"
|
||||
udomdiff "^1.1.2"
|
||||
optionalDependencies:
|
||||
"@preact/signals-core" "^1.8.0"
|
||||
"@webreflection/signal" "^2.1.2"
|
||||
|
||||
uint8arrays@3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/uint8arrays/-/uint8arrays-3.0.0.tgz#260869efb8422418b6f04e3fac73a3908175c63b"
|
||||
@@ -1026,7 +1214,7 @@ xtend@^4.0.0:
|
||||
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==
|
||||
zod@^3.23.8:
|
||||
version "3.25.76"
|
||||
resolved "https://registry.yarnpkg.com/zod/-/zod-3.25.76.tgz#26841c3f6fd22a6a2760e7ccb719179768471e34"
|
||||
integrity sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==
|
||||
|
||||
Reference in New Issue
Block a user