rework
This commit is contained in:
@@ -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
@@ -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 () => {
|
||||||
|
|||||||
Vendored
-113
@@ -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())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Vendored
+5
-5
@@ -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.',
|
||||||
|
|||||||
Vendored
+130
-47
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>) {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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)}'" />
|
||||||
|
|||||||
@@ -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>
|
||||||
`
|
`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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)
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
Reference in New Issue
Block a user