Skip to content
Closed
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
2 changes: 1 addition & 1 deletion biome.jsonc
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"$schema": "https://biomejs.dev/schemas/2.4.6/schema.json",
"$schema": "https://biomejs.dev/schemas/2.4.8/schema.json",
"vcs": {
"enabled": true,
"clientKind": "git",
Expand Down
36 changes: 18 additions & 18 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions knip.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,12 @@ const knipConfig: KnipConfig = {
"src/lib/crypto/**",
// Test files are run via bun test
"src/__tests__/**",
// WIP: Warden client (not yet integrated)
"src/lib/warden/**",
// Instrumentation loaded via --import flag at runtime
"src/instrumentation.ts",
// Events client (not yet wired into app entrypoint)
"src/lib/events/**",
// Warden barrel re-exports public API surface consumed by IDP webhooks and authorize
"src/lib/warden/index.ts",
],
ignoreDependencies: [
// Used by vortex-worker relay (vortex-api pushes via HTTP, but SDK
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
"@biomejs/biome"
],
"devDependencies": {
"@biomejs/biome": "2.4.6",
"@biomejs/biome": "2.4.8",
"@types/bun": "^1.3.11",
"@types/ms": "^2.1.0",
"drizzle-kit": "^0.31.10",
Expand Down Expand Up @@ -86,7 +86,7 @@
"jose": "^6.2.2",
"ms": "^2.1.3",
"pg": "^8.20.0",
"postgraphile": "5.0.0-rc.9",
"postgraphile": "5.0.0-rc.10",
"postgraphile-plugin-connection-filter": "3.0.0-rc.3",
"ts-pattern": "^5.9.0"
},
Expand Down
12 changes: 6 additions & 6 deletions src/__tests__/entitlements.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,32 +30,32 @@ const { assertUnderLimit } = await import("lib/entitlements/enforce");

describe("assertUnderLimit", () => {
it("should throw SafeError when count meets limit", () => {
expect(() => assertUnderLimit(3, 3, "workflows")).toThrow(SafeError);
expect(() => assertUnderLimit(5, 5, "workflows")).toThrow(SafeError);
});

it("should throw SafeError when count exceeds limit", () => {
expect(() => assertUnderLimit(3, 10, "workflows")).toThrow(SafeError);
expect(() => assertUnderLimit(5, 10, "workflows")).toThrow(SafeError);
});

it("should include count and limit in error message", () => {
try {
assertUnderLimit(3, 10, "workflows");
assertUnderLimit(5, 10, "workflows");
expect(true).toBe(false); // Should not reach here
} catch (err) {
expect((err as Error).message).toContain("10/3");
expect((err as Error).message).toContain("10/5");
expect((err as Error).message).toContain("Upgrade your plan");
}
});

it("should not throw when count is under limit", () => {
expect(() => assertUnderLimit(3, 2, "workflows")).not.toThrow();
expect(() => assertUnderLimit(5, 2, "workflows")).not.toThrow();
});

it("should not throw when limit is -1 (unlimited)", () => {
expect(() => assertUnderLimit(-1, 999, "workflows")).not.toThrow();
});

it("should not throw when count is 0", () => {
expect(() => assertUnderLimit(3, 0, "workflows")).not.toThrow();
expect(() => assertUnderLimit(5, 0, "workflows")).not.toThrow();
});
});
4 changes: 2 additions & 2 deletions src/__tests__/errorMasking.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ const customMaskError = (
describe("error masking", () => {
it("should pass through SafeError messages wrapped in GraphQLError", () => {
const safeErr = new SafeError(
"Plan limit reached: workflows (10/3). Upgrade your plan to continue.",
"Plan limit reached: workflows (10/5). Upgrade your plan to continue.",
);
const gqlErr = new GraphQLError(safeErr.message, {
originalError: safeErr,
Expand All @@ -37,7 +37,7 @@ describe("error masking", () => {

expect(result).toBe(gqlErr);
expect((result as GraphQLError).message).toBe(
"Plan limit reached: workflows (10/3). Upgrade your plan to continue.",
"Plan limit reached: workflows (10/5). Upgrade your plan to continue.",
);
});

Expand Down
40 changes: 40 additions & 0 deletions src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
} from "lib/entitlements/enforce";
import logger from "lib/logger";
import oauthRoutes from "lib/oauth/routes";
import authorize from "lib/warden/authorize";
import dlqRoutes from "routes/dlq";
import functionRoutes from "routes/functions";
import internalRoutes from "routes/internal";
Expand Down Expand Up @@ -432,6 +433,19 @@ const api = new Elysia({ prefix: "/api/v1" })
const { organizationId } = authInfo;
const { name } = params;

// Verify Warden authorization (member required for create/update)
if (authInfo.userId) {
const allowed = await authorize(
authInfo.userId,
"organization",
organizationId,
"member",
);
if (!allowed) {
return status(403, { error: "Forbidden: insufficient permissions" });
}
}

const existing = await db.query.workflowTable.findFirst({
where: and(
eq(workflowTable.name, name),
Expand Down Expand Up @@ -543,6 +557,19 @@ const api = new Elysia({ prefix: "/api/v1" })
const { organizationId } = authInfo;
const { workflowId } = params;

// Verify Warden authorization (member required for clone)
if (authInfo.userId) {
const allowed = await authorize(
authInfo.userId,
"organization",
organizationId,
"member",
);
if (!allowed) {
return status(403, { error: "Forbidden: insufficient permissions" });
}
}

// Fetch workflow and verify ownership
const workflow = await db.query.workflowTable.findFirst({
where: and(
Expand Down Expand Up @@ -616,6 +643,19 @@ const api = new Elysia({ prefix: "/api/v1" })
const { organizationId } = authInfo;
const { workflowId } = params;

// Verify Warden authorization (admin required for delete)
if (authInfo.userId) {
const allowed = await authorize(
authInfo.userId,
"organization",
organizationId,
"admin",
);
if (!allowed) {
return status(403, { error: "Forbidden: insufficient permissions" });
}
}

// Verify workflow exists and belongs to org
const workflow = await db.query.workflowTable.findFirst({
where: and(
Expand Down
2 changes: 1 addition & 1 deletion src/data/integrations/catalog.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"$schema": "./catalog.schema.json",
"generatedAt": "2026-03-18T23:45:17.517Z",
"generatedAt": "2026-03-19T06:06:12.988Z",
"total": 601,
"entries": [
{
Expand Down
2 changes: 2 additions & 0 deletions src/lib/entitlements/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ export const FEATURE_KEYS = {
MAX_INTEGRATIONS: "max_integrations",
MAX_PLUGINS: "max_plugins",
MAX_USERS: "max_users",
MAX_FUNCTIONS: "max_functions",
MAX_SUBSCRIPTIONS: "max_subscriptions",
SSO_ENABLED: "sso_enabled",
AUDIT_LOGS: "audit_logs",
CUSTOM_PLUGINS: "custom_plugins",
Expand Down
4 changes: 3 additions & 1 deletion src/lib/entitlements/enforce.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,11 @@ const APP_ID = "vortex";
* Prevents hard failures for orgs that haven't been provisioned in Aether.
*/
const DEFAULT_LIMITS: Record<string, Record<string, number>> = {
max_workflows: { free: 3 },
max_workflows: { free: 5 },
max_integrations: { free: 10 },
max_plugins: { free: 2 },
max_functions: { free: 5 },
max_subscriptions: { free: 10 },
max_runs_per_month: { free: 1000 },
max_users: { free: 1 },
audit_logs: { free: 0 },
Expand Down
Loading
Loading