Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions .changeset/workerd-typed-bridge.md
Original file line number Diff line number Diff line change
@@ -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<string, unknown>`.
- `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.
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
11 changes: 10 additions & 1 deletion .oxlintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,21 @@
}
],
"unicorn/filename-case": "off",
"unicorn/prevent-abbreviations": "off",
"unicorn/no-null": "off",
"unicorn/prefer-add-event-listener": "off",
"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",
Expand Down
8 changes: 4 additions & 4 deletions apps/aggregator/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<RecordsJob>, 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<RecordsJob>, 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<BackfillJob>, 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<BackfillJob>, env);
return;
default:
Expand Down
4 changes: 2 additions & 2 deletions apps/aggregator/src/routes/xrpc/views.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand All @@ -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,
Expand Down
3 changes: 1 addition & 2 deletions lunaria.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"],
Expand Down
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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"
Expand Down
2 changes: 1 addition & 1 deletion packages/admin/src/components/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ function NavMenuLink({ item, isActive }: { item: NavItem; isActive: boolean }) {

const link = (
<Link
// eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- TanStack Router requires literal route types
// eslint-disable-next-line typescript/no-unsafe-type-assertion -- TanStack Router requires literal route types
to={item.to as "/"}
params={item.params}
aria-current={isActive ? "page" : undefined}
Expand Down
6 changes: 3 additions & 3 deletions packages/admin/src/lib/api/import.ts
Original file line number Diff line number Diff line change
Expand Up @@ -288,11 +288,11 @@ export async function importWxrMedia(
try {
const parsed: { type?: string; imported?: unknown } = JSON.parse(line);
if (parsed.type === "progress") {
// eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- SSE event data is parsed JSON; discriminated by type === "progress"
// eslint-disable-next-line typescript/no-unsafe-type-assertion -- SSE event data is parsed JSON; discriminated by type === "progress"
onProgress(parsed as MediaImportProgress);
} else if (parsed.type === "result" || parsed.imported) {
// Final result (has type: "result" or is the result object)
// eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- SSE event data is parsed JSON; discriminated by type === "result"
// eslint-disable-next-line typescript/no-unsafe-type-assertion -- SSE event data is parsed JSON; discriminated by type === "result"
result = parsed as MediaImportResult;
}
} catch {
Expand All @@ -307,7 +307,7 @@ export async function importWxrMedia(
try {
const parsed: { type?: string; imported?: unknown } = JSON.parse(buffer);
if (parsed.type === "result" || parsed.imported) {
// eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- SSE event data is parsed JSON; discriminated by type === "result"
// eslint-disable-next-line typescript/no-unsafe-type-assertion -- SSE event data is parsed JSON; discriminated by type === "result"
result = parsed as MediaImportResult;
}
} catch {
Expand Down
2 changes: 1 addition & 1 deletion packages/auth/src/adapters/kysely.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ interface AllowedDomainTable {

export function createKyselyAdapter<T extends AuthTables>(db: Kysely<T>): AuthAdapter {
// Type cast to work with generic Kysely instance
// eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- generic Kysely<T extends AuthTables> narrowed to concrete AuthTables for internal queries
// eslint-disable-next-line typescript/no-unsafe-type-assertion -- generic Kysely<T extends AuthTables> narrowed to concrete AuthTables for internal queries
const kdb = db as unknown as Kysely<AuthTables>;

return {
Expand Down
2 changes: 1 addition & 1 deletion packages/auth/src/rbac.ts
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,7 @@ const SCOPE_MIN_ROLE: Record<ApiTokenScope, RoleLevel> = {
* 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<ApiTokenScope[]>((acc, [scope, minRole]) => {
if (role >= minRole) acc.push(scope);
Expand Down
6 changes: 3 additions & 3 deletions packages/cloudflare/src/db/d1-introspector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions packages/cloudflare/src/db/d1.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<any>({
dialect: new EmDashD1Dialect({ database: sessionAsDatabase }),
Expand Down Expand Up @@ -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<string, unknown>)[config.binding] as D1Database | undefined;
return db ?? null;
}
6 changes: 3 additions & 3 deletions packages/cloudflare/src/db/do-class.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ export class EmDashPreviewDB extends DurableObject {

const rows: Record<string, unknown>[] = [];
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<string, unknown>);
}

Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions packages/cloudflare/src/db/do-dialect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,13 +106,13 @@ class PreviewDOConnection implements DatabaseConnection {

async executeQuery<O>(compiledQuery: CompiledQuery): Promise<QueryResult<O>> {
const sqlText = compiledQuery.sql;
// eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- CompiledQuery.parameters is ReadonlyArray<unknown>, stub expects unknown[]
// eslint-disable-next-line typescript/no-unsafe-type-assertion -- CompiledQuery.parameters is ReadonlyArray<unknown>, 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,
};
Expand Down
6 changes: 3 additions & 3 deletions packages/cloudflare/src/db/do-preview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, unknown>)[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<EmDashPreviewDB>;
const doId = namespace.idFromName(sessionToken);
const stub = namespace.get(doId);
Expand Down Expand Up @@ -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 });
Expand Down
6 changes: 3 additions & 3 deletions packages/cloudflare/src/db/do.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, unknown>)[config.binding];

if (!ns) {
Expand All @@ -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<EmDashPreviewDB>;
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;
};

Expand Down
12 changes: 6 additions & 6 deletions packages/cloudflare/src/db/playground-middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ const initializedSessions = new Set<string>();
* 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) {
Expand All @@ -70,29 +70,29 @@ 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<string, unknown>)[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<EmDashPreviewDB>;
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;
}

/**
* Get the full DO stub for direct RPC calls (e.g. setTtlAlarm).
*/
function getFullStub(binding: string, token: string): DurableObjectStub<EmDashPreviewDB> {
// 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<string, unknown>)[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<EmDashPreviewDB>;
const doId = namespace.idFromName(token);
return namespace.get(doId);
Expand Down
2 changes: 1 addition & 1 deletion packages/cloudflare/src/media/images-runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, string | undefined>)[envVar];
if (!value) {
throw new Error(
Expand Down
Loading
Loading