Files
template-app/convex/effectScope.ts
T

72 lines
2.9 KiB
TypeScript

// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// The tenant wall of the effect actions (lib/effects.ts). The kernel in an action resolves the
// tenant scope and checks the permission here, inside a query, with the same tenancy hooks over
// ctx.db that every other procedure uses. Both are internal: only this deployment's own actions
// call them, and they derive the actor and the organization pin from the caller's verified
// identity, never from their arguments.
import { type Actor, type AnyScope, notFound, toProblem, unauthenticated } from "@conusai-kit/core"
import { ConvexError, type Value, v } from "convex/values"
import { internalQuery } from "./_generated/server"
import { callContextOf, tenancyOver } from "./lib/runtime"
const level = v.union(v.literal("org"), v.literal("workspace"), v.literal("project"))
async function verifiedActor(
auth: Parameters<typeof callContextOf>[0],
): Promise<{ actor: Actor; pinnedOrgId?: string }> {
const { actor, pinnedOrgId } = await callContextOf(auth)
if (!actor) throw unauthenticated()
return { actor, ...(pinnedOrgId === undefined ? {} : { pinnedOrgId }) }
}
const problem = (error: unknown): never => {
throw new ConvexError(toProblem(error) as unknown as Value)
}
/** The scope ids of a tenant-level call, or the kernel's problem (a stranger's notFound). */
export const resolve = internalQuery({
args: { level, input: v.any() },
handler: async (ctx, args) => {
try {
const { actor, pinnedOrgId } = await verifiedActor(ctx.auth)
const scope = (await tenancyOver(ctx.db).resolveScope(args.level, args.input, actor, pinnedOrgId)) as AnyScope & {
orgId: string
workspaceId?: string
projectId?: string
}
if (pinnedOrgId !== undefined && scope.orgId !== pinnedOrgId) throw notFound("no scope in this organization")
return {
orgId: scope.orgId,
...(scope.workspaceId === undefined ? {} : { workspaceId: scope.workspaceId }),
...(scope.projectId === undefined ? {} : { projectId: scope.projectId }),
}
} catch (error) {
return problem(error)
}
},
})
/** Whether the verified actor holds `permission` in the scope, for this procedure. */
export const authorize = internalQuery({
args: {
level,
scope: v.object({ orgId: v.string(), workspaceId: v.optional(v.string()), projectId: v.optional(v.string()) }),
permission: v.string(),
procedure: v.object({
name: v.string(),
kind: v.union(v.literal("read"), v.literal("write"), v.literal("effect")),
}),
},
handler: async (ctx, args) => {
try {
const { actor, pinnedOrgId } = await verifiedActor(ctx.auth)
if (pinnedOrgId !== undefined && args.scope.orgId !== pinnedOrgId) return false
const scope = { level: args.level, actor, ...args.scope } as AnyScope
return await tenancyOver(ctx.db).authorize(scope, args.permission, args.procedure)
} catch (error) {
return problem(error)
}
},
})