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",
"pg": "^8.12.0",
"pino": "^9.2.0",
"uhtml": "^4.7.1",
"uint8arrays": "^5.1.0"
},
"devDependencies": {
+4 -2
View File
@@ -13,10 +13,12 @@ async function main() {
const link = await LinkService.create(cfg)
if (cfg.service.safelinkEnabled) {
cfg.eventCache.adaptiveFetchAndUpdate()
if (link.ctx.cfg.service.safelinkEnabled) {
link.ctx.safelinkClient.runFetchEvents()
}
console.log('here')
await link.start()
httpLogger.info('link service is running')
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 = {
id: 1,
eventType: '#addRule',
eventType: 'addRule',
url: 'https://malicious.example.com/phishing',
pattern: '#domain',
action: '#block',
reason: '#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.',
+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 {type ServiceConfig} from '../config'
import {SafelinkRule, RulePatternType} from '../db/schema'
import Database from '../db'
import {redirectLogger} from '../logger'
import {type ServiceConfig} from '../config.js'
import type Database from '../db/index.js'
import {type RulePatternType, type SafelinkRule} from '../db/schema.js'
import {redirectLogger} from '../logger.js'
export class SafelinkClient {
private domainCache: LRUCache<string, SafelinkRule | 'ok'>
@@ -12,14 +16,11 @@ export class SafelinkClient {
private db: Database
constructor({
db,
}: {
identifier: string
password: string
pdsHost: string
db: Database
}) {
private ozoneAgent: OzoneAgent
private cursor: string
constructor({cfg, db}: {cfg: ServiceConfig; db: Database}) {
this.domainCache = new LRUCache<string, SafelinkRule | 'ok'>({
max: 10000,
})
@@ -29,6 +30,14 @@ export class SafelinkClient {
})
this.db = db
this.ozoneAgent = new OzoneAgent(
cfg.ozoneUrl!,
cfg.ozoneAgentHandle!,
cfg.ozoneAgentPass!,
)
this.cursor = ''
}
public async tryFindRule(link: string): Promise<SafelinkRule | 'ok'> {
@@ -50,7 +59,7 @@ export class SafelinkClient {
}
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)
return maybeUrlRule
} catch (e) {
@@ -58,7 +67,7 @@ export class SafelinkClient {
}
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)
return maybeDomainRule
} catch (e) {
@@ -68,16 +77,21 @@ export class SafelinkClient {
return 'ok'
}
private getRule(url: string, pattern: RulePatternType) {
return this.db.db
private async getRule(
db: Database,
url: string,
pattern: RulePatternType,
): Promise<SafelinkRule> {
// @ts-ignore
return db.db
.selectFrom('safelink_rule')
.where('url', '=', url)
.where('pattern', '=', pattern)
.executeTakeFirstOrThrow()
}
private addRule(rule: SafelinkRule) {
this.db.db
private async addRule(db: Database, rule: SafelinkRule) {
db.db
.insertInto('safelink_rule')
.values(rule)
.execute()
@@ -88,17 +102,17 @@ export class SafelinkClient {
)
})
if (rule.pattern === '#domain') {
if (rule.pattern === 'domain') {
this.domainCache.set(rule.url, rule)
} else {
this.urlCache.set(rule.url, rule)
}
}
private async removeRule(rule: SafelinkRule) {
await this.db.db
private async removeRule(db: Database, rule: SafelinkRule) {
await db.db
.deleteFrom('safelink_rule')
.where('pattern', '=', '#domain')
.where('pattern', '=', 'domain')
.where('url', '=', rule.url)
.execute()
.catch(err => {
@@ -108,28 +122,103 @@ export class SafelinkClient {
)
})
if (rule.pattern === '#domain') {
if (rule.pattern === 'domain') {
this.domainCache.delete(rule.url)
} else {
this.urlCache.delete(rule.url)
}
}
public run() {
// poll and add/remove rules as needed
public async runFetchEvents() {
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 {
public session: CredentialSession
public agent: AtpAgent
private cfg: ServiceConfig
private identifier: string
private password: string
constructor(cfg: ServiceConfig) {
this.cfg = cfg
this.session = new CredentialSession(
new URL(cfg.ozoneUrl || 'http://localhost:2583'),
)
private session: CredentialSession
private agent: AtpAgent
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)
}
@@ -141,25 +230,19 @@ export class OzoneAgent {
}
public async getAgent(): Promise<AtpAgent> {
if (!this.cfg.ozoneAgentHandle && !this.cfg.ozoneAgentPass) {
if (!this.identifier && !this.password) {
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})
}
redirectLogger.info('creating Ozone session')
await this.session.login({
identifier: this.identifier,
password: this.password,
})
redirectLogger.info('ozone session created successfully')
}
return this.agent
-7
View File
@@ -1,11 +1,7 @@
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
}
@@ -96,11 +92,8 @@ export const envToCfg = (env: Environment): Config => {
},
}
const eventCache = new EventCache(serviceCfg)
return {
service: serviceCfg,
eventCache: eventCache,
db: dbCfg,
}
}
+5 -2
View File
@@ -1,5 +1,5 @@
import {SafelinkClient} from './cache/safelinkClient.js'
import {Config} from './config.js'
import {type Config} from './config.js'
import Database from './db/index.js'
export type AppContextOptions = {
@@ -16,7 +16,10 @@ export class AppContext {
constructor(private opts: AppContextOptions) {
this.cfg = this.opts.cfg
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>) {
+23 -6
View File
@@ -1,8 +1,9 @@
import {Selectable} from 'kysely'
import {type GeneratedAlways, type Selectable} from 'kysely'
export type DbSchema = {
link: Link
safelink_rule: SafelinkRule
safelink_cursor: SafelinkCursor
}
export interface Link {
@@ -15,10 +16,19 @@ export enum LinkType {
StarterPack = 1,
}
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 type RuleEventType =
| 'addRule'
| 'updateRule'
| '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 {
id: number
@@ -32,5 +42,12 @@ export interface SafelinkRule {
comment?: string
}
export interface SafelinkCursor {
id: GeneratedAlways<number>
cursor: string
createdAt: Date
}
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 {type Hole, html} from 'uhtml'
export function linkRedirectContents(link: string): Hole {
return html`
export function linkRedirectContents(link: string): string {
return `
<html>
<head>
<meta http-equiv="refresh" content="0; URL='${escapeHTML(link)}'" />
+23 -21
View File
@@ -1,6 +1,5 @@
import escapeHTML from 'escape-html'
import {type Request} from 'express'
import {type Hole, html} from 'uhtml'
export function linkWarningContents(
req: Request,
@@ -8,35 +7,38 @@ export function linkWarningContents(
type: 'warn' | 'block'
link: string
},
): Hole {
return html`
): string {
const continueButton =
opts.type === 'warn'
? `<a class="button secondary" href="${escapeHTML(opts.link)}">${req.__('Continue Anyway')}</a>`
: ''
return `
<div class="warning-icon">⚠️</div>
<h1>
${opts.type === 'warn'
? req.__('Potentially Dangerous Link')
: req.__('Blocked Link')}
${
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.',
)}
${
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
>
${continueButton}
<a class="button primary" href="https://bsky.app">${req.__('Return to Bluesky')}</a>
</div>
`
}
+3 -18
View File
@@ -1,11 +1,10 @@
import escapeHTML from 'escape-html'
import {type Hole, html} from 'uhtml'
export function linkWarningLayout(
title: string,
containerContents: Hole,
): Hole {
return html`
containerContents: string,
): string {
return `
<!DOCTYPE html>
<html>
<head>
@@ -23,7 +22,6 @@ export function linkWarningLayout(
padding: 0;
box-sizing: border-box;
}
body {
font-family:
-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Arial,
@@ -35,25 +33,21 @@ export function linkWarningLayout(
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;
@@ -61,7 +55,6 @@ export function linkWarningLayout(
margin-bottom: 24px;
padding: 0 20px;
}
.blocked-site {
background-color: #f7f9fa;
border-radius: 12px;
@@ -70,7 +63,6 @@ export function linkWarningLayout(
text-align: left;
word-break: break-all;
}
.site-name {
font-size: 16px;
font-weight: 500;
@@ -80,7 +72,6 @@ export function linkWarningLayout(
display: block;
text-align: center;
}
.site-url {
font-size: 14px;
color: #536471;
@@ -88,7 +79,6 @@ export function linkWarningLayout(
display: block;
text-align: center;
}
.button {
border: none;
border-radius: 24px;
@@ -100,23 +90,18 @@ export function linkWarningLayout(
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;
+10 -12
View File
@@ -2,7 +2,6 @@ import assert from 'node:assert'
import {DAY, SECOND} from '@atproto/common'
import {type Express} from 'express'
import {type Hole} from 'uhtml'
import {type AppContext} from '../context.js'
import {linkRedirectContents} from '../html/linkRedirectContents.js'
@@ -48,18 +47,17 @@ export default function (ctx: AppContext, app: Express) {
res.status(200)
res.type('html')
let hole: Hole | undefined
let html: string | undefined
if (ctx.cfg.service.safelinkEnabled) {
const rule = await ctx.safelinkClient.tryFindRule(link)
if (rule !== 'ok') {
switch (rule.action) {
case '#whitelist':
case 'whitelist':
redirectLogger.info(`Whitelist rule matched for ${rule.url}`)
break
case '#block':
hole = linkWarningLayout(
case 'block':
html = linkWarningLayout(
'Blocked Link Warning',
linkWarningContents(req, {
type: 'block',
@@ -69,8 +67,8 @@ export default function (ctx: AppContext, app: Express) {
res.setHeader('Cache-Control', 'no-store')
redirectLogger.info(`Block rule matched for ${rule.url}`)
break
case '#warn':
hole = linkWarningLayout(
case 'warn':
html = linkWarningLayout(
'Malicious Link Warning',
linkWarningContents(req, {
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 (!hole) {
hole = linkRedirectContents(url.href)
// If there is no html defined yet, we will create a redirect html
if (!html) {
html = linkRedirectContents(url.href)
}
return res.end(String(hole))
return res.end(html)
}),
)
}
-103
View File
@@ -98,11 +98,6 @@
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"
@@ -136,18 +131,6 @@
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"
@@ -281,11 +264,6 @@ 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"
@@ -329,41 +307,6 @@ 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"
@@ -374,11 +317,6 @@ 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"
@@ -488,11 +426,6 @@ 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"
@@ -540,21 +473,6 @@ 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"
@@ -1159,27 +1077,6 @@ 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"