This commit is contained in:
Hailey
2025-08-27 10:08:24 -07:00
parent 573b1f6ac2
commit 5809fdde51
13 changed files with 205 additions and 340 deletions
-1
View File
@@ -20,7 +20,6 @@
"lru-cache": "^11.1.0", "lru-cache": "^11.1.0",
"pg": "^8.12.0", "pg": "^8.12.0",
"pino": "^9.2.0", "pino": "^9.2.0",
"uhtml": "^4.7.1",
"uint8arrays": "^5.1.0" "uint8arrays": "^5.1.0"
}, },
"devDependencies": { "devDependencies": {
+4 -2
View File
@@ -13,10 +13,12 @@ async function main() {
const link = await LinkService.create(cfg) const link = await LinkService.create(cfg)
if (cfg.service.safelinkEnabled) { if (link.ctx.cfg.service.safelinkEnabled) {
cfg.eventCache.adaptiveFetchAndUpdate() link.ctx.safelinkClient.runFetchEvents()
} }
console.log('here')
await link.start() await link.start()
httpLogger.info('link service is running') httpLogger.info('link service is running')
process.on('SIGTERM', async () => { process.on('SIGTERM', async () => {
-113
View File
@@ -1,113 +0,0 @@
import {ToolsOzoneSafelinkDefs} from '@atproto/api'
import {type ServiceConfig} from '../config.js'
import {redirectLogger} from '../logger.js'
import {OzoneAgent} from './safelinkClient.js'
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
}
// 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())
}
}
+5 -5
View File
@@ -1,12 +1,12 @@
import {SafelinkRule} from '../db/schema' import {type SafelinkRule} from '../db/schema'
export const exampleRule: SafelinkRule = { export const exampleRule: SafelinkRule = {
id: 1, id: 1,
eventType: '#addRule', eventType: 'addRule',
url: 'https://malicious.example.com/phishing', url: 'https://malicious.example.com/phishing',
pattern: '#domain', pattern: 'domain',
action: '#block', action: 'block',
reason: '#phishing', reason: 'phishing',
createdBy: 'did:plc:adminozonetools', createdBy: 'did:plc:adminozonetools',
createdAt: '2024-06-01T12:00:00Z', createdAt: '2024-06-01T12:00:00Z',
comment: 'Known phishing domain detected by automated scan.', comment: 'Known phishing domain detected by automated scan.',
+130 -47
View File
@@ -1,10 +1,14 @@
import {Agent, AtpAgent, CredentialSession} from '@atproto/api' import {
AtpAgent,
CredentialSession,
type ToolsOzoneSafelinkQueryEvents,
} from '@atproto/api'
import {LRUCache} from 'lru-cache' import {LRUCache} from 'lru-cache'
import {type ServiceConfig} from '../config' import {type ServiceConfig} from '../config.js'
import {SafelinkRule, RulePatternType} from '../db/schema' import type Database from '../db/index.js'
import Database from '../db' import {type RulePatternType, type SafelinkRule} from '../db/schema.js'
import {redirectLogger} from '../logger' import {redirectLogger} from '../logger.js'
export class SafelinkClient { export class SafelinkClient {
private domainCache: LRUCache<string, SafelinkRule | 'ok'> private domainCache: LRUCache<string, SafelinkRule | 'ok'>
@@ -12,14 +16,11 @@ export class SafelinkClient {
private db: Database private db: Database
constructor({ private ozoneAgent: OzoneAgent
db,
}: { private cursor: string
identifier: string
password: string constructor({cfg, db}: {cfg: ServiceConfig; db: Database}) {
pdsHost: string
db: Database
}) {
this.domainCache = new LRUCache<string, SafelinkRule | 'ok'>({ this.domainCache = new LRUCache<string, SafelinkRule | 'ok'>({
max: 10000, max: 10000,
}) })
@@ -29,6 +30,14 @@ export class SafelinkClient {
}) })
this.db = db this.db = db
this.ozoneAgent = new OzoneAgent(
cfg.ozoneUrl!,
cfg.ozoneAgentHandle!,
cfg.ozoneAgentPass!,
)
this.cursor = ''
} }
public async tryFindRule(link: string): Promise<SafelinkRule | 'ok'> { public async tryFindRule(link: string): Promise<SafelinkRule | 'ok'> {
@@ -50,7 +59,7 @@ export class SafelinkClient {
} }
try { try {
const maybeUrlRule = await this.getRule(u.href, '#url') const maybeUrlRule = await this.getRule(this.db, u.href, 'url')
this.urlCache.set(u.href, maybeUrlRule) this.urlCache.set(u.href, maybeUrlRule)
return maybeUrlRule return maybeUrlRule
} catch (e) { } catch (e) {
@@ -58,7 +67,7 @@ export class SafelinkClient {
} }
try { try {
const maybeDomainRule = await this.getRule(u.href, '#domain') const maybeDomainRule = await this.getRule(this.db, u.href, 'domain')
this.domainCache.set(d.href, maybeDomainRule) this.domainCache.set(d.href, maybeDomainRule)
return maybeDomainRule return maybeDomainRule
} catch (e) { } catch (e) {
@@ -68,16 +77,21 @@ export class SafelinkClient {
return 'ok' return 'ok'
} }
private getRule(url: string, pattern: RulePatternType) { private async getRule(
return this.db.db db: Database,
url: string,
pattern: RulePatternType,
): Promise<SafelinkRule> {
// @ts-ignore
return db.db
.selectFrom('safelink_rule') .selectFrom('safelink_rule')
.where('url', '=', url) .where('url', '=', url)
.where('pattern', '=', pattern) .where('pattern', '=', pattern)
.executeTakeFirstOrThrow() .executeTakeFirstOrThrow()
} }
private addRule(rule: SafelinkRule) { private async addRule(db: Database, rule: SafelinkRule) {
this.db.db db.db
.insertInto('safelink_rule') .insertInto('safelink_rule')
.values(rule) .values(rule)
.execute() .execute()
@@ -88,17 +102,17 @@ export class SafelinkClient {
) )
}) })
if (rule.pattern === '#domain') { if (rule.pattern === 'domain') {
this.domainCache.set(rule.url, rule) this.domainCache.set(rule.url, rule)
} else { } else {
this.urlCache.set(rule.url, rule) this.urlCache.set(rule.url, rule)
} }
} }
private async removeRule(rule: SafelinkRule) { private async removeRule(db: Database, rule: SafelinkRule) {
await this.db.db await db.db
.deleteFrom('safelink_rule') .deleteFrom('safelink_rule')
.where('pattern', '=', '#domain') .where('pattern', '=', 'domain')
.where('url', '=', rule.url) .where('url', '=', rule.url)
.execute() .execute()
.catch(err => { .catch(err => {
@@ -108,28 +122,103 @@ export class SafelinkClient {
) )
}) })
if (rule.pattern === '#domain') { if (rule.pattern === 'domain') {
this.domainCache.delete(rule.url) this.domainCache.delete(rule.url)
} else { } else {
this.urlCache.delete(rule.url) this.urlCache.delete(rule.url)
} }
} }
public run() { public async runFetchEvents() {
// poll and add/remove rules as needed let agent: AtpAgent
try {
agent = await this.ozoneAgent.getAgent()
} catch (err) {
redirectLogger.error({error: err}, 'error getting Ozone agent')
setTimeout(() => this.runFetchEvents(), 10_000)
return
}
let res: ToolsOzoneSafelinkQueryEvents.Response
try {
const cursor = await this.getCursor()
res = await agent.tools.ozone.safelink.queryEvents({
cursor,
limit: 100,
})
} catch (err) {
redirectLogger.error(
{error: err},
'error fetching safelink events from Ozone',
)
setTimeout(() => this.runFetchEvents(), 10_000)
return
}
if (res.data.cursor === this.cursor) {
setTimeout(() => this.runFetchEvents(), 10_000)
} else {
await this.db.transaction(async db => {
for (const rule of res.data.events) {
if (rule.eventType === 'removeRule') {
await this.removeRule(db, rule)
} else {
await this.addRule(db, rule)
}
}
})
if (res.data.cursor) {
await this.setCursor(res.data.cursor)
}
setTimeout(() => this.runFetchEvents(), 1_000)
}
}
private async getCursor() {
if (this.cursor === '') {
// TODO: catch err
const res = await this.db.db
.selectFrom('safelink_cursor')
.orderBy('createdAt desc')
.limit(1)
.executeTakeFirst()
if (!res) {
return ''
}
this.cursor = res.cursor
}
return this.cursor
}
private async setCursor(cursor: string) {
try {
await this.db.db
.insertInto('safelink_cursor')
.values({
cursor,
createdAt: new Date(),
})
.execute()
} catch (err) {
redirectLogger.error({error: err}, 'failed to update safelink cursor')
}
} }
} }
export class OzoneAgent { export class OzoneAgent {
public session: CredentialSession private identifier: string
public agent: AtpAgent private password: string
private cfg: ServiceConfig
constructor(cfg: ServiceConfig) { private session: CredentialSession
this.cfg = cfg private agent: AtpAgent
this.session = new CredentialSession(
new URL(cfg.ozoneUrl || 'http://localhost:2583'), constructor(pdsHost: string, identifier: string, password: string) {
) this.identifier = identifier
this.password = password
this.session = new CredentialSession(new URL(pdsHost))
this.agent = new AtpAgent(this.session) this.agent = new AtpAgent(this.session)
} }
@@ -141,25 +230,19 @@ export class OzoneAgent {
} }
public async getAgent(): Promise<AtpAgent> { public async getAgent(): Promise<AtpAgent> {
if (!this.cfg.ozoneAgentHandle && !this.cfg.ozoneAgentPass) { if (!this.identifier && !this.password) {
throw new Error( throw new Error(
'OZONE_AGENT_HANDLE and OZONE_AGENT_PASS environment variables must be set', '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) { if (!this.session.hasSession) {
await this.session.login({identifier, password}) redirectLogger.info('creating Ozone session')
} await this.session.login({
identifier: this.identifier,
try { password: this.password,
await this.agent.com.atproto.server.getSession() })
} catch (err) { redirectLogger.info('ozone session created successfully')
if ((err as any).status === 401) {
await this.session.login({identifier, password})
}
} }
return this.agent return this.agent
-7
View File
@@ -1,11 +1,7 @@
import {envBool, 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 = { export type Config = {
service: ServiceConfig service: ServiceConfig
eventCache: EventCache
db: DbConfig db: DbConfig
} }
@@ -96,11 +92,8 @@ export const envToCfg = (env: Environment): Config => {
}, },
} }
const eventCache = new EventCache(serviceCfg)
return { return {
service: serviceCfg, service: serviceCfg,
eventCache: eventCache,
db: dbCfg, db: dbCfg,
} }
} }
+5 -2
View File
@@ -1,5 +1,5 @@
import {SafelinkClient} from './cache/safelinkClient.js' import {SafelinkClient} from './cache/safelinkClient.js'
import {Config} from './config.js' import {type Config} from './config.js'
import Database from './db/index.js' import Database from './db/index.js'
export type AppContextOptions = { export type AppContextOptions = {
@@ -16,7 +16,10 @@ export class AppContext {
constructor(private opts: AppContextOptions) { constructor(private opts: AppContextOptions) {
this.cfg = this.opts.cfg this.cfg = this.opts.cfg
this.db = this.opts.db this.db = this.opts.db
this.safelinkClient = new SafelinkClient({db: this.opts.db}) this.safelinkClient = new SafelinkClient({
cfg: this.opts.cfg.service,
db: this.opts.db,
})
} }
static async fromConfig(cfg: Config, overrides?: Partial<AppContextOptions>) { static async fromConfig(cfg: Config, overrides?: Partial<AppContextOptions>) {
+23 -6
View File
@@ -1,8 +1,9 @@
import {Selectable} from 'kysely' import {type GeneratedAlways, type Selectable} from 'kysely'
export type DbSchema = { export type DbSchema = {
link: Link link: Link
safelink_rule: SafelinkRule safelink_rule: SafelinkRule
safelink_cursor: SafelinkCursor
} }
export interface Link { export interface Link {
@@ -15,10 +16,19 @@ export enum LinkType {
StarterPack = 1, StarterPack = 1,
} }
export type RuleEventType = '#addRule' | '#updateRule' | '#removeRule' export type RuleEventType =
export type RulePatternType = '#domain' | '#url' | 'addRule'
export type RuleActionType = '#block' | '#warn' | '#whitelist' | 'updateRule'
export type RuleReasonType = '#csam' | '#spam' | '#phishing' | '#none' | 'removeRule'
| (string & {})
export type RulePatternType = 'domain' | 'url' | (string & {})
export type RuleActionType = 'block' | 'warn' | 'whitelist' | (string & {})
export type RuleReasonType =
| 'csam'
| 'spam'
| 'phishing'
| 'none'
| (string & {})
export interface SafelinkRule { export interface SafelinkRule {
id: number id: number
@@ -32,5 +42,12 @@ export interface SafelinkRule {
comment?: string comment?: string
} }
export interface SafelinkCursor {
id: GeneratedAlways<number>
cursor: string
createdAt: Date
}
export type LinkEntry = Selectable<Link> export type LinkEntry = Selectable<Link>
export type RuleEntry = Selectable<SafelinkRule> export type SafelinkRuleEntry = Selectable<SafelinkRule>
export type SafelinkCursorEntry = Selectable<SafelinkCursor>
+2 -3
View File
@@ -1,8 +1,7 @@
import escapeHTML from 'escape-html' import escapeHTML from 'escape-html'
import {type Hole, html} from 'uhtml'
export function linkRedirectContents(link: string): Hole { export function linkRedirectContents(link: string): string {
return html` return `
<html> <html>
<head> <head>
<meta http-equiv="refresh" content="0; URL='${escapeHTML(link)}'" /> <meta http-equiv="refresh" content="0; URL='${escapeHTML(link)}'" />
+17 -15
View File
@@ -1,6 +1,5 @@
import escapeHTML from 'escape-html' import escapeHTML from 'escape-html'
import {type Request} from 'express' import {type Request} from 'express'
import {type Hole, html} from 'uhtml'
export function linkWarningContents( export function linkWarningContents(
req: Request, req: Request,
@@ -8,35 +7,38 @@ export function linkWarningContents(
type: 'warn' | 'block' type: 'warn' | 'block'
link: string link: string
}, },
): Hole { ): string {
return html` const continueButton =
opts.type === 'warn'
? `<a class="button secondary" href="${escapeHTML(opts.link)}">${req.__('Continue Anyway')}</a>`
: ''
return `
<div class="warning-icon">⚠️</div> <div class="warning-icon">⚠️</div>
<h1> <h1>
${opts.type === 'warn' ${
opts.type === 'warn'
? req.__('Potentially Dangerous Link') ? req.__('Potentially Dangerous Link')
: req.__('Blocked Link')} : req.__('Blocked Link')
}
</h1> </h1>
<p class="warning-text"> <p class="warning-text">
${opts.type === 'warn' ${
opts.type === 'warn'
? req.__( ? req.__(
'This link may be malicious. You should proceed at your own risk.', 'This link may be malicious. You should proceed at your own risk.',
) )
: req.__( : req.__(
'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.',
)} )
}
</p> </p>
<div class="blocked-site"> <div class="blocked-site">
<p class="site-url">${escapeHTML(opts.link)}</p> <p class="site-url">${escapeHTML(opts.link)}</p>
</div> </div>
<div class="button-group"> <div class="button-group">
${opts.type === 'warn' ${continueButton}
? html`<a class="button secondary" href="${escapeHTML(opts.link)}" <a class="button primary" href="https://bsky.app">${req.__('Return to Bluesky')}</a>
>${req.__('Continue Anyway')}</a
>`
: null}
<a class="button primary" href="https://bsky.app"
>${req.__('Return to Bluesky')}</a
>
</div> </div>
` `
} }
+3 -18
View File
@@ -1,11 +1,10 @@
import escapeHTML from 'escape-html' import escapeHTML from 'escape-html'
import {type Hole, html} from 'uhtml'
export function linkWarningLayout( export function linkWarningLayout(
title: string, title: string,
containerContents: Hole, containerContents: string,
): Hole { ): string {
return html` return `
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
@@ -23,7 +22,6 @@ export function linkWarningLayout(
padding: 0; padding: 0;
box-sizing: border-box; box-sizing: border-box;
} }
body { body {
font-family: font-family:
-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Arial, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Arial,
@@ -35,25 +33,21 @@ export function linkWarningLayout(
justify-content: center; justify-content: center;
padding: 20px; padding: 20px;
} }
.container { .container {
width: 100%; width: 100%;
max-width: 400px; max-width: 400px;
text-align: center; text-align: center;
} }
.warning-icon { .warning-icon {
font-size: 48px; font-size: 48px;
margin-bottom: 16px; margin-bottom: 16px;
} }
h1 { h1 {
font-size: 20px; font-size: 20px;
font-weight: 600; font-weight: 600;
margin-bottom: 12px; margin-bottom: 12px;
color: #000000; color: #000000;
} }
.warning-text { .warning-text {
font-size: 15px; font-size: 15px;
color: #536471; color: #536471;
@@ -61,7 +55,6 @@ export function linkWarningLayout(
margin-bottom: 24px; margin-bottom: 24px;
padding: 0 20px; padding: 0 20px;
} }
.blocked-site { .blocked-site {
background-color: #f7f9fa; background-color: #f7f9fa;
border-radius: 12px; border-radius: 12px;
@@ -70,7 +63,6 @@ export function linkWarningLayout(
text-align: left; text-align: left;
word-break: break-all; word-break: break-all;
} }
.site-name { .site-name {
font-size: 16px; font-size: 16px;
font-weight: 500; font-weight: 500;
@@ -80,7 +72,6 @@ export function linkWarningLayout(
display: block; display: block;
text-align: center; text-align: center;
} }
.site-url { .site-url {
font-size: 14px; font-size: 14px;
color: #536471; color: #536471;
@@ -88,7 +79,6 @@ export function linkWarningLayout(
display: block; display: block;
text-align: center; text-align: center;
} }
.button { .button {
border: none; border: none;
border-radius: 24px; border-radius: 24px;
@@ -100,23 +90,18 @@ export function linkWarningLayout(
max-width: 280px; max-width: 280px;
transition: background-color 0.2s; transition: background-color 0.2s;
} }
.primary { .primary {
background-color: #1d9bf0; background-color: #1d9bf0;
color: white; color: white;
} }
.secondary { .secondary {
} }
.back-button:hover { .back-button:hover {
background-color: #1a8cd8; background-color: #1a8cd8;
} }
.back-button:active { .back-button:active {
background-color: #1681c4; background-color: #1681c4;
} }
@media (max-width: 480px) { @media (max-width: 480px) {
.warning-text { .warning-text {
padding: 0 10px; padding: 0 10px;
+10 -12
View File
@@ -2,7 +2,6 @@ import assert from 'node:assert'
import {DAY, SECOND} from '@atproto/common' import {DAY, SECOND} from '@atproto/common'
import {type Express} from 'express' import {type Express} from 'express'
import {type Hole} from 'uhtml'
import {type AppContext} from '../context.js' import {type AppContext} from '../context.js'
import {linkRedirectContents} from '../html/linkRedirectContents.js' import {linkRedirectContents} from '../html/linkRedirectContents.js'
@@ -48,18 +47,17 @@ export default function (ctx: AppContext, app: Express) {
res.status(200) res.status(200)
res.type('html') res.type('html')
let hole: Hole | undefined let html: string | undefined
if (ctx.cfg.service.safelinkEnabled) { if (ctx.cfg.service.safelinkEnabled) {
const rule = await ctx.safelinkClient.tryFindRule(link) const rule = await ctx.safelinkClient.tryFindRule(link)
if (rule !== 'ok') { if (rule !== 'ok') {
switch (rule.action) { switch (rule.action) {
case '#whitelist': case 'whitelist':
redirectLogger.info(`Whitelist rule matched for ${rule.url}`) redirectLogger.info(`Whitelist rule matched for ${rule.url}`)
break break
case '#block': case 'block':
hole = linkWarningLayout( html = linkWarningLayout(
'Blocked Link Warning', 'Blocked Link Warning',
linkWarningContents(req, { linkWarningContents(req, {
type: 'block', type: 'block',
@@ -69,8 +67,8 @@ export default function (ctx: AppContext, app: Express) {
res.setHeader('Cache-Control', 'no-store') res.setHeader('Cache-Control', 'no-store')
redirectLogger.info(`Block rule matched for ${rule.url}`) redirectLogger.info(`Block rule matched for ${rule.url}`)
break break
case '#warn': case 'warn':
hole = linkWarningLayout( html = linkWarningLayout(
'Malicious Link Warning', 'Malicious Link Warning',
linkWarningContents(req, { linkWarningContents(req, {
type: 'warn', type: 'warn',
@@ -88,12 +86,12 @@ export default function (ctx: AppContext, app: Express) {
} }
} }
// If there is no hole defined yet, we will create a redirect hole // If there is no html defined yet, we will create a redirect html
if (!hole) { if (!html) {
hole = linkRedirectContents(url.href) html = linkRedirectContents(url.href)
} }
return res.end(String(hole)) return res.end(html)
}), }),
) )
} }
-103
View File
@@ -98,11 +98,6 @@
dependencies: dependencies:
make-plural "^7.0.0" 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": "@types/cors@^2.8.17":
version "2.8.17" version "2.8.17"
resolved "https://registry.yarnpkg.com/@types/cors/-/cors-2.8.17.tgz#5d718a5e494a8166f569d986794e49c48b216b2b" resolved "https://registry.yarnpkg.com/@types/cors/-/cors-2.8.17.tgz#5d718a5e494a8166f569d986794e49c48b216b2b"
@@ -136,18 +131,6 @@
pg-protocol "*" pg-protocol "*"
pg-types "^4.0.1" 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: abort-controller@^3.0.0:
version "3.0.0" version "3.0.0"
resolved "https://registry.yarnpkg.com/abort-controller/-/abort-controller-3.0.0.tgz#eaf54d53b62bae4138e809ca225c8439a6efb392" resolved "https://registry.yarnpkg.com/abort-controller/-/abort-controller-3.0.0.tgz#eaf54d53b62bae4138e809ca225c8439a6efb392"
@@ -281,11 +264,6 @@ cors@^2.8.5:
object-assign "^4" object-assign "^4"
vary "^1" 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: debug@2.6.9:
version "2.6.9" version "2.6.9"
resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
@@ -329,41 +307,6 @@ detect-libc@^2.0.1:
resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.0.3.tgz#f0cd503b40f9939b894697d19ad50895e30cf700" resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.0.3.tgz#f0cd503b40f9939b894697d19ad50895e30cf700"
integrity sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw== 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: ee-first@1.1.1:
version "1.1.1" version "1.1.1"
resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d"
@@ -374,11 +317,6 @@ encodeurl@~1.0.2:
resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59"
integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w== 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: es-define-property@^1.0.0:
version "1.0.0" version "1.0.0"
resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.0.tgz#c7faefbdff8b2696cf5f46921edfb77cc4ba3845" resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.0.tgz#c7faefbdff8b2696cf5f46921edfb77cc4ba3845"
@@ -488,11 +426,6 @@ function-bind@^1.1.2:
resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c"
integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== 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: get-intrinsic@^1.1.3, get-intrinsic@^1.2.4:
version "1.2.4" version "1.2.4"
resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.4.tgz#e385f5a4b5227d449c3eabbad05494ef0abbeadd" resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.4.tgz#e385f5a4b5227d449c3eabbad05494ef0abbeadd"
@@ -540,21 +473,6 @@ hasown@^2.0.0:
dependencies: dependencies:
function-bind "^1.1.2" 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: http-errors@2.0.0:
version "2.0.0" version "2.0.0"
resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.0.tgz#b7774a1486ef73cf7667ac9ae0858c012c57b9d3" resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.0.tgz#b7774a1486ef73cf7667ac9ae0858c012c57b9d3"
@@ -1159,27 +1077,6 @@ typescript@^5.4.5:
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.4.5.tgz#42ccef2c571fdbd0f6718b1d1f5e6e5ef006f611" resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.4.5.tgz#42ccef2c571fdbd0f6718b1d1f5e6e5ef006f611"
integrity sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ== 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: uint8arrays@3.0.0:
version "3.0.0" version "3.0.0"
resolved "https://registry.yarnpkg.com/uint8arrays/-/uint8arrays-3.0.0.tgz#260869efb8422418b6f04e3fac73a3908175c63b" resolved "https://registry.yarnpkg.com/uint8arrays/-/uint8arrays-3.0.0.tgz#260869efb8422418b6f04e3fac73a3908175c63b"