From c6478a5406dd9a5f9d0f801bad42a76a8c95c645 Mon Sep 17 00:00:00 2001 From: Matt Kane Date: Fri, 22 May 2026 15:37:57 +0100 Subject: [PATCH 1/4] fix(workerd): clear all lint warnings and tsgo errors - Replace untyped `as T` casts in bridge dispatch with predicate-backed `require*`/`optional*` helpers - Introduce `asContentDb()` for dynamic ec_* tables (single justified narrowing) - Drop unnecessary `as keyof Database` casts for tables already in the static schema - Validate marshaled RequestInit at the http/fetch boundary - Typed HttpError class in backing-service for status-bearing errors - getPluginStorageConfig now returns the real PluginStorageConfig shape - WorkerdSandboxedPlugin implements SandboxedPluginInstance (the previous SandboxedPlugin symbol did not exist) - Add typecheck script so the package participates in pnpm typecheck No runtime behaviour changes. --- .changeset/workerd-typed-bridge.md | 15 + packages/workerd/package.json | 3 +- .../workerd/src/sandbox/backing-service.ts | 44 +- .../workerd/src/sandbox/bridge-handler.ts | 544 +++++++++++++----- packages/workerd/src/sandbox/dev-runner.ts | 17 +- packages/workerd/src/sandbox/runner.ts | 50 +- 6 files changed, 515 insertions(+), 158 deletions(-) create mode 100644 .changeset/workerd-typed-bridge.md diff --git a/.changeset/workerd-typed-bridge.md b/.changeset/workerd-typed-bridge.md new file mode 100644 index 000000000..e4b52c6d0 --- /dev/null +++ b/.changeset/workerd-typed-bridge.md @@ -0,0 +1,15 @@ +--- +"@emdash-cms/sandbox-workerd": patch +--- + +Tightens the workerd sandbox internals so the package now lints and type-checks cleanly. + +- Bridge call bodies are validated with predicate-backed `require*` / `optional*` helpers instead of unchecked `as` casts. A misbehaving plugin that sends a malformed JSON-RPC body now gets a clear "Parameter X must be Y" error rather than triggering a downstream type confusion. +- Content table access (`ec_*` collections) is centralised behind a typed `asContentDb()` helper. Known tables (`users`, `media`, `_plugin_storage`) drop their `as keyof Database` casts entirely. +- HTTP `init` marshalling validates each field at the bridge boundary, including form-data parts. +- The backing service uses a typed `HttpError` class for status-bearing errors and validates incoming chunks/body shape defensively. +- `getPluginStorageConfig()` returns the real `PluginStorageConfig` shape from the manifest instead of `Record`. +- `WorkerdSandboxedPlugin` now implements the correct `SandboxedPluginInstance` interface (the old `SandboxedPlugin` symbol did not exist). +- Adds a `typecheck` script (`tsgo --noEmit`) so the package participates in `pnpm typecheck` going forward. + +No runtime behaviour changes. diff --git a/packages/workerd/package.json b/packages/workerd/package.json index 2f9189ee3..d21f78f55 100644 --- a/packages/workerd/package.json +++ b/packages/workerd/package.json @@ -21,7 +21,8 @@ "scripts": { "build": "tsdown", "dev": "tsdown --watch", - "test": "vitest run" + "test": "vitest run", + "typecheck": "tsgo --noEmit" }, "dependencies": { "emdash": "workspace:*", diff --git a/packages/workerd/src/sandbox/backing-service.ts b/packages/workerd/src/sandbox/backing-service.ts index b4f9e0c01..ad4f32242 100644 --- a/packages/workerd/src/sandbox/backing-service.ts +++ b/packages/workerd/src/sandbox/backing-service.ts @@ -24,6 +24,16 @@ export interface BackingServiceHandler { removePlugin: (pluginId: string) => void; } +/** Error carrying an HTTP status code, used to surface request-level failures. */ +class HttpError extends Error { + constructor( + message: string, + readonly statusCode: number, + ) { + super(message); + } +} + /** * Create an HTTP request handler for the backing service. */ @@ -59,9 +69,7 @@ export function createBackingServiceHandler(runner: WorkerdSandboxRunner): Backi capabilities: claims.capabilities, allowedHosts: claims.allowedHosts, storageCollections: claims.storageCollections, - storageConfig: runner.getPluginStorageConfig(claims.pluginId, claims.version) as - | Record }> - | undefined, + storageConfig: runner.getPluginStorageConfig(claims.pluginId, claims.version), db: runner.db, emailSend: () => runner.emailSend, storage: runner.mediaStorage, @@ -85,10 +93,7 @@ export function createBackingServiceHandler(runner: WorkerdSandboxRunner): Backi res.writeHead(webResponse.status, { "Content-Type": "application/json" }); res.end(responseBody); } catch (error) { - const statusCode = - error instanceof Error && "statusCode" in error - ? (error as Error & { statusCode: number }).statusCode - : 500; + const statusCode = error instanceof HttpError ? error.statusCode : 500; const message = error instanceof Error ? error.message : "Internal error"; res.writeHead(statusCode, { "Content-Type": "application/json" }); res.end(JSON.stringify({ error: message })); @@ -105,18 +110,31 @@ export function createBackingServiceHandler(runner: WorkerdSandboxRunner): Backi const MAX_BRIDGE_BODY_BYTES = 10 * 1024 * 1024; +function isJsonObject(value: unknown): value is Record { + return value !== null && typeof value === "object" && !Array.isArray(value); +} + async function readBody(req: IncomingMessage): Promise> { + // IncomingMessage is a Node Readable; async-iteration yields chunks typed + // as `any`. Validate each chunk is a Buffer (the runtime guarantee for + // non-object-mode streams) so we can compose them safely. const chunks: Buffer[] = []; let totalBytes = 0; for await (const chunk of req) { - totalBytes += (chunk as Buffer).length; + if (!Buffer.isBuffer(chunk)) { + throw new HttpError("Request body has unexpected chunk type", 400); + } + totalBytes += chunk.length; if (totalBytes > MAX_BRIDGE_BODY_BYTES) { - const err = new Error("Request body too large"); - (err as Error & { statusCode: number }).statusCode = 413; - throw err; + throw new HttpError("Request body too large", 413); } - chunks.push(chunk as Buffer); + chunks.push(chunk); } const raw = Buffer.concat(chunks).toString(); - return raw ? (JSON.parse(raw) as Record) : {}; + if (!raw) return {}; + const parsed: unknown = JSON.parse(raw); + if (!isJsonObject(parsed)) { + throw new HttpError("Request body must be a JSON object", 400); + } + return parsed; } diff --git a/packages/workerd/src/sandbox/bridge-handler.ts b/packages/workerd/src/sandbox/bridge-handler.ts index afdd47db7..775b23e09 100644 --- a/packages/workerd/src/sandbox/bridge-handler.ts +++ b/packages/workerd/src/sandbox/bridge-handler.ts @@ -14,11 +14,47 @@ * must produce same outputs, same return shapes, same error messages. */ -// @ts-ignore -- value exports used at runtime import { createHttpAccess, createUnrestrictedHttpAccess, PluginStorageRepository } from "emdash"; -import type { Database } from "emdash"; -import type { SandboxEmailSendCallback } from "emdash"; -import { sql, type Kysely } from "kysely"; +import type { Database, SandboxEmailSendCallback } from "emdash"; +import { sql, type Kysely, type RawBuilder } from "kysely"; + +/** + * Schema view of a content table (ec_${collection}) for kysely. The standard + * system columns are typed; user-defined fields are addressed via the open + * `[key: string]` index. Each kysely call resolves the table name dynamically + * via `asContentDb()`. + */ +interface ContentTableRow { + id: string; + slug: string | null; + status: string; + author_id: string | null; + created_at: string; + updated_at: string; + published_at: string | null; + scheduled_at: string | null; + deleted_at: string | null; + version: number; + live_revision_id: string | null; + draft_revision_id: string | null; + // User-defined fields. kysely.set()/values() accept these because they're + // typed as unknown rather than never. + [key: string]: unknown; +} + +type ContentSchema = { [tableName: string]: ContentTableRow }; + +/** + * View the host db as a content schema where any `ec_*` table is addressable. + * Centralizes the one unavoidable narrowing for dynamic content tables (whose + * names are computed from user-defined collection slugs and so cannot appear + * in the static `Database` interface). The runtime SQL is identical; only the + * type lens changes. + */ +function asContentDb(db: Kysely): Kysely { + // eslint-disable-next-line typescript-eslint/no-unsafe-type-assertion -- ec_* content tables are created at runtime by SchemaRegistry and cannot be expressed in the static Database interface. ContentSchema is a structural view of any ec_* table. + return db as unknown as Kysely; +} /** Validates collection/field names to prevent SQL injection */ const COLLECTION_NAME_RE = /^[a-z][a-z0-9_]*$/; @@ -82,7 +118,11 @@ export function createBridgeHandler( if (request.method === "POST") { const text = await request.text(); if (text) { - body = JSON.parse(text) as Record; + const parsed: unknown = JSON.parse(text); + if (!isRecord(parsed)) { + throw new Error("Bridge request body must be a JSON object"); + } + body = parsed; } } @@ -116,7 +156,7 @@ async function dispatch( case "kv/delete": return kvDelete(db, pluginId, requireString(body, "key")); case "kv/list": - return kvList(db, pluginId, (body.prefix as string) ?? ""); + return kvList(db, pluginId, optionalString(body, "prefix") ?? ""); // ── Content ───────────────────────────────────────────────────── case "content/get": @@ -127,18 +167,14 @@ async function dispatch( return contentList(db, requireString(body, "collection"), body); case "content/create": requireCapability(opts, "write:content"); - return contentCreate( - db, - requireString(body, "collection"), - body.data as Record, - ); + return contentCreate(db, requireString(body, "collection"), requireRecord(body, "data")); case "content/update": requireCapability(opts, "write:content"); return contentUpdate( db, requireString(body, "collection"), requireString(body, "id"), - body.data as Record, + requireRecord(body, "data"), ); case "content/delete": requireCapability(opts, "write:content"); @@ -148,18 +184,22 @@ async function dispatch( return contentCreateMany( db, requireString(body, "collection"), - body.items as Array>, + requireRecordArray(body, "items"), ); case "content/updateMany": requireCapability(opts, "write:content"); return contentUpdateMany( db, requireString(body, "collection"), - body.items as Array<{ id: string; data: Record }>, + requireUpdateManyItems(body, "items"), ); case "content/deleteMany": requireCapability(opts, "write:content"); - return contentDeleteMany(db, requireString(body, "collection"), body.ids as string[]); + return contentDeleteMany( + db, + requireString(body, "collection"), + requireStringArray(body, "ids"), + ); // ── Media ─────────────────────────────────────────────────────── case "media/get": @@ -174,8 +214,8 @@ async function dispatch( db, requireString(body, "filename"), requireString(body, "contentType"), - body.bytes as string | number[], - body.encoding as string | undefined, + requireMediaBytes(body, "bytes"), + optionalString(body, "encoding"), opts.storage, ); case "media/delete": @@ -190,15 +230,7 @@ async function dispatch( // ── Email ─────────────────────────────────────────────────────── case "email/send": { requireCapability(opts, "email:send"); - const message = body.message as { - to: string; - subject: string; - text: string; - html?: string; - }; - if (!message?.to || !message?.subject || !message?.text) { - throw new Error("email/send requires message with to, subject, and text"); - } + const message = requireEmailMessage(body, "message"); const emailSend = opts.emailSend(); if (!emailSend) throw new Error("Email is not configured. No email provider is available."); await emailSend(message, pluginId); @@ -236,28 +268,32 @@ async function dispatch( return storageQuery(opts, requireString(body, "collection"), body); case "storage/count": validateStorageCollection(opts, requireString(body, "collection")); - return storageCount( + return storageCount(opts, requireString(body, "collection"), optionalRecord(body, "where")); + case "storage/getMany": + validateStorageCollection(opts, requireString(body, "collection")); + return storageGetMany( opts, requireString(body, "collection"), - body.where as Record | undefined, + requireStringArray(body, "ids"), ); - case "storage/getMany": - validateStorageCollection(opts, requireString(body, "collection")); - return storageGetMany(opts, requireString(body, "collection"), body.ids as string[]); case "storage/putMany": validateStorageCollection(opts, requireString(body, "collection")); return storagePutMany( opts, requireString(body, "collection"), - body.items as Array<{ id: string; data: unknown }>, + requireStorageItems(body, "items"), ); case "storage/deleteMany": validateStorageCollection(opts, requireString(body, "collection")); - return storageDeleteMany(opts, requireString(body, "collection"), body.ids as string[]); + return storageDeleteMany( + opts, + requireString(body, "collection"), + requireStringArray(body, "ids"), + ); // ── Logging ───────────────────────────────────────────────────── case "log": { - const level = requireString(body, "level") as "debug" | "info" | "warn" | "error"; + const level = requireLogLevel(body, "level"); const msg = requireString(body, "msg"); console[level](`[plugin:${pluginId}]`, msg, body.data ?? ""); return null; @@ -275,6 +311,79 @@ async function dispatch( } // ── Validation ─────────────────────────────────────────────────────────── +// +// Bridge call bodies are JSON-RPC-style payloads constructed by the workerd +// plugin wrapper (see ./wrapper.ts) and consumed here. We control both ends +// of the protocol, so these assertions exist to catch buggy or malicious +// plugins rather than to parse an open API surface — that's why they throw +// rather than return tagged errors. The bridge top-level catch turns thrown +// errors into JSON error responses the plugin sees as bridge call failures. +// +// Each `require*` helper is backed by a narrowing predicate so the returned +// value is typed via flow analysis rather than via a `as T` assertion. This +// keeps the @typescript-eslint/no-unsafe-type-assertion rule clean. + +type EmailMessage = { to: string; subject: string; text: string; html?: string }; +type LogLevel = "debug" | "info" | "warn" | "error"; +type UpdateManyItem = { id: string; data: Record }; +type StorageItem = { id: string; data: unknown }; + +const LOG_LEVELS = new Set(["debug", "info", "warn", "error"]); + +function isRecord(value: unknown): value is Record { + return value !== null && typeof value === "object" && !Array.isArray(value); +} + +function isStringArray(value: unknown): value is string[] { + return Array.isArray(value) && value.every((v) => typeof v === "string"); +} + +function isRecordArray(value: unknown): value is Array> { + return Array.isArray(value) && value.every(isRecord); +} + +function isUpdateManyItem(value: unknown): value is UpdateManyItem { + if (!isRecord(value)) return false; + return typeof value.id === "string" && isRecord(value.data); +} + +function isUpdateManyItemArray(value: unknown): value is UpdateManyItem[] { + return Array.isArray(value) && value.every(isUpdateManyItem); +} + +function isStorageItem(value: unknown): value is StorageItem { + if (!isRecord(value)) return false; + return typeof value.id === "string"; +} + +function isStorageItemArray(value: unknown): value is StorageItem[] { + return Array.isArray(value) && value.every(isStorageItem); +} + +function isNumberArray(value: unknown): value is number[] { + return Array.isArray(value) && value.every((v) => typeof v === "number"); +} + +function isEmailMessage(value: unknown): value is EmailMessage { + if (!isRecord(value)) return false; + if (typeof value.to !== "string") return false; + if (typeof value.subject !== "string") return false; + if (typeof value.text !== "string") return false; + if (value.html !== undefined && typeof value.html !== "string") return false; + return true; +} + +function isLogLevel(value: unknown): value is LogLevel { + return typeof value === "string" && LOG_LEVELS.has(value); +} + +function isOrderBy(value: unknown): value is Record { + if (!isRecord(value)) return false; + for (const dir of Object.values(value)) { + if (dir !== "asc" && dir !== "desc") return false; + } + return true; +} function requireString(body: Record, key: string): string { const value = body[key]; @@ -282,6 +391,95 @@ function requireString(body: Record, key: string): string { return value; } +function optionalString(body: Record, key: string): string | undefined { + const value = body[key]; + if (value === undefined) return undefined; + if (typeof value !== "string") throw new Error(`Parameter ${key} must be a string when provided`); + return value; +} + +function requireRecord(body: Record, key: string): Record { + const value = body[key]; + if (!isRecord(value)) throw new Error(`Missing required object parameter: ${key}`); + return value; +} + +function optionalRecord( + body: Record, + key: string, +): Record | undefined { + const value = body[key]; + if (value === undefined) return undefined; + if (!isRecord(value)) throw new Error(`Parameter ${key} must be an object when provided`); + return value; +} + +function requireStringArray(body: Record, key: string): string[] { + const value = body[key]; + if (!isStringArray(value)) throw new Error(`Parameter ${key} must be an array of strings`); + return value; +} + +function requireRecordArray( + body: Record, + key: string, +): Array> { + const value = body[key]; + if (!isRecordArray(value)) throw new Error(`Parameter ${key} must be an array of objects`); + return value; +} + +function requireUpdateManyItems(body: Record, key: string): UpdateManyItem[] { + const value = body[key]; + if (!isUpdateManyItemArray(value)) { + throw new Error(`Parameter ${key} must be an array of { id: string, data: object } items`); + } + return value; +} + +function requireStorageItems(body: Record, key: string): StorageItem[] { + const value = body[key]; + if (!isStorageItemArray(value)) { + throw new Error(`Parameter ${key} must be an array of { id: string, data } items`); + } + return value; +} + +function requireMediaBytes(body: Record, key: string): string | number[] { + const value = body[key]; + if (typeof value === "string") return value; + if (isNumberArray(value)) return value; + throw new Error(`Parameter ${key} must be a string or array of numbers`); +} + +function requireEmailMessage(body: Record, key: string): EmailMessage { + const value = body[key]; + if (!isEmailMessage(value)) { + throw new Error("email/send requires message with to, subject, and text"); + } + return value; +} + +function requireLogLevel(body: Record, key: string): LogLevel { + const value = body[key]; + if (!isLogLevel(value)) { + throw new Error(`Parameter ${key} must be one of: debug, info, warn, error`); + } + return value; +} + +function requireOrderBy( + body: Record, + key: string, +): Record | undefined { + const value = body[key]; + if (value === undefined) return undefined; + if (!isOrderBy(value)) { + throw new Error(`Parameter ${key} must be an object mapping field to "asc"|"desc"`); + } + return value; +} + function requireCapability(opts: BridgeHandlerOptions, capability: string): void { // Strict capability check matching the Cloudflare PluginBridge. // We do NOT imply write → read here: a plugin that declares only @@ -368,7 +566,7 @@ function rowToContentItem( async function kvGet(db: Kysely, pluginId: string, key: string): Promise { const row = await db - .selectFrom("_plugin_storage" as keyof Database) + .selectFrom("_plugin_storage") .where("plugin_id", "=", pluginId) .where("collection", "=", "__kv") .where("id", "=", key) @@ -376,7 +574,7 @@ async function kvGet(db: Kysely, pluginId: string, key: string): Promi .executeTakeFirst(); if (!row) return null; try { - return JSON.parse(row.data as string); + return JSON.parse(row.data); } catch { return row.data; } @@ -391,7 +589,7 @@ async function kvSet( const serialized = JSON.stringify(value); const now = new Date().toISOString(); await db - .insertInto("_plugin_storage" as keyof Database) + .insertInto("_plugin_storage") .values({ plugin_id: pluginId, collection: "__kv", @@ -399,19 +597,19 @@ async function kvSet( data: serialized, created_at: now, updated_at: now, - } as never) + }) .onConflict((oc) => - oc.columns(["plugin_id", "collection", "id"] as never[]).doUpdateSet({ + oc.columns(["plugin_id", "collection", "id"]).doUpdateSet({ data: serialized, updated_at: now, - } as never), + }), ) .execute(); } async function kvDelete(db: Kysely, pluginId: string, key: string): Promise { const result = await db - .deleteFrom("_plugin_storage" as keyof Database) + .deleteFrom("_plugin_storage") .where("plugin_id", "=", pluginId) .where("collection", "=", "__kv") .where("id", "=", key) @@ -425,7 +623,7 @@ async function kvList( prefix: string, ): Promise> { const rows = await db - .selectFrom("_plugin_storage" as keyof Database) + .selectFrom("_plugin_storage") .where("plugin_id", "=", pluginId) .where("collection", "=", "__kv") .where("id", "like", `${prefix}%`) @@ -433,8 +631,8 @@ async function kvList( .execute(); return rows.map((r) => ({ - key: r.id as string, - value: JSON.parse(r.data as string), + key: r.id, + value: JSON.parse(r.data), })); } @@ -452,15 +650,16 @@ async function contentGet( updatedAt: string; } | null> { validateCollectionName(collection); + const table = `ec_${collection}`; try { - const row = await db - .selectFrom(`ec_${collection}` as keyof Database) + const row = await asContentDb(db) + .selectFrom(table) .where("id", "=", id) .where("deleted_at", "is", null) .selectAll() .executeTakeFirst(); if (!row) return null; - return rowToContentItem(collection, row as Record); + return rowToContentItem(collection, row); } catch { return null; } @@ -482,10 +681,11 @@ async function contentList( hasMore: boolean; }> { validateCollectionName(collection); + const table = `ec_${collection}`; const limit = Math.max(1, Math.min(Number(opts.limit) || 50, 100)); try { - let query = db - .selectFrom(`ec_${collection}` as keyof Database) + let query = asContentDb(db) + .selectFrom(table) .where("deleted_at", "is", null) .selectAll() .orderBy("id", "desc"); @@ -496,9 +696,7 @@ async function contentList( const rows = await query.limit(limit + 1).execute(); const pageRows = rows.slice(0, limit); - const items = pageRows.map((row) => - rowToContentItem(collection, row as Record), - ); + const items = pageRows.map((row) => rowToContentItem(collection, row)); const hasMore = rows.length > limit; return { @@ -523,6 +721,7 @@ async function contentCreate( updatedAt: string; }> { validateCollectionName(collection); + const table = `ec_${collection}`; // Generate ULID for the new content item const { ulid } = await import("ulidx"); @@ -547,14 +746,12 @@ async function contentCreate( } } - await db - .insertInto(`ec_${collection}` as keyof Database) - .values(values as never) - .execute(); + const cdb = asContentDb(db); + await cdb.insertInto(table).values(values).execute(); // Re-read the created row - const created = await db - .selectFrom(`ec_${collection}` as keyof Database) + const created = await cdb + .selectFrom(table) .where("id", "=", id) .where("deleted_at", "is", null) .selectAll() @@ -563,7 +760,7 @@ async function contentCreate( if (!created) { return { id, type: collection, data: {}, createdAt: now, updatedAt: now }; } - return rowToContentItem(collection, created as Record); + return rowToContentItem(collection, created); } async function contentUpdate( @@ -579,40 +776,46 @@ async function contentUpdate( updatedAt: string; }> { validateCollectionName(collection); + const table = `ec_${collection}`; + const cdb = asContentDb(db); const now = new Date().toISOString(); - // Build update: always bump updated_at and version - let query = db - .updateTable(`ec_${collection}` as keyof Database) - .set({ updated_at: now } as never) - .set({ version: sql`version + 1` } as never) - .where("id", "=", id) - .where("deleted_at", "is", null); + // Build update: always bump updated_at and version. Collect every column + // change into a single .set() so the value-bag type is `unknown` per + // column and we don't need narrowing casts. + const updates: Record = { + updated_at: now, + version: sql`version + 1` satisfies RawBuilder, + }; - // System field updates if (typeof data.status === "string") { - query = query.set({ status: data.status } as never); + updates.status = data.status; } if (data.slug !== undefined) { - query = query.set({ slug: typeof data.slug === "string" ? data.slug : null } as never); + updates.slug = typeof data.slug === "string" ? data.slug : null; } - // User data fields for (const [key, value] of Object.entries(data)) { if (!SYSTEM_COLUMNS.has(key) && COLLECTION_NAME_RE.test(key)) { - query = query.set({ [key]: serializeValue(value) } as never); + updates[key] = serializeValue(value); } } - const result = await query.executeTakeFirst(); + const result = await cdb + .updateTable(table) + .set(updates) + .where("id", "=", id) + .where("deleted_at", "is", null) + .executeTakeFirst(); + if (BigInt(result.numUpdatedRows) === 0n) { throw new Error(`Content not found or deleted: ${collection}/${id}`); } // Re-read the updated row - const updated = await db - .selectFrom(`ec_${collection}` as keyof Database) + const updated = await cdb + .selectFrom(table) .where("id", "=", id) .where("deleted_at", "is", null) .selectAll() @@ -621,7 +824,7 @@ async function contentUpdate( if (!updated) { throw new Error(`Content not found: ${collection}/${id}`); } - return rowToContentItem(collection, updated as Record); + return rowToContentItem(collection, updated); } async function contentDelete( @@ -630,12 +833,13 @@ async function contentDelete( id: string, ): Promise { validateCollectionName(collection); + const table = `ec_${collection}`; // Soft-delete: set deleted_at timestamp (matching Cloudflare bridge) const now = new Date().toISOString(); - const result = await db - .updateTable(`ec_${collection}` as keyof Database) - .set({ deleted_at: now, updated_at: now } as never) + const result = await asContentDb(db) + .updateTable(table) + .set({ deleted_at: now, updated_at: now }) .where("id", "=", id) .where("deleted_at", "is", null) .executeTakeFirst(); @@ -717,16 +921,14 @@ async function contentDeleteMany( // ── Media Operations ───────────────────────────────────────────────────── -interface MediaRow { +function rowToMediaItem(row: { id: string; filename: string; mime_type: string; size: number | null; storage_key: string; created_at: string; -} - -function rowToMediaItem(row: MediaRow) { +}) { return { id: row.id, filename: row.filename, @@ -748,13 +950,9 @@ async function mediaGet( url: string; createdAt: string; } | null> { - const row = await db - .selectFrom("media" as keyof Database) - .where("id", "=", id) - .selectAll() - .executeTakeFirst(); + const row = await db.selectFrom("media").where("id", "=", id).selectAll().executeTakeFirst(); if (!row) return null; - return rowToMediaItem(row as unknown as MediaRow); + return rowToMediaItem(row); } async function mediaList( @@ -776,7 +974,7 @@ async function mediaList( // Only return ready items (matching Cloudflare bridge) let query = db - .selectFrom("media" as keyof Database) + .selectFrom("media") .where("status", "=", "ready") .selectAll() .orderBy("id", "desc"); @@ -791,7 +989,7 @@ async function mediaList( const rows = await query.limit(limit + 1).execute(); const pageRows = rows.slice(0, limit); - const items = pageRows.map((row) => rowToMediaItem(row as unknown as MediaRow)); + const items = pageRows.map((row) => rowToMediaItem(row)); const hasMore = rows.length > limit; return { @@ -838,8 +1036,10 @@ async function mediaUpload( const binary = atob(bytes); byteArray = new Uint8Array(binary.length); for (let i = 0; i < binary.length; i++) byteArray[i] = binary.charCodeAt(i); + } else if (Array.isArray(bytes)) { + byteArray = new Uint8Array(bytes); } else { - byteArray = new Uint8Array(bytes as number[]); + throw new Error("media/upload: bytes must be a base64-encoded string or an array of bytes"); } // Write bytes to storage first, then create DB record. @@ -850,7 +1050,7 @@ async function mediaUpload( try { await db - .insertInto("media" as keyof Database) + .insertInto("media") .values({ id: mediaId, filename, @@ -859,7 +1059,7 @@ async function mediaUpload( storage_key: storageKey, status: "ready", created_at: now, - } as never) + }) .execute(); } catch (error) { // Best-effort cleanup of the orphaned storage object. Log if cleanup @@ -890,7 +1090,7 @@ async function mediaDelete( ): Promise { // Look up storage key before deleting const media = await db - .selectFrom("media" as keyof Database) + .selectFrom("media") .where("id", "=", id) .select("storage_key") .executeTakeFirst(); @@ -898,22 +1098,16 @@ async function mediaDelete( if (!media) return false; // Delete the DB row first - const result = await db - .deleteFrom("media" as keyof Database) - .where("id", "=", id) - .executeTakeFirst(); + const result = await db.deleteFrom("media").where("id", "=", id).executeTakeFirst(); // Delete the storage object. If this fails, log but don't throw — // the DB row is already deleted and the orphan cleanup cron will // catch it. Matches the Cloudflare bridge's behavior. - if (storage && (media as { storage_key: string }).storage_key) { + if (storage && media.storage_key) { try { - await storage.delete((media as { storage_key: string }).storage_key); + await storage.delete(media.storage_key); } catch (error) { - console.warn( - `[bridge] Failed to delete storage object ${(media as { storage_key: string }).storage_key}:`, - error, - ); + console.warn(`[bridge] Failed to delete storage object ${media.storage_key}:`, error); } } @@ -922,14 +1116,106 @@ async function mediaDelete( // ── HTTP Operations ────────────────────────────────────────────────────── -/** Marshaled RequestInit shape sent over the bridge from the wrapper */ +/** A multipart form part as marshaled by the wrapper. */ +interface MarshaledFormDataPart { + name: string; + value: string; + filename?: string; + type?: string; + isBlob?: boolean; +} + +/** Marshaled RequestInit shape sent over the bridge from the wrapper. */ interface MarshaledRequestInit { method?: string; redirect?: RequestRedirect; /** List of [name, value] pairs to preserve multi-value headers */ headers?: Array<[string, string]>; + /** + * Body is discriminated by bodyType. The wrapper (see wrapper.ts: + * marshalRequestInit) guarantees the shape, but we validate defensively + * at unmarshal time so a misbehaving plugin can't smuggle unexpected + * data into the host fetch. + */ bodyType?: "string" | "base64" | "formdata"; - body?: unknown; + body?: string | MarshaledFormDataPart[]; +} + +function isFormDataPart(value: unknown): value is MarshaledFormDataPart { + if (!isRecord(value)) return false; + if (typeof value.name !== "string") return false; + if (typeof value.value !== "string") return false; + if (value.filename !== undefined && typeof value.filename !== "string") return false; + if (value.type !== undefined && typeof value.type !== "string") return false; + if (value.isBlob !== undefined && typeof value.isBlob !== "boolean") return false; + return true; +} + +function isFormDataPartArray(value: unknown): value is MarshaledFormDataPart[] { + return Array.isArray(value) && value.every(isFormDataPart); +} + +function isMarshaledHeaders(value: unknown): value is Array<[string, string]> { + return ( + Array.isArray(value) && + value.every( + (entry) => + Array.isArray(entry) && + entry.length === 2 && + typeof entry[0] === "string" && + typeof entry[1] === "string", + ) + ); +} + +function parseMarshaledRequestInit(value: unknown): MarshaledRequestInit | undefined { + if (value === undefined) return undefined; + if (!isRecord(value)) { + throw new Error("http/fetch: init must be an object"); + } + const out: MarshaledRequestInit = {}; + if (value.method !== undefined) { + if (typeof value.method !== "string") + throw new Error("http/fetch: init.method must be a string"); + out.method = value.method; + } + if (value.redirect !== undefined) { + const r = value.redirect; + if (r !== "follow" && r !== "error" && r !== "manual") { + throw new Error('http/fetch: init.redirect must be "follow", "error", or "manual"'); + } + out.redirect = r; + } + if (value.headers !== undefined) { + if (!isMarshaledHeaders(value.headers)) { + throw new Error("http/fetch: init.headers must be an array of [name, value] pairs"); + } + out.headers = value.headers; + } + if (value.bodyType !== undefined) { + if ( + value.bodyType !== "string" && + value.bodyType !== "base64" && + value.bodyType !== "formdata" + ) { + throw new Error('http/fetch: init.bodyType must be "string", "base64", or "formdata"'); + } + out.bodyType = value.bodyType; + } + if (value.body !== undefined) { + if (out.bodyType === "formdata") { + if (!isFormDataPartArray(value.body)) { + throw new Error("http/fetch: formdata body must be an array of form parts"); + } + out.body = value.body; + } else { + if (typeof value.body !== "string") { + throw new Error("http/fetch: string/base64 body must be a string"); + } + out.body = value.body; + } + } + return out; } /** @@ -955,21 +1241,17 @@ function unmarshalRequestInit( if (marshaled.bodyType && marshaled.body !== undefined) { switch (marshaled.bodyType) { case "string": - init.body = marshaled.body as string; + if (typeof marshaled.body !== "string") break; + init.body = marshaled.body; break; case "base64": - init.body = Buffer.from(marshaled.body as string, "base64"); + if (typeof marshaled.body !== "string") break; + init.body = Buffer.from(marshaled.body, "base64"); break; case "formdata": { + if (!Array.isArray(marshaled.body)) break; const fd = new FormData(); - const parts = marshaled.body as Array<{ - name: string; - value: string; - filename?: string; - type?: string; - isBlob?: boolean; - }>; - for (const part of parts) { + for (const part of marshaled.body) { if (part.isBlob) { const bytes = Buffer.from(part.value, "base64"); const blob = new Blob([bytes], { type: part.type || "application/octet-stream" }); @@ -1001,7 +1283,7 @@ async function httpFetch( ? createUnrestrictedHttpAccess(opts.pluginId) : createHttpAccess(opts.pluginId, opts.allowedHosts || []); - const init = unmarshalRequestInit(marshaledInit as MarshaledRequestInit | undefined); + const init = unmarshalRequestInit(parseMarshaledRequestInit(marshaledInit)); const res = await httpAccess.fetch(url, init); // Read as bytes to preserve binary content (images, audio, etc.) const bytes = new Uint8Array(await res.arrayBuffer()); @@ -1019,15 +1301,13 @@ async function httpFetch( // ── User Operations ────────────────────────────────────────────────────── -interface UserRow { +function rowToUser(row: { id: string; email: string; name: string | null; role: number; created_at: string; -} - -function rowToUser(row: UserRow) { +}) { return { id: row.id, email: row.email, @@ -1048,12 +1328,12 @@ async function userGet( createdAt: string; } | null> { const row = await db - .selectFrom("users" as keyof Database) + .selectFrom("users") .where("id", "=", id) .select(["id", "email", "name", "role", "created_at"]) .executeTakeFirst(); if (!row) return null; - return rowToUser(row as unknown as UserRow); + return rowToUser(row); } async function userGetByEmail( @@ -1067,12 +1347,12 @@ async function userGetByEmail( createdAt: string; } | null> { const row = await db - .selectFrom("users" as keyof Database) + .selectFrom("users") .where("email", "=", email.toLowerCase()) .select(["id", "email", "name", "role", "created_at"]) .executeTakeFirst(); if (!row) return null; - return rowToUser(row as unknown as UserRow); + return rowToUser(row); } async function userList( @@ -1085,7 +1365,7 @@ async function userList( const limit = Math.max(1, Math.min(Number(opts.limit) || 50, 100)); let query = db - .selectFrom("users" as keyof Database) + .selectFrom("users") .select(["id", "email", "name", "role", "created_at"]) .orderBy("id", "desc"); @@ -1098,7 +1378,7 @@ async function userList( const rows = await query.limit(limit + 1).execute(); const pageRows = rows.slice(0, limit); - const items = pageRows.map((row) => rowToUser(row as unknown as UserRow)); + const items = pageRows.map((row) => rowToUser(row)); const hasMore = rows.length > limit; return { @@ -1156,10 +1436,12 @@ async function storageQuery( queryOpts: Record, ): Promise<{ items: Array<{ id: string; data: unknown }>; hasMore: boolean; cursor?: string }> { const repo = getStorageRepo(opts, collection); - // eslint-disable-next-line typescript-eslint/no-unsafe-type-assertion -- WhereClause is structurally Record + const where = optionalRecord(queryOpts, "where"); + const orderBy = requireOrderBy(queryOpts, "orderBy"); const result = await repo.query({ - where: queryOpts.where as never, - orderBy: queryOpts.orderBy as Record | undefined, + // eslint-disable-next-line typescript-eslint/no-unsafe-type-assertion -- repo.query accepts a generic WhereClause; we've validated `where` is a Record. + where: where as never, + orderBy, limit: typeof queryOpts.limit === "number" ? Math.max(1, Math.min(queryOpts.limit, 100)) : undefined, cursor: typeof queryOpts.cursor === "string" ? queryOpts.cursor : undefined, @@ -1177,7 +1459,7 @@ async function storageCount( where?: Record, ): Promise { const repo = getStorageRepo(opts, collection); - // eslint-disable-next-line typescript-eslint/no-unsafe-type-assertion -- WhereClause is structurally Record + // eslint-disable-next-line typescript-eslint/no-unsafe-type-assertion -- repo.count accepts a generic WhereClause; the caller validated `where` is a Record. return repo.count(where as never); } diff --git a/packages/workerd/src/sandbox/dev-runner.ts b/packages/workerd/src/sandbox/dev-runner.ts index 47a22760d..320d2035f 100644 --- a/packages/workerd/src/sandbox/dev-runner.ts +++ b/packages/workerd/src/sandbox/dev-runner.ts @@ -30,6 +30,10 @@ import type { PluginManifest } from "emdash"; import { createBridgeHandler } from "./bridge-handler.js"; import { generatePluginWrapper } from "./wrapper.js"; +function isRecord(value: unknown): value is Record { + return value !== null && typeof value === "object" && !Array.isArray(value); +} + const SAFE_ID_RE = /[^a-z0-9_-]/gi; /** @@ -216,6 +220,11 @@ export class MiniflareDevRunner implements SandboxRunner { /** * Dispatch a fetch to a specific plugin worker in miniflare. + * + * Miniflare's `worker.fetch` uses undici's Request/Response/RequestInit + * types, which are structurally compatible with the platform globals but + * declared as distinct nominal types. Callers only consume status/ok and + * the body via text()/json(), so we widen at this boundary. */ async dispatchToPlugin(pluginId: string, url: string, init?: RequestInit): Promise { if (!this.mf) { @@ -223,7 +232,8 @@ export class MiniflareDevRunner implements SandboxRunner { } const workerName = pluginId.replace(SAFE_ID_RE, "_"); const worker = await this.mf.getWorker(workerName); - return worker.fetch(url, init); + // eslint-disable-next-line typescript-eslint/no-unsafe-type-assertion -- miniflare's Response_2 / RequestInit_2 are structurally compatible with the global types we use here. See JSDoc above. + return worker.fetch(url, init as never) as unknown as Response; } } @@ -258,7 +268,10 @@ class MiniflareDevPlugin implements SandboxedPluginInstance { const text = await res.text(); throw new Error(`Plugin ${this.id} hook ${hookName} failed: ${text}`); } - const result = (await res.json()) as { value: unknown }; + const result: unknown = await res.json(); + if (!isRecord(result)) { + throw new Error(`Plugin ${this.id} hook ${hookName} returned a non-object response`); + } return result.value; }); } diff --git a/packages/workerd/src/sandbox/runner.ts b/packages/workerd/src/sandbox/runner.ts index 0d232ec03..f5e1a48be 100644 --- a/packages/workerd/src/sandbox/runner.ts +++ b/packages/workerd/src/sandbox/runner.ts @@ -254,6 +254,34 @@ export function waitForProcessExit(proc: ChildProcess, timeoutMs = 5000): Promis return exitPromise; } +/** Decoded claims encoded in a per-plugin auth token. */ +export interface PluginTokenClaims { + pluginId: string; + version: string; + capabilities: string[]; + allowedHosts: string[]; + storageCollections: string[]; +} + +function isStringArray(value: unknown): value is string[] { + return Array.isArray(value) && value.every((v) => typeof v === "string"); +} + +function isRecord(value: unknown): value is Record { + return value !== null && typeof value === "object" && !Array.isArray(value); +} + +function isTokenClaims(value: unknown): value is PluginTokenClaims { + if (!isRecord(value)) return false; + return ( + typeof value.pluginId === "string" && + typeof value.version === "string" && + isStringArray(value.capabilities) && + isStringArray(value.allowedHosts) && + isStringArray(value.storageCollections) + ); +} + /** * Workerd sandbox runner for Node.js deployments. * @@ -627,17 +655,14 @@ export class WorkerdSandboxRunner implements SandboxRunner { const b = Buffer.from(expectedHmac); if (a.length !== b.length || !timingSafeEqual(a, b)) return null; + let parsed: unknown; try { - return JSON.parse(payload) as { - pluginId: string; - version: string; - capabilities: string[]; - allowedHosts: string[]; - storageCollections: string[]; - }; + parsed = JSON.parse(payload); } catch { return null; } + if (!isTokenClaims(parsed)) return null; + return parsed; } /** @@ -860,10 +885,10 @@ export class WorkerdSandboxRunner implements SandboxRunner { * could return a stale version's storage schema after a plugin upgrade, * so we require both id and version. */ - getPluginStorageConfig(pluginId: string, version: string): Record | undefined { + getPluginStorageConfig(pluginId: string, version: string): PluginManifest["storage"] | undefined { const plugin = this.plugins.get(`${pluginId}:${version}`); if (plugin) { - return plugin.manifest.storage as Record | undefined; + return plugin.manifest.storage; } return undefined; } @@ -877,7 +902,7 @@ export class WorkerdSandboxRunner implements SandboxRunner { /** * A plugin running in a workerd V8 isolate. */ -class WorkerdSandboxedPlugin implements SandboxedPlugin { +class WorkerdSandboxedPlugin implements SandboxedPluginInstance { readonly id: string; private manifest: PluginManifest; private port: number; @@ -927,7 +952,10 @@ class WorkerdSandboxedPlugin implements SandboxedPlugin { const text = await res.text(); throw new Error(`Plugin ${this.id} hook ${hookName} failed: ${text}`); } - const result = (await res.json()) as { value: unknown }; + const result: unknown = await res.json(); + if (!isRecord(result)) { + throw new Error(`Plugin ${this.id} hook ${hookName} returned a non-object response`); + } return result.value; }); } From d13af32ab04848b299a11c896a1e880e495e6763 Mon Sep 17 00:00:00 2001 From: Matt Kane Date: Fri, 22 May 2026 15:42:50 +0100 Subject: [PATCH 2/4] chore: bump oxlint and update disable-comment format Upgrade oxlint (1.49 -> 1.66) and oxlint-tsgolint (0.15 -> 0.23). The newer oxlint renamed the unused unicorn/prevent-abbreviations rule and switched the canonical typescript-eslint plugin name to typescript, which changes how inline disable comments are written. Mechanical rename: `typescript-eslint(rule-name)` -> `typescript/rule-name` in all eslint-disable comments. Without this, ~120 disable comments stopped suppressing the rules they were meant to. Also drops unicorn/prevent-abbreviations from .oxlintrc.json (no longer a valid rule). --- .oxlintrc.json | 1 - apps/aggregator/src/index.ts | 8 +- apps/aggregator/src/routes/xrpc/views.ts | 4 +- package.json | 4 +- packages/admin/src/components/Sidebar.tsx | 2 +- packages/admin/src/lib/api/import.ts | 6 +- packages/auth/src/adapters/kysely.ts | 2 +- packages/auth/src/rbac.ts | 2 +- packages/cloudflare/src/db/d1-introspector.ts | 6 +- packages/cloudflare/src/db/d1.ts | 4 +- packages/cloudflare/src/db/do-class.ts | 6 +- packages/cloudflare/src/db/do-dialect.ts | 4 +- packages/cloudflare/src/db/do-preview.ts | 6 +- packages/cloudflare/src/db/do.ts | 6 +- .../src/db/playground-middleware.ts | 12 +- .../cloudflare/src/media/images-runtime.ts | 2 +- .../cloudflare/src/media/stream-runtime.ts | 2 +- .../src/plugins/vectorize-search.ts | 4 +- packages/cloudflare/src/sandbox/runner.ts | 4 +- packages/cloudflare/src/storage/r2.ts | 2 +- .../tests/sandbox/bridge-http.test.ts | 8 +- .../core/src/api/handlers/oauth-clients.ts | 2 +- packages/core/src/api/openapi/document.ts | 2 +- .../core/src/astro/integration/runtime.ts | 2 +- .../core/src/astro/integration/vite-config.ts | 2 +- .../src/astro/middleware/request-context.ts | 2 +- .../api/admin/themes/marketplace/index.ts | 2 +- .../astro/routes/api/auth/oauth/[provider].ts | 4 +- .../api/auth/oauth/[provider]/callback.ts | 4 +- .../routes/api/content/[collection]/[id].ts | 12 +- .../[collection]/[id]/discard-draft.ts | 4 +- .../content/[collection]/[id]/duplicate.ts | 4 +- .../api/content/[collection]/[id]/publish.ts | 4 +- .../api/content/[collection]/[id]/restore.ts | 4 +- .../api/content/[collection]/[id]/schedule.ts | 4 +- .../[collection]/[id]/terms/[taxonomy].ts | 4 +- .../content/[collection]/[id]/translations.ts | 2 +- .../content/[collection]/[id]/unpublish.ts | 4 +- .../api/import/wordpress-plugin/execute.ts | 4 +- .../routes/api/import/wordpress/execute.ts | 2 +- .../routes/api/import/wordpress/prepare.ts | 4 +- .../api/import/wordpress/rewrite-urls.ts | 6 +- .../src/astro/routes/api/media/upload-url.ts | 2 +- .../collections/[slug]/fields/[fieldSlug].ts | 2 +- .../api/schema/collections/[slug]/index.ts | 2 +- .../routes/api/schema/collections/index.ts | 2 +- packages/core/src/auth/rate-limit.ts | 2 +- packages/core/src/auth/trusted-proxy.ts | 2 +- .../components/InlinePortableTextEditor.tsx | 2 +- packages/core/src/config/secrets.ts | 6 +- .../database/migrations/006_taxonomy_defs.ts | 2 +- .../migrations/014_draft_revisions.ts | 12 +- .../core/src/database/repositories/audit.ts | 4 +- .../core/src/database/repositories/media.ts | 2 +- .../core/src/database/repositories/menu.ts | 2 +- .../core/src/database/repositories/options.ts | 6 +- .../database/repositories/plugin-storage.ts | 6 +- packages/core/src/i18n/config.ts | 2 +- packages/core/src/loader.ts | 2 +- packages/core/src/mcp/server.ts | 6 +- packages/core/src/plugins/email-console.ts | 2 +- packages/core/src/plugins/marketplace.ts | 2 +- packages/core/src/request-cache.ts | 6 +- packages/core/src/request-context.ts | 2 +- packages/core/src/settings/index.ts | 8 +- packages/core/src/storage/local.ts | 2 +- packages/core/src/storage/s3.ts | 6 +- packages/core/src/widgets/index.ts | 2 +- .../astro/comments-rate-limit.test.ts | 2 +- .../astro/media-upload-widening.test.ts | 4 +- .../astro/setup-admin-nonce-success.test.ts | 4 +- .../astro/setup-admin-nonce.test.ts | 4 +- .../astro/setup-site-url-lock.test.ts | 2 +- .../tests/integration/auth/rate-limit.test.ts | 4 +- .../core/tests/integration/mcp/drafts.test.ts | 2 +- .../tests/integration/mcp/validation.test.ts | 2 +- .../unit/astro/content-routes-authz.test.ts | 2 +- .../unit/astro/middleware-redirect.test.ts | 2 +- .../unit/astro/signup-rate-limit.test.ts | 8 +- packages/core/tests/unit/import/ssrf.test.ts | 2 +- .../unit/import/wp-prepare-invalidate.test.ts | 4 +- .../tests/unit/import/wxr-taxonomies.test.ts | 8 +- .../tests/unit/runtime/manifest-build.test.ts | 2 +- packages/core/tests/utils/mcp-runtime.ts | 4 +- packages/core/tsdown.config.ts | 2 +- packages/marketplace/src/routes/author.ts | 6 +- packages/marketplace/src/routes/dev.ts | 4 +- packages/marketplace/src/routes/public.ts | 4 +- packages/marketplace/src/routes/themes.ts | 6 +- packages/marketplace/src/workflows/audit.ts | 8 +- pnpm-lock.yaml | 232 +++++++++--------- 91 files changed, 292 insertions(+), 293 deletions(-) diff --git a/.oxlintrc.json b/.oxlintrc.json index 3e367a41e..68f416693 100644 --- a/.oxlintrc.json +++ b/.oxlintrc.json @@ -17,7 +17,6 @@ } ], "unicorn/filename-case": "off", - "unicorn/prevent-abbreviations": "off", "unicorn/no-null": "off", "unicorn/prefer-add-event-listener": "off", "typescript/no-unsafe-type-assertion": "warn", diff --git a/apps/aggregator/src/index.ts b/apps/aggregator/src/index.ts index 35dd9b5d3..854bcd651 100644 --- a/apps/aggregator/src/index.ts +++ b/apps/aggregator/src/index.ts @@ -271,19 +271,19 @@ export default { // the queue name, which is a runtime tag the compiler can't see. switch (batch.queue) { case RECORDS_QUEUE_NAME: - // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- narrowed by queue name + // eslint-disable-next-line typescript/no-unsafe-type-assertion -- narrowed by queue name await processBatch(batch as MessageBatch, env); return; case RECORDS_DLQ_NAME: - // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- narrowed by queue name + // eslint-disable-next-line typescript/no-unsafe-type-assertion -- narrowed by queue name await drainDeadLetterBatch(batch as MessageBatch, env); return; case BACKFILL_QUEUE_NAME: - // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- narrowed by queue name + // eslint-disable-next-line typescript/no-unsafe-type-assertion -- narrowed by queue name await processBackfillBatch(batch as MessageBatch, env); return; case BACKFILL_DLQ_NAME: - // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- narrowed by queue name + // eslint-disable-next-line typescript/no-unsafe-type-assertion -- narrowed by queue name drainBackfillDeadLetterBatch(batch as MessageBatch, env); return; default: diff --git a/apps/aggregator/src/routes/xrpc/views.ts b/apps/aggregator/src/routes/xrpc/views.ts index cf7570017..97703b70b 100644 --- a/apps/aggregator/src/routes/xrpc/views.ts +++ b/apps/aggregator/src/routes/xrpc/views.ts @@ -126,7 +126,7 @@ export function packageView(row: PackageRow): AggregatorDefs.PackageView { const view: AggregatorDefs.PackageView = { uri, cid, - // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- `did` is consumer-validated at write time + // eslint-disable-next-line typescript/no-unsafe-type-assertion -- `did` is consumer-validated at write time did: row.did as `did:${string}:${string}`, slug: row.slug, profile: synthesizePackageProfile(row, uri), @@ -152,7 +152,7 @@ export function releaseView(row: ReleaseRow): AggregatorDefs.ReleaseView { return { uri, cid, - // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- `did` is consumer-validated at write time + // eslint-disable-next-line typescript/no-unsafe-type-assertion -- `did` is consumer-validated at write time did: row.did as `did:${string}:${string}`, package: row.package, version: row.version, diff --git a/package.json b/package.json index 18c8161c7..dc22ca053 100644 --- a/package.json +++ b/package.json @@ -45,8 +45,8 @@ "emdash": "workspace:*", "knip": "^5.84.1", "oxfmt": "^0.34.0", - "oxlint": "^1.49.0", - "oxlint-tsgolint": "^0.15.0", + "oxlint": "^1.66.0", + "oxlint-tsgolint": "^0.23.0", "prettier": "^3.8.1", "prettier-plugin-astro": "^0.14.1", "typescript": "6.0.0-beta" diff --git a/packages/admin/src/components/Sidebar.tsx b/packages/admin/src/components/Sidebar.tsx index d653e3e6b..395c4c657 100644 --- a/packages/admin/src/components/Sidebar.tsx +++ b/packages/admin/src/components/Sidebar.tsx @@ -92,7 +92,7 @@ function NavMenuLink({ item, isActive }: { item: NavItem; isActive: boolean }) { const link = ( (db: Kysely): AuthAdapter { // Type cast to work with generic Kysely instance - // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- generic Kysely narrowed to concrete AuthTables for internal queries + // eslint-disable-next-line typescript/no-unsafe-type-assertion -- generic Kysely narrowed to concrete AuthTables for internal queries const kdb = db as unknown as Kysely; return { diff --git a/packages/auth/src/rbac.ts b/packages/auth/src/rbac.ts index 8a4e89863..704f84f2b 100644 --- a/packages/auth/src/rbac.ts +++ b/packages/auth/src/rbac.ts @@ -202,7 +202,7 @@ const SCOPE_MIN_ROLE: Record = { * to enforce: effective_scopes = requested_scopes ∩ scopesForRole(role). */ export function scopesForRole(role: RoleLevel): ApiTokenScope[] { - // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- Object.entries loses tuple types; SCOPE_MIN_ROLE keys are ApiTokenScope by construction + // eslint-disable-next-line typescript/no-unsafe-type-assertion -- Object.entries loses tuple types; SCOPE_MIN_ROLE keys are ApiTokenScope by construction const entries = Object.entries(SCOPE_MIN_ROLE) as [ApiTokenScope, RoleLevel][]; return entries.reduce((acc, [scope, minRole]) => { if (role >= minRole) acc.push(scope); diff --git a/packages/cloudflare/src/db/d1-introspector.ts b/packages/cloudflare/src/db/d1-introspector.ts index 4d9cecc76..4c40fc55a 100644 --- a/packages/cloudflare/src/db/d1-introspector.ts +++ b/packages/cloudflare/src/db/d1-introspector.ts @@ -59,11 +59,11 @@ export class D1Introspector implements DatabaseIntrospector { const result: TableMetadata[] = []; for (const table of tables) { - // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- Kysely's DatabaseIntrospector returns untyped results + // eslint-disable-next-line typescript/no-unsafe-type-assertion -- Kysely's DatabaseIntrospector returns untyped results const tableName = table.name as string; - // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- Kysely's DatabaseIntrospector returns untyped results + // eslint-disable-next-line typescript/no-unsafe-type-assertion -- Kysely's DatabaseIntrospector returns untyped results const tableType = table.type as string; - // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- Kysely's DatabaseIntrospector returns untyped results + // eslint-disable-next-line typescript/no-unsafe-type-assertion -- Kysely's DatabaseIntrospector returns untyped results const tableSql = table.sql as string | null; // Get columns for this specific table diff --git a/packages/cloudflare/src/db/d1.ts b/packages/cloudflare/src/db/d1.ts index fca5b220f..a8ca49db1 100644 --- a/packages/cloudflare/src/db/d1.ts +++ b/packages/cloudflare/src/db/d1.ts @@ -160,7 +160,7 @@ export function createRequestScopedDb(opts: RequestScopedDbOpts): RequestScopedD const session = binding.withSession(constraint); // kysely-d1 only touches .prepare() and .batch() on the database argument, // both of which D1DatabaseSession implements. - // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- session is structurally compatible with the subset D1Dialect uses + // eslint-disable-next-line typescript/no-unsafe-type-assertion -- session is structurally compatible with the subset D1Dialect uses const sessionAsDatabase = session as unknown as D1Database; const db = new Kysely({ dialect: new EmDashD1Dialect({ database: sessionAsDatabase }), @@ -190,7 +190,7 @@ function isSessionEnabled(config: D1Config): boolean { } function getBinding(config: D1Config): D1Database | null { - // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- Worker binding accessed from untyped env object + // eslint-disable-next-line typescript/no-unsafe-type-assertion -- Worker binding accessed from untyped env object const db = (env as Record)[config.binding] as D1Database | undefined; return db ?? null; } diff --git a/packages/cloudflare/src/db/do-class.ts b/packages/cloudflare/src/db/do-class.ts index df35b77af..1b968f7bc 100644 --- a/packages/cloudflare/src/db/do-class.ts +++ b/packages/cloudflare/src/db/do-class.ts @@ -58,7 +58,7 @@ export class EmDashPreviewDB extends DurableObject { const rows: Record[] = []; for (const row of cursor) { - // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- SqlStorageCursor yields record-like objects + // eslint-disable-next-line typescript/no-unsafe-type-assertion -- SqlStorageCursor yields record-like objects rows.push(row as Record); } @@ -141,7 +141,7 @@ export class EmDashPreviewDB extends DurableObject { const gen = this.ctx.storage.sql .exec("SELECT value FROM _emdash_do_meta WHERE key = 'snapshot_generated_at'") .one(); - // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- SqlStorageCursor yields loosely-typed rows + // eslint-disable-next-line typescript/no-unsafe-type-assertion -- SqlStorageCursor yields loosely-typed rows return { generatedAt: String(gen.value as string | number) }; } } catch (error) { @@ -220,7 +220,7 @@ export class EmDashPreviewDB extends DurableObject { ), ]; for (const row of tables) { - // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- SqlStorageCursor yields loosely-typed rows + // eslint-disable-next-line typescript/no-unsafe-type-assertion -- SqlStorageCursor yields loosely-typed rows const name = String(row.name as string); if (!SAFE_IDENTIFIER.test(name)) { // Skip tables with unsafe names rather than interpolating them diff --git a/packages/cloudflare/src/db/do-dialect.ts b/packages/cloudflare/src/db/do-dialect.ts index 391b15f3e..cb3e786ec 100644 --- a/packages/cloudflare/src/db/do-dialect.ts +++ b/packages/cloudflare/src/db/do-dialect.ts @@ -106,13 +106,13 @@ class PreviewDOConnection implements DatabaseConnection { async executeQuery(compiledQuery: CompiledQuery): Promise> { const sqlText = compiledQuery.sql; - // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- CompiledQuery.parameters is ReadonlyArray, stub expects unknown[] + // eslint-disable-next-line typescript/no-unsafe-type-assertion -- CompiledQuery.parameters is ReadonlyArray, stub expects unknown[] const params = compiledQuery.parameters as unknown[]; const result = await this.#stub.query(sqlText, params); return { - // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- Kysely generic O is the caller's row type; we trust the DB returned matching rows + // eslint-disable-next-line typescript/no-unsafe-type-assertion -- Kysely generic O is the caller's row type; we trust the DB returned matching rows rows: result.rows as O[], numAffectedRows: result.changes !== undefined ? BigInt(result.changes) : undefined, }; diff --git a/packages/cloudflare/src/db/do-preview.ts b/packages/cloudflare/src/db/do-preview.ts index 9cd8b47aa..6c1e9241d 100644 --- a/packages/cloudflare/src/db/do-preview.ts +++ b/packages/cloudflare/src/db/do-preview.ts @@ -160,13 +160,13 @@ export function createPreviewMiddleware(config: PreviewMiddlewareConfig): Middle } // --- 2. Get DO stub --- - // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- Worker binding from untyped env + // eslint-disable-next-line typescript/no-unsafe-type-assertion -- Worker binding from untyped env const ns = (env as Record)[binding]; if (!ns) { console.error(`Preview binding "${binding}" not found in environment`); return new Response("Preview service misconfigured", { status: 500 }); } - // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- DO namespace from untyped env + // eslint-disable-next-line typescript/no-unsafe-type-assertion -- DO namespace from untyped env const namespace = ns as DurableObjectNamespace; const doId = namespace.idFromName(sessionToken); const stub = namespace.get(doId); @@ -220,7 +220,7 @@ export function createPreviewMiddleware(config: PreviewMiddlewareConfig): Middle // --- 4. Create Kysely dialect pointing at the DO --- const getStub = (): PreviewDBStub => { - // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- RPC type limitation + // eslint-disable-next-line typescript/no-unsafe-type-assertion -- RPC type limitation return stub as unknown as PreviewDBStub; }; const dialect = new PreviewDODialect({ getStub }); diff --git a/packages/cloudflare/src/db/do.ts b/packages/cloudflare/src/db/do.ts index c07ee3014..1b6489053 100644 --- a/packages/cloudflare/src/db/do.ts +++ b/packages/cloudflare/src/db/do.ts @@ -23,7 +23,7 @@ import type { PreviewDOConfig } from "./do-types.js"; * This is passed as `config.name` by the preview middleware. */ export function createDialect(config: PreviewDOConfig & { name: string }): Dialect { - // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- Worker binding accessed from untyped env object + // eslint-disable-next-line typescript/no-unsafe-type-assertion -- Worker binding accessed from untyped env object const ns = (env as Record)[config.binding]; if (!ns) { @@ -40,14 +40,14 @@ export function createDialect(config: PreviewDOConfig & { name: string }): Diale ); } - // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- DO namespace binding from untyped env object + // eslint-disable-next-line typescript/no-unsafe-type-assertion -- DO namespace binding from untyped env object const namespace = ns as DurableObjectNamespace; const id = namespace.idFromName(config.name); // Return a factory that creates a fresh stub per connection. const getStub = (): PreviewDBStub => { const stub = namespace.get(id); - // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- Rpc type limitation with unknown in return types + // eslint-disable-next-line typescript/no-unsafe-type-assertion -- Rpc type limitation with unknown in return types return stub as unknown as PreviewDBStub; }; diff --git a/packages/cloudflare/src/db/playground-middleware.ts b/packages/cloudflare/src/db/playground-middleware.ts index d01d22f8f..9bc70a5d6 100644 --- a/packages/cloudflare/src/db/playground-middleware.ts +++ b/packages/cloudflare/src/db/playground-middleware.ts @@ -54,7 +54,7 @@ const initializedSessions = new Set(); * The database config has the binding in `config.database.config.binding`. */ function getBindingName(): string { - // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- virtual module import + // eslint-disable-next-line typescript/no-unsafe-type-assertion -- virtual module import const config = virtualConfig as { database?: { config?: { binding?: string } } } | null; const binding = config?.database?.config?.binding; if (!binding) { @@ -70,16 +70,16 @@ function getBindingName(): string { * Get a PreviewDBStub for the given session token. */ function getStub(binding: string, token: string): PreviewDBStub { - // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- Worker binding from untyped env + // eslint-disable-next-line typescript/no-unsafe-type-assertion -- Worker binding from untyped env const ns = (env as Record)[binding]; if (!ns) { throw new Error(`Playground binding "${binding}" not found in environment`); } - // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- DO namespace from untyped env + // eslint-disable-next-line typescript/no-unsafe-type-assertion -- DO namespace from untyped env const namespace = ns as DurableObjectNamespace; const doId = namespace.idFromName(token); const stub = namespace.get(doId); - // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- RPC type limitation + // eslint-disable-next-line typescript/no-unsafe-type-assertion -- RPC type limitation return stub as unknown as PreviewDBStub; } @@ -87,12 +87,12 @@ function getStub(binding: string, token: string): PreviewDBStub { * Get the full DO stub for direct RPC calls (e.g. setTtlAlarm). */ function getFullStub(binding: string, token: string): DurableObjectStub { - // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- Worker binding from untyped env + // eslint-disable-next-line typescript/no-unsafe-type-assertion -- Worker binding from untyped env const ns = (env as Record)[binding]; if (!ns) { throw new Error(`Playground binding "${binding}" not found in environment`); } - // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- DO namespace from untyped env + // eslint-disable-next-line typescript/no-unsafe-type-assertion -- DO namespace from untyped env const namespace = ns as DurableObjectNamespace; const doId = namespace.idFromName(token); return namespace.get(doId); diff --git a/packages/cloudflare/src/media/images-runtime.ts b/packages/cloudflare/src/media/images-runtime.ts index 6f71b0450..786715ac7 100644 --- a/packages/cloudflare/src/media/images-runtime.ts +++ b/packages/cloudflare/src/media/images-runtime.ts @@ -33,7 +33,7 @@ function resolveEnvValue( ): string { if (directValue) return directValue; const envVar = envVarName || defaultEnvVar; - // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- Worker binding accessed from untyped env object + // eslint-disable-next-line typescript/no-unsafe-type-assertion -- Worker binding accessed from untyped env object const value = (env as Record)[envVar]; if (!value) { throw new Error( diff --git a/packages/cloudflare/src/media/stream-runtime.ts b/packages/cloudflare/src/media/stream-runtime.ts index f698d099f..7cd3926d9 100644 --- a/packages/cloudflare/src/media/stream-runtime.ts +++ b/packages/cloudflare/src/media/stream-runtime.ts @@ -38,7 +38,7 @@ function resolveEnvValue( ): string { if (directValue) return directValue; const envVar = envVarName || defaultEnvVar; - // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- Worker binding accessed from untyped env object + // eslint-disable-next-line typescript/no-unsafe-type-assertion -- Worker binding accessed from untyped env object const value = (env as Record)[envVar]; if (!value) { throw new Error( diff --git a/packages/cloudflare/src/plugins/vectorize-search.ts b/packages/cloudflare/src/plugins/vectorize-search.ts index 836845232..34ecbdd9c 100644 --- a/packages/cloudflare/src/plugins/vectorize-search.ts +++ b/packages/cloudflare/src/plugins/vectorize-search.ts @@ -84,7 +84,7 @@ export interface VectorizeSearchConfig { function getCloudflareEnv(request: Request): CloudflareEnv | null { // Access runtime.env from Astro's Cloudflare adapter // This is available when running on Cloudflare Workers - // eslint-disable-next-line @typescript-eslint/no-explicit-any, typescript-eslint(no-unsafe-type-assertion) -- Astro locals accessed via internal symbol; no typed API available + // eslint-disable-next-line @typescript-eslint/no-explicit-any, typescript/no-unsafe-type-assertion -- Astro locals accessed via internal symbol; no typed API available const locals = (request as any)[Symbol.for("astro.locals")]; if (locals?.runtime?.env) { return locals.runtime.env; @@ -113,7 +113,7 @@ function extractSearchableText(content: Record): string { if (text) parts.push(text); } else if (Array.isArray(value)) { // Assume Portable Text array - // eslint-disable-next-line @typescript-eslint/no-explicit-any, typescript-eslint(no-unsafe-type-assertion) -- Portable Text arrays are untyped at this point; extractPlainText handles validation + // eslint-disable-next-line @typescript-eslint/no-explicit-any, typescript/no-unsafe-type-assertion -- Portable Text arrays are untyped at this point; extractPlainText handles validation const text = extractPlainText(value as any); if (text) parts.push(text); } diff --git a/packages/cloudflare/src/sandbox/runner.ts b/packages/cloudflare/src/sandbox/runner.ts index b82bcac4a..eb9262215 100644 --- a/packages/cloudflare/src/sandbox/runner.ts +++ b/packages/cloudflare/src/sandbox/runner.ts @@ -59,7 +59,7 @@ export interface PluginBridgeProps { * Get the Worker Loader binding from env */ function getLoader(): WorkerLoader | null { - // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- Worker Loader binding accessed from untyped env object + // eslint-disable-next-line typescript/no-unsafe-type-assertion -- Worker Loader binding accessed from untyped env object return (env as Record).LOADER as WorkerLoader | null; } @@ -67,7 +67,7 @@ function getLoader(): WorkerLoader | null { * Get the PluginBridge from exports (loopback binding) */ function getPluginBridge(): ((opts: { props: PluginBridgeProps }) => PluginBridgeBinding) | null { - // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- PluginBridge accessed from untyped cloudflare:workers exports + // eslint-disable-next-line typescript/no-unsafe-type-assertion -- PluginBridge accessed from untyped cloudflare:workers exports return (exports as Record).PluginBridge as | ((opts: { props: PluginBridgeProps }) => PluginBridgeBinding) | null; diff --git a/packages/cloudflare/src/storage/r2.ts b/packages/cloudflare/src/storage/r2.ts index 7b076b06b..864b020c7 100644 --- a/packages/cloudflare/src/storage/r2.ts +++ b/packages/cloudflare/src/storage/r2.ts @@ -169,7 +169,7 @@ export function createStorage(config: Record): Storage { } // env from cloudflare:workers doesn't have an index signature, so cast is needed - // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- R2Bucket binding accessed from untyped env object + // eslint-disable-next-line typescript/no-unsafe-type-assertion -- R2Bucket binding accessed from untyped env object const bucket = (env as Record)[binding] as R2Bucket | undefined; if (!bucket) { diff --git a/packages/cloudflare/tests/sandbox/bridge-http.test.ts b/packages/cloudflare/tests/sandbox/bridge-http.test.ts index 4b9e87a07..355eef8a0 100644 --- a/packages/cloudflare/tests/sandbox/bridge-http.test.ts +++ b/packages/cloudflare/tests/sandbox/bridge-http.test.ts @@ -33,7 +33,7 @@ function mockFetchSequence(responses: Response[]): FetchImpl { const next = queue.shift(); if (!next) throw new Error("fetch called more times than expected"); return next; - // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- vi.fn's generic signature doesn't line up with Workers' fetch type; cast to the injectable contract + // eslint-disable-next-line typescript/no-unsafe-type-assertion -- vi.fn's generic signature doesn't line up with Workers' fetch type; cast to the injectable contract }) as unknown as FetchImpl; } @@ -148,7 +148,7 @@ describe("sandboxHttpFetch — credential header stripping", () => { }, ); - // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- vi.Mock type hygiene + // eslint-disable-next-line typescript/no-unsafe-type-assertion -- vi.Mock type hygiene const secondCall = (fetchImpl as unknown as { mock: { calls: unknown[][] } }).mock.calls[1]; const init = secondCall?.[1] as RequestInit | undefined; const headers = new Headers(init?.headers); @@ -173,7 +173,7 @@ describe("sandboxHttpFetch — credential header stripping", () => { }, ); - // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- vi.Mock type hygiene + // eslint-disable-next-line typescript/no-unsafe-type-assertion -- vi.Mock type hygiene const secondCall = (fetchImpl as unknown as { mock: { calls: unknown[][] } }).mock.calls[1]; const init = secondCall?.[1] as RequestInit | undefined; const headers = new Headers(init?.headers); @@ -201,7 +201,7 @@ describe("sandboxHttpFetch — credential header stripping", () => { }, ); - // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- vi.Mock type hygiene + // eslint-disable-next-line typescript/no-unsafe-type-assertion -- vi.Mock type hygiene const secondCall = (fetchImpl as unknown as { mock: { calls: unknown[][] } }).mock.calls[1]; const init = secondCall?.[1] as RequestInit | undefined; const headers = new Headers(init?.headers); diff --git a/packages/core/src/api/handlers/oauth-clients.ts b/packages/core/src/api/handlers/oauth-clients.ts index a360e0445..778180c1d 100644 --- a/packages/core/src/api/handlers/oauth-clients.ts +++ b/packages/core/src/api/handlers/oauth-clients.ts @@ -18,7 +18,7 @@ import type { ApiResult } from "../types.js"; /** Parse a JSON string column into a typed value. */ function parseJsonColumn(value: string): T { - // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- JSON.parse returns unknown, callers provide the expected shape + // eslint-disable-next-line typescript/no-unsafe-type-assertion -- JSON.parse returns unknown, callers provide the expected shape return JSON.parse(value) as T; } diff --git a/packages/core/src/api/openapi/document.ts b/packages/core/src/api/openapi/document.ts index 1f82aaf2d..44b7147d5 100644 --- a/packages/core/src/api/openapi/document.ts +++ b/packages/core/src/api/openapi/document.ts @@ -2379,7 +2379,7 @@ export function generateOpenApiDocument( }, }, security: [{ session: [] }, { bearer: [] }], - // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- readonly const paths are compatible at runtime + // eslint-disable-next-line typescript/no-unsafe-type-assertion -- readonly const paths are compatible at runtime paths: buildAllPaths(maxUploadSize) as unknown as ZodOpenApiPathsObject, }); } diff --git a/packages/core/src/astro/integration/runtime.ts b/packages/core/src/astro/integration/runtime.ts index a07b9d2c4..3fe2b44d7 100644 --- a/packages/core/src/astro/integration/runtime.ts +++ b/packages/core/src/astro/integration/runtime.ts @@ -537,7 +537,7 @@ const configHolder = globalThis as Record; * This is set by the virtual module at build time */ export function getStoredConfig(): EmDashConfig | null { - // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- globalThis singleton pattern (see request-context.ts) + // eslint-disable-next-line typescript/no-unsafe-type-assertion -- globalThis singleton pattern (see request-context.ts) return (configHolder[STORED_CONFIG_KEY] as EmDashConfig | undefined) ?? null; } diff --git a/packages/core/src/astro/integration/vite-config.ts b/packages/core/src/astro/integration/vite-config.ts index 124161936..669de9076 100644 --- a/packages/core/src/astro/integration/vite-config.ts +++ b/packages/core/src/astro/integration/vite-config.ts @@ -346,7 +346,7 @@ export function createViteConfig( { find: "use-sync-external-store/shim", replacement: "use-sync-external-store" }, ], }, - // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- Monorepo has both vite 6 (docs) and vite 7 (core). tsgo resolves correctly. + // eslint-disable-next-line typescript/no-unsafe-type-assertion -- Monorepo has both vite 6 (docs) and vite 7 (core). tsgo resolves correctly. plugins: [ createVirtualModulesPlugin(options), // In dev mode with source alias, compile Lingui macros on the fly diff --git a/packages/core/src/astro/middleware/request-context.ts b/packages/core/src/astro/middleware/request-context.ts index e9f96168d..7c0f68591 100644 --- a/packages/core/src/astro/middleware/request-context.ts +++ b/packages/core/src/astro/middleware/request-context.ts @@ -78,7 +78,7 @@ export const onRequest = defineMiddleware(async (context, next) => { const editMode = hasEditCookie && isEditor; // Read locale from Astro's i18n routing - // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- Astro context includes currentLocale when i18n is configured + // eslint-disable-next-line typescript/no-unsafe-type-assertion -- Astro context includes currentLocale when i18n is configured const locale = (context as { currentLocale?: string }).currentLocale; // Verify preview token if present. diff --git a/packages/core/src/astro/routes/api/admin/themes/marketplace/index.ts b/packages/core/src/astro/routes/api/admin/themes/marketplace/index.ts index 9262acaf3..6ddd78c4f 100644 --- a/packages/core/src/astro/routes/api/admin/themes/marketplace/index.ts +++ b/packages/core/src/astro/routes/api/admin/themes/marketplace/index.ts @@ -28,7 +28,7 @@ export const GET: APIRoute = async ({ url, locals }) => { const validSorts = new Set(["name", "created", "updated"]); let sort: "name" | "created" | "updated" | undefined; if (sortParam && validSorts.has(sortParam)) { - sort = sortParam as "name" | "created" | "updated"; // eslint-disable-line typescript-eslint(no-unsafe-type-assertion) -- validated by Set.has() + sort = sortParam as "name" | "created" | "updated"; // eslint-disable-line typescript/no-unsafe-type-assertion -- validated by Set.has() } const cursor = url.searchParams.get("cursor") ?? undefined; const limitParam = url.searchParams.get("limit"); diff --git a/packages/core/src/astro/routes/api/auth/oauth/[provider].ts b/packages/core/src/astro/routes/api/auth/oauth/[provider].ts index 0d7d8fa63..a9706888f 100644 --- a/packages/core/src/astro/routes/api/auth/oauth/[provider].ts +++ b/packages/core/src/astro/routes/api/auth/oauth/[provider].ts @@ -95,9 +95,9 @@ export const GET: APIRoute = async ({ params, request, locals, redirect }) => { // Get OAuth providers from environment // Access via locals.runtime for Cloudflare, or import.meta.env for Node - // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- locals.runtime is injected by the Cloudflare adapter at runtime; not declared on App.Locals since the adapter is optional + // eslint-disable-next-line typescript/no-unsafe-type-assertion -- locals.runtime is injected by the Cloudflare adapter at runtime; not declared on App.Locals since the adapter is optional const runtimeLocals = locals as unknown as { runtime?: { env?: Record } }; - // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- import.meta.env is typed as ImportMetaEnv but we need Record for getOAuthConfig + // eslint-disable-next-line typescript/no-unsafe-type-assertion -- import.meta.env is typed as ImportMetaEnv but we need Record for getOAuthConfig const env = runtimeLocals.runtime?.env ?? (import.meta.env as Record); const providers = getOAuthConfig(env); diff --git a/packages/core/src/astro/routes/api/auth/oauth/[provider]/callback.ts b/packages/core/src/astro/routes/api/auth/oauth/[provider]/callback.ts index f7e7cc311..e49c2acdc 100644 --- a/packages/core/src/astro/routes/api/auth/oauth/[provider]/callback.ts +++ b/packages/core/src/astro/routes/api/auth/oauth/[provider]/callback.ts @@ -116,9 +116,9 @@ export const GET: APIRoute = async ({ params, request, locals, session, redirect try { // Get OAuth providers from environment - // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- locals.runtime is injected by the Cloudflare adapter at runtime; not declared on App.Locals since the adapter is optional + // eslint-disable-next-line typescript/no-unsafe-type-assertion -- locals.runtime is injected by the Cloudflare adapter at runtime; not declared on App.Locals since the adapter is optional const runtimeLocals = locals as unknown as { runtime?: { env?: Record } }; - // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- import.meta.env is typed as ImportMetaEnv but we need Record for getOAuthConfig + // eslint-disable-next-line typescript/no-unsafe-type-assertion -- import.meta.env is typed as ImportMetaEnv but we need Record for getOAuthConfig const env = runtimeLocals.runtime?.env ?? (import.meta.env as Record); const providers = getOAuthConfig(env); diff --git a/packages/core/src/astro/routes/api/content/[collection]/[id].ts b/packages/core/src/astro/routes/api/content/[collection]/[id].ts index b064279e9..e253c312f 100644 --- a/packages/core/src/astro/routes/api/content/[collection]/[id].ts +++ b/packages/core/src/astro/routes/api/content/[collection]/[id].ts @@ -35,12 +35,12 @@ export const GET: APIRoute = async ({ params, url, locals }) => { if (result.success && !hasPermission(user, "content:read_drafts")) { const data = result.data && typeof result.data === "object" - ? // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- handler returns unknown data; narrowed by typeof check + ? // eslint-disable-next-line typescript/no-unsafe-type-assertion -- handler returns unknown data; narrowed by typeof check (result.data as Record) : undefined; const item = data?.item && typeof data.item === "object" - ? // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- narrowed by typeof check + ? // eslint-disable-next-line typescript/no-unsafe-type-assertion -- narrowed by typeof check (data.item as Record) : undefined; const status = typeof item?.status === "string" ? item.status : null; @@ -87,13 +87,13 @@ export const PUT: APIRoute = async ({ params, request, locals, cache }) => { const existingData = existing.data && typeof existing.data === "object" - ? // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- handler returns unknown data; narrowed by typeof check above + ? // eslint-disable-next-line typescript/no-unsafe-type-assertion -- handler returns unknown data; narrowed by typeof check above (existing.data as Record) : undefined; // Handler returns { item, _rev } — extract the item for ownership and ID resolution const existingItem = existingData?.item && typeof existingData.item === "object" - ? // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- narrowed by typeof check above + ? // eslint-disable-next-line typescript/no-unsafe-type-assertion -- narrowed by typeof check above (existingData.item as Record) : existingData; const authorId = typeof existingItem?.authorId === "string" ? existingItem.authorId : ""; @@ -151,13 +151,13 @@ export const DELETE: APIRoute = async ({ params, locals, cache }) => { const deleteData = existing.data && typeof existing.data === "object" - ? // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- handler returns unknown data; narrowed by typeof check above + ? // eslint-disable-next-line typescript/no-unsafe-type-assertion -- handler returns unknown data; narrowed by typeof check above (existing.data as Record) : undefined; // Handler returns { item, _rev } — extract the item for ownership and ID resolution const deleteItem = deleteData?.item && typeof deleteData.item === "object" - ? // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- narrowed by typeof check above + ? // eslint-disable-next-line typescript/no-unsafe-type-assertion -- narrowed by typeof check above (deleteData.item as Record) : deleteData; const authorId = typeof deleteItem?.authorId === "string" ? deleteItem.authorId : ""; diff --git a/packages/core/src/astro/routes/api/content/[collection]/[id]/discard-draft.ts b/packages/core/src/astro/routes/api/content/[collection]/[id]/discard-draft.ts index 00738dc5a..321d7d21e 100644 --- a/packages/core/src/astro/routes/api/content/[collection]/[id]/discard-draft.ts +++ b/packages/core/src/astro/routes/api/content/[collection]/[id]/discard-draft.ts @@ -31,13 +31,13 @@ export const POST: APIRoute = async ({ params, locals, cache }) => { } const existingData = existing.data && typeof existing.data === "object" - ? // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- handler returns unknown data; narrowed by typeof check above + ? // eslint-disable-next-line typescript/no-unsafe-type-assertion -- handler returns unknown data; narrowed by typeof check above (existing.data as Record) : undefined; // Handler returns { item, _rev } — extract the item for ownership check const existingItem = existingData?.item && typeof existingData.item === "object" - ? // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- narrowed by typeof check above + ? // eslint-disable-next-line typescript/no-unsafe-type-assertion -- narrowed by typeof check above (existingData.item as Record) : existingData; const authorId = typeof existingItem?.authorId === "string" ? existingItem.authorId : ""; diff --git a/packages/core/src/astro/routes/api/content/[collection]/[id]/duplicate.ts b/packages/core/src/astro/routes/api/content/[collection]/[id]/duplicate.ts index d5107fcad..68b4d2963 100644 --- a/packages/core/src/astro/routes/api/content/[collection]/[id]/duplicate.ts +++ b/packages/core/src/astro/routes/api/content/[collection]/[id]/duplicate.ts @@ -35,13 +35,13 @@ export const POST: APIRoute = async ({ params, locals, cache }) => { const existingData = existing.data && typeof existing.data === "object" - ? // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- handler returns unknown data; narrowed by typeof check above + ? // eslint-disable-next-line typescript/no-unsafe-type-assertion -- handler returns unknown data; narrowed by typeof check above (existing.data as Record) : undefined; // Handler returns { item, _rev } — extract the item for ownership check const existingItem = existingData?.item && typeof existingData.item === "object" - ? // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- narrowed by typeof check above + ? // eslint-disable-next-line typescript/no-unsafe-type-assertion -- narrowed by typeof check above (existingData.item as Record) : existingData; const authorId = typeof existingItem?.authorId === "string" ? existingItem.authorId : ""; diff --git a/packages/core/src/astro/routes/api/content/[collection]/[id]/publish.ts b/packages/core/src/astro/routes/api/content/[collection]/[id]/publish.ts index 7311034ba..8a80e6c6e 100644 --- a/packages/core/src/astro/routes/api/content/[collection]/[id]/publish.ts +++ b/packages/core/src/astro/routes/api/content/[collection]/[id]/publish.ts @@ -46,12 +46,12 @@ export const POST: APIRoute = async ({ params, request, locals, cache }) => { const existingData = existing.data && typeof existing.data === "object" - ? // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- handler returns unknown data; narrowed by typeof check above + ? // eslint-disable-next-line typescript/no-unsafe-type-assertion -- handler returns unknown data; narrowed by typeof check above (existing.data as Record) : undefined; const existingItem = existingData?.item && typeof existingData.item === "object" - ? // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- narrowed by typeof check above + ? // eslint-disable-next-line typescript/no-unsafe-type-assertion -- narrowed by typeof check above (existingData.item as Record) : existingData; const authorId = typeof existingItem?.authorId === "string" ? existingItem.authorId : ""; diff --git a/packages/core/src/astro/routes/api/content/[collection]/[id]/restore.ts b/packages/core/src/astro/routes/api/content/[collection]/[id]/restore.ts index 071a2453d..e4b793cf9 100644 --- a/packages/core/src/astro/routes/api/content/[collection]/[id]/restore.ts +++ b/packages/core/src/astro/routes/api/content/[collection]/[id]/restore.ts @@ -31,13 +31,13 @@ export const POST: APIRoute = async ({ params, locals, cache }) => { } const existingData = existing.data && typeof existing.data === "object" - ? // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- handler returns unknown data; narrowed by typeof check above + ? // eslint-disable-next-line typescript/no-unsafe-type-assertion -- handler returns unknown data; narrowed by typeof check above (existing.data as Record) : undefined; // Handler returns { item, _rev } — extract the item for ownership check const existingItem = existingData?.item && typeof existingData.item === "object" - ? // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- narrowed by typeof check above + ? // eslint-disable-next-line typescript/no-unsafe-type-assertion -- narrowed by typeof check above (existingData.item as Record) : existingData; const authorId = typeof existingItem?.authorId === "string" ? existingItem.authorId : ""; diff --git a/packages/core/src/astro/routes/api/content/[collection]/[id]/schedule.ts b/packages/core/src/astro/routes/api/content/[collection]/[id]/schedule.ts index 67f2caecf..74a0dd5aa 100644 --- a/packages/core/src/astro/routes/api/content/[collection]/[id]/schedule.ts +++ b/packages/core/src/astro/routes/api/content/[collection]/[id]/schedule.ts @@ -20,12 +20,12 @@ export const prerender = false; function extractOwnership(data: unknown): { authorId: string; resolvedId: string | undefined } { const obj = data && typeof data === "object" - ? // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- handler returns unknown; narrowed by typeof + ? // eslint-disable-next-line typescript/no-unsafe-type-assertion -- handler returns unknown; narrowed by typeof (data as Record) : undefined; const item = obj?.item && typeof obj.item === "object" - ? // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- narrowed by typeof + ? // eslint-disable-next-line typescript/no-unsafe-type-assertion -- narrowed by typeof (obj.item as Record) : obj; return { diff --git a/packages/core/src/astro/routes/api/content/[collection]/[id]/terms/[taxonomy].ts b/packages/core/src/astro/routes/api/content/[collection]/[id]/terms/[taxonomy].ts index 6cd724722..761d460da 100644 --- a/packages/core/src/astro/routes/api/content/[collection]/[id]/terms/[taxonomy].ts +++ b/packages/core/src/astro/routes/api/content/[collection]/[id]/terms/[taxonomy].ts @@ -85,13 +85,13 @@ export const POST: APIRoute = async ({ params, request, locals }) => { // Check ownership for edit permission const existingData = existing.data && typeof existing.data === "object" - ? // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- handler returns unknown data; narrowed by typeof check above + ? // eslint-disable-next-line typescript/no-unsafe-type-assertion -- handler returns unknown data; narrowed by typeof check above (existing.data as Record) : undefined; // Handler returns { item, _rev } — extract the item for ownership check const existingItem = existingData?.item && typeof existingData.item === "object" - ? // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- narrowed by typeof check above + ? // eslint-disable-next-line typescript/no-unsafe-type-assertion -- narrowed by typeof check above (existingData.item as Record) : existingData; const authorId = typeof existingItem?.authorId === "string" ? existingItem.authorId : ""; diff --git a/packages/core/src/astro/routes/api/content/[collection]/[id]/translations.ts b/packages/core/src/astro/routes/api/content/[collection]/[id]/translations.ts index 15bf3e70a..9f4866a9f 100644 --- a/packages/core/src/astro/routes/api/content/[collection]/[id]/translations.ts +++ b/packages/core/src/astro/routes/api/content/[collection]/[id]/translations.ts @@ -41,7 +41,7 @@ export const GET: APIRoute = async ({ params, locals }) => { if (result.success && !hasPermission(user, "content:read_drafts")) { const data = result.data && typeof result.data === "object" - ? // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- handler returns unknown data; narrowed by typeof check + ? // eslint-disable-next-line typescript/no-unsafe-type-assertion -- handler returns unknown data; narrowed by typeof check (result.data as Record) : undefined; const translations = Array.isArray(data?.translations) ? data.translations : []; diff --git a/packages/core/src/astro/routes/api/content/[collection]/[id]/unpublish.ts b/packages/core/src/astro/routes/api/content/[collection]/[id]/unpublish.ts index 29e6ee100..1092612e2 100644 --- a/packages/core/src/astro/routes/api/content/[collection]/[id]/unpublish.ts +++ b/packages/core/src/astro/routes/api/content/[collection]/[id]/unpublish.ts @@ -32,12 +32,12 @@ export const POST: APIRoute = async ({ params, locals, cache }) => { const existingData = existing.data && typeof existing.data === "object" - ? // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- handler returns unknown data; narrowed by typeof check above + ? // eslint-disable-next-line typescript/no-unsafe-type-assertion -- handler returns unknown data; narrowed by typeof check above (existing.data as Record) : undefined; const existingItem = existingData?.item && typeof existingData.item === "object" - ? // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- narrowed by typeof check above + ? // eslint-disable-next-line typescript/no-unsafe-type-assertion -- narrowed by typeof check above (existingData.item as Record) : existingData; const authorId = typeof existingItem?.authorId === "string" ? existingItem.authorId : ""; diff --git a/packages/core/src/astro/routes/api/import/wordpress-plugin/execute.ts b/packages/core/src/astro/routes/api/import/wordpress-plugin/execute.ts index c802e8da0..2a383eb09 100644 --- a/packages/core/src/astro/routes/api/import/wordpress-plugin/execute.ts +++ b/packages/core/src/astro/routes/api/import/wordpress-plugin/execute.ts @@ -60,7 +60,7 @@ export const POST: APIRoute = async ({ request, locals }) => { return apiError("SSRF_BLOCKED", msg, 400); } - // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- Zod schema output narrowed to WpPluginImportConfig + // eslint-disable-next-line typescript/no-unsafe-type-assertion -- Zod schema output narrowed to WpPluginImportConfig const config = body.config as unknown as WpPluginImportConfig; // Get the WordPress plugin source @@ -316,7 +316,7 @@ async function importContent( // Track translation group: first item in a group establishes the mapping if (item.translationGroup && !translationGroupMap.has(item.translationGroup)) { - // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- handler success data includes id + // eslint-disable-next-line typescript/no-unsafe-type-assertion -- handler success data includes id const createdData = createResult.data as { id?: string } | undefined; if (createdData?.id) { translationGroupMap.set(item.translationGroup, createdData.id); diff --git a/packages/core/src/astro/routes/api/import/wordpress/execute.ts b/packages/core/src/astro/routes/api/import/wordpress/execute.ts index 60f256e52..d6598ca8c 100644 --- a/packages/core/src/astro/routes/api/import/wordpress/execute.ts +++ b/packages/core/src/astro/routes/api/import/wordpress/execute.ts @@ -376,7 +376,7 @@ export async function importContent( // `api/handlers/content.ts`). `HandlerResponse.data` is // typed as `unknown` to avoid coupling the route surface to // internal handler types, so we narrow here. - // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- handler contract documented at handleContentCreate + // eslint-disable-next-line typescript/no-unsafe-type-assertion -- handler contract documented at handleContentCreate const createdItem = (createResult.data as { item: { id: string } } | undefined)?.item; // Track translation group: the first imported post in a group diff --git a/packages/core/src/astro/routes/api/import/wordpress/prepare.ts b/packages/core/src/astro/routes/api/import/wordpress/prepare.ts index 86c1d201e..ed11a1f71 100644 --- a/packages/core/src/astro/routes/api/import/wordpress/prepare.ts +++ b/packages/core/src/astro/routes/api/import/wordpress/prepare.ts @@ -20,7 +20,7 @@ import { capitalize, sanitizeSlug, singularize, type ImportFieldDef } from "./an /** Validate that a string is a known FieldType, returning undefined if not */ function asFieldType(value: string): FieldType | undefined { - // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- validated by includes check + // eslint-disable-next-line typescript/no-unsafe-type-assertion -- validated by includes check return (FIELD_TYPES as readonly string[]).includes(value) ? (value as FieldType) : undefined; } @@ -55,7 +55,7 @@ export const POST: APIRoute = async ({ request, locals }) => { const body = await parseBody(request, wpPrepareBody); if (isParseError(body)) return body; - // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- Zod schema output narrowed to PrepareRequest + // eslint-disable-next-line typescript/no-unsafe-type-assertion -- Zod schema output narrowed to PrepareRequest const result = await prepareImport(emdash.db, body as PrepareRequest); // Invalidate the URL pattern cache when prepare adds new collections so diff --git a/packages/core/src/astro/routes/api/import/wordpress/rewrite-urls.ts b/packages/core/src/astro/routes/api/import/wordpress/rewrite-urls.ts index eabd7b0a2..bc83e3fb1 100644 --- a/packages/core/src/astro/routes/api/import/wordpress/rewrite-urls.ts +++ b/packages/core/src/astro/routes/api/import/wordpress/rewrite-urls.ts @@ -134,7 +134,7 @@ async function rewriteUrls( if (!value || typeof value !== "string") continue; try { - // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- JSON.parse returns unknown; validated by Array.isArray below + // eslint-disable-next-line typescript/no-unsafe-type-assertion -- JSON.parse returns unknown; validated by Array.isArray below const blocks = JSON.parse(value) as PortableTextBlock[]; if (!Array.isArray(blocks)) continue; @@ -192,11 +192,11 @@ async function rewriteUrls( if (rowUpdated) { try { // Build update query dynamically - // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- Kysely dynamic table requires type assertion + // eslint-disable-next-line typescript/no-unsafe-type-assertion -- Kysely dynamic table requires type assertion let query = db.updateTable(tableName as any).where("id", "=", row.id); for (const [key, value] of Object.entries(updates)) { - // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- Kysely dynamic column update requires type assertion + // eslint-disable-next-line typescript/no-unsafe-type-assertion -- Kysely dynamic column update requires type assertion query = query.set({ [key]: value } as any); } diff --git a/packages/core/src/astro/routes/api/media/upload-url.ts b/packages/core/src/astro/routes/api/media/upload-url.ts index da690a370..edfa95ff2 100644 --- a/packages/core/src/astro/routes/api/media/upload-url.ts +++ b/packages/core/src/astro/routes/api/media/upload-url.ts @@ -136,7 +136,7 @@ export const POST: APIRoute = async ({ request, locals }) => { if ( error instanceof Error && "code" in error && - // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- narrowing error to check custom code property after "code" in error guard + // eslint-disable-next-line typescript/no-unsafe-type-assertion -- narrowing error to check custom code property after "code" in error guard (error as { code: string }).code === "NOT_SUPPORTED" ) { return apiError( diff --git a/packages/core/src/astro/routes/api/schema/collections/[slug]/fields/[fieldSlug].ts b/packages/core/src/astro/routes/api/schema/collections/[slug]/fields/[fieldSlug].ts index 5e9a5d58e..3d1e19a8d 100644 --- a/packages/core/src/astro/routes/api/schema/collections/[slug]/fields/[fieldSlug].ts +++ b/packages/core/src/astro/routes/api/schema/collections/[slug]/fields/[fieldSlug].ts @@ -50,7 +50,7 @@ export const PUT: APIRoute = async ({ params, request, locals }) => { const body = await parseBody(request, updateFieldBody); if (isParseError(body)) return body; - // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- body is Zod-validated via parseBody(request, updateFieldBody) above + // eslint-disable-next-line typescript/no-unsafe-type-assertion -- body is Zod-validated via parseBody(request, updateFieldBody) above const result = await handleSchemaFieldUpdate( emdash!.db, collectionSlug, diff --git a/packages/core/src/astro/routes/api/schema/collections/[slug]/index.ts b/packages/core/src/astro/routes/api/schema/collections/[slug]/index.ts index d574f78ce..e402b228c 100644 --- a/packages/core/src/astro/routes/api/schema/collections/[slug]/index.ts +++ b/packages/core/src/astro/routes/api/schema/collections/[slug]/index.ts @@ -56,7 +56,7 @@ export const PUT: APIRoute = async ({ params, request, locals }) => { const result = await handleSchemaCollectionUpdate( emdash!.db, slug, - // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- parseBody validates via Zod + // eslint-disable-next-line typescript/no-unsafe-type-assertion -- parseBody validates via Zod body as UpdateCollectionInput, ); emdash!.invalidateUrlPatternCache(); diff --git a/packages/core/src/astro/routes/api/schema/collections/index.ts b/packages/core/src/astro/routes/api/schema/collections/index.ts index 4920b8a7b..ee2df40ab 100644 --- a/packages/core/src/astro/routes/api/schema/collections/index.ts +++ b/packages/core/src/astro/routes/api/schema/collections/index.ts @@ -41,7 +41,7 @@ export const POST: APIRoute = async ({ request, locals }) => { const body = await parseBody(request, createCollectionBody); if (isParseError(body)) return body; - // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- Zod schema output narrowed to CreateCollectionInput + // eslint-disable-next-line typescript/no-unsafe-type-assertion -- Zod schema output narrowed to CreateCollectionInput const result = await handleSchemaCollectionCreate(emdash!.db, body as CreateCollectionInput); emdash!.invalidateUrlPatternCache(); return unwrapResult(result, 201); diff --git a/packages/core/src/auth/rate-limit.ts b/packages/core/src/auth/rate-limit.ts index 49766cf3c..f27688db0 100644 --- a/packages/core/src/auth/rate-limit.ts +++ b/packages/core/src/auth/rate-limit.ts @@ -119,7 +119,7 @@ export function rateLimitResponse(retryAfterSeconds: number): Response { */ export function getClientIp(request: Request, trustedHeaders: string[] = []): string | null { const headers = request.headers; - // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- CF Workers runtime shape + // eslint-disable-next-line typescript/no-unsafe-type-assertion -- CF Workers runtime shape const cf = (request as unknown as { cf?: Record }).cf; // On Cloudflare, prefer the cryptographically trustworthy headers. An diff --git a/packages/core/src/auth/trusted-proxy.ts b/packages/core/src/auth/trusted-proxy.ts index 961b79417..8e3b8f4ab 100644 --- a/packages/core/src/auth/trusted-proxy.ts +++ b/packages/core/src/auth/trusted-proxy.ts @@ -56,7 +56,7 @@ function getEnvTrustedHeaders(): string[] { // value at runtime (Vite/Astro inline import.meta.env at build time, // which locks the value into the bundle). Fall back to import.meta.env // for bundler-managed environments where process.env isn't populated. - // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- import.meta.env shape varies by bundler + // eslint-disable-next-line typescript/no-unsafe-type-assertion -- import.meta.env shape varies by bundler const importMetaEnv = (import.meta as unknown as { env?: Record }) .env; raw = diff --git a/packages/core/src/components/InlinePortableTextEditor.tsx b/packages/core/src/components/InlinePortableTextEditor.tsx index c191a16a9..bec775f97 100644 --- a/packages/core/src/components/InlinePortableTextEditor.tsx +++ b/packages/core/src/components/InlinePortableTextEditor.tsx @@ -1137,7 +1137,7 @@ function InlineMediaPicker({ const r = await ecFetch(url); const d = await r.json(); const raw = d.data.items ?? []; - // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- API response items mapped to MediaItem shape + // eslint-disable-next-line typescript/no-unsafe-type-assertion -- API response items mapped to MediaItem shape const typedRaw = raw as Array<{ id: string; filename?: string; diff --git a/packages/core/src/config/secrets.ts b/packages/core/src/config/secrets.ts index 96e9bcb41..b2f87de7c 100644 --- a/packages/core/src/config/secrets.ts +++ b/packages/core/src/config/secrets.ts @@ -381,7 +381,7 @@ interface SecretsCacheHolder { } function getSecretsCache(): WeakMap, Promise> { - // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- globalThis singleton pattern + // eslint-disable-next-line typescript/no-unsafe-type-assertion -- globalThis singleton pattern const holder = globalThis as Record; let entry = holder[SECRETS_CACHE_KEY]; if (!entry) { @@ -419,7 +419,7 @@ export function resolveSecretsCached(db: Kysely): Promise; holder[SECRETS_CACHE_KEY] = undefined; } @@ -513,7 +513,7 @@ function decodeBase64urlStrict(input: string): Uint8Array | null { * shared-with-CLI exception. */ function readDefaultEnv(): SecretsEnv { - // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- import.meta.env is loose by design + // eslint-disable-next-line typescript/no-unsafe-type-assertion -- import.meta.env is loose by design const meta = (import.meta.env ?? {}) as Record; const proc = typeof process !== "undefined" && process.env ? process.env : {}; diff --git a/packages/core/src/database/migrations/006_taxonomy_defs.ts b/packages/core/src/database/migrations/006_taxonomy_defs.ts index cf4494a17..5dd2eb436 100644 --- a/packages/core/src/database/migrations/006_taxonomy_defs.ts +++ b/packages/core/src/database/migrations/006_taxonomy_defs.ts @@ -23,7 +23,7 @@ export async function up(db: Kysely): Promise { // Seed default taxonomies await db - // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- Kysely migration runs against unknown schema + // eslint-disable-next-line typescript/no-unsafe-type-assertion -- Kysely migration runs against unknown schema .insertInto("_emdash_taxonomy_defs" as never) .values([ { diff --git a/packages/core/src/database/migrations/014_draft_revisions.ts b/packages/core/src/database/migrations/014_draft_revisions.ts index 46b8d6099..eedccd10c 100644 --- a/packages/core/src/database/migrations/014_draft_revisions.ts +++ b/packages/core/src/database/migrations/014_draft_revisions.ts @@ -4,13 +4,13 @@ import { sql } from "kysely"; export async function up(db: Kysely): Promise { // Get all content tables const tables = await db - // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- Kysely migration runs against unknown schema + // eslint-disable-next-line typescript/no-unsafe-type-assertion -- Kysely migration runs against unknown schema .selectFrom("_emdash_collections" as never) - // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- Kysely migration runs against unknown schema + // eslint-disable-next-line typescript/no-unsafe-type-assertion -- Kysely migration runs against unknown schema .select("slug" as never) .execute(); - // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- Kysely execute returns unknown[]; narrowing to known shape + // eslint-disable-next-line typescript/no-unsafe-type-assertion -- Kysely execute returns unknown[]; narrowing to known shape for (const row of tables as Array<{ slug: string }>) { const tableName = `ec_${row.slug}`; @@ -41,13 +41,13 @@ export async function up(db: Kysely): Promise { export async function down(db: Kysely): Promise { const tables = await db - // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- Kysely migration runs against unknown schema + // eslint-disable-next-line typescript/no-unsafe-type-assertion -- Kysely migration runs against unknown schema .selectFrom("_emdash_collections" as never) - // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- Kysely migration runs against unknown schema + // eslint-disable-next-line typescript/no-unsafe-type-assertion -- Kysely migration runs against unknown schema .select("slug" as never) .execute(); - // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- Kysely execute returns unknown[]; narrowing to known shape + // eslint-disable-next-line typescript/no-unsafe-type-assertion -- Kysely execute returns unknown[]; narrowing to known shape for (const row of tables as Array<{ slug: string }>) { const tableName = `ec_${row.slug}`; diff --git a/packages/core/src/database/repositories/audit.ts b/packages/core/src/database/repositories/audit.ts index 5bf789ddf..5ab950dfe 100644 --- a/packages/core/src/database/repositories/audit.ts +++ b/packages/core/src/database/repositories/audit.ts @@ -280,12 +280,12 @@ export class AuditRepository { timestamp: row.timestamp, actorId: row.actor_id, actorIp: row.actor_ip, - // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- DB stores string; validated at insert but linter can't follow + // eslint-disable-next-line typescript/no-unsafe-type-assertion -- DB stores string; validated at insert but linter can't follow action: row.action as AuditAction, resourceType: row.resource_type, resourceId: row.resource_id, details: row.details ? JSON.parse(row.details) : null, - // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- DB stores string; validated at insert but linter can't follow + // eslint-disable-next-line typescript/no-unsafe-type-assertion -- DB stores string; validated at insert but linter can't follow status: row.status as AuditStatus | null, }; } diff --git a/packages/core/src/database/repositories/media.ts b/packages/core/src/database/repositories/media.ts index bf689a42b..a94b27274 100644 --- a/packages/core/src/database/repositories/media.ts +++ b/packages/core/src/database/repositories/media.ts @@ -365,7 +365,7 @@ export class MediaRepository { contentHash: row.content_hash, blurhash: row.blurhash, dominantColor: row.dominant_color, - // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- DB stores string; validated at insert but linter can't follow + // eslint-disable-next-line typescript/no-unsafe-type-assertion -- DB stores string; validated at insert but linter can't follow status: row.status as MediaStatus, createdAt: row.created_at, authorId: row.author_id, diff --git a/packages/core/src/database/repositories/menu.ts b/packages/core/src/database/repositories/menu.ts index 205104dc0..3dc2de463 100644 --- a/packages/core/src/database/repositories/menu.ts +++ b/packages/core/src/database/repositories/menu.ts @@ -463,7 +463,7 @@ export class MenuRepository { .where("menu_id", "=", menuId) .where("parent_id", "is", input.parentId ?? null) .executeTakeFirst(); - // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- Kysely fn.max returns unknown; always a number for sort_order column + // eslint-disable-next-line typescript/no-unsafe-type-assertion -- Kysely fn.max returns unknown; always a number for sort_order column sortOrder = ((maxOrder?.max as number) ?? -1) + 1; } diff --git a/packages/core/src/database/repositories/options.ts b/packages/core/src/database/repositories/options.ts index 01557a0c2..0a6317c59 100644 --- a/packages/core/src/database/repositories/options.ts +++ b/packages/core/src/database/repositories/options.ts @@ -26,7 +26,7 @@ export class OptionsRepository { .executeTakeFirst(); if (!row) return null; - // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- JSON.parse returns any; generic callers provide T + // eslint-disable-next-line typescript/no-unsafe-type-assertion -- JSON.parse returns any; generic callers provide T return JSON.parse(row.value) as T; } @@ -116,7 +116,7 @@ export class OptionsRepository { const result = new Map(); for (const row of rows) { - // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- JSON.parse returns any; generic callers provide T + // eslint-disable-next-line typescript/no-unsafe-type-assertion -- JSON.parse returns any; generic callers provide T result.set(row.name, JSON.parse(row.value) as T); } return result; @@ -160,7 +160,7 @@ export class OptionsRepository { const result = new Map(); for (const row of rows) { - // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- JSON.parse returns any; generic callers provide T + // eslint-disable-next-line typescript/no-unsafe-type-assertion -- JSON.parse returns any; generic callers provide T result.set(row.name, JSON.parse(row.value) as T); } return result; diff --git a/packages/core/src/database/repositories/plugin-storage.ts b/packages/core/src/database/repositories/plugin-storage.ts index 0f0ba521d..7815285fe 100644 --- a/packages/core/src/database/repositories/plugin-storage.ts +++ b/packages/core/src/database/repositories/plugin-storage.ts @@ -57,7 +57,7 @@ export class PluginStorageRepository implements StorageCollection implements StorageCollection(); for (const row of rows) { - // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- JSON.parse returns any; generic callers provide T + // eslint-disable-next-line typescript/no-unsafe-type-assertion -- JSON.parse returns any; generic callers provide T result.set(row.id, JSON.parse(row.data) as T); } return result; @@ -255,7 +255,7 @@ export class PluginStorageRepository implements StorageCollection limit; const items = rows.slice(0, limit).map((row) => ({ id: row.id, - // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- JSON.parse returns any; generic callers provide T + // eslint-disable-next-line typescript/no-unsafe-type-assertion -- JSON.parse returns any; generic callers provide T data: JSON.parse(row.data) as T, })); diff --git a/packages/core/src/i18n/config.ts b/packages/core/src/i18n/config.ts index 842d8310f..6732cc394 100644 --- a/packages/core/src/i18n/config.ts +++ b/packages/core/src/i18n/config.ts @@ -51,7 +51,7 @@ export function getFallbackChain(locale: string): string[] { const visited = new Set([locale]); while (_config.fallback?.[current]) { - // eslint-disable-next-line typescript-eslint(no-unnecessary-type-assertion) -- noUncheckedIndexedAccess + // eslint-disable-next-line typescript/no-unnecessary-type-assertion -- noUncheckedIndexedAccess const next = _config.fallback[current]!; if (visited.has(next)) break; // prevent cycles chain.push(next); diff --git a/packages/core/src/loader.ts b/packages/core/src/loader.ts index bde4d8895..926da494b 100644 --- a/packages/core/src/loader.ts +++ b/packages/core/src/loader.ts @@ -471,7 +471,7 @@ export async function getDb(): Promise> { // Per-request DB override via ALS (normal mode) const ctx = getRequestContext(); if (ctx?.db) { - return ctx.db as Kysely; // eslint-disable-line typescript-eslint(no-unsafe-type-assertion) -- db is typed as unknown in RequestContext to avoid circular deps + return ctx.db as Kysely; // eslint-disable-line typescript/no-unsafe-type-assertion -- db is typed as unknown in RequestContext to avoid circular deps } if (!dbInstance) { diff --git a/packages/core/src/mcp/server.ts b/packages/core/src/mcp/server.ts index 2f9bf84ff..e099a9867 100644 --- a/packages/core/src/mcp/server.ts +++ b/packages/core/src/mcp/server.ts @@ -511,12 +511,12 @@ export function createMcpServer(): McpServer { if (result.success && !canReadDrafts(extra)) { const data = result.data && typeof result.data === "object" - ? // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- handler returns unknown data; narrowed by typeof check + ? // eslint-disable-next-line typescript/no-unsafe-type-assertion -- handler returns unknown data; narrowed by typeof check (result.data as Record) : undefined; const item = data?.item && typeof data.item === "object" - ? // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- narrowed by typeof check + ? // eslint-disable-next-line typescript/no-unsafe-type-assertion -- narrowed by typeof check (data.item as Record) : undefined; const status = typeof item?.status === "string" ? item.status : null; @@ -1140,7 +1140,7 @@ export function createMcpServer(): McpServer { if (result.success && !canReadDrafts(extra)) { const data = result.data && typeof result.data === "object" - ? // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- handler returns unknown data; narrowed by typeof check + ? // eslint-disable-next-line typescript/no-unsafe-type-assertion -- handler returns unknown data; narrowed by typeof check (result.data as Record) : undefined; const translations = Array.isArray(data?.translations) ? data.translations : []; diff --git a/packages/core/src/plugins/email-console.ts b/packages/core/src/plugins/email-console.ts index 3cac170e9..e0f6c78db 100644 --- a/packages/core/src/plugins/email-console.ts +++ b/packages/core/src/plugins/email-console.ts @@ -33,7 +33,7 @@ export interface StoredEmail { const GLOBAL_KEY = Symbol.for("emdash:dev-emails"); const g = globalThis as Record; const storedEmails: StoredEmail[] = (() => { - // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- globalThis singleton pattern (see request-context.ts) + // eslint-disable-next-line typescript/no-unsafe-type-assertion -- globalThis singleton pattern (see request-context.ts) const existing = g[GLOBAL_KEY] as StoredEmail[] | undefined; if (existing) return existing; const fresh: StoredEmail[] = []; diff --git a/packages/core/src/plugins/marketplace.ts b/packages/core/src/plugins/marketplace.ts index 073c80d48..fa7dff719 100644 --- a/packages/core/src/plugins/marketplace.ts +++ b/packages/core/src/plugins/marketplace.ts @@ -509,7 +509,7 @@ export async function extractBundle(tarballBytes: Uint8Array): Promise b.toString(16).padStart(2, "0")).join(""); diff --git a/packages/core/src/request-cache.ts b/packages/core/src/request-cache.ts index 6fcec13f0..3df07c7ff 100644 --- a/packages/core/src/request-cache.ts +++ b/packages/core/src/request-cache.ts @@ -22,7 +22,7 @@ type CacheStore = WeakMap>>; const STORE_KEY = Symbol.for("emdash:request-cache"); const g = globalThis as Record; const store: CacheStore = - // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- globalThis singleton pattern (see request-context.ts) + // eslint-disable-next-line typescript/no-unsafe-type-assertion -- globalThis singleton pattern (see request-context.ts) (g[STORE_KEY] as CacheStore | undefined) ?? (() => { const wm: CacheStore = new WeakMap(); @@ -50,7 +50,7 @@ export function requestCached(key: string, fn: () => Promise): Promise const existing = cache.get(key); if (existing) { if (ctx.metrics) ctx.metrics.cacheHits += 1; - // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- heterogeneous cache; key namespacing guarantees the stored promise resolves to T + // eslint-disable-next-line typescript/no-unsafe-type-assertion -- heterogeneous cache; key namespacing guarantees the stored promise resolves to T return existing as Promise; } if (ctx.metrics) ctx.metrics.cacheMisses += 1; @@ -80,7 +80,7 @@ export function peekRequestCache(key: string): Promise | undefined { const ctx = getRequestContext(); if (!ctx) return undefined; const cache = store.get(ctx); - // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- heterogeneous cache; caller is responsible for using a T-compatible key + // eslint-disable-next-line typescript/no-unsafe-type-assertion -- heterogeneous cache; caller is responsible for using a T-compatible key return cache?.get(key) as Promise | undefined; } diff --git a/packages/core/src/request-context.ts b/packages/core/src/request-context.ts index 01240f608..48cb43daa 100644 --- a/packages/core/src/request-context.ts +++ b/packages/core/src/request-context.ts @@ -99,7 +99,7 @@ export interface EmDashRequestContext { const ALS_KEY = Symbol.for("emdash:request-context"); const storage: AsyncLocalStorage = - // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- globalThis singleton pattern + // eslint-disable-next-line typescript/no-unsafe-type-assertion -- globalThis singleton pattern ((globalThis as Record)[ALS_KEY] as | AsyncLocalStorage | undefined) ?? diff --git a/packages/core/src/settings/index.ts b/packages/core/src/settings/index.ts index 4dfac181c..92df2816b 100644 --- a/packages/core/src/settings/index.ts +++ b/packages/core/src/settings/index.ts @@ -44,7 +44,7 @@ interface SiteSettingsHolder { const SITE_SETTINGS_CACHE_KEY = Symbol.for("emdash:site-settings"); const g = globalThis as Record; const holder: SiteSettingsHolder = - // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- globalThis singleton pattern (see request-context.ts) + // eslint-disable-next-line typescript/no-unsafe-type-assertion -- globalThis singleton pattern (see request-context.ts) (g[SITE_SETTINGS_CACHE_KEY] as SiteSettingsHolder | undefined) ?? (() => { const h: SiteSettingsHolder = { version: 0, cached: null, cachedVersion: -1 }; @@ -174,19 +174,19 @@ export async function getSiteSettingWithDb( // We use the non-generic getSiteSettingsWithDb for media resolution instead. if ((key === "logo" || key === "favicon") && isMediaReference(value)) { const resolved = await resolveMediaReference(value, db, storage); - // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- TS can't narrow generic K from key equality; resolved type is correct + // eslint-disable-next-line typescript/no-unsafe-type-assertion -- TS can't narrow generic K from key equality; resolved type is correct return resolved as SiteSettings[K] | undefined; } if (key === "seo" && value && typeof value === "object") { - // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- TS can't narrow generic K from key equality + // eslint-disable-next-line typescript/no-unsafe-type-assertion -- TS can't narrow generic K from key equality const seo = value as SeoSettings; if (seo.defaultOgImage) { const resolved = { ...seo, defaultOgImage: await resolveMediaReference(seo.defaultOgImage, db, storage), }; - // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- TS can't narrow generic K from key equality + // eslint-disable-next-line typescript/no-unsafe-type-assertion -- TS can't narrow generic K from key equality return resolved as SiteSettings[K] | undefined; } } diff --git a/packages/core/src/storage/local.ts b/packages/core/src/storage/local.ts index 8244874da..0b2202ab4 100644 --- a/packages/core/src/storage/local.ts +++ b/packages/core/src/storage/local.ts @@ -121,7 +121,7 @@ export class LocalStorage implements Storage { // Convert Node.js stream to web ReadableStream // Readable.toWeb returns ReadableStream (which is ReadableStream), // but Node ReadStreams produce Buffer/Uint8Array chunks - // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- Readable.toWeb returns ReadableStream; Node ReadStreams produce Uint8Array chunks + // eslint-disable-next-line typescript/no-unsafe-type-assertion -- Readable.toWeb returns ReadableStream; Node ReadStreams produce Uint8Array chunks const webStream: ReadableStream = Readable.toWeb( nodeStream, ) as ReadableStream; diff --git a/packages/core/src/storage/s3.ts b/packages/core/src/storage/s3.ts index f740cb1ea..99ca870b1 100644 --- a/packages/core/src/storage/s3.ts +++ b/packages/core/src/storage/s3.ts @@ -134,7 +134,7 @@ export class S3Storage implements Storage { // S3ClientConfig types `credentials` as required, but the SDK accepts // omitted credentials at runtime (falls back to the provider chain). - /* eslint-disable typescript-eslint(no-unsafe-type-assertion) -- upstream @aws-sdk/client-s3 overstates required fields */ + /* eslint-disable typescript/no-unsafe-type-assertion -- upstream @aws-sdk/client-s3 overstates required fields */ const clientConfig = { endpoint: config.endpoint, region: config.region || "auto", @@ -149,7 +149,7 @@ export class S3Storage implements Storage { } : {}), } as S3ClientConfig; - /* eslint-enable typescript-eslint(no-unsafe-type-assertion) */ + /* eslint-enable typescript/no-unsafe-type-assertion */ this.client = new S3Client(clientConfig); } @@ -260,7 +260,7 @@ export class S3Storage implements Storage { async list(options: ListOptions = {}): Promise { try { - // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- S3 client.send returns generic output; narrowing to ListObjectsV2Response + // eslint-disable-next-line typescript/no-unsafe-type-assertion -- S3 client.send returns generic output; narrowing to ListObjectsV2Response const response = (await this.client.send( new ListObjectsV2Command({ Bucket: this.bucket, diff --git a/packages/core/src/widgets/index.ts b/packages/core/src/widgets/index.ts index ccc0e0f4c..48aae86ef 100644 --- a/packages/core/src/widgets/index.ts +++ b/packages/core/src/widgets/index.ts @@ -55,7 +55,7 @@ export async function getWidgetArea(name: string): Promise { // they're all non-null once w_id is (we match on widgets.area_id, so // a widget row always has the not-null columns filled). Cast is the // price of that structural fact. - // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- left-join row is non-null when w_id is set; see above + // eslint-disable-next-line typescript/no-unsafe-type-assertion -- left-join row is non-null when w_id is set; see above const widgetRow = { id: row.w_id, type: row.w_type, diff --git a/packages/core/tests/integration/astro/comments-rate-limit.test.ts b/packages/core/tests/integration/astro/comments-rate-limit.test.ts index 9f1ea7596..1c9a2b764 100644 --- a/packages/core/tests/integration/astro/comments-rate-limit.test.ts +++ b/packages/core/tests/integration/astro/comments-rate-limit.test.ts @@ -56,7 +56,7 @@ function buildContext(opts: { db: Kysely; request: Request }): APICont }, user: null, }, - // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- minimal stub for tests + // eslint-disable-next-line typescript/no-unsafe-type-assertion -- minimal stub for tests } as unknown as APIContext; } diff --git a/packages/core/tests/integration/astro/media-upload-widening.test.ts b/packages/core/tests/integration/astro/media-upload-widening.test.ts index 8a3b37f18..b04a3bdfb 100644 --- a/packages/core/tests/integration/astro/media-upload-widening.test.ts +++ b/packages/core/tests/integration/astro/media-upload-widening.test.ts @@ -125,7 +125,7 @@ function buildContext(opts: { role: 50 as const, }, }, - // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- minimal stub for tests + // eslint-disable-next-line typescript/no-unsafe-type-assertion -- minimal stub for tests } as unknown as APIContext; } @@ -288,7 +288,7 @@ function buildUploadUrlContext(opts: { role: 50 as const, }, }, - // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- minimal stub for tests + // eslint-disable-next-line typescript/no-unsafe-type-assertion -- minimal stub for tests } as unknown as APIContext; } diff --git a/packages/core/tests/integration/astro/setup-admin-nonce-success.test.ts b/packages/core/tests/integration/astro/setup-admin-nonce-success.test.ts index 71ec9753a..9bf794a48 100644 --- a/packages/core/tests/integration/astro/setup-admin-nonce-success.test.ts +++ b/packages/core/tests/integration/astro/setup-admin-nonce-success.test.ts @@ -85,7 +85,7 @@ function createCookieJar(initial: Record = {}): CookieJar { const record = jar.get(name); return !!record && !record.deleted; }, - // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- minimal stub + // eslint-disable-next-line typescript/no-unsafe-type-assertion -- minimal stub } as unknown as AstroCookies; return { jar, cookies }; @@ -120,7 +120,7 @@ function buildContext(db: Kysely, request: Request, cookies: AstroCook storage: undefined, }, }, - // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- minimal stub + // eslint-disable-next-line typescript/no-unsafe-type-assertion -- minimal stub } as unknown as APIContext; } diff --git a/packages/core/tests/integration/astro/setup-admin-nonce.test.ts b/packages/core/tests/integration/astro/setup-admin-nonce.test.ts index ce3bb4c39..5ca364dc1 100644 --- a/packages/core/tests/integration/astro/setup-admin-nonce.test.ts +++ b/packages/core/tests/integration/astro/setup-admin-nonce.test.ts @@ -55,7 +55,7 @@ function createCookieJar(initial: Record = {}): CookieJar { has(name: string) { return jar.has(name); }, - // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- minimal stub + // eslint-disable-next-line typescript/no-unsafe-type-assertion -- minimal stub } as unknown as AstroCookies; return { jar, cookies }; @@ -90,7 +90,7 @@ function buildContext(db: Kysely, request: Request, cookies: AstroCook storage: undefined, }, }, - // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- minimal stub + // eslint-disable-next-line typescript/no-unsafe-type-assertion -- minimal stub } as unknown as APIContext; } diff --git a/packages/core/tests/integration/astro/setup-site-url-lock.test.ts b/packages/core/tests/integration/astro/setup-site-url-lock.test.ts index 1628d5bee..d4cc9d615 100644 --- a/packages/core/tests/integration/astro/setup-site-url-lock.test.ts +++ b/packages/core/tests/integration/astro/setup-site-url-lock.test.ts @@ -54,7 +54,7 @@ function buildContext(db: Kysely, request: Request): APIContext { storage: undefined, }, }, - // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- minimal stub + // eslint-disable-next-line typescript/no-unsafe-type-assertion -- minimal stub } as unknown as APIContext; } diff --git a/packages/core/tests/integration/auth/rate-limit.test.ts b/packages/core/tests/integration/auth/rate-limit.test.ts index 7cf1d45d6..86b446800 100644 --- a/packages/core/tests/integration/auth/rate-limit.test.ts +++ b/packages/core/tests/integration/auth/rate-limit.test.ts @@ -118,7 +118,7 @@ describe("getClientIp", () => { /** Create a request with a fake `cf` object to simulate Cloudflare. */ function cfRequest(url: string, init?: RequestInit): Request { const req = new Request(url, init); - // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- test helper + // eslint-disable-next-line typescript/no-unsafe-type-assertion -- test helper (req as unknown as { cf: Record }).cf = { country: "US" }; return req; } @@ -171,7 +171,7 @@ describe("getClientIp with trusted proxy headers", () => { function cfRequest(url: string, init?: RequestInit): Request { const req = new Request(url, init); - // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- test helper + // eslint-disable-next-line typescript/no-unsafe-type-assertion -- test helper (req as unknown as { cf: Record }).cf = { country: "US" }; return req; } diff --git a/packages/core/tests/integration/mcp/drafts.test.ts b/packages/core/tests/integration/mcp/drafts.test.ts index 395c5b402..0dcfaa462 100644 --- a/packages/core/tests/integration/mcp/drafts.test.ts +++ b/packages/core/tests/integration/mcp/drafts.test.ts @@ -220,7 +220,7 @@ describe("MCP drafts — content_get and content_update round-trip (bug #2)", () // Read body the same way const body = item.data && typeof item.data === "object" && "body" in item.data - ? // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- shape narrowed by 'in' check + ? // eslint-disable-next-line typescript/no-unsafe-type-assertion -- shape narrowed by 'in' check (item.data as { body?: unknown }).body : (item as Record).body; expect(body).toBe("B1"); diff --git a/packages/core/tests/integration/mcp/validation.test.ts b/packages/core/tests/integration/mcp/validation.test.ts index c83f93c1b..e19aae3e9 100644 --- a/packages/core/tests/integration/mcp/validation.test.ts +++ b/packages/core/tests/integration/mcp/validation.test.ts @@ -107,7 +107,7 @@ describe("MCP validation — required fields (bug #4)", () => { name: "content_create", arguments: { collection: "post", - // eslint-disable-next-line typescript-eslint(no-explicit-any) -- intentionally bypass MCP type to hit runtime validation + // eslint-disable-next-line typescript/no-explicit-any -- intentionally bypass MCP type to hit runtime validation data: { title: 42 } as any, }, }); diff --git a/packages/core/tests/unit/astro/content-routes-authz.test.ts b/packages/core/tests/unit/astro/content-routes-authz.test.ts index c2d240bb6..9d639f1af 100644 --- a/packages/core/tests/unit/astro/content-routes-authz.test.ts +++ b/packages/core/tests/unit/astro/content-routes-authz.test.ts @@ -164,7 +164,7 @@ function ctx(opts: { emdash: opts.emdash, }, cache: { enabled: false, invalidate: vi.fn() }, - // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- minimal stub for tests + // eslint-disable-next-line typescript/no-unsafe-type-assertion -- minimal stub for tests } as unknown as APIContext; } diff --git a/packages/core/tests/unit/astro/middleware-redirect.test.ts b/packages/core/tests/unit/astro/middleware-redirect.test.ts index bb38f47fa..b44342e6a 100644 --- a/packages/core/tests/unit/astro/middleware-redirect.test.ts +++ b/packages/core/tests/unit/astro/middleware-redirect.test.ts @@ -48,7 +48,7 @@ function buildContext({ pathname, emdashDb }: BuildContextOpts): { locals, redirect, }; - // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- minimal Astro-shaped object for the middleware under test + // eslint-disable-next-line typescript/no-unsafe-type-assertion -- minimal Astro-shaped object for the middleware under test return { context: ctx as unknown as MiddlewareContext, redirect }; } diff --git a/packages/core/tests/unit/astro/signup-rate-limit.test.ts b/packages/core/tests/unit/astro/signup-rate-limit.test.ts index c1d80534d..b663a579d 100644 --- a/packages/core/tests/unit/astro/signup-rate-limit.test.ts +++ b/packages/core/tests/unit/astro/signup-rate-limit.test.ts @@ -34,7 +34,7 @@ function cfRequest(url: string, body: unknown): Request { }, body: JSON.stringify(body), }); - // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- test harness + // eslint-disable-next-line typescript/no-unsafe-type-assertion -- test harness (req as unknown as { cf: Record }).cf = { country: "US" }; return req; } @@ -65,7 +65,7 @@ function ctx(opts: { email: opts.email, }, }, - // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- minimal stub for tests + // eslint-disable-next-line typescript/no-unsafe-type-assertion -- minimal stub for tests } as unknown as APIContext; } @@ -131,7 +131,7 @@ describe("POST /auth/signup/request rate limiting", () => { }, body: JSON.stringify({ email: addr }), }); - // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- test harness + // eslint-disable-next-line typescript/no-unsafe-type-assertion -- test harness (r as unknown as { cf: Record }).cf = { country: "US" }; return r; } @@ -140,7 +140,7 @@ describe("POST /auth/signup/request rate limiting", () => { return { request, locals: { emdash: { db, email } }, - // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- minimal stub + // eslint-disable-next-line typescript/no-unsafe-type-assertion -- minimal stub } as unknown as APIContext; } diff --git a/packages/core/tests/unit/import/ssrf.test.ts b/packages/core/tests/unit/import/ssrf.test.ts index 49a146d54..69f45cd44 100644 --- a/packages/core/tests/unit/import/ssrf.test.ts +++ b/packages/core/tests/unit/import/ssrf.test.ts @@ -655,7 +655,7 @@ describe("cloudflareDohResolver", () => { const res = responses[type]; if (res.throws) throw res.throws; return new Response(JSON.stringify(res.body ?? {}), { status: res.status ?? 200 }); - // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- minimal stub + // eslint-disable-next-line typescript/no-unsafe-type-assertion -- minimal stub }) as unknown as typeof globalThis.fetch; } diff --git a/packages/core/tests/unit/import/wp-prepare-invalidate.test.ts b/packages/core/tests/unit/import/wp-prepare-invalidate.test.ts index e5b5ae319..2a3aa75f0 100644 --- a/packages/core/tests/unit/import/wp-prepare-invalidate.test.ts +++ b/packages/core/tests/unit/import/wp-prepare-invalidate.test.ts @@ -51,7 +51,7 @@ describe("POST /api/import/wordpress/prepare", () => { }; const ctx = buildContext(emdash); - // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) + // eslint-disable-next-line typescript/no-unsafe-type-assertion const response = await POST(ctx as any); expect(response.status).toBe(200); @@ -82,7 +82,7 @@ describe("POST /api/import/wordpress/prepare", () => { }; const ctx = buildContext(emdash); - // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) + // eslint-disable-next-line typescript/no-unsafe-type-assertion const response = await POST(ctx as any); expect(response.status).toBe(200); diff --git a/packages/core/tests/unit/import/wxr-taxonomies.test.ts b/packages/core/tests/unit/import/wxr-taxonomies.test.ts index d61375f54..7b97e3962 100644 --- a/packages/core/tests/unit/import/wxr-taxonomies.test.ts +++ b/packages/core/tests/unit/import/wxr-taxonomies.test.ts @@ -54,9 +54,9 @@ async function createPostsCollectionWithEntry( // this test — we just need an existing row to attach taxonomies to). const id = `post_${Math.random().toString(36).slice(2, 10)}`; await db - // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- ec_* tables aren't typed in Database + // eslint-disable-next-line typescript/no-unsafe-type-assertion -- ec_* tables aren't typed in Database .insertInto("ec_posts" as never) - // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- dynamic content table shape + // eslint-disable-next-line typescript/no-unsafe-type-assertion -- dynamic content table shape .values({ id, slug, status: "published" } as never) .execute(); return id; @@ -303,9 +303,9 @@ describe("attachPostTaxonomies", () => { const productId = "prod_test1"; await db - // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- ec_* tables aren't typed in Database + // eslint-disable-next-line typescript/no-unsafe-type-assertion -- ec_* tables aren't typed in Database .insertInto("ec_products" as never) - // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- dynamic content table shape + // eslint-disable-next-line typescript/no-unsafe-type-assertion -- dynamic content table shape .values({ id: productId, slug: "thing", status: "published" } as never) .execute(); diff --git a/packages/core/tests/unit/runtime/manifest-build.test.ts b/packages/core/tests/unit/runtime/manifest-build.test.ts index a35797c26..ee9e37900 100644 --- a/packages/core/tests/unit/runtime/manifest-build.test.ts +++ b/packages/core/tests/unit/runtime/manifest-build.test.ts @@ -32,7 +32,7 @@ function buildRuntime(db: Kysely): EmDashRuntime { const runtimeDeps = { config, plugins: [], - // eslint-disable-next-line typescript-eslint(no-explicit-any) -- match RuntimeDependencies signature + // eslint-disable-next-line typescript/no-explicit-any -- match RuntimeDependencies signature createDialect: (() => { throw new Error("createDialect not used in this test"); }) as any, diff --git a/packages/core/tests/utils/mcp-runtime.ts b/packages/core/tests/utils/mcp-runtime.ts index 423f23cda..e7cb96dbb 100644 --- a/packages/core/tests/utils/mcp-runtime.ts +++ b/packages/core/tests/utils/mcp-runtime.ts @@ -45,7 +45,7 @@ class AuthInjectingTransport extends InMemoryTransport { ): Promise { const existingExtra = options?.authInfo && typeof options.authInfo === "object" && "extra" in options.authInfo - ? // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- narrowed by typeof + 'in' check + ? // eslint-disable-next-line typescript/no-unsafe-type-assertion -- narrowed by typeof + 'in' check (options.authInfo.extra as Record) : {}; return super.send(message, { @@ -123,7 +123,7 @@ export function createTestRuntime( const runtimeDeps = { config, plugins, - // eslint-disable-next-line typescript-eslint(no-explicit-any) -- match RuntimeDependencies signature + // eslint-disable-next-line typescript/no-explicit-any -- match RuntimeDependencies signature createDialect: (() => { throw new Error("createDialect not available in test runtime"); }) as any, diff --git a/packages/core/tsdown.config.ts b/packages/core/tsdown.config.ts index 09573cae9..fe677f406 100644 --- a/packages/core/tsdown.config.ts +++ b/packages/core/tsdown.config.ts @@ -135,7 +135,7 @@ export default defineConfig({ inputOptions: (options) => { // tsdown has already normalized the `entry` array into an input record // by this hook; we only augment it with the route map. - // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- normalized input record at the inputOptions hook + // eslint-disable-next-line typescript/no-unsafe-type-assertion -- normalized input record at the inputOptions hook options.input = { ...(options.input as Record), ...routeInputMap() }; return options; }, diff --git a/packages/marketplace/src/routes/author.ts b/packages/marketplace/src/routes/author.ts index f87c11f80..5bce562a2 100644 --- a/packages/marketplace/src/routes/author.ts +++ b/packages/marketplace/src/routes/author.ts @@ -64,7 +64,7 @@ async function authenticateWithGitHubToken( throw new Error(`Failed to fetch GitHub user: ${userResponse.status}`); } - // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- GitHub API response + // eslint-disable-next-line typescript/no-unsafe-type-assertion -- GitHub API response const githubUser: GitHubUser = await userResponse.json(); const githubId = String(githubUser.id); @@ -131,7 +131,7 @@ authorRoutes.post("/auth/github", async (c) => { }), }); - // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- GitHub OAuth response + // eslint-disable-next-line typescript/no-unsafe-type-assertion -- GitHub OAuth response const tokenData: { access_token?: string; error?: string; @@ -921,7 +921,7 @@ async function extractTarball(data: ArrayBuffer): Promise(value: string | null, fallback: T): T { if (!value) return fallback; try { - // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- caller provides type parameter + // eslint-disable-next-line typescript/no-unsafe-type-assertion -- caller provides type parameter const parsed: T = JSON.parse(value); return parsed; } catch { diff --git a/packages/marketplace/src/routes/dev.ts b/packages/marketplace/src/routes/dev.ts index 7cda21fd3..0a720a0a2 100644 --- a/packages/marketplace/src/routes/dev.ts +++ b/packages/marketplace/src/routes/dev.ts @@ -118,7 +118,7 @@ devRoutes.post("/dev/audit", async (c) => { if (iconData) { imageFiles.push({ filename: "icon.png", - // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- Uint8Array.buffer is ArrayBuffer at runtime + // eslint-disable-next-line typescript/no-unsafe-type-assertion -- Uint8Array.buffer is ArrayBuffer at runtime data: iconData.buffer as ArrayBuffer, }); } @@ -126,7 +126,7 @@ devRoutes.post("/dev/audit", async (c) => { if (path.startsWith("screenshots/")) { imageFiles.push({ filename: path, - // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- Uint8Array.buffer is ArrayBuffer at runtime + // eslint-disable-next-line typescript/no-unsafe-type-assertion -- Uint8Array.buffer is ArrayBuffer at runtime data: data.buffer as ArrayBuffer, }); } diff --git a/packages/marketplace/src/routes/public.ts b/packages/marketplace/src/routes/public.ts index a6e4651e1..167137f31 100644 --- a/packages/marketplace/src/routes/public.ts +++ b/packages/marketplace/src/routes/public.ts @@ -35,7 +35,7 @@ publicRoutes.get("/plugins", async (c) => { const validSorts = new Set(["installs", "updated", "created", "name"]); let sort: "installs" | "updated" | "created" | "name" | undefined; if (sortParam && validSorts.has(sortParam)) { - // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- validated by Set.has check above + // eslint-disable-next-line typescript/no-unsafe-type-assertion -- validated by Set.has check above sort = sortParam as "installs" | "updated" | "created" | "name"; } const cursor = url.searchParams.get("cursor") ?? undefined; @@ -296,7 +296,7 @@ publicRoutes.get("/plugins/:id/versions/:version/image-audit", async (c) => { function safeJsonParse(value: string | null, fallback: T): T { if (!value) return fallback; try { - // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- caller provides type parameter + // eslint-disable-next-line typescript/no-unsafe-type-assertion -- caller provides type parameter const parsed: T = JSON.parse(value); return parsed; } catch { diff --git a/packages/marketplace/src/routes/themes.ts b/packages/marketplace/src/routes/themes.ts index d1fa42a7f..458cdbbd5 100644 --- a/packages/marketplace/src/routes/themes.ts +++ b/packages/marketplace/src/routes/themes.ts @@ -74,9 +74,9 @@ themeRoutes.get("/themes", async (c) => { const keyword = url.searchParams.get("keyword") ?? undefined; const sortParam = url.searchParams.get("sort"); let sort: ThemeSortOption | undefined; - // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- validated by VALID_THEME_SORTS.has() + // eslint-disable-next-line typescript/no-unsafe-type-assertion -- validated by VALID_THEME_SORTS.has() if (sortParam && VALID_THEME_SORTS.has(sortParam as ThemeSortOption)) { - // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- validated by VALID_THEME_SORTS.has() on the line above + // eslint-disable-next-line typescript/no-unsafe-type-assertion -- validated by VALID_THEME_SORTS.has() on the line above sort = sortParam as ThemeSortOption; } const cursor = url.searchParams.get("cursor") ?? undefined; @@ -373,7 +373,7 @@ themeRoutes.put("/themes/:id/images", async (c) => { function safeJsonParse(value: string | null, fallback: T): T { if (!value) return fallback; try { - // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- caller provides type parameter + // eslint-disable-next-line typescript/no-unsafe-type-assertion -- caller provides type parameter const parsed: T = JSON.parse(value); return parsed; } catch { diff --git a/packages/marketplace/src/workflows/audit.ts b/packages/marketplace/src/workflows/audit.ts index 53db2389e..a5c851510 100644 --- a/packages/marketplace/src/workflows/audit.ts +++ b/packages/marketplace/src/workflows/audit.ts @@ -118,7 +118,7 @@ export class AuditWorkflow extends WorkflowEntrypoint { verdict: auditResult.verdict, riskScore: auditResult.riskScore, summary: auditResult.summary, - // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- findings shape is preserved from AuditResult + // eslint-disable-next-line typescript/no-unsafe-type-assertion -- findings shape is preserved from AuditResult findings: auditResult.findings as unknown[], model: auditResult.model, durationMs: auditResult.durationMs, @@ -131,7 +131,7 @@ export class AuditWorkflow extends WorkflowEntrypoint { pluginId, version, verdict: imageAuditResult.verdict, - // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- images shape is preserved from ImageAuditResult + // eslint-disable-next-line typescript/no-unsafe-type-assertion -- images shape is preserved from ImageAuditResult findings: imageAuditResult.images as unknown[], model: imageAuditResult.model, durationMs: imageAuditResult.durationMs, @@ -185,12 +185,12 @@ export class AuditWorkflow extends WorkflowEntrypoint { const iconData = files.get("icon.png"); if (iconData) { - // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- Uint8Array.buffer is ArrayBuffer at runtime + // eslint-disable-next-line typescript/no-unsafe-type-assertion -- Uint8Array.buffer is ArrayBuffer at runtime imageFiles.push({ filename: "icon.png", data: iconData.buffer as ArrayBuffer }); } for (const [path, data] of files) { if (path.startsWith("screenshots/")) { - // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- Uint8Array.buffer is ArrayBuffer at runtime + // eslint-disable-next-line typescript/no-unsafe-type-assertion -- Uint8Array.buffer is ArrayBuffer at runtime imageFiles.push({ filename: path, data: data.buffer as ArrayBuffer }); } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1fc052d91..f42890335 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -269,7 +269,7 @@ importers: version: 2.29.8(@types/node@24.10.13) '@e18e/eslint-plugin': specifier: ^0.2.0 - version: 0.2.0(oxlint@1.49.0(oxlint-tsgolint@0.15.0)) + version: 0.2.0(oxlint@1.66.0(oxlint-tsgolint@0.23.0)) '@lunariajs/core': specifier: https://pkg.pr.new/lunariajs/lunaria/@lunariajs/core@83617cc version: https://pkg.pr.new/lunariajs/lunaria/@lunariajs/core@83617cc @@ -292,11 +292,11 @@ importers: specifier: ^0.34.0 version: 0.34.0 oxlint: - specifier: ^1.49.0 - version: 1.49.0(oxlint-tsgolint@0.15.0) + specifier: ^1.66.0 + version: 1.66.0(oxlint-tsgolint@0.23.0) oxlint-tsgolint: - specifier: ^0.15.0 - version: 0.15.0 + specifier: ^0.23.0 + version: 0.23.0 prettier: specifier: ^3.8.1 version: 3.8.1 @@ -4408,154 +4408,154 @@ packages: cpu: [x64] os: [win32] - '@oxlint-tsgolint/darwin-arm64@0.15.0': - resolution: {integrity: sha512-d7Ch+A6hic+RYrm32+Gh1o4lOrQqnFsHi721ORdHUDBiQPea+dssKUEMwIbA6MKmCy6TVJ02sQyi24OEfCiGzw==} + '@oxlint-tsgolint/darwin-arm64@0.23.0': + resolution: {integrity: sha512-gOs9PVr2wEg4ox9z0aJo+RKhhImW86YL5N6yav8BK/rgPsIrwN/igSZ+pbRr723NFvUNKde9fgMhRA6JrXAOZw==} cpu: [arm64] os: [darwin] - '@oxlint-tsgolint/darwin-x64@0.15.0': - resolution: {integrity: sha512-Aoai2wAkaUJqp/uEs1gml6TbaPW4YmyO5Ai/vOSkiizgHqVctjhjKqmRiWTX2xuPY94VkwOLqp+Qr3y/0qSpWQ==} + '@oxlint-tsgolint/darwin-x64@0.23.0': + resolution: {integrity: sha512-kjJ8B+7n4tB9VJdxS5A9GdJt6/bYpzbu4lXp2uO1S3sRmCB5gDEABlGoiePNApRWaW+xqL4b4xgiE727jSLhuA==} cpu: [x64] os: [darwin] - '@oxlint-tsgolint/linux-arm64@0.15.0': - resolution: {integrity: sha512-4og13a7ec4Vku5t2Y7s3zx6YJP6IKadb1uA9fOoRH6lm/wHWoCnxjcfJmKHXRZJII81WmbdJMSPxaBfwN/S68Q==} + '@oxlint-tsgolint/linux-arm64@0.23.0': + resolution: {integrity: sha512-6dCZuKNu135seMXilkRk9SpCx6i1XgmiipYGalLij5WVRX6ZYS8c4xI7preN/zv9fCXhsQclTIMDu2Y/cytTjw==} cpu: [arm64] os: [linux] - '@oxlint-tsgolint/linux-x64@0.15.0': - resolution: {integrity: sha512-9b9xzh/1Harn3a+XiKTK/8LrWw3VcqLfYp/vhV5/zAVR2Mt0d63WSp4FL+wG7DKnI2T/CbMFUFHwc7kCQjDMzQ==} + '@oxlint-tsgolint/linux-x64@0.23.0': + resolution: {integrity: sha512-3bdilnyA7kmSTjK27rvjIjSxL5SIg3wt7vwNiRkouWB83ytssyKnuGvxSYJxgMEmFpSutzaBzcCUM2jDtPGcgA==} cpu: [x64] os: [linux] - '@oxlint-tsgolint/win32-arm64@0.15.0': - resolution: {integrity: sha512-nNac5hewHdkk5mowOwTqB1ZD76zB/FsUiyUvdCyupq5cG54XyKqSLEp9QGbx7wFJkWCkeWmuwRed4sfpAlKaeA==} + '@oxlint-tsgolint/win32-arm64@0.23.0': + resolution: {integrity: sha512-j+OEp44SVYiQ+ZD+uttsX7u6L9SvmbbQ77SO1pSFCcJlsVMeCk8qZsjhKfGKuT/jIA+ipOJMVs/+pqUfObBWNw==} cpu: [arm64] os: [win32] - '@oxlint-tsgolint/win32-x64@0.15.0': - resolution: {integrity: sha512-ioAY2XLpy83E2EqOLH9p1cEgj0G2qB1lmAn0a3yFV1jHQB29LIPIKGNsu/tYCClpwmHN79pT5KZAHZOgWxxqNg==} + '@oxlint-tsgolint/win32-x64@0.23.0': + resolution: {integrity: sha512-5MyjFuqf+g8OUPJBSGWHJtmoWnzFJYyOg4To9WMQshZYEWig/vtu7JtJ03VWnzHv9LJkAUeApY0gVCOywFR/iQ==} cpu: [x64] os: [win32] - '@oxlint/binding-android-arm-eabi@1.49.0': - resolution: {integrity: sha512-2WPoh/2oK9r/i2R4o4J18AOrm3HVlWiHZ8TnuCaS4dX8m5ZzRmHW0I3eLxEurQLHWVruhQN7fHgZnah+ag5iQg==} + '@oxlint/binding-android-arm-eabi@1.66.0': + resolution: {integrity: sha512-f7kq8N51T4phpzqfBpA2qaVTI/KrkCmNwaj3t/97I/WLTDI+UhlP5GL9eER+zVxBhtlx5rKXWByJU1/zDAvyaw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [android] - '@oxlint/binding-android-arm64@1.49.0': - resolution: {integrity: sha512-YqJAGvNB11EzoKm1euVhZntb79alhMvWW/j12bYqdvVxn6xzEQWrEDCJg9BPo3A3tBCSUBKH7bVkAiCBqK/L1w==} + '@oxlint/binding-android-arm64@1.66.0': + resolution: {integrity: sha512-xu6QO71tdDS9mjmLZ3AqhtaVHBvdmsOKkYnReNNDgh+XiwnsipeQOIxbiYOOO0iAXycJ+GK0wdMSZP/2j/AmSg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] - '@oxlint/binding-darwin-arm64@1.49.0': - resolution: {integrity: sha512-WFocCRlvVkMhChCJ2qpJfp1Gj/IjvyjuifH9Pex8m8yHonxxQa3d8DZYreuDQU3T4jvSY8rqhoRqnpc61Nlbxw==} + '@oxlint/binding-darwin-arm64@1.66.0': + resolution: {integrity: sha512-HZ24VimSOC7mxuEA99e0H2FS0C1yO3+iW13jPRAk+e2njsUs3QeAXsafCDyaIrV/MirdOVez+etQNQsJE43zNQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] - '@oxlint/binding-darwin-x64@1.49.0': - resolution: {integrity: sha512-BN0KniwvehbUfYztOMwEDkYoojGm/narf5oJf+/ap+6PnzMeWLezMaVARNIS0j3OdMkjHTEP8s3+GdPJ7WDywQ==} + '@oxlint/binding-darwin-x64@1.66.0': + resolution: {integrity: sha512-awhj8ZvJrrRSnXj7V++rpZvTmnl99L6mi0B7gg7Cp7BN6cKpzuI481bHNLvXGA9GB1/oEgA3ponuyoAc6Md12A==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] - '@oxlint/binding-freebsd-x64@1.49.0': - resolution: {integrity: sha512-SnkAc/DPIY6joMCiP/+53Q+N2UOGMU6ULvbztpmvPJNF/jYPGhNbKtN982uj2Gs6fpbxYkmyj08QnpkD4fbHJA==} + '@oxlint/binding-freebsd-x64@1.66.0': + resolution: {integrity: sha512-KQF0oVV21/FjIqkRuL8Q1vh8ECsE5+ocdH5tcqTQ4ZnYuDVoYibQUNfqBjQaUsP6UIIda5Y75Wpm5p4RgQWiWw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] - '@oxlint/binding-linux-arm-gnueabihf@1.49.0': - resolution: {integrity: sha512-6Z3EzRvpQVIpO7uFhdiGhdE8Mh3S2VWKLL9xuxVqD6fzPhyI3ugthpYXlCChXzO8FzcYIZ3t1+Kau+h2NY1hqA==} + '@oxlint/binding-linux-arm-gnueabihf@1.66.0': + resolution: {integrity: sha512-9u1rgwZSEXWb30vbFZzQ78HVXBo0WCKNwJ3a2InRUTNMRng+PUDIoSFmA+m4HdUfBaIqftShq8J8qHc+eE/Vig==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@oxlint/binding-linux-arm-musleabihf@1.49.0': - resolution: {integrity: sha512-wdjXaQYAL/L25732mLlngfst4Jdmi/HLPVHb3yfCoP5mE3lO/pFFrmOJpqWodgv29suWY74Ij+RmJ/YIG5VuzQ==} + '@oxlint/binding-linux-arm-musleabihf@1.66.0': + resolution: {integrity: sha512-Ynot2HR1bHxUaNWoC280MVTDfZuaWuP3XfSMRDhyuZrVjhzoaBCVFlw8h8qeZjWKVUBhPWFIxB7AQTlK8Z2WWg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@oxlint/binding-linux-arm64-gnu@1.49.0': - resolution: {integrity: sha512-oSHpm8zmSvAG1BWUumbDRSg7moJbnwoEXKAkwDf/xTQJOzvbUknq95NVQdw/AduZr5dePftalB8rzJNGBogUMg==} + '@oxlint/binding-linux-arm64-gnu@1.66.0': + resolution: {integrity: sha512-xCbgzciGgo+A4aQZEknsNrNiIwY7sU5SfRuMmRjPIvZAgdF34cIHiKvwOsS5XRLjlTVSFwitmq6YclTtHTfU+g==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [glibc] - '@oxlint/binding-linux-arm64-musl@1.49.0': - resolution: {integrity: sha512-xeqkMOARgGBlEg9BQuPDf6ZW711X6BT5qjDyeM5XNowCJeTSdmMhpePJjTEiVbbr3t21sIlK8RE6X5bc04nWyQ==} + '@oxlint/binding-linux-arm64-musl@1.66.0': + resolution: {integrity: sha512-hmo+ZB/lHkR1HdDmnziNpzSLmulnUSu10VEqX2Yex7OwvoBAbjJQLvy4gIBRV3AAwWnCvAxKp5Nv1GE6LU1QMg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [musl] - '@oxlint/binding-linux-ppc64-gnu@1.49.0': - resolution: {integrity: sha512-uvcqRO6PnlJGbL7TeePhTK5+7/JXbxGbN+C6FVmfICDeeRomgQqrfVjf0lUrVpUU8ii8TSkIbNdft3M+oNlOsQ==} + '@oxlint/binding-linux-ppc64-gnu@1.66.0': + resolution: {integrity: sha512-2Invd4Uyy81mVooQC5FBtfxSNrvcX1OxbMlVQ6M2erRrNI2awFYF26YNW2yFxdVFZ4ffNOWKghtMjhnUPsXsVA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] libc: [glibc] - '@oxlint/binding-linux-riscv64-gnu@1.49.0': - resolution: {integrity: sha512-Dw1HkdXAwHNH+ZDserHP2RzXQmhHtpsYYI0hf8fuGAVCIVwvS6w1+InLxpPMY25P8ASRNiFN3hADtoh6lI+4lg==} + '@oxlint/binding-linux-riscv64-gnu@1.66.0': + resolution: {integrity: sha512-s0iXPDQVdgayE3RGa/N2DZF7tjgg0TwEtD1sGoDxqPDGrIXgo45H0yHknT0f9A0yteASsweYZtDyTuVlM4aSag==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] libc: [glibc] - '@oxlint/binding-linux-riscv64-musl@1.49.0': - resolution: {integrity: sha512-EPlMYaA05tJ9km/0dI9K57iuMq3Tw+nHst7TNIegAJZrBPtsOtYaMFZEaWj02HA8FI5QvSnRHMt+CI+RIhXJBQ==} + '@oxlint/binding-linux-riscv64-musl@1.66.0': + resolution: {integrity: sha512-OekL4XFiu7RPK0JIZi8VeHgtIXPREf42t8Cy/rKEsC+P3gcqDgNAAGiyuUOpdbG4wwbfue1q4CHcCO7spSve6w==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] libc: [musl] - '@oxlint/binding-linux-s390x-gnu@1.49.0': - resolution: {integrity: sha512-yZiQL9qEwse34aMbnMb5VqiAWfDY+fLFuoJbHOuzB1OaJZbN1MRF9Nk+W89PIpGr5DNPDipwjZb8+Q7wOywoUQ==} + '@oxlint/binding-linux-s390x-gnu@1.66.0': + resolution: {integrity: sha512-Ga1D0kj1SFslm34ThA/BdkUlyAYEnTsXyRC4pF0C5agZSwtGdHYWMTQWemUfBGp4RCG4QWXgdO+HmmmKqOtlBg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] libc: [glibc] - '@oxlint/binding-linux-x64-gnu@1.49.0': - resolution: {integrity: sha512-CcCDwMMXSchNkhdgvhVn3DLZ4EnBXAD8o8+gRzahg+IdSt/72y19xBgShJgadIRF0TsRcV/MhDUMwL5N/W54aQ==} + '@oxlint/binding-linux-x64-gnu@1.66.0': + resolution: {integrity: sha512-p5jfP1wUZe/IC3qpQO84n9DRnf9g3lKRtLBlQq23ykyrDglHcVx7sWmVTlPuU6SBw8mNnPzyOn022G3XZHnlww==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [glibc] - '@oxlint/binding-linux-x64-musl@1.49.0': - resolution: {integrity: sha512-u3HfKV8BV6t6UCCbN0RRiyqcymhrnpunVmLFI8sEa5S/EBu+p/0bJ3D7LZ2KT6PsBbrB71SWq4DeFrskOVgIZg==} + '@oxlint/binding-linux-x64-musl@1.66.0': + resolution: {integrity: sha512-vUB/sYlYZorDL1ZD+o9mRv7zbsykrrFRtmgS6R8musZqLtrPRQn1gc1eGpuX+sfdccz42STl/AqldY6XRb2upQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [musl] - '@oxlint/binding-openharmony-arm64@1.49.0': - resolution: {integrity: sha512-dRDpH9fw+oeUMpM4br0taYCFpW6jQtOuEIec89rOgDA1YhqwmeRcx0XYeCv7U48p57qJ1XZHeMGM9LdItIjfzA==} + '@oxlint/binding-openharmony-arm64@1.66.0': + resolution: {integrity: sha512-yde+6p/F59xRkGR9H1HfngWRif1QRJjynZK349l+UI0H6w9hL3G8/AVaTHFyTtLVQ56qtNbX2/5Dc77n1ovnOg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] - '@oxlint/binding-win32-arm64-msvc@1.49.0': - resolution: {integrity: sha512-6rrKe/wL9tn0qnOy76i1/0f4Dc3dtQnibGlU4HqR/brVHlVjzLSoaH0gAFnLnznh9yQ6gcFTBFOPrcN/eKPDGA==} + '@oxlint/binding-win32-arm64-msvc@1.66.0': + resolution: {integrity: sha512-O9GLucgoTdmOrbBX+EjzNe7o/Ze5TFOvXcib6bzUOtBOmj6cV+zw18NgB+cGKAkDw1Pdqs8vGkfHbbsLuDtXWg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] - '@oxlint/binding-win32-ia32-msvc@1.49.0': - resolution: {integrity: sha512-CXHLWAtLs2xG/aVy1OZiYJzrULlq0QkYpI6cd7VKMrab+qur4fXVE/B1Bp1m0h1qKTj5/FTGg6oU4qaXMjS/ug==} + '@oxlint/binding-win32-ia32-msvc@1.66.0': + resolution: {integrity: sha512-m3Pjwc2MfTcom4E4gOv7DyuGyt7OfGNCbmqDHd+N7EzXmP+ppHuudm2NjcA3AjV5TSeGxaguVF4SbTKHe1USYA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ia32] os: [win32] - '@oxlint/binding-win32-x64-msvc@1.49.0': - resolution: {integrity: sha512-VteIelt78kwzSglOozaQcs6BCS4Lk0j+QA+hGV0W8UeyaqQ3XpbZRhDU55NW1PPvCy1tg4VXsTlEaPovqto7nQ==} + '@oxlint/binding-win32-x64-msvc@1.66.0': + resolution: {integrity: sha512-/DbBvw8UFBhja6PqudUjV4UtfsJr0Oa7jUjWVKB0g86lj/VwnPrkngn0sFql3c9RDA0O16dh7ozsXb6GjNAzBQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] @@ -8765,16 +8765,16 @@ packages: engines: {node: ^20.19.0 || >=22.12.0} hasBin: true - oxlint-tsgolint@0.15.0: - resolution: {integrity: sha512-iwvFmhKQVZzVTFygUVI4t2S/VKEm+Mqkw3jQRJwfDuTcUYI5LCIYzdO5Dbuv4mFOkXZCcXaRRh0m+uydB5xdqw==} + oxlint-tsgolint@0.23.0: + resolution: {integrity: sha512-3mBv3CoPbh8dFbzfDGIWa2ytZjn2v+3EX4aKRXjIhsoGFzG8GCjfRirz3rwZf1wYbZzsNLTSgpw8VjQuWdp/jA==} hasBin: true - oxlint@1.49.0: - resolution: {integrity: sha512-YZffp0gM+63CJoRhHjtjRnwKtAgUnXM6j63YQ++aigji2NVvLGsUlrXo9gJUXZOdcbfShLYtA6RuTu8GZ4lzOQ==} + oxlint@1.66.0: + resolution: {integrity: sha512-N4LLxYLd94KEBqXDMDM5f+2PUpItTjDLreXe2Gn5KhjhCK4Qp2YUXaBi8Yu325ryOgKwt22m45fpD7nPOn69Yw==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: - oxlint-tsgolint: '>=0.14.1' + oxlint-tsgolint: '>=0.22.1' peerDependenciesMeta: oxlint-tsgolint: optional: true @@ -12318,11 +12318,11 @@ snapshots: react: 19.2.4 tslib: 2.8.1 - '@e18e/eslint-plugin@0.2.0(oxlint@1.49.0(oxlint-tsgolint@0.15.0))': + '@e18e/eslint-plugin@0.2.0(oxlint@1.66.0(oxlint-tsgolint@0.23.0))': dependencies: eslint-plugin-depend: 1.4.0 optionalDependencies: - oxlint: 1.49.0(oxlint-tsgolint@0.15.0) + oxlint: 1.66.0(oxlint-tsgolint@0.23.0) '@emmetio/abbreviation@2.3.3': dependencies: @@ -13266,79 +13266,79 @@ snapshots: '@oxfmt/binding-win32-x64-msvc@0.34.0': optional: true - '@oxlint-tsgolint/darwin-arm64@0.15.0': + '@oxlint-tsgolint/darwin-arm64@0.23.0': optional: true - '@oxlint-tsgolint/darwin-x64@0.15.0': + '@oxlint-tsgolint/darwin-x64@0.23.0': optional: true - '@oxlint-tsgolint/linux-arm64@0.15.0': + '@oxlint-tsgolint/linux-arm64@0.23.0': optional: true - '@oxlint-tsgolint/linux-x64@0.15.0': + '@oxlint-tsgolint/linux-x64@0.23.0': optional: true - '@oxlint-tsgolint/win32-arm64@0.15.0': + '@oxlint-tsgolint/win32-arm64@0.23.0': optional: true - '@oxlint-tsgolint/win32-x64@0.15.0': + '@oxlint-tsgolint/win32-x64@0.23.0': optional: true - '@oxlint/binding-android-arm-eabi@1.49.0': + '@oxlint/binding-android-arm-eabi@1.66.0': optional: true - '@oxlint/binding-android-arm64@1.49.0': + '@oxlint/binding-android-arm64@1.66.0': optional: true - '@oxlint/binding-darwin-arm64@1.49.0': + '@oxlint/binding-darwin-arm64@1.66.0': optional: true - '@oxlint/binding-darwin-x64@1.49.0': + '@oxlint/binding-darwin-x64@1.66.0': optional: true - '@oxlint/binding-freebsd-x64@1.49.0': + '@oxlint/binding-freebsd-x64@1.66.0': optional: true - '@oxlint/binding-linux-arm-gnueabihf@1.49.0': + '@oxlint/binding-linux-arm-gnueabihf@1.66.0': optional: true - '@oxlint/binding-linux-arm-musleabihf@1.49.0': + '@oxlint/binding-linux-arm-musleabihf@1.66.0': optional: true - '@oxlint/binding-linux-arm64-gnu@1.49.0': + '@oxlint/binding-linux-arm64-gnu@1.66.0': optional: true - '@oxlint/binding-linux-arm64-musl@1.49.0': + '@oxlint/binding-linux-arm64-musl@1.66.0': optional: true - '@oxlint/binding-linux-ppc64-gnu@1.49.0': + '@oxlint/binding-linux-ppc64-gnu@1.66.0': optional: true - '@oxlint/binding-linux-riscv64-gnu@1.49.0': + '@oxlint/binding-linux-riscv64-gnu@1.66.0': optional: true - '@oxlint/binding-linux-riscv64-musl@1.49.0': + '@oxlint/binding-linux-riscv64-musl@1.66.0': optional: true - '@oxlint/binding-linux-s390x-gnu@1.49.0': + '@oxlint/binding-linux-s390x-gnu@1.66.0': optional: true - '@oxlint/binding-linux-x64-gnu@1.49.0': + '@oxlint/binding-linux-x64-gnu@1.66.0': optional: true - '@oxlint/binding-linux-x64-musl@1.49.0': + '@oxlint/binding-linux-x64-musl@1.66.0': optional: true - '@oxlint/binding-openharmony-arm64@1.49.0': + '@oxlint/binding-openharmony-arm64@1.66.0': optional: true - '@oxlint/binding-win32-arm64-msvc@1.49.0': + '@oxlint/binding-win32-arm64-msvc@1.66.0': optional: true - '@oxlint/binding-win32-ia32-msvc@1.49.0': + '@oxlint/binding-win32-ia32-msvc@1.66.0': optional: true - '@oxlint/binding-win32-x64-msvc@1.49.0': + '@oxlint/binding-win32-x64-msvc@1.66.0': optional: true '@pagefind/darwin-arm64@1.4.0': @@ -18619,37 +18619,37 @@ snapshots: '@oxfmt/binding-win32-ia32-msvc': 0.34.0 '@oxfmt/binding-win32-x64-msvc': 0.34.0 - oxlint-tsgolint@0.15.0: + oxlint-tsgolint@0.23.0: optionalDependencies: - '@oxlint-tsgolint/darwin-arm64': 0.15.0 - '@oxlint-tsgolint/darwin-x64': 0.15.0 - '@oxlint-tsgolint/linux-arm64': 0.15.0 - '@oxlint-tsgolint/linux-x64': 0.15.0 - '@oxlint-tsgolint/win32-arm64': 0.15.0 - '@oxlint-tsgolint/win32-x64': 0.15.0 - - oxlint@1.49.0(oxlint-tsgolint@0.15.0): + '@oxlint-tsgolint/darwin-arm64': 0.23.0 + '@oxlint-tsgolint/darwin-x64': 0.23.0 + '@oxlint-tsgolint/linux-arm64': 0.23.0 + '@oxlint-tsgolint/linux-x64': 0.23.0 + '@oxlint-tsgolint/win32-arm64': 0.23.0 + '@oxlint-tsgolint/win32-x64': 0.23.0 + + oxlint@1.66.0(oxlint-tsgolint@0.23.0): optionalDependencies: - '@oxlint/binding-android-arm-eabi': 1.49.0 - '@oxlint/binding-android-arm64': 1.49.0 - '@oxlint/binding-darwin-arm64': 1.49.0 - '@oxlint/binding-darwin-x64': 1.49.0 - '@oxlint/binding-freebsd-x64': 1.49.0 - '@oxlint/binding-linux-arm-gnueabihf': 1.49.0 - '@oxlint/binding-linux-arm-musleabihf': 1.49.0 - '@oxlint/binding-linux-arm64-gnu': 1.49.0 - '@oxlint/binding-linux-arm64-musl': 1.49.0 - '@oxlint/binding-linux-ppc64-gnu': 1.49.0 - '@oxlint/binding-linux-riscv64-gnu': 1.49.0 - '@oxlint/binding-linux-riscv64-musl': 1.49.0 - '@oxlint/binding-linux-s390x-gnu': 1.49.0 - '@oxlint/binding-linux-x64-gnu': 1.49.0 - '@oxlint/binding-linux-x64-musl': 1.49.0 - '@oxlint/binding-openharmony-arm64': 1.49.0 - '@oxlint/binding-win32-arm64-msvc': 1.49.0 - '@oxlint/binding-win32-ia32-msvc': 1.49.0 - '@oxlint/binding-win32-x64-msvc': 1.49.0 - oxlint-tsgolint: 0.15.0 + '@oxlint/binding-android-arm-eabi': 1.66.0 + '@oxlint/binding-android-arm64': 1.66.0 + '@oxlint/binding-darwin-arm64': 1.66.0 + '@oxlint/binding-darwin-x64': 1.66.0 + '@oxlint/binding-freebsd-x64': 1.66.0 + '@oxlint/binding-linux-arm-gnueabihf': 1.66.0 + '@oxlint/binding-linux-arm-musleabihf': 1.66.0 + '@oxlint/binding-linux-arm64-gnu': 1.66.0 + '@oxlint/binding-linux-arm64-musl': 1.66.0 + '@oxlint/binding-linux-ppc64-gnu': 1.66.0 + '@oxlint/binding-linux-riscv64-gnu': 1.66.0 + '@oxlint/binding-linux-riscv64-musl': 1.66.0 + '@oxlint/binding-linux-s390x-gnu': 1.66.0 + '@oxlint/binding-linux-x64-gnu': 1.66.0 + '@oxlint/binding-linux-x64-musl': 1.66.0 + '@oxlint/binding-openharmony-arm64': 1.66.0 + '@oxlint/binding-win32-arm64-msvc': 1.66.0 + '@oxlint/binding-win32-ia32-msvc': 1.66.0 + '@oxlint/binding-win32-x64-msvc': 1.66.0 + oxlint-tsgolint: 0.23.0 p-all@5.0.1: dependencies: From 17b226498d6070f67cff0c4448b0cf35fa7ee379 Mon Sep 17 00:00:00 2001 From: Matt Kane Date: Fri, 22 May 2026 15:51:25 +0100 Subject: [PATCH 3/4] ci: gate lint warnings so they cannot regress `pnpm lint` now passes `--deny-warnings` to oxlint, so any warning is a non-zero exit. CI inherits this through the existing lint job. The blocker was the existing pile of warnings, so this commit also clears them: - `packages/core/src/astro/middleware.ts`: collapse duplicated `virtualSandboxRunnerModule as Record<...>` casts behind one local binding inside a block-form disable, and convert the remaining parenthesis-form disables that newer oxlint stopped recognising. - `packages/core/src/astro/middleware/auth.ts` and 14 API route files: drop unnecessary `emdash!` non-null assertions; the preceding `requireDb(emdash?.db)` guard already narrows. - `packages/core/src/emdash-runtime.ts`: drop unnecessary `as ResolvedPlugin[]` and `emdash!` casts; annotate the two remaining trusted dynamic-import sites with a single disable line. - `packages/plugin-cli/src/build/pipeline.ts`: replace the chain of `as Record` casts with `isRecord` / `isStringArray` predicate narrowing. - `packages/cloudflare/src/sandbox/bridge.ts`: drop the `this.env.DB as D1Database` cast (already that type). - `packages/core/src/client/index.ts`, `packages/core/src/astro/integration/index.ts`, `packages/registry-client/src/credentials/index.ts`: remove three unused imports. Newer oxlint flagged config-level issues too: - `packages/workerd/tsconfig.json`: add explicit `rootDir`. - `packages/contentful-to-portable-text/tsconfig.json`: drop the `rootDir` that excluded `test/**/*` from `include`. - `packages/core/src/page/absolute-url.ts`: fix the disable comment rule name so `no-control-regex` is suppressed (the regex intentionally matches control chars). - `lunaria.config.ts`: convert block-form disable to next-line form with the new rule path. Finally, four newer rules are disabled at the repo level: `no-underscore-dangle` (Portable Text uses `_type`/`_key` by spec), `typescript/consistent-return`, `typescript/no-unnecessary-type-conversion`, `typescript/no-unnecessary-type-parameters`, and `typescript/no-useless-default-assignment` (the rule errors out under `strict: false`, which the test plugins use deliberately). These can be re-enabled in follow-up PRs once their hits are triaged. --- .github/workflows/ci.yml | 2 +- .oxlintrc.json | 10 ++++++ lunaria.config.ts | 3 +- package.json | 2 +- packages/cloudflare/src/sandbox/bridge.ts | 7 ++-- .../contentful-to-portable-text/tsconfig.json | 1 - packages/core/src/astro/integration/index.ts | 2 +- packages/core/src/astro/middleware.ts | 35 +++++++++---------- packages/core/src/astro/middleware/auth.ts | 14 ++++---- .../[collection]/[id]/terms/[taxonomy].ts | 8 ++--- .../astro/routes/api/redirects/404s/index.ts | 6 ++-- .../routes/api/redirects/404s/summary.ts | 2 +- .../src/astro/routes/api/redirects/[id].ts | 6 ++-- .../src/astro/routes/api/redirects/index.ts | 4 +-- .../collections/[slug]/fields/[fieldSlug].ts | 6 ++-- .../schema/collections/[slug]/fields/index.ts | 8 ++--- .../collections/[slug]/fields/reorder.ts | 2 +- .../api/schema/collections/[slug]/index.ts | 10 +++--- .../routes/api/schema/collections/index.ts | 6 ++-- .../core/src/astro/routes/api/schema/index.ts | 2 +- .../astro/routes/api/schema/orphans/[slug].ts | 2 +- .../astro/routes/api/schema/orphans/index.ts | 2 +- .../src/astro/routes/api/sections/[slug].ts | 6 ++-- .../src/astro/routes/api/sections/index.ts | 4 +-- packages/core/src/client/index.ts | 2 +- packages/core/src/emdash-runtime.ts | 29 ++++++++++----- packages/core/src/page/absolute-url.ts | 2 +- .../registry-client/src/credentials/index.ts | 1 - packages/workerd/tsconfig.json | 3 +- 29 files changed, 100 insertions(+), 87 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f0b357bd3..c46f0b798 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -49,7 +49,7 @@ jobs: cache: pnpm - run: pnpm install --frozen-lockfile - run: pnpm build - - run: pnpm lint:json + - run: pnpm lint version-check: name: Version Check diff --git a/.oxlintrc.json b/.oxlintrc.json index 68f416693..10160f5d4 100644 --- a/.oxlintrc.json +++ b/.oxlintrc.json @@ -22,6 +22,16 @@ "typescript/no-unsafe-type-assertion": "warn", "typescript/unbound-method": "off", "typescript/no-unnecessary-boolean-literal-compare": "off", + // Noisy/stylistic rules added in newer oxlint. Disabled while we triage them + // (see https://github.com/emdash-cms/emdash/issues — track follow-ups before + // re-enabling). + "no-underscore-dangle": "off", + "typescript/consistent-return": "off", + "typescript/no-unnecessary-type-conversion": "off", + "typescript/no-unnecessary-type-parameters": "off", + // The rule degrades to an unactionable error on configs without + // strictNullChecks (the test plugins under packages/plugins/*-test/). + "typescript/no-useless-default-assignment": "off", "import/no-named-as-default": "off", "import/no-unassigned-import": [ "warn", diff --git a/lunaria.config.ts b/lunaria.config.ts index 3573e7e4b..376b108bf 100644 --- a/lunaria.config.ts +++ b/lunaria.config.ts @@ -12,12 +12,11 @@ export default defineConfig({ lang: SOURCE_LOCALE.code, }, // Lunaria requires a non-empty tuple; TARGET_LOCALES is authored with 10+ entries. - /* eslint-disable typescript-eslint(no-unsafe-type-assertion) -- non-empty by construction (see packages/admin/src/locales/locales.ts) */ + // eslint-disable-next-line typescript/no-unsafe-type-assertion -- non-empty by construction (see packages/admin/src/locales/locales.ts) locales: TARGET_LOCALES.map((l) => ({ label: l.label, lang: l.code, })) as [{ label: string; lang: string }, ...{ label: string; lang: string }[]], - /* eslint-enable typescript-eslint(no-unsafe-type-assertion) */ files: [ { include: ["packages/admin/src/locales/en/messages.po"], diff --git a/package.json b/package.json index dc22ca053..caf9eddbf 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ "format": "oxfmt --ignore-path .gitignore && prettier --write .", "format:check": "oxfmt --ignore-path .gitignore --check && prettier --check .", "format:astro": "prettier --write .", - "lint": "oxlint --type-aware", + "lint": "oxlint --type-aware --deny-warnings", "lint:quick": "oxlint -f json", "lint:json": "oxlint --type-aware -f json", "lint:fix": "oxlint --type-aware --fix", diff --git a/packages/cloudflare/src/sandbox/bridge.ts b/packages/cloudflare/src/sandbox/bridge.ts index 8063b45fe..8cbccfade 100644 --- a/packages/cloudflare/src/sandbox/bridge.ts +++ b/packages/cloudflare/src/sandbox/bridge.ts @@ -163,11 +163,10 @@ export class PluginBridge extends WorkerEntrypoint({ - dialect: new D1Dialect({ database: this.env.DB as D1Database }), + dialect: new D1Dialect({ database: this.env.DB }), }); - // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- Kysely is compatible with PluginStorageRepository's expected db + // eslint-disable-next-line typescript-eslint/no-unsafe-type-assertion -- Kysely is compatible with PluginStorageRepository's expected db return new PluginStorageRepository(db as never, pluginId, collection, allIndexes); } @@ -289,8 +288,8 @@ export class PluginBridge extends WorkerEntrypoint const result = await repo.query({ + // eslint-disable-next-line typescript-eslint/no-unsafe-type-assertion -- WhereClause is structurally Record where: opts.where as never, orderBy: opts.orderBy, limit: opts.limit, diff --git a/packages/contentful-to-portable-text/tsconfig.json b/packages/contentful-to-portable-text/tsconfig.json index 4da6bb751..629af451e 100644 --- a/packages/contentful-to-portable-text/tsconfig.json +++ b/packages/contentful-to-portable-text/tsconfig.json @@ -8,7 +8,6 @@ "declarationMap": true, "sourceMap": true, "outDir": "dist", - "rootDir": "src", "esModuleInterop": true, "skipLibCheck": true, "noUncheckedIndexedAccess": true, diff --git a/packages/core/src/astro/integration/index.ts b/packages/core/src/astro/integration/index.ts index 6b415125d..be342fd2c 100644 --- a/packages/core/src/astro/integration/index.ts +++ b/packages/core/src/astro/integration/index.ts @@ -22,7 +22,7 @@ import { injectAuthProviderRoutes, injectMcpRoute, } from "./routes.js"; -import type { EmDashConfig, PluginDescriptor } from "./runtime.js"; +import type { EmDashConfig } from "./runtime.js"; import { createViteConfig } from "./vite-config.js"; // Re-export runtime types and functions diff --git a/packages/core/src/astro/middleware.ts b/packages/core/src/astro/middleware.ts index 347b04f97..3b695ad28 100644 --- a/packages/core/src/astro/middleware.ts +++ b/packages/core/src/astro/middleware.ts @@ -79,11 +79,11 @@ function getConfig(): EmDashConfig | null { // Initialize i18n config on first access (once per worker lifetime) if (!i18nInitialized) { i18nInitialized = true; - // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- virtual module checked as object above + // eslint-disable-next-line typescript/no-unsafe-type-assertion -- virtual module checked as object above const config = virtualConfig as Record; if (config.i18n && typeof config.i18n === "object") { setI18nConfig( - // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- runtime-checked above + // eslint-disable-next-line typescript/no-unsafe-type-assertion -- runtime-checked above config.i18n as { defaultLocale: string; locales: string[]; @@ -95,7 +95,7 @@ function getConfig(): EmDashConfig | null { } } - // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- virtual module import is untyped (@ts-ignore above) + // eslint-disable-next-line typescript/no-unsafe-type-assertion -- virtual module import is untyped (@ts-ignore above) return virtualConfig as EmDashConfig; } return null; @@ -105,7 +105,7 @@ function getConfig(): EmDashConfig | null { * Get plugins from virtual module */ function getPlugins(): ResolvedPlugin[] { - // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- virtual module import is untyped (@ts-ignore above) + // eslint-disable-next-line typescript/no-unsafe-type-assertion -- virtual module import is untyped (@ts-ignore above) return (virtualPlugins as ResolvedPlugin[]) || []; } @@ -113,23 +113,22 @@ function getPlugins(): ResolvedPlugin[] { * Build runtime dependencies from virtual modules */ function buildDependencies(config: EmDashConfig): RuntimeDependencies { + /* eslint-disable typescript-eslint/no-unsafe-type-assertion -- + The virtual:emdash/* imports above use @ts-ignore because tsgo/IDE + resolution can't see virtual-modules.d.ts in every consumer setup, + so they arrive as `any`. The casts here line each entry up with + RuntimeDependencies's expected shape. The contract is enforced by + the integration that populates these virtual modules. */ + const sandboxModule = virtualSandboxRunnerModule as Record; return { config, plugins: getPlugins(), - // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- virtual module import is untyped (@ts-ignore above) createDialect: virtualCreateDialect as (config: Record) => unknown, - // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- virtual module import is untyped (@ts-ignore above) createStorage: virtualCreateStorage as ((config: Record) => Storage) | null, - // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- virtual module import is untyped (@ts-ignore above) - sandboxEnabled: (virtualSandboxRunnerModule as Record) - .sandboxEnabled as boolean, - sandboxBypassed: - ((virtualSandboxRunnerModule as Record).sandboxBypassed as boolean) ?? false, - // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- virtual module import is untyped (@ts-ignore above) + sandboxEnabled: sandboxModule.sandboxEnabled as boolean, + sandboxBypassed: (sandboxModule.sandboxBypassed as boolean) ?? false, sandboxedPluginEntries: (virtualSandboxedPlugins as SandboxedPluginEntry[]) || [], - // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- virtual module import is untyped (@ts-ignore above) - createSandboxRunner: (virtualSandboxRunnerModule as Record) - .createSandboxRunner as + createSandboxRunner: sandboxModule.createSandboxRunner as | ((opts: { db: Kysely; mediaStorage?: { @@ -142,9 +141,9 @@ function buildDependencies(config: EmDashConfig): RuntimeDependencies { }; }) => SandboxRunner) | null, - // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- virtual module import is untyped (@ts-ignore above) mediaProviderEntries: (virtualMediaProviders as MediaProviderEntry[]) || [], }; + /* eslint-enable typescript-eslint/no-unsafe-type-assertion */ } /** @@ -266,7 +265,7 @@ function createRequestScopedDb( opts: RequestScopedDbOpts, ): { db: Kysely; commit: () => void } | null { if (typeof virtualCreateRequestScopedDb !== "function") return null; - // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- adapter returns Kysely; cast to Database since core owns that type + // eslint-disable-next-line typescript/no-unsafe-type-assertion -- adapter returns Kysely; cast to Database since core owns that type const fn = virtualCreateRequestScopedDb as ( o: RequestScopedDbOpts, ) => { db: Kysely; commit: () => void } | null; @@ -369,7 +368,7 @@ export const onRequest = defineMiddleware(async (context, next) => { try { const runtime = await getRuntime(config, initSubTimings); setupVerified = true; - // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- partial object; getPageRuntime() only checks for the page-contribution methods + // eslint-disable-next-line typescript/no-unsafe-type-assertion -- partial object; getPageRuntime() only checks for the page-contribution methods locals.emdash = { collectPageMetadata: runtime.collectPageMetadata.bind(runtime), collectPageFragments: runtime.collectPageFragments.bind(runtime), diff --git a/packages/core/src/astro/middleware/auth.ts b/packages/core/src/astro/middleware/auth.ts index 46d34979f..29bf57f96 100644 --- a/packages/core/src/astro/middleware/auth.ts +++ b/packages/core/src/astro/middleware/auth.ts @@ -475,11 +475,11 @@ async function handleExternalAuth( const authResult = await virtualAuthenticate(request, authMode.config); // Get external auth config for auto-provision settings - // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- narrowing AuthModeConfig to ExternalAuthConfig after provider check + // eslint-disable-next-line typescript/no-unsafe-type-assertion -- narrowing AuthModeConfig to ExternalAuthConfig after provider check const externalConfig = authMode.config as ExternalAuthConfig; // Find or create user - const adapter = createKyselyAdapter(emdash!.db); + const adapter = createKyselyAdapter(emdash.db); let user = await adapter.getUserByEmail(authResult.email); if (!user) { @@ -492,9 +492,9 @@ async function handleExternalAuth( } // Check if this is the first user (they become admin) - const userCount = await emdash!.db + const userCount = await emdash.db .selectFrom("users") - .select(emdash!.db.fn.count("id").as("count")) + .select(emdash.db.fn.count("id").as("count")) .executeTakeFirst(); const isFirstUser = Number(userCount?.count ?? 0) === 0; @@ -512,7 +512,7 @@ async function handleExternalAuth( updated_at: now, }; - await emdash!.db.insertInto("users").values(newUser).execute(); + await emdash.db.insertInto("users").values(newUser).execute(); user = await adapter.getUserByEmail(authResult.email); @@ -539,7 +539,7 @@ async function handleExternalAuth( if (Object.keys(updates).length > 0) { updates.updated_at = new Date().toISOString(); - await emdash!.db.updateTable("users").set(updates).where("id", "=", user.id).execute(); + await emdash.db.updateTable("users").set(updates).where("id", "=", user.id).execute(); user = { ...user, @@ -665,7 +665,7 @@ async function handlePasskeyAuth( } // Get full user from database - const adapter = createKyselyAdapter(emdash!.db); + const adapter = createKyselyAdapter(emdash.db); const user = await adapter.getUserById(sessionUser.id); if (!user) { diff --git a/packages/core/src/astro/routes/api/content/[collection]/[id]/terms/[taxonomy].ts b/packages/core/src/astro/routes/api/content/[collection]/[id]/terms/[taxonomy].ts index 761d460da..af76d5ee1 100644 --- a/packages/core/src/astro/routes/api/content/[collection]/[id]/terms/[taxonomy].ts +++ b/packages/core/src/astro/routes/api/content/[collection]/[id]/terms/[taxonomy].ts @@ -34,7 +34,7 @@ export const GET: APIRoute = async ({ params, locals }) => { if (dbErr) return dbErr; try { - const repo = new TaxonomyRepository(emdash!.db); + const repo = new TaxonomyRepository(emdash.db); const terms = await repo.getTermsForEntry(collection, id, taxonomy); return apiSuccess({ @@ -68,12 +68,12 @@ export const POST: APIRoute = async ({ params, request, locals }) => { const dbErr = requireDb(emdash?.db); if (dbErr) return dbErr; - if (!emdash!.handleContentGet) { + if (!emdash.handleContentGet) { return apiError("NOT_CONFIGURED", "EmDash is not initialized", 500); } // Verify the content exists before modifying its terms - const existing = await emdash!.handleContentGet(collection, id); + const existing = await emdash.handleContentGet(collection, id); if (!existing.success) { return apiError( existing.error?.code ?? "NOT_FOUND", @@ -107,7 +107,7 @@ export const POST: APIRoute = async ({ params, request, locals }) => { if (isParseError(body)) return body; const { termIds } = body; - const repo = new TaxonomyRepository(emdash!.db); + const repo = new TaxonomyRepository(emdash.db); // Verify all term IDs exist and belong to the correct taxonomy for (const termId of termIds) { diff --git a/packages/core/src/astro/routes/api/redirects/404s/index.ts b/packages/core/src/astro/routes/api/redirects/404s/index.ts index 3ae66c802..f5919dc37 100644 --- a/packages/core/src/astro/routes/api/redirects/404s/index.ts +++ b/packages/core/src/astro/routes/api/redirects/404s/index.ts @@ -24,7 +24,7 @@ export const GET: APIRoute = async ({ url, locals }) => { const { emdash, user } = locals; const dbErr = requireDb(emdash?.db); if (dbErr) return dbErr; - const db = emdash!.db; + const db = emdash.db; const denied = requirePerm(user, "redirects:read"); if (denied) return denied; @@ -44,7 +44,7 @@ export const DELETE: APIRoute = async ({ locals }) => { const { emdash, user } = locals; const dbErr = requireDb(emdash?.db); if (dbErr) return dbErr; - const db = emdash!.db; + const db = emdash.db; const denied = requirePerm(user, "redirects:manage"); if (denied) return denied; @@ -61,7 +61,7 @@ export const POST: APIRoute = async ({ request, locals }) => { const { emdash, user } = locals; const dbErr = requireDb(emdash?.db); if (dbErr) return dbErr; - const db = emdash!.db; + const db = emdash.db; const denied = requirePerm(user, "redirects:manage"); if (denied) return denied; diff --git a/packages/core/src/astro/routes/api/redirects/404s/summary.ts b/packages/core/src/astro/routes/api/redirects/404s/summary.ts index a7b608354..eb544ca4e 100644 --- a/packages/core/src/astro/routes/api/redirects/404s/summary.ts +++ b/packages/core/src/astro/routes/api/redirects/404s/summary.ts @@ -18,7 +18,7 @@ export const GET: APIRoute = async ({ url, locals }) => { const { emdash, user } = locals; const dbErr = requireDb(emdash?.db); if (dbErr) return dbErr; - const db = emdash!.db; + const db = emdash.db; const denied = requirePerm(user, "redirects:read"); if (denied) return denied; diff --git a/packages/core/src/astro/routes/api/redirects/[id].ts b/packages/core/src/astro/routes/api/redirects/[id].ts index ab6d6b366..b05a3d37e 100644 --- a/packages/core/src/astro/routes/api/redirects/[id].ts +++ b/packages/core/src/astro/routes/api/redirects/[id].ts @@ -25,7 +25,7 @@ export const GET: APIRoute = async ({ params, locals }) => { const { emdash, user } = locals; const dbErr = requireDb(emdash?.db); if (dbErr) return dbErr; - const db = emdash!.db; + const db = emdash.db; const { id } = params; const denied = requirePerm(user, "redirects:read"); @@ -47,7 +47,7 @@ export const PUT: APIRoute = async ({ params, request, locals }) => { const { emdash, user } = locals; const dbErr = requireDb(emdash?.db); if (dbErr) return dbErr; - const db = emdash!.db; + const db = emdash.db; const { id } = params; const denied = requirePerm(user, "redirects:manage"); @@ -73,7 +73,7 @@ export const DELETE: APIRoute = async ({ params, locals }) => { const { emdash, user } = locals; const dbErr = requireDb(emdash?.db); if (dbErr) return dbErr; - const db = emdash!.db; + const db = emdash.db; const { id } = params; const denied = requirePerm(user, "redirects:manage"); diff --git a/packages/core/src/astro/routes/api/redirects/index.ts b/packages/core/src/astro/routes/api/redirects/index.ts index 5c740ccdf..befb03983 100644 --- a/packages/core/src/astro/routes/api/redirects/index.ts +++ b/packages/core/src/astro/routes/api/redirects/index.ts @@ -20,7 +20,7 @@ export const GET: APIRoute = async ({ url, locals }) => { const { emdash, user } = locals; const dbErr = requireDb(emdash?.db); if (dbErr) return dbErr; - const db = emdash!.db; + const db = emdash.db; const denied = requirePerm(user, "redirects:read"); if (denied) return denied; @@ -40,7 +40,7 @@ export const POST: APIRoute = async ({ request, locals }) => { const { emdash, user } = locals; const dbErr = requireDb(emdash?.db); if (dbErr) return dbErr; - const db = emdash!.db; + const db = emdash.db; const denied = requirePerm(user, "redirects:manage"); if (denied) return denied; diff --git a/packages/core/src/astro/routes/api/schema/collections/[slug]/fields/[fieldSlug].ts b/packages/core/src/astro/routes/api/schema/collections/[slug]/fields/[fieldSlug].ts index 3d1e19a8d..7f760dc03 100644 --- a/packages/core/src/astro/routes/api/schema/collections/[slug]/fields/[fieldSlug].ts +++ b/packages/core/src/astro/routes/api/schema/collections/[slug]/fields/[fieldSlug].ts @@ -32,7 +32,7 @@ export const GET: APIRoute = async ({ params, locals }) => { const denied = requirePerm(user, "schema:read"); if (denied) return denied; - const result = await handleSchemaFieldGet(emdash!.db, collectionSlug, fieldSlug); + const result = await handleSchemaFieldGet(emdash.db, collectionSlug, fieldSlug); return unwrapResult(result); }; @@ -52,7 +52,7 @@ export const PUT: APIRoute = async ({ params, request, locals }) => { // eslint-disable-next-line typescript/no-unsafe-type-assertion -- body is Zod-validated via parseBody(request, updateFieldBody) above const result = await handleSchemaFieldUpdate( - emdash!.db, + emdash.db, collectionSlug, fieldSlug, body as UpdateFieldInput, @@ -71,6 +71,6 @@ export const DELETE: APIRoute = async ({ params, locals }) => { const denied = requirePerm(user, "schema:manage"); if (denied) return denied; - const result = await handleSchemaFieldDelete(emdash!.db, collectionSlug, fieldSlug); + const result = await handleSchemaFieldDelete(emdash.db, collectionSlug, fieldSlug); return unwrapResult(result); }; diff --git a/packages/core/src/astro/routes/api/schema/collections/[slug]/fields/index.ts b/packages/core/src/astro/routes/api/schema/collections/[slug]/fields/index.ts index 78df8db9e..bf4f1ab40 100644 --- a/packages/core/src/astro/routes/api/schema/collections/[slug]/fields/index.ts +++ b/packages/core/src/astro/routes/api/schema/collections/[slug]/fields/index.ts @@ -26,7 +26,7 @@ export const GET: APIRoute = async ({ params, locals }) => { const denied = requirePerm(user, "schema:read"); if (denied) return denied; - const result = await handleSchemaFieldList(emdash!.db, collectionSlug); + const result = await handleSchemaFieldList(emdash.db, collectionSlug); return unwrapResult(result); }; @@ -43,10 +43,6 @@ export const POST: APIRoute = async ({ params, request, locals }) => { const body = await parseBody(request, createFieldBody); if (isParseError(body)) return body; - const result = await handleSchemaFieldCreate( - emdash!.db, - collectionSlug, - body as CreateFieldInput, - ); + const result = await handleSchemaFieldCreate(emdash.db, collectionSlug, body as CreateFieldInput); return unwrapResult(result, 201); }; diff --git a/packages/core/src/astro/routes/api/schema/collections/[slug]/fields/reorder.ts b/packages/core/src/astro/routes/api/schema/collections/[slug]/fields/reorder.ts index 80e97a524..1fe10d014 100644 --- a/packages/core/src/astro/routes/api/schema/collections/[slug]/fields/reorder.ts +++ b/packages/core/src/astro/routes/api/schema/collections/[slug]/fields/reorder.ts @@ -27,6 +27,6 @@ export const POST: APIRoute = async ({ params, request, locals }) => { const body = await parseBody(request, fieldReorderBody); if (isParseError(body)) return body; - const result = await handleSchemaFieldReorder(emdash!.db, collectionSlug, body.fieldSlugs); + const result = await handleSchemaFieldReorder(emdash.db, collectionSlug, body.fieldSlugs); return unwrapResult(result); }; diff --git a/packages/core/src/astro/routes/api/schema/collections/[slug]/index.ts b/packages/core/src/astro/routes/api/schema/collections/[slug]/index.ts index e402b228c..63caf6b18 100644 --- a/packages/core/src/astro/routes/api/schema/collections/[slug]/index.ts +++ b/packages/core/src/astro/routes/api/schema/collections/[slug]/index.ts @@ -34,7 +34,7 @@ export const GET: APIRoute = async ({ params, url, locals }) => { const query = parseQuery(url, collectionGetQuery); if (isParseError(query)) return query; - const result = await handleSchemaCollectionGet(emdash!.db, slug, { + const result = await handleSchemaCollectionGet(emdash.db, slug, { includeFields: query.includeFields ?? false, }); return unwrapResult(result); @@ -54,12 +54,12 @@ export const PUT: APIRoute = async ({ params, request, locals }) => { if (isParseError(body)) return body; const result = await handleSchemaCollectionUpdate( - emdash!.db, + emdash.db, slug, // eslint-disable-next-line typescript/no-unsafe-type-assertion -- parseBody validates via Zod body as UpdateCollectionInput, ); - emdash!.invalidateUrlPatternCache(); + emdash.invalidateUrlPatternCache(); return unwrapResult(result); }; @@ -74,9 +74,9 @@ export const DELETE: APIRoute = async ({ params, url, locals }) => { const denied = requirePerm(user, "schema:manage"); if (denied) return denied; - const result = await handleSchemaCollectionDelete(emdash!.db, slug, { + const result = await handleSchemaCollectionDelete(emdash.db, slug, { force, }); - emdash!.invalidateUrlPatternCache(); + emdash.invalidateUrlPatternCache(); return unwrapResult(result); }; diff --git a/packages/core/src/astro/routes/api/schema/collections/index.ts b/packages/core/src/astro/routes/api/schema/collections/index.ts index ee2df40ab..d8183953a 100644 --- a/packages/core/src/astro/routes/api/schema/collections/index.ts +++ b/packages/core/src/astro/routes/api/schema/collections/index.ts @@ -25,7 +25,7 @@ export const GET: APIRoute = async ({ locals }) => { const denied = requirePerm(user, "schema:read"); if (denied) return denied; - const result = await handleSchemaCollectionList(emdash!.db); + const result = await handleSchemaCollectionList(emdash.db); return unwrapResult(result); }; @@ -42,7 +42,7 @@ export const POST: APIRoute = async ({ request, locals }) => { if (isParseError(body)) return body; // eslint-disable-next-line typescript/no-unsafe-type-assertion -- Zod schema output narrowed to CreateCollectionInput - const result = await handleSchemaCollectionCreate(emdash!.db, body as CreateCollectionInput); - emdash!.invalidateUrlPatternCache(); + const result = await handleSchemaCollectionCreate(emdash.db, body as CreateCollectionInput); + emdash.invalidateUrlPatternCache(); return unwrapResult(result, 201); }; diff --git a/packages/core/src/astro/routes/api/schema/index.ts b/packages/core/src/astro/routes/api/schema/index.ts index 8a9e81846..beed2dff2 100644 --- a/packages/core/src/astro/routes/api/schema/index.ts +++ b/packages/core/src/astro/routes/api/schema/index.ts @@ -29,7 +29,7 @@ export const GET: APIRoute = async ({ request, locals }) => { if (denied) return denied; try { - const registry = new SchemaRegistry(emdash!.db); + const registry = new SchemaRegistry(emdash.db); // Get all collections with their fields const collections = await registry.listCollections(); diff --git a/packages/core/src/astro/routes/api/schema/orphans/[slug].ts b/packages/core/src/astro/routes/api/schema/orphans/[slug].ts index e59fd0aff..3a05ae9b6 100644 --- a/packages/core/src/astro/routes/api/schema/orphans/[slug].ts +++ b/packages/core/src/astro/routes/api/schema/orphans/[slug].ts @@ -31,6 +31,6 @@ export const POST: APIRoute = async ({ params, request, locals }) => { const options = await parseOptionalBody(request, orphanRegisterBody, {}); if (isParseError(options)) return options; - const result = await handleOrphanedTableRegister(emdash!.db, slug, options); + const result = await handleOrphanedTableRegister(emdash.db, slug, options); return unwrapResult(result, 201); }; diff --git a/packages/core/src/astro/routes/api/schema/orphans/index.ts b/packages/core/src/astro/routes/api/schema/orphans/index.ts index ff4841dbc..1041f84ae 100644 --- a/packages/core/src/astro/routes/api/schema/orphans/index.ts +++ b/packages/core/src/astro/routes/api/schema/orphans/index.ts @@ -21,6 +21,6 @@ export const GET: APIRoute = async ({ locals }) => { const denied = requirePerm(user, "schema:manage"); if (denied) return denied; - const result = await handleOrphanedTableList(emdash!.db); + const result = await handleOrphanedTableList(emdash.db); return unwrapResult(result); }; diff --git a/packages/core/src/astro/routes/api/sections/[slug].ts b/packages/core/src/astro/routes/api/sections/[slug].ts index 184d4561a..a1d8bc40c 100644 --- a/packages/core/src/astro/routes/api/sections/[slug].ts +++ b/packages/core/src/astro/routes/api/sections/[slug].ts @@ -24,7 +24,7 @@ export const GET: APIRoute = async ({ params, locals }) => { const { emdash, user } = locals; const dbErr = requireDb(emdash?.db); if (dbErr) return dbErr; - const db = emdash!.db; + const db = emdash.db; const { slug } = params; const denied = requirePerm(user, "sections:read"); @@ -46,7 +46,7 @@ export const PUT: APIRoute = async ({ params, request, locals }) => { const { emdash, user } = locals; const dbErr = requireDb(emdash?.db); if (dbErr) return dbErr; - const db = emdash!.db; + const db = emdash.db; const { slug } = params; const denied = requirePerm(user, "sections:manage"); @@ -71,7 +71,7 @@ export const DELETE: APIRoute = async ({ params, locals }) => { const { emdash, user } = locals; const dbErr = requireDb(emdash?.db); if (dbErr) return dbErr; - const db = emdash!.db; + const db = emdash.db; const { slug } = params; const denied = requirePerm(user, "sections:manage"); diff --git a/packages/core/src/astro/routes/api/sections/index.ts b/packages/core/src/astro/routes/api/sections/index.ts index daf8446c0..9ff4062e1 100644 --- a/packages/core/src/astro/routes/api/sections/index.ts +++ b/packages/core/src/astro/routes/api/sections/index.ts @@ -19,7 +19,7 @@ export const GET: APIRoute = async ({ url, locals }) => { const { emdash, user } = locals; const dbErr = requireDb(emdash?.db); if (dbErr) return dbErr; - const db = emdash!.db; + const db = emdash.db; const denied = requirePerm(user, "sections:read"); if (denied) return denied; @@ -39,7 +39,7 @@ export const POST: APIRoute = async ({ request, locals }) => { const { emdash, user } = locals; const dbErr = requireDb(emdash?.db); if (dbErr) return dbErr; - const db = emdash!.db; + const db = emdash.db; const denied = requirePerm(user, "sections:manage"); if (denied) return denied; diff --git a/packages/core/src/client/index.ts b/packages/core/src/client/index.ts index 346d224b4..aa31f4b85 100644 --- a/packages/core/src/client/index.ts +++ b/packages/core/src/client/index.ts @@ -19,7 +19,7 @@ import mime from "mime/lite"; -import type { PortableTextBlock, FieldSchema } from "./portable-text.js"; +import type { FieldSchema } from "./portable-text.js"; import { convertDataForRead, convertDataForWrite } from "./portable-text.js"; import type { Interceptor } from "./transport.js"; import { diff --git a/packages/core/src/emdash-runtime.ts b/packages/core/src/emdash-runtime.ts index 71aedaa42..388e474ba 100644 --- a/packages/core/src/emdash-runtime.ts +++ b/packages/core/src/emdash-runtime.ts @@ -403,7 +403,7 @@ export class EmDashRuntime { get db(): Kysely { const ctx = getRequestContext(); if (ctx?.db) { - // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- db in context is set by middleware with correct type + // eslint-disable-next-line typescript/no-unsafe-type-assertion -- db in context is set by middleware with correct type return ctx.db as Kysely; } return this._db; @@ -690,9 +690,8 @@ export class EmDashRuntime { private removePluginFromLists(pluginId: string): void { const allIdx = this.allPipelinePlugins.findIndex((p) => p.id === pluginId); if (allIdx !== -1) this.allPipelinePlugins.splice(allIdx, 1); - const configured = this.configuredPlugins as ResolvedPlugin[]; - const configIdx = configured.findIndex((p) => p.id === pluginId); - if (configIdx !== -1) configured.splice(configIdx, 1); + const configIdx = this.configuredPlugins.findIndex((p) => p.id === pluginId); + if (configIdx !== -1) this.configuredPlugins.splice(configIdx, 1); } /** @@ -746,6 +745,11 @@ export class EmDashRuntime { const handler = typeof deactivateHook === "function" ? deactivateHook : deactivateHook.handler; if (typeof handler === "function") { + // Sandbox-bypass cleanup: the plugin context isn't constructable + // here (no DB binding, no media, etc.), but well-behaved + // deactivate hooks should be no-op safe. If a hook does require + // ctx, it throws and the surrounding catch logs it. + // eslint-disable-next-line typescript-eslint/no-unsafe-type-assertion -- best-effort cleanup; see comment above await handler({ pluginId }, {} as never); } } @@ -797,6 +801,10 @@ export class EmDashRuntime { try { const dataUrl = `data:text/javascript;base64,${Buffer.from(bundle.backendCode).toString("base64")}`; + // Dynamic data: import returns `any` from a base64-encoded module. + // We trust the bundle to be shaped like a plugin (built by plugin-cli); + // adaptSandboxEntry then validates fields it cares about. + // eslint-disable-next-line typescript-eslint/no-unsafe-type-assertion -- dynamic module from trusted bundle const pluginModule = (await import(/* @vite-ignore */ dataUrl)) as Record< string, unknown @@ -822,7 +830,7 @@ export class EmDashRuntime { }); newPlugins.push(adapted); this.allPipelinePlugins.push(adapted); - (this.configuredPlugins as ResolvedPlugin[]).push(adapted); + this.configuredPlugins.push(adapted); this.enabledPlugins.add(adapted.id); } catch (error) { console.error( @@ -1243,7 +1251,7 @@ export class EmDashRuntime { // path gives us a fresh singleton instead. const ctx = getRequestContext(); if (ctx?.dbIsIsolated && ctx.db) { - // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- db in context is typed as unknown to avoid circular deps + // eslint-disable-next-line typescript/no-unsafe-type-assertion -- db in context is typed as unknown to avoid circular deps return ctx.db as Kysely; } @@ -1377,6 +1385,7 @@ export class EmDashRuntime { for (const entry of entries) { try { const dataUrl = `data:text/javascript;base64,${Buffer.from(entry.code).toString("base64")}`; + // eslint-disable-next-line typescript-eslint/no-unsafe-type-assertion -- dynamic module from trusted bundle (built by plugin-cli); adaptSandboxEntry validates required fields. const pluginModule = (await import(/* @vite-ignore */ dataUrl)) as Record; const pluginDef = (pluginModule.default ?? pluginModule) as Parameters< typeof adaptSandboxEntry @@ -1415,6 +1424,7 @@ export class EmDashRuntime { entrypoint: "", capabilities: entry.capabilities, allowedHosts: entry.allowedHosts, + // eslint-disable-next-line typescript-eslint/no-unsafe-type-assertion -- adaptSandboxEntry copies storage through storage: entry.storage as never, adminPages, adminWidgets, @@ -1691,6 +1701,7 @@ export class EmDashRuntime { // Evaluate the bundled ESM and adapt it as a trusted plugin const dataUrl = `data:text/javascript;base64,${Buffer.from(bundle.backendCode).toString("base64")}`; + // eslint-disable-next-line typescript-eslint/no-unsafe-type-assertion -- dynamic module from trusted bundle (built by plugin-cli); adaptSandboxEntry validates required fields. const pluginModule = (await import(/* @vite-ignore */ dataUrl)) as Record< string, unknown @@ -2137,7 +2148,7 @@ export class EmDashRuntime { */ private async hydrateDraftData(result: T): Promise { if (!result || typeof result !== "object") return result; - // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- shape probed below + // eslint-disable-next-line typescript/no-unsafe-type-assertion -- shape probed below const r = result as { success?: boolean; data?: { item?: Record }; @@ -2151,7 +2162,7 @@ export class EmDashRuntime { if (!revision) return result; const liveData = item.data && typeof item.data === "object" - ? // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- narrowed to object above + ? // eslint-disable-next-line typescript/no-unsafe-type-assertion -- narrowed to object above (item.data as Record) : {}; // Strip leading-underscore keys (`_slug`, `_rev`, etc.) from the @@ -2172,7 +2183,7 @@ export class EmDashRuntime { // hydrated item without going back through `unknown`. return { ...result, - // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- shape preserved; result has been narrowed to the {success,data:{item}} envelope + // eslint-disable-next-line typescript/no-unsafe-type-assertion -- shape preserved; result has been narrowed to the {success,data:{item}} envelope data: { ...r.data, item: { ...item, data: mergedData, liveData }, diff --git a/packages/core/src/page/absolute-url.ts b/packages/core/src/page/absolute-url.ts index 202f69c62..cb8bcfe36 100644 --- a/packages/core/src/page/absolute-url.ts +++ b/packages/core/src/page/absolute-url.ts @@ -44,7 +44,7 @@ const OTHER_SCHEME_RE = /^[a-z][a-z0-9+.-]*:/i; * exploitable, plus more pathological shapes like leading newlines that * could inject across header boundaries downstream. */ -// eslint-disable-next-line eslint(no-control-regex) -- intentional: rejecting control chars is the whole point of this regex +// eslint-disable-next-line no-control-regex -- intentional: rejecting control chars is the whole point of this regex const WHITESPACE_OR_CONTROL_RE = /[\s\u0000-\u001f\u007f-\u009f]/; const TRAILING_SLASH_RE = /\/$/; diff --git a/packages/registry-client/src/credentials/index.ts b/packages/registry-client/src/credentials/index.ts index e03e55b1e..d36f126b5 100644 --- a/packages/registry-client/src/credentials/index.ts +++ b/packages/registry-client/src/credentials/index.ts @@ -17,7 +17,6 @@ import { EnvCredentialStore } from "./env.js"; import { FileCredentialStore } from "./file.js"; -import { MemoryCredentialStore } from "./memory.js"; import type { CredentialStore } from "./types.js"; export { diff --git a/packages/workerd/tsconfig.json b/packages/workerd/tsconfig.json index 7d1576d39..1a399d2a9 100644 --- a/packages/workerd/tsconfig.json +++ b/packages/workerd/tsconfig.json @@ -9,7 +9,8 @@ "verbatimModuleSyntax": true, "skipLibCheck": true, "declaration": true, - "outDir": "dist" + "rootDir": "./src", + "outDir": "./dist" }, "include": ["src"] } From 8ab9f622f12b4a4352e7aa52417772db212a82f5 Mon Sep 17 00:00:00 2001 From: Matt Kane Date: Fri, 22 May 2026 16:16:06 +0100 Subject: [PATCH 4/4] fix(workerd): return 400 (not 500) for malformed JSON bridge bodies The HttpError class introduced in the previous commit covers size and chunk-type errors, but JSON.parse() can still throw SyntaxError, which the bridge catch block treats as a 500. A malformed body from a plugin is a client-level error and should be 400. Caught by adversarial review on #1147. --- packages/workerd/src/sandbox/backing-service.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/workerd/src/sandbox/backing-service.ts b/packages/workerd/src/sandbox/backing-service.ts index ad4f32242..dc117ff1d 100644 --- a/packages/workerd/src/sandbox/backing-service.ts +++ b/packages/workerd/src/sandbox/backing-service.ts @@ -132,7 +132,13 @@ async function readBody(req: IncomingMessage): Promise> } const raw = Buffer.concat(chunks).toString(); if (!raw) return {}; - const parsed: unknown = JSON.parse(raw); + let parsed: unknown; + try { + parsed = JSON.parse(raw); + } catch (error) { + const message = error instanceof Error ? error.message : "Invalid JSON"; + throw new HttpError(`Invalid JSON: ${message}`, 400); + } if (!isJsonObject(parsed)) { throw new HttpError("Request body must be a JSON object", 400); }