diff --git a/.oxlintrc.jsonc b/.oxlintrc.jsonc
index 5a044da96..b6a2517f4 100644
--- a/.oxlintrc.jsonc
+++ b/.oxlintrc.jsonc
@@ -24,5 +24,19 @@
},
],
},
+ "overrides": [
+ {
+ "files": ["packages/plugins/workos-vault/src/**/*.{ts,tsx}"],
+ "rules": {
+ "executor/no-inline-object-type-assertion": "error",
+ "executor/no-instanceof-tagged-error": "error",
+ "executor/no-manual-tag-check": "error",
+ "executor/no-promise-client-surface": "error",
+ "executor/no-raw-error-throw": "error",
+ "executor/no-redundant-error-factory": "error",
+ "executor/no-unknown-shape-probing": "error",
+ },
+ },
+ ],
"ignorePatterns": [".astro/", "**/routeTree.gen.ts", ".references/"],
}
diff --git a/packages/plugins/workos-vault/src/sdk/client.ts b/packages/plugins/workos-vault/src/sdk/client.ts
index 6122921c4..7b2fe37ed 100644
--- a/packages/plugins/workos-vault/src/sdk/client.ts
+++ b/packages/plugins/workos-vault/src/sdk/client.ts
@@ -1,5 +1,9 @@
import type { WorkOS } from "@workos-inc/node/worker";
-import { WorkOS as WorkOSClient } from "@workos-inc/node/worker";
+import {
+ GenericServerException,
+ NotFoundException,
+ WorkOS as WorkOSClient,
+} from "@workos-inc/node/worker";
import { Data, Effect, Result } from "effect";
export interface WorkOSVaultObjectMetadata {
@@ -16,10 +20,48 @@ export interface WorkOSVaultObject {
readonly value?: string;
}
+// Minimal shape carrying an HTTP-style status code. Production WorkOS errors
+// (`GenericServerException`/`NotFoundException`) and test fakes both populate
+// a numeric `status`, so the boundary normalises against this named type
+// rather than probing arbitrary unknown shapes.
+interface ErrorWithStatus extends Error {
+ readonly status: number;
+}
+
+const isErrorWithStatus = (cause: unknown): cause is ErrorWithStatus =>
+ cause instanceof Error && typeof (cause as ErrorWithStatus).status === "number";
+
+const statusFromWorkOSCause = (cause: unknown): number | undefined => {
+ if (cause instanceof GenericServerException || cause instanceof NotFoundException) {
+ return cause.status;
+ }
+ if (isErrorWithStatus(cause)) return cause.status;
+ return undefined;
+};
+
+const messageFromWorkOSCause = (cause: unknown): string =>
+ cause instanceof Error ? cause.message : typeof cause === "string" ? cause : "";
+
export class WorkOSVaultClientError extends Data.TaggedError("WorkOSVaultClientError")<{
readonly cause: unknown;
+ readonly message: string;
readonly operation: string;
-}> {}
+ readonly status?: number;
+}> {
+ constructor(options: {
+ readonly cause: unknown;
+ readonly message?: string;
+ readonly operation: string;
+ readonly status?: number;
+ }) {
+ super({
+ cause: options.cause,
+ message: options.message ?? messageFromWorkOSCause(options.cause),
+ operation: options.operation,
+ status: options.status ?? statusFromWorkOSCause(options.cause),
+ });
+ }
+}
export class WorkOSVaultClientInstantiationError extends Data.TaggedError(
"WorkOSVaultClientInstantiationError",
@@ -27,7 +69,10 @@ export class WorkOSVaultClientInstantiationError extends Data.TaggedError(
readonly cause: unknown;
}> {}
-export interface WorkOSVaultSdk {
+// Promise-shaped facade onto the underlying WorkOS SDK. Module-private — the
+// public surface in `WorkOSVaultClient` is Effect-only. Test doubles import
+// this type to stand up an in-memory equivalent.
+export interface WorkOSVaultPromiseApi {
readonly createObject: (options: {
readonly name: string;
readonly value: string;
@@ -55,7 +100,7 @@ interface WorkOSVaultUseOptions {
export interface WorkOSVaultClient {
readonly use: (
operation: string,
- fn: (client: WorkOSVaultSdk) => Promise,
+ fn: (client: WorkOSVaultPromiseApi) => Promise,
options?: WorkOSVaultUseOptions,
) => Effect.Effect;
readonly createObject: (options: {
@@ -76,30 +121,20 @@ export interface WorkOSVaultClient {
}) => Effect.Effect;
}
-const vaultErrorStatus = (error: WorkOSVaultClientError): number | null => {
- const cause = error.cause;
- return typeof cause === "object" &&
- cause !== null &&
- "status" in cause &&
- typeof (cause as { readonly status: unknown }).status === "number"
- ? (cause as { readonly status: number }).status
- : null;
-};
-
const isExpectedVaultError = (
error: WorkOSVaultClientError,
options: WorkOSVaultUseOptions | undefined,
): boolean => {
- const status = vaultErrorStatus(error);
- return status !== null && (options?.expectedErrorStatuses?.includes(status) ?? false);
+ if (error.status === undefined) return false;
+ return options?.expectedErrorStatuses?.includes(error.status) ?? false;
};
export const makeWorkOSVaultClient = (workos: Pick): WorkOSVaultClient => {
- const client: WorkOSVaultSdk = workos.vault;
+ const client: WorkOSVaultPromiseApi = workos.vault;
const use = (
operation: string,
- fn: (vault: WorkOSVaultSdk) => Promise,
+ fn: (vault: WorkOSVaultPromiseApi) => Promise,
options?: WorkOSVaultUseOptions,
): Effect.Effect => {
const attempt = Effect.tryPromise({
@@ -116,7 +151,7 @@ export const makeWorkOSVaultClient = (workos: Pick): WorkOSVaul
return outcome;
}
- const status = vaultErrorStatus(outcome.failure);
+ const status = outcome.failure.status;
if (isExpectedVaultError(outcome.failure, options)) {
yield* Effect.annotateCurrentSpan({
"workos_vault.outcome": options?.expectedErrorOutcome ?? "expected_error",
diff --git a/packages/plugins/workos-vault/src/sdk/index.ts b/packages/plugins/workos-vault/src/sdk/index.ts
index 2279a2510..9cb7082ed 100644
--- a/packages/plugins/workos-vault/src/sdk/index.ts
+++ b/packages/plugins/workos-vault/src/sdk/index.ts
@@ -7,7 +7,6 @@ export {
type WorkOSVaultCredentials,
type WorkOSVaultObject,
type WorkOSVaultObjectMetadata,
- type WorkOSVaultSdk,
} from "./client";
export {
workosVaultPlugin,
diff --git a/packages/plugins/workos-vault/src/sdk/secret-store.test.ts b/packages/plugins/workos-vault/src/sdk/secret-store.test.ts
index 47de6a6fc..ef0e12e08 100644
--- a/packages/plugins/workos-vault/src/sdk/secret-store.test.ts
+++ b/packages/plugins/workos-vault/src/sdk/secret-store.test.ts
@@ -22,6 +22,14 @@ import {
} from "./client";
import { workosVaultPlugin } from "./plugin";
+interface VaultMetadataRow {
+ readonly id: string;
+ readonly scope_id: string;
+ readonly name: string;
+ readonly purpose: string | null;
+ readonly created_at: Date;
+}
+
// ---------------------------------------------------------------------------
// Fake status errors — the real provider's isStatusError check pattern-
// matches on a `status` field, so these bare Error subclasses are
@@ -465,13 +473,11 @@ describe("WorkOS Vault secret provider — multi-scope isolation", () => {
}),
);
- const rows = yield* adapter.findMany({
+ const rows = (yield* adapter.findMany({
model: "workos_vault_metadata",
where: [{ field: "id", value: "api-token" }],
- });
- const scopes = rows
- .map((r) => (r as { scope_id: string }).scope_id)
- .sort();
+ })) as readonly VaultMetadataRow[];
+ const scopes = rows.map((r) => r.scope_id).sort();
expect(scopes).toEqual([outerId, innerId].sort());
}),
);
diff --git a/packages/plugins/workos-vault/src/sdk/secret-store.ts b/packages/plugins/workos-vault/src/sdk/secret-store.ts
index a49639ced..c487ce8aa 100644
--- a/packages/plugins/workos-vault/src/sdk/secret-store.ts
+++ b/packages/plugins/workos-vault/src/sdk/secret-store.ts
@@ -1,5 +1,4 @@
import { Effect } from "effect";
-import { GenericServerException, NotFoundException } from "@workos-inc/node/worker";
import {
defineSchema,
@@ -10,8 +9,8 @@ import {
} from "@executor-js/sdk/core";
import {
- WorkOSVaultClientError,
type WorkOSVaultClient,
+ type WorkOSVaultClientError,
type WorkOSVaultObject,
} from "./client";
@@ -155,35 +154,11 @@ export const makeWorkosVaultStore = (
// Vault helpers — scope-prefixed object naming + 409-retry upsert.
// ---------------------------------------------------------------------------
-const unwrapVaultError = (error: unknown): unknown =>
- error instanceof WorkOSVaultClientError ? error.cause : error;
-
-const isStatusError = (error: unknown, status: number): boolean => {
- const cause = unwrapVaultError(error);
- return (
- ((cause instanceof GenericServerException ||
- cause instanceof NotFoundException) &&
- cause.status === status) ||
- (typeof cause === "object" &&
- cause !== null &&
- "status" in cause &&
- typeof (cause as { status: unknown }).status === "number" &&
- (cause as { status: number }).status === status)
- );
-};
+const isStatusError = (error: WorkOSVaultClientError, status: number): boolean =>
+ error.status === status;
-const isKekNotReadyError = (error: unknown): boolean => {
- const cause = unwrapVaultError(error);
- const message =
- cause instanceof Error
- ? cause.message
- : typeof cause === "string"
- ? cause
- : typeof cause === "object" && cause !== null && "message" in cause
- ? String((cause as { message: unknown }).message)
- : "";
- return message.includes("KEK was created but is not yet ready");
-};
+const isKekNotReadyError = (error: WorkOSVaultClientError): boolean =>
+ error.message.includes("KEK was created but is not yet ready");
// Default context builder. Each semantic piece of a scope id lives in
// its own vault-context key so WorkOS's KEK matcher sees individual
@@ -327,11 +302,8 @@ const deleteSecretValue = (
return true;
});
-const formatVaultError = (error: unknown): StorageError => {
- const cause = unwrapVaultError(error);
- const message = cause instanceof Error ? cause.message : String(cause);
- return new StorageError({ message, cause });
-};
+const formatVaultError = (error: WorkOSVaultClientError): StorageError =>
+ new StorageError({ message: error.message, cause: error.cause });
// ---------------------------------------------------------------------------
// makeWorkOSVaultSecretProvider — builds a SecretProvider backed by
diff --git a/scripts/oxlint-plugin-executor.js b/scripts/oxlint-plugin-executor.js
index 659ed2dfa..94ea9e2ae 100644
--- a/scripts/oxlint-plugin-executor.js
+++ b/scripts/oxlint-plugin-executor.js
@@ -2,7 +2,14 @@ import noConditionalTests from "./oxlint-plugin-executor/rules/no-conditional-te
import noCrossPackageRelativeImports from "./oxlint-plugin-executor/rules/no-cross-package-relative-imports.js";
import noDoubleCast from "./oxlint-plugin-executor/rules/no-double-cast.js";
import noEffectInternalTags from "./oxlint-plugin-executor/rules/no-effect-internal-tags.js";
+import noInlineObjectTypeAssertion from "./oxlint-plugin-executor/rules/no-inline-object-type-assertion.js";
+import noInstanceofTaggedError from "./oxlint-plugin-executor/rules/no-instanceof-tagged-error.js";
+import noManualTagCheck from "./oxlint-plugin-executor/rules/no-manual-tag-check.js";
+import noPromiseClientSurface from "./oxlint-plugin-executor/rules/no-promise-client-surface.js";
+import noRawErrorThrow from "./oxlint-plugin-executor/rules/no-raw-error-throw.js";
+import noRedundantErrorFactory from "./oxlint-plugin-executor/rules/no-redundant-error-factory.js";
import noTsNocheck from "./oxlint-plugin-executor/rules/no-ts-nocheck.js";
+import noUnknownShapeProbing from "./oxlint-plugin-executor/rules/no-unknown-shape-probing.js";
import noVitestImport from "./oxlint-plugin-executor/rules/no-vitest-import.js";
import requireReactivityKeys from "./oxlint-plugin-executor/rules/require-reactivity-keys.js";
@@ -18,5 +25,12 @@ export default {
"require-reactivity-keys": requireReactivityKeys,
"no-effect-internal-tags": noEffectInternalTags,
"no-ts-nocheck": noTsNocheck,
+ "no-inline-object-type-assertion": noInlineObjectTypeAssertion,
+ "no-instanceof-tagged-error": noInstanceofTaggedError,
+ "no-manual-tag-check": noManualTagCheck,
+ "no-promise-client-surface": noPromiseClientSurface,
+ "no-raw-error-throw": noRawErrorThrow,
+ "no-redundant-error-factory": noRedundantErrorFactory,
+ "no-unknown-shape-probing": noUnknownShapeProbing,
},
};
diff --git a/scripts/oxlint-plugin-executor/rules/no-inline-object-type-assertion.js b/scripts/oxlint-plugin-executor/rules/no-inline-object-type-assertion.js
new file mode 100644
index 000000000..a741f199d
--- /dev/null
+++ b/scripts/oxlint-plugin-executor/rules/no-inline-object-type-assertion.js
@@ -0,0 +1,40 @@
+import { isIdentifier } from "../utils.js";
+
+const message =
+ "Do not assert against inline object-shaped types. Use a named type, Schema, or a proper type guard.";
+
+const isUnknownKeyword = (node) => node?.type === "TSUnknownKeyword";
+
+const isStringKey = (node) =>
+ node?.type === "TSStringKeyword" ||
+ (node?.type === "TSLiteralType" && typeof node.literal?.value === "string");
+
+const isRecordUnknown = (node) =>
+ node?.type === "TSTypeReference" &&
+ isIdentifier(node.typeName, "Record") &&
+ node.typeArguments?.params?.length === 2 &&
+ isStringKey(node.typeArguments.params[0]) &&
+ isUnknownKeyword(node.typeArguments.params[1]);
+
+const isBannedType = (node) => node?.type === "TSTypeLiteral" || isRecordUnknown(node);
+
+export default {
+ meta: {
+ type: "problem",
+ docs: {
+ description: message,
+ },
+ },
+ create(context) {
+ const check = (node) => {
+ if (isBannedType(node.typeAnnotation)) {
+ context.report({ node, message });
+ }
+ };
+
+ return {
+ TSAsExpression: check,
+ TSTypeAssertion: check,
+ };
+ },
+};
diff --git a/scripts/oxlint-plugin-executor/rules/no-instanceof-tagged-error.js b/scripts/oxlint-plugin-executor/rules/no-instanceof-tagged-error.js
new file mode 100644
index 000000000..d8a566b14
--- /dev/null
+++ b/scripts/oxlint-plugin-executor/rules/no-instanceof-tagged-error.js
@@ -0,0 +1,27 @@
+import { isIdentifier, nodeName } from "../utils.js";
+
+const message =
+ "Do not use instanceof for tagged errors. Use Effect.catchTag, Effect.catchTags, or a _tag-based guard.";
+
+const looksLikeTaggedErrorName = (name) =>
+ typeof name === "string" && name !== "Error" && name.endsWith("Error");
+
+export default {
+ meta: {
+ type: "problem",
+ docs: {
+ description: message,
+ },
+ },
+ create(context) {
+ return {
+ BinaryExpression(node) {
+ if (node.operator !== "instanceof") return;
+ const rightName = nodeName(node.right);
+ if (isIdentifier(node.right) && looksLikeTaggedErrorName(rightName)) {
+ context.report({ node, message });
+ }
+ },
+ };
+ },
+};
diff --git a/scripts/oxlint-plugin-executor/rules/no-manual-tag-check.js b/scripts/oxlint-plugin-executor/rules/no-manual-tag-check.js
new file mode 100644
index 000000000..f6ebe68da
--- /dev/null
+++ b/scripts/oxlint-plugin-executor/rules/no-manual-tag-check.js
@@ -0,0 +1,25 @@
+import { isIdentifier, isStringLiteral } from "../utils.js";
+
+const message =
+ "Do not inspect _tag manually. Use Effect.catchTag, Effect.catchTags, Predicate.isTagged, or another Effect tagged-error API.";
+
+const isTagProperty = (node) =>
+ isIdentifier(node, "_tag") || (isStringLiteral(node) && node.value === "_tag");
+
+export default {
+ meta: {
+ type: "problem",
+ docs: {
+ description: message,
+ },
+ },
+ create(context) {
+ return {
+ MemberExpression(node) {
+ if (isTagProperty(node.property)) {
+ context.report({ node, message });
+ }
+ },
+ };
+ },
+};
diff --git a/scripts/oxlint-plugin-executor/rules/no-promise-client-surface.js b/scripts/oxlint-plugin-executor/rules/no-promise-client-surface.js
new file mode 100644
index 000000000..3ae5916f6
--- /dev/null
+++ b/scripts/oxlint-plugin-executor/rules/no-promise-client-surface.js
@@ -0,0 +1,42 @@
+import { containsPromiseType, nodeName } from "../utils.js";
+
+const message =
+ "Do not expose Promise-shaped client surfaces. Wrap third-party SDK promises at the adapter boundary and expose Effect methods.";
+
+const isExported = (node) => node?.parent?.type === "ExportNamedDeclaration";
+
+const isClientInterface = (node) => {
+ const name = nodeName(node.id);
+ return (
+ typeof name === "string" &&
+ (name.endsWith("Client") || (isExported(node) && name.endsWith("Sdk")))
+ );
+};
+
+const methodReturnsPromise = (node) => containsPromiseType(node.returnType);
+
+const propertyReturnsPromise = (node) => containsPromiseType(node.typeAnnotation);
+
+export default {
+ meta: {
+ type: "problem",
+ docs: {
+ description: message,
+ },
+ },
+ create(context) {
+ return {
+ TSInterfaceDeclaration(node) {
+ if (!isClientInterface(node)) return;
+ for (const member of node.body?.body ?? []) {
+ if (
+ (member.type === "TSMethodSignature" && methodReturnsPromise(member)) ||
+ (member.type === "TSPropertySignature" && propertyReturnsPromise(member))
+ ) {
+ context.report({ node: member, message });
+ }
+ }
+ },
+ };
+ },
+};
diff --git a/scripts/oxlint-plugin-executor/rules/no-raw-error-throw.js b/scripts/oxlint-plugin-executor/rules/no-raw-error-throw.js
new file mode 100644
index 000000000..92d9bc8d4
--- /dev/null
+++ b/scripts/oxlint-plugin-executor/rules/no-raw-error-throw.js
@@ -0,0 +1,24 @@
+import { isIdentifier } from "../utils.js";
+
+const message =
+ "Do not throw raw Error objects in Effect code. Return Effect.fail with a tagged error or assert directly in tests.";
+
+const isNewError = (node) => node?.type === "NewExpression" && isIdentifier(node.callee, "Error");
+
+export default {
+ meta: {
+ type: "problem",
+ docs: {
+ description: message,
+ },
+ },
+ create(context) {
+ return {
+ ThrowStatement(node) {
+ if (isNewError(node.argument)) {
+ context.report({ node, message });
+ }
+ },
+ };
+ },
+};
diff --git a/scripts/oxlint-plugin-executor/rules/no-redundant-error-factory.js b/scripts/oxlint-plugin-executor/rules/no-redundant-error-factory.js
new file mode 100644
index 000000000..1b1426712
--- /dev/null
+++ b/scripts/oxlint-plugin-executor/rules/no-redundant-error-factory.js
@@ -0,0 +1,52 @@
+import { isIdentifier } from "../utils.js";
+
+const message =
+ "Do not add redundant make*Error wrappers that only construct a tagged error. Construct the tagged error directly.";
+
+const isErrorFactoryName = (name) => /^make[A-Z].*Error$/.test(name);
+
+const isNewErrorExpression = (node) =>
+ node?.type === "NewExpression" && isIdentifier(node.callee) && node.callee.name.endsWith("Error");
+
+const returnsOnlyNewError = (node) => {
+ if (isNewErrorExpression(node)) return true;
+ if (node?.type !== "BlockStatement") return false;
+ const statements = node.body ?? [];
+ return (
+ statements.length === 1 &&
+ statements[0]?.type === "ReturnStatement" &&
+ isNewErrorExpression(statements[0].argument)
+ );
+};
+
+const reportIfRedundantFactory = (context, name, body, node) => {
+ if (isErrorFactoryName(name) && returnsOnlyNewError(body)) {
+ context.report({ node, message });
+ }
+};
+
+export default {
+ meta: {
+ type: "problem",
+ docs: {
+ description: message,
+ },
+ },
+ create(context) {
+ return {
+ FunctionDeclaration(node) {
+ reportIfRedundantFactory(context, node.id?.name, node.body, node);
+ },
+ VariableDeclarator(node) {
+ if (!isIdentifier(node.id)) return;
+ if (
+ node.init?.type !== "ArrowFunctionExpression" &&
+ node.init?.type !== "FunctionExpression"
+ ) {
+ return;
+ }
+ reportIfRedundantFactory(context, node.id.name, node.init.body, node);
+ },
+ };
+ },
+};
diff --git a/scripts/oxlint-plugin-executor/rules/no-unknown-shape-probing.js b/scripts/oxlint-plugin-executor/rules/no-unknown-shape-probing.js
new file mode 100644
index 000000000..f409aef2a
--- /dev/null
+++ b/scripts/oxlint-plugin-executor/rules/no-unknown-shape-probing.js
@@ -0,0 +1,32 @@
+import { isIdentifier, isStringLiteral } from "../utils.js";
+
+const message =
+ "Do not probe unknown object shapes in domain code. Normalize at a boundary with Schema, a typed adapter, or a named guard.";
+
+const isReflectGet = (node) =>
+ node?.type === "MemberExpression" &&
+ isIdentifier(node.object, "Reflect") &&
+ isIdentifier(node.property, "get");
+
+export default {
+ meta: {
+ type: "problem",
+ docs: {
+ description: message,
+ },
+ },
+ create(context) {
+ return {
+ CallExpression(node) {
+ if (isReflectGet(node.callee)) {
+ context.report({ node, message });
+ }
+ },
+ BinaryExpression(node) {
+ if (node.operator === "in" && isStringLiteral(node.left)) {
+ context.report({ node, message });
+ }
+ },
+ };
+ },
+};
diff --git a/scripts/oxlint-plugin-executor/utils.js b/scripts/oxlint-plugin-executor/utils.js
index c63740703..77cab8b7c 100644
--- a/scripts/oxlint-plugin-executor/utils.js
+++ b/scripts/oxlint-plugin-executor/utils.js
@@ -71,3 +71,60 @@ export function getStringValue(node) {
if (expression?.type === "StringLiteral") return expression.value;
return undefined;
}
+
+export function isIdentifier(node, name) {
+ return node?.type === "Identifier" && (name === undefined || node.name === name);
+}
+
+export function isStringLiteral(node) {
+ return (
+ (node?.type === "Literal" && typeof node.value === "string") ||
+ node?.type === "StringLiteral"
+ );
+}
+
+export function typeName(node) {
+ if (node?.type === "Identifier") return node.name;
+ if (node?.type === "TSQualifiedName") {
+ const left = typeName(node.left);
+ const right = typeName(node.right);
+ return left && right ? `${left}.${right}` : undefined;
+ }
+ return undefined;
+}
+
+export function typeReferenceName(node) {
+ return node?.type === "TSTypeReference" ? typeName(node.typeName) : undefined;
+}
+
+export function isPromiseType(node) {
+ return typeReferenceName(node) === "Promise";
+}
+
+export function containsPromiseType(node) {
+ if (!node || typeof node !== "object") return false;
+ if (isPromiseType(node)) return true;
+
+ switch (node.type) {
+ case "TSTypeAnnotation":
+ return containsPromiseType(node.typeAnnotation);
+ case "TSFunctionType":
+ return containsPromiseType(node.returnType);
+ case "TSParenthesizedType":
+ return containsPromiseType(node.typeAnnotation);
+ case "TSUnionType":
+ case "TSIntersectionType":
+ return (node.types ?? []).some(containsPromiseType);
+ case "TSConditionalType":
+ return containsPromiseType(node.trueType) || containsPromiseType(node.falseType);
+ default:
+ return false;
+ }
+}
+
+export function nodeName(node) {
+ if (isIdentifier(node)) return node.name;
+ if (node?.type === "PrivateIdentifier") return node.name;
+ if (isStringLiteral(node)) return node.value;
+ return undefined;
+}