From 5e8ee142b1f25a932838d7e0621e9fd3fbb64cbe Mon Sep 17 00:00:00 2001 From: will berry Date: Tue, 10 Jun 2025 15:37:31 -0500 Subject: [PATCH] preliminary review ready impl of safelink --- bskylink/.swp | Bin 0 -> 12288 bytes bskylink/src/bin.ts | 10 +- bskylink/src/cache/cache.ts | 256 +++++++++++++++++++++++++ bskylink/src/cache/ozoneAgent.ts | 49 +++++ bskylink/src/cache/rule.ts | 29 +++ bskylink/src/config.ts | 22 +++ bskylink/src/index.ts | 6 +- bskylink/src/logger.ts | 18 +- bskylink/src/routes/createShortLink.ts | 13 +- bskylink/src/routes/index.ts | 4 +- bskylink/src/routes/redirect.ts | 228 +++++++++++++++++++++- bskylink/tsconfig.json | 27 ++- 12 files changed, 633 insertions(+), 29 deletions(-) create mode 100644 bskylink/.swp create mode 100644 bskylink/src/cache/cache.ts create mode 100644 bskylink/src/cache/ozoneAgent.ts create mode 100644 bskylink/src/cache/rule.ts diff --git a/bskylink/.swp b/bskylink/.swp new file mode 100644 index 0000000000000000000000000000000000000000..8c3c63a12c3a78df82ac73209b28a844bb4b5114 GIT binary patch literal 12288 zcmeI&&uZH+7yxj0w;cv!djW4d^yGg@8)!g>W-*;<>V!BB>=a~6BqEAsBspQH9rhr7 z#=XI0$H{hC2^1PApCOSy$@=4;z63&?`Sa+(zHtM~9PjJ5{qdVy)=p)I@X2z=I9JZY zVB%t}xyUhh3#y;&0*xR50xvG`Htlu-zn=EX=TG*>59y1q044+h5C8!X009sH0T2KI z5ICQJE-$Q~X8Jen5-$Ezo^ORPGYEhH2!H?xfB*=900@8p2!H?xfWUJFFtMy(?=0)l zyz2k|55NDL__>2ae-Hox5C8!X009sH0T2LzlLd&#*HqfFDs0DD38f3Xp{RG&>s=F) z(T-0{eWj>Gg&@?N(_)Jzx3h1^tCaMbCviQMv7-M^d<_3KG*$c1oVMe{<6*d%htnh) z$Ef4GLEr{$)A!qHd~6EbPR9*y+NMzy-!CTP=?ry!-)}2l%Y8H?VLV5r6r{>E69=qx zzDSdB5seq=G>Y#~edk-jE3;i5E=!MKjZKw#Dtag68fB7^WxZi_M3`(YvYpbd(fX~N zq)Ox0Lf7QQa9~m+ncYpp#5j%6O6yYfy?vtA`Q5+1>UP_tIRm!T=7VIW=QyJrT?)B$ zMM;ZX6dJRFO69T*UQ(A(l}lFE`;P`{{lgxyVr?f>Zdpz*PZ)rk>k35`O5;)VHH?#R zcuF@VDw)$G+mh*3mASdtcp0y#S%{-V&M^1(g<~?W9@cz+Xl63el)3E?J2O{uv*Nnl F`U9-Hg6{wT literal 0 HcmV?d00001 diff --git a/bskylink/src/bin.ts b/bskylink/src/bin.ts index 17f068841b..f799fe9ef3 100644 --- a/bskylink/src/bin.ts +++ b/bskylink/src/bin.ts @@ -1,5 +1,5 @@ +import {adaptiveFetchAndUpdate, eventCache} from './cache/cache.js' import {Database, envToCfg, httpLogger, LinkService, readEnv} from './index.js' - async function main() { const env = readEnv() const cfg = envToCfg(env) @@ -11,9 +11,17 @@ async function main() { await migrateDb.migrateToLatestOrThrow() await migrateDb.close() } + const link = await LinkService.create(cfg) + + if (cfg.service.safelink === 1) { + eventCache.init(cfg.service) + adaptiveFetchAndUpdate() + } + await link.start() httpLogger.info('link service is running') + console.log('running service') process.on('SIGTERM', async () => { httpLogger.info('link service is stopping') await link.destroy() diff --git a/bskylink/src/cache/cache.ts b/bskylink/src/cache/cache.ts new file mode 100644 index 0000000000..f22a6868e6 --- /dev/null +++ b/bskylink/src/cache/cache.ts @@ -0,0 +1,256 @@ +import {ToolsOzoneSafelinkDefs} from '@atproto/api' + +import {type ServiceConfig} from '../config.js' +import {redirectLogger} from '../logger.js' +// import {ozoneAgent} from './agent.js' +import {OzoneAgent} from './ozoneAgent.js' +let cacheCursor: string | undefined + +export class EventCache { + private rules = new Map() + private cfg: ServiceConfig | undefined = undefined + + constructor() { + this.cfg = undefined + } + async init(cfg: ServiceConfig) { + this.cfg = cfg + } + + async getConfig(): Promise { + return this.cfg + } + + insert(key: string, evt: ToolsOzoneSafelinkDefs.Event) { + const existing = this.rules.get(key) + if (!existing || new Date(evt.createdAt) > new Date(existing.createdAt)) { + this.rules.set(key, evt) + } + } + + smartUpdateDomain(event: ToolsOzoneSafelinkDefs.Event) { + try { + const domain = new URL(event.url).hostname + event.url = domain + + redirectLogger.info( + `[EventCache] smartUpdateDomain called for domain: ${domain}, action: ${event.action}`, + ) + + if (event.action === ToolsOzoneSafelinkDefs.REMOVERULE) { + // If the action is to remove the rule, delete it from the cache + this.insert(domain, event) + redirectLogger.info( + `[EventCache] Removed rule for domain, adjusted audit log: ${domain}`, + ) + return + } + // if update should happen + if (event.action === ToolsOzoneSafelinkDefs.WHITELIST) { + this.insert(domain, event) + redirectLogger.info(`[EventCache] Whitelisted domain: ${domain}`) + return + } + if (event.action === ToolsOzoneSafelinkDefs.BLOCK) { + this.insert(domain, event) + redirectLogger.info(`[EventCache] Blocked domain: ${domain}`) + return + } + if (event.action === ToolsOzoneSafelinkDefs.WARN) { + this.insert(domain, event) + redirectLogger.info(`[EventCache] Warned domain: ${domain}`) + return + } + } catch (error) { + redirectLogger.error( + `[EventCache:smartUpdateDomain] Error in smartUpdateDomain: ${error}`, + ) + throw new Error( + `[EventCache:smartUpdateDomain] Error processing domain event: ${error}`, + ) + } + } + + smartUpdateUrl(event: ToolsOzoneSafelinkDefs.Event) { + redirectLogger.info( + `[EventCache] smartUpdateUrl called for url: ${event.url}, action: ${event.action}`, + ) + if (event.action === ToolsOzoneSafelinkDefs.REMOVERULE) { + // If the action is to remove the rule, delete it from the cache + this.insert(event.url, event) + redirectLogger.info(`[EventCache] Removed rule for url: ${event.url}`) + return + } + // if update should happen + if (event.action === ToolsOzoneSafelinkDefs.WHITELIST) { + this.insert(event.url, event) + redirectLogger.info(`[EventCache] Whitelisted url: ${event.url}`) + return + } + if (event.action === ToolsOzoneSafelinkDefs.BLOCK) { + this.insert(event.url, event) + redirectLogger.info(`[EventCache] Blocked url: ${event.url}`) + return + } + if (event.action === ToolsOzoneSafelinkDefs.WARN) { + this.insert(event.url, event) + redirectLogger.info(`[EventCache] Warned url: ${event.url}`) + return + } + } + + // Insert or update an event + smartUpdate(event: ToolsOzoneSafelinkDefs.Event) { + if (event.pattern === ToolsOzoneSafelinkDefs.DOMAIN) { + redirectLogger.info( + `[EventCache] smartUpdate called for domain event: ${event.url}, performing$ ${event.action}`, + ) + return this.smartUpdateDomain(event) + } + if (event.pattern === ToolsOzoneSafelinkDefs.URL) { + redirectLogger.info( + `[EventCache] smartUpdate called for domain event: ${event.url}`, + ) + return this.smartUpdateUrl(event) + } + throw new Error('[EventCache] Unknown event pattern') + } + + /** + * Attempts to retrieve an event for the given URL. + * Checks in order: domain, domain+path, then full URL. + */ + smartGet(url: string): ToolsOzoneSafelinkDefs.Event | undefined { + const parsedUrl = new URL(url) + const domain = parsedUrl.hostname + const domainAndPath = domain + parsedUrl.pathname + + // 1. Check for a rule by domain only + const byDomain = this.rules.get(domain) + if (byDomain) { + return byDomain + } + + // 2. Check for a rule by domain + path + const byDomainAndPath = this.rules.get(domainAndPath) + if (byDomainAndPath) { + return byDomainAndPath + } + + // 3. Check for a rule by full URL + return this.rules.get(url) + } + + delete(event: ToolsOzoneSafelinkDefs.Event) { + this.rules.delete(event.url) + } + + // Get an event by full URL + get(url: string): ToolsOzoneSafelinkDefs.Event | undefined { + const event = this.rules.get(url) + return event + } + + // List all events + list(): ToolsOzoneSafelinkDefs.Event[] { + return Array.from(this.rules.values()) + } +} + +// Adaptive polling: slow down if no new events, speed up if updates found +let pollInterval = 1 * 1000 // start at 1 seconds + +export async function adaptiveFetchAndUpdate() { + const prevCursor = cacheCursor + const eventConfig = await eventCache.getConfig() + if (eventConfig === undefined) { + redirectLogger.info( + `[adaptiveFetchAndUpdate] No Configuration found, skipping fetch.`, + ) + } else { + await fetchAndUpdateEvents(eventConfig) + } + // If no new events, increase interval (up to 10 minutes), else reset to 5s + if (cacheCursor === prevCursor) { + pollInterval = Math.min(pollInterval * 2, 10 * 60 * 1000) + redirectLogger.info( + `[adaptiveFetchAndUpdate] No new events, backing off. Next poll in ${ + pollInterval / 1000 + }s`, + ) + } else { + pollInterval = 5 * 1000 + redirectLogger.info( + `[adaptiveFetchAndUpdate] New events found, resetting poll interval to ${ + pollInterval / 1000 + }s`, + ) + } + setTimeout(adaptiveFetchAndUpdate, pollInterval) +} + +// Export a singleton instance +export const eventCache = new EventCache() +// Start adaptive polling +// adaptiveFetchAndUpdate() + +// Function to fetch and update events from the server +export async function 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}`, + ) + // Use current session DID instead of env variable + const ozoneAgent = await 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, // Adjust as needed + }) + + 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) { + eventCache.smartUpdate(event) + } + + cacheCursor = res.data?.cursor + + redirectLogger.info( + '[eventCache:fetchAndUpdateEvents] Current cache contents:', + ) + redirectLogger.info(eventCache.list()) +} diff --git a/bskylink/src/cache/ozoneAgent.ts b/bskylink/src/cache/ozoneAgent.ts new file mode 100644 index 0000000000..4bbbd4865b --- /dev/null +++ b/bskylink/src/cache/ozoneAgent.ts @@ -0,0 +1,49 @@ +import {AtpAgent, CredentialSession} from '@atproto/api' + +import {type ServiceConfig} from '../config' + +export class OzoneAgent { + public session: CredentialSession + public agent: AtpAgent + private cfg?: ServiceConfig + + constructor(cfg: ServiceConfig) { + this.cfg = cfg + this.session = new CredentialSession( + new URL(cfg?.ozoneUrl || 'http://localhost:2583'), + ) + this.agent = new AtpAgent(this.session) + } + + public async getSession(): Promise { + if (!this.session.hasSession) { + await this.getAgent() + } + return this.session + } + + public async getAgent(): Promise { + if (!this.cfg?.ozoneAgentHandle && !this.cfg?.ozoneAgentPass) { + throw new Error( + 'OZONE_AGENT_HANDLE and OZONE_AGENT_PASS environment variables must be set', + ) + } + + const identifier = this.cfg?.ozoneAgentHandle || 'did:plc:invalid' + const password = this.cfg?.ozoneAgentPass || 'invalid' + + if (!this.session.hasSession) { + await this.session.login({identifier, password}) + } + + try { + await this.agent.com.atproto.server.getSession() + } catch (err) { + if ((err as any).status === 401) { + await this.session.login({identifier, password}) + } + } + + return this.agent + } +} diff --git a/bskylink/src/cache/rule.ts b/bskylink/src/cache/rule.ts new file mode 100644 index 0000000000..d5831da794 --- /dev/null +++ b/bskylink/src/cache/rule.ts @@ -0,0 +1,29 @@ +export type RuleEventType = '#addRule' | '#updateRule' | '#removeRule' +export type RulePatternType = '#domain' | '#url' +export type RuleActionType = '#block' | '#warn' | '#whitelist' +export type RuleReasonType = '#csam' | '#spam' | '#phishing' | '#none' + +export interface Rule { + id: number + eventType: RuleEventType + url: string + pattern: RulePatternType + action: RuleActionType + reason: RuleReasonType + createdBy: string // DID format + createdAt: string // ISO datetime string + comment?: string +} + +// Example Rule object +export const exampleRule: Rule = { + id: 1, + eventType: '#addRule', + url: 'https://malicious.example.com/phishing', + pattern: '#domain', + action: '#block', + reason: '#phishing', + createdBy: 'did:plc:adminozonetools', + createdAt: '2024-06-01T12:00:00Z', + comment: 'Known phishing domain detected by automated scan.', +} diff --git a/bskylink/src/config.ts b/bskylink/src/config.ts index ce409cccca..e31aafe572 100644 --- a/bskylink/src/config.ts +++ b/bskylink/src/config.ts @@ -1,7 +1,11 @@ import {envInt, envList, envStr} from '@atproto/common' +// import { type EventCache, eventCache } from '../cache/cache.js' +import {type EventCache, eventCache} from './cache/cache.js' + export type Config = { service: ServiceConfig + eventCache: EventCache db: DbConfig } @@ -10,6 +14,10 @@ export type ServiceConfig = { version?: string hostnames: string[] appHostname: string + safelink: number + ozoneUrl?: string + ozoneAgentHandle?: string + ozoneAgentPass?: string } export type DbConfig = { @@ -36,6 +44,10 @@ export type Environment = { dbPostgresPoolSize?: number dbPostgresPoolMaxUses?: number dbPostgresPoolIdleTimeoutMs?: number + safelink?: number + ozoneUrl?: string + ozoneAgentHandle?: string + ozoneAgentPass?: string } export const readEnv = (): Environment => { @@ -52,6 +64,10 @@ export const readEnv = (): Environment => { dbPostgresPoolIdleTimeoutMs: envInt( 'LINK_DB_POSTGRES_POOL_IDLE_TIMEOUT_MS', ), + safelink: envInt('SAFELINK'), + ozoneUrl: envStr('OZONE_URL'), + ozoneAgentHandle: envStr('OZONE_AGENT_HANDLE'), + ozoneAgentPass: envStr('OZONE_AGENT_PASS'), } } @@ -61,6 +77,10 @@ export const envToCfg = (env: Environment): Config => { version: env.version, hostnames: env.hostnames, appHostname: env.appHostname || 'bsky.app', + safelink: env.safelink || 0, + ozoneUrl: env.ozoneUrl || undefined, + ozoneAgentHandle: env.ozoneAgentHandle || undefined, + ozoneAgentPass: env.ozoneAgentPass || undefined, } if (!env.dbPostgresUrl) { throw new Error('Must configure postgres url (LINK_DB_POSTGRES_URL)') @@ -75,8 +95,10 @@ export const envToCfg = (env: Environment): Config => { size: env.dbPostgresPoolSize ?? 10, }, } + return { service: serviceCfg, + eventCache: eventCache, db: dbCfg, } } diff --git a/bskylink/src/index.ts b/bskylink/src/index.ts index ca425eee8c..f19f4149a7 100644 --- a/bskylink/src/index.ts +++ b/bskylink/src/index.ts @@ -1,11 +1,11 @@ import events from 'node:events' -import http from 'node:http' +import type http from 'node:http' import cors from 'cors' import express from 'express' -import {createHttpTerminator, HttpTerminator} from 'http-terminator' +import {createHttpTerminator, type HttpTerminator} from 'http-terminator' -import {Config} from './config.js' +import {type Config} from './config.js' import {AppContext} from './context.js' import {default as routes, errorHandler} from './routes/index.js' diff --git a/bskylink/src/logger.ts b/bskylink/src/logger.ts index 25bb590a1d..afcb5f7f64 100644 --- a/bskylink/src/logger.ts +++ b/bskylink/src/logger.ts @@ -1,4 +1,18 @@ import {subsystemLogger} from '@atproto/common' +import {type Logger} from 'pino' -export const httpLogger = subsystemLogger('bskylink') -export const dbLogger = subsystemLogger('bskylink:db') +export const httpLogger: Logger = subsystemLogger('bskylink') +export const dbLogger: Logger = subsystemLogger('bskylink:db') +export const redirectLogger: Logger = subsystemLogger('bskylink:redirect') + +// Also log to stdout +redirectLogger.info = ( + orig => + (...args: any[]) => { + const [msg, ...rest] = args + orig.apply(redirectLogger, [String(msg), ...rest]) + // Print to stdout as well + // You can format this as needed + console.log('[bskylink:redirect]', ...args) + } +)(redirectLogger.info) as typeof redirectLogger.info diff --git a/bskylink/src/routes/createShortLink.ts b/bskylink/src/routes/createShortLink.ts index db7c3f8090..411cbb7c67 100644 --- a/bskylink/src/routes/createShortLink.ts +++ b/bskylink/src/routes/createShortLink.ts @@ -1,9 +1,9 @@ import assert from 'node:assert' import bodyParser from 'body-parser' -import {Express, Request} from 'express' +import {type Express, type Request} from 'express' -import {AppContext} from '../context.js' +import {type AppContext} from '../context.js' import {LinkType} from '../db/schema.js' import {randomId} from '../util.js' import {handler} from './util.js' @@ -78,13 +78,14 @@ const getUrl = (ctx: AppContext, req: Request, id: string) => { if (!ctx.cfg.service.hostnames.length) { assert(req.headers.host, 'request must be made with host header') const baseUrl = - req.protocol === 'http' && req.headers.host.startsWith('localhost:') + req.protocol === 'http' && req.headers.host!.startsWith('localhost:') ? `http://${req.headers.host}` : `https://${req.headers.host}` return `${baseUrl}/${id}` } - const baseUrl = ctx.cfg.service.hostnames.includes(req.headers.host) - ? `https://${req.headers.host}` + const host = req.headers.host ?? '' + const baseUrl = ctx.cfg.service.hostnames.includes(host) + ? `https://${host}` : `https://${ctx.cfg.service.hostnames[0]}` return `${baseUrl}/${id}` } @@ -94,7 +95,7 @@ const normalizedPathFromParts = (parts: string[]): string => { '/' + parts .map(encodeURIComponent) - .map(part => part.replaceAll('%3A', ':')) // preserve colons + .map(part => part.replace(/%3A/g, ':')) // preserve colons .join('/') ) } diff --git a/bskylink/src/routes/index.ts b/bskylink/src/routes/index.ts index 9fd20d276b..d0122ff8bf 100644 --- a/bskylink/src/routes/index.ts +++ b/bskylink/src/routes/index.ts @@ -1,6 +1,6 @@ -import {Express} from 'express' +import {type Express} from 'express' -import {AppContext} from '../context.js' +import {type AppContext} from '../context.js' import {default as createShortLink} from './createShortLink.js' import {default as health} from './health.js' import {default as redirect} from './redirect.js' diff --git a/bskylink/src/routes/redirect.ts b/bskylink/src/routes/redirect.ts index 468d250192..274a45696f 100644 --- a/bskylink/src/routes/redirect.ts +++ b/bskylink/src/routes/redirect.ts @@ -1,10 +1,14 @@ import assert from 'node:assert' +import {ToolsOzoneSafelinkDefs} from '@atproto/api' import {DAY, SECOND} from '@atproto/common' import escapeHTML from 'escape-html' -import {Express} from 'express' +import {type Express} from 'express' -import {AppContext} from '../context.js' +// IMPORTANT: Ensure this import path matches exactly (including casing and extension) everywhere it's used +// Ensure this import path matches exactly everywhere in your project to avoid module duplication +import {type AppContext} from '../context.js' +import {redirectLogger} from '../logger.js' import {handler} from './util.js' const INTERNAL_IP_REGEX = new RegExp( @@ -41,12 +45,224 @@ export default function (ctx: AppContext, app: Express) { res.setHeader('Cache-Control', `max-age=${(7 * DAY) / SECOND}`) res.type('html') - res.status(200) + res.status(302) + + if (ctx.cfg.service.safelink) { + const rulePresent: ToolsOzoneSafelinkDefs.Event | undefined = + ctx.cfg.eventCache.smartGet(link) + + // begin link safety checks + if ( + rulePresent && + rulePresent.eventType === ToolsOzoneSafelinkDefs.REMOVERULE + ) { + redirectLogger.info( + `No rule or remove rule matched for ${rulePresent.url}`, + ) + const escaped = escapeHTML(url.href) + + return safe_redirect(escaped) + } + + if ( + rulePresent && + rulePresent.action === ToolsOzoneSafelinkDefs.WHITELIST + ) { + redirectLogger.info(`Whitelist rule matched for ${rulePresent.url}`) + const escaped = escapeHTML(url.href) + + return safe_redirect(escaped) + } + + if ( + rulePresent && + rulePresent.action === ToolsOzoneSafelinkDefs.BLOCK + ) { + redirectLogger.info(`Block rule matched for ${rulePresent.url}`) + res.setHeader('Cache-Control', 'no-store') + res.status(403) + return res.send( + warnRedirect( + 'Blocked Link', + 'This link has been identified as malicious, it has been blocked to protect your account and data', + 'Go Back To BlueSky', + 'DANGER', + escapeHTML(url.toString()), + `https://${ctx.cfg.service.appHostname}`, + ), + ) + } + + if (rulePresent && rulePresent.action === ToolsOzoneSafelinkDefs.WARN) { + redirectLogger.info(`Warn rule matched for ${rulePresent.url}`) + res.setHeader('Cache-Control', 'no-store') + res.status(403) + const escaped = escapeHTML(url.href) + return res.send( + warnRedirect( + 'Warning: Potentially Malicious Link', + 'This link could be malicious, proceed at your own risk', + 'Proceed at Your Own Risk', + 'WARNING', + escapeHTML(url.toString()), + escaped, // Pass the actual URL as returnUrl so the button proceeds to the link + ), + ) + } + } const escaped = escapeHTML(url.href) - return res.send( - ``, - ) + return safe_redirect(escaped) }), ) } + +const safe_redirect = (escaped: string) => + ` + + + + + + ` + +const warnRedirect = ( + mainText: string, + warningText: string, + buttonText: string, + reason: string, + siteUrl: string, + returnUrl = 'https://bsky.app', +) => { + return ` + + + + + + + + ${mainText} + + + +
+
⚠️
+

${mainText}

+

${escapeHTML(warningText)}

+
+ ${escapeHTML(reason)} + ${escapeHTML(siteUrl)} +
+ +
+ + + ` +} diff --git a/bskylink/tsconfig.json b/bskylink/tsconfig.json index 3c382acc41..a13b320338 100644 --- a/bskylink/tsconfig.json +++ b/bskylink/tsconfig.json @@ -1,10 +1,19 @@ { - "compilerOptions": { - "module": "NodeNext", - "esModuleInterop": true, - "moduleResolution": "NodeNext", - "outDir": "dist", - "lib": ["ES2021.String"] - }, - "include": ["./src/index.ts", "./src/bin.ts"] - } + "compilerOptions": { + "target": "ES2020", + "module": "ESNext", + "moduleResolution": "bundler", + "allowSyntheticDefaultImports": true, + "esModuleInterop": true, + "skipLibCheck": true, + "strict": true, + "outDir": "./dist", + "rootDir": "./src", + "declaration": true, + "declarationMap": true, + "sourceMap": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} +