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
14 changes: 14 additions & 0 deletions .oxlintrc.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -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/"],
}
73 changes: 54 additions & 19 deletions packages/plugins/workos-vault/src/sdk/client.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -16,18 +20,59 @@ 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",
)<{
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;
Expand Down Expand Up @@ -55,7 +100,7 @@ interface WorkOSVaultUseOptions {
export interface WorkOSVaultClient {
readonly use: <A>(
operation: string,
fn: (client: WorkOSVaultSdk) => Promise<A>,
fn: (client: WorkOSVaultPromiseApi) => Promise<A>,
options?: WorkOSVaultUseOptions,
) => Effect.Effect<A, WorkOSVaultClientError, never>;
readonly createObject: (options: {
Expand All @@ -76,30 +121,20 @@ export interface WorkOSVaultClient {
}) => Effect.Effect<void, WorkOSVaultClientError, never>;
}

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<WorkOS, "vault">): WorkOSVaultClient => {
const client: WorkOSVaultSdk = workos.vault;
const client: WorkOSVaultPromiseApi = workos.vault;

const use = <A>(
operation: string,
fn: (vault: WorkOSVaultSdk) => Promise<A>,
fn: (vault: WorkOSVaultPromiseApi) => Promise<A>,
options?: WorkOSVaultUseOptions,
): Effect.Effect<A, WorkOSVaultClientError, never> => {
const attempt = Effect.tryPromise({
Expand All @@ -116,7 +151,7 @@ export const makeWorkOSVaultClient = (workos: Pick<WorkOS, "vault">): 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",
Expand Down
1 change: 0 additions & 1 deletion packages/plugins/workos-vault/src/sdk/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ export {
type WorkOSVaultCredentials,
type WorkOSVaultObject,
type WorkOSVaultObjectMetadata,
type WorkOSVaultSdk,
} from "./client";
export {
workosVaultPlugin,
Expand Down
16 changes: 11 additions & 5 deletions packages/plugins/workos-vault/src/sdk/secret-store.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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());
}),
);
Expand Down
42 changes: 7 additions & 35 deletions packages/plugins/workos-vault/src/sdk/secret-store.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { Effect } from "effect";
import { GenericServerException, NotFoundException } from "@workos-inc/node/worker";

import {
defineSchema,
Expand All @@ -10,8 +9,8 @@ import {
} from "@executor-js/sdk/core";

import {
WorkOSVaultClientError,
type WorkOSVaultClient,
type WorkOSVaultClientError,
type WorkOSVaultObject,
} from "./client";

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
14 changes: 14 additions & 0 deletions scripts/oxlint-plugin-executor.js
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand All @@ -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,
},
};
Original file line number Diff line number Diff line change
@@ -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,
};
},
};
27 changes: 27 additions & 0 deletions scripts/oxlint-plugin-executor/rules/no-instanceof-tagged-error.js
Original file line number Diff line number Diff line change
@@ -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 });
}
},
};
},
};
25 changes: 25 additions & 0 deletions scripts/oxlint-plugin-executor/rules/no-manual-tag-check.js
Original file line number Diff line number Diff line change
@@ -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 });
}
},
};
},
};
Loading
Loading