diff --git a/blocks/action.ts b/blocks/action.ts index 88dbc5ed8..40baf2ae4 100644 --- a/blocks/action.ts +++ b/blocks/action.ts @@ -1,14 +1,19 @@ // deno-lint-ignore-file no-explicit-any -import { applyProps, type FnProps } from "../blocks/utils.tsx"; +import { + applyProps, + type FnProps, + type GateKeeperAccess, +} from "../blocks/utils.tsx"; import JsonViewer from "../components/JsonViewer.tsx"; import type { Block, BlockModule, InstanceOf } from "../engine/block.ts"; +import { gateKeeper } from "./utils.tsx"; export type Action = InstanceOf; -export type ActionModule< +export interface ActionModule< TProps = any, TResp = any, -> = BlockModule>; +> extends BlockModule>, GateKeeperAccess {} const actionBlock: Block = { type: "actions", @@ -16,7 +21,9 @@ const actionBlock: Block = { TProps = any, >( mod: ActionModule, + key: string, ) => [ + gateKeeper(mod.defaultVisibility, key), applyProps(mod), ], defaultPreview: (result) => { diff --git a/blocks/loader.ts b/blocks/loader.ts index 0edb2491d..45f36631e 100644 --- a/blocks/loader.ts +++ b/blocks/loader.ts @@ -19,6 +19,8 @@ import { applyProps, type FnContext, type FnProps, + gateKeeper, + type GateKeeperAccess, type RequestState, type SingleFlightKeyFunc, } from "./utils.tsx"; @@ -28,7 +30,7 @@ export type Loader = InstanceOf; export interface LoaderModule< TProps = any, TState = any, -> extends BlockModule> { +> extends BlockModule>, GateKeeperAccess { /** * Specifies caching behavior for the loader and its dependencies. * @@ -332,10 +334,13 @@ const wrapLoader = ( const loaderBlock: Block = { type: "loaders", introspect: { includeReturn: true }, - adapt: (mod: LoaderModule) => [ + adapt: (mod: LoaderModule, key: string) => [ + gateKeeper(mod.defaultVisibility, key), wrapCaughtErrors, (props: TProps, ctx: HttpContext<{ global: any } & RequestState>) => - applyProps(wrapLoader(mod, ctx.resolveChain, ctx.context.state.release))( + applyProps( + wrapLoader(mod, ctx.resolveChain, ctx.context.state.release), + )( props, ctx, ), diff --git a/blocks/utils.tsx b/blocks/utils.tsx index 89e8c2ade..8a9e81c89 100644 --- a/blocks/utils.tsx +++ b/blocks/utils.tsx @@ -27,6 +27,11 @@ import type { InvocationProxy } from "../utils/invoke.types.ts"; import { type Device, deviceOf, isBot as isUABot } from "../utils/userAgent.ts"; import type { HttpContext } from "./handler.ts"; import type { Vary } from "../utils/vary.ts"; +import { Context } from "../mod.ts"; + +export interface GateKeeperAccess { + defaultVisibility?: "private" | "public"; +} export type SingleFlightKeyFunc = ( args: TConfig, @@ -138,6 +143,7 @@ export const fnContextFromHttpContext = ( }, }; }; + /** * Applies the given props to the target block function. * @@ -248,3 +254,23 @@ export const buildImportMap = (manifest: AppManifest): ImportMap => { ); return buildImportMapWith(manifest, builder); }; + +export const gateKeeper = + (defaultVisibility: GateKeeperAccess["defaultVisibility"], key: string) => + < + TContext extends ResolverMiddlewareContext = ResolverMiddlewareContext< + any + >, + >(_props: unknown, ctx: TContext) => { + const currentContext = Context.active(); + + const visibility = currentContext.visibilityOverrides?.[key] ?? + defaultVisibility ?? "public"; + + if (visibility === "private" && !isInvokeCtx(ctx)) { + return new Response(null, { + status: 403, + }); + } + return ctx.next!(); + }; diff --git a/deco.ts b/deco.ts index 3bb1c89a1..6612b5242 100644 --- a/deco.ts +++ b/deco.ts @@ -6,6 +6,8 @@ import type { ReleaseResolver } from "./engine/core/mod.ts"; import type { DecofileProvider } from "./engine/decofile/provider.ts"; import type { AppManifest } from "./types.ts"; import { randId } from "./utils/rand.ts"; +import { BlockKeys } from "./mod.ts"; +import { GateKeeperAccess } from "./blocks/utils.tsx"; export interface DecoRuntimeState< TAppManifest extends AppManifest = AppManifest, @@ -55,6 +57,12 @@ export interface DecoContext { runtime?: Promise>; instance: InstanceInfo; request?: RequestContext; + + visibilityOverrides?: Record< + BlockKeys extends undefined ? string + : BlockKeys, + GateKeeperAccess["defaultVisibility"] + >; } export interface RequestContextBinder { @@ -107,7 +115,7 @@ export const Context = { // Function to retrieve the active context active: (): DecoContext => { // Retrieve the context associated with the async ID - return asyncLocalStorage.getStore() ?? defaultContext; + return asyncLocalStorage.getStore() as DecoContext ?? defaultContext; }, bind: ( ctx: DecoContext, diff --git a/engine/block.ts b/engine/block.ts index 922e170d8..7c6f02a48 100644 --- a/engine/block.ts +++ b/engine/block.ts @@ -117,7 +117,7 @@ export type InstanceOf< export type BlockTypes = keyof Omit< TManifest, - "config" | "baseUrl" + "config" | "baseUrl" | "name" >; export type BlockKeys = { diff --git a/engine/manifest/manifest.ts b/engine/manifest/manifest.ts index 235f85cd5..93c722c65 100644 --- a/engine/manifest/manifest.ts +++ b/engine/manifest/manifest.ts @@ -409,6 +409,7 @@ export const newContext = < instanceId: string | undefined = undefined, site: string | undefined = undefined, namespace: string = "site", + visibilityOverrides?: DecoContext["visibilityOverrides"], ): Promise> => { const currentContext = Context.active(); const ctx: DecoContext = { @@ -419,6 +420,7 @@ export const newContext = < id: instanceId ?? randId(), startedAt: new Date(), }, + visibilityOverrides, }; return fulfillContext(ctx, m, currentImportMap, release); diff --git a/runtime/fresh/plugin.tsx b/runtime/fresh/plugin.tsx index 646bf6269..bd3f80ec4 100644 --- a/runtime/fresh/plugin.tsx +++ b/runtime/fresh/plugin.tsx @@ -1,6 +1,6 @@ // TODO make fresh plugin use @deco/deco from JSR. so that we can use the same code for both -import type { AppManifest, SiteInfo } from "@deco/deco"; +import type { AppManifest, DecoContext, SiteInfo } from "@deco/deco"; import { Deco, type PageData, type PageParams } from "@deco/deco"; import { framework as htmxFramework } from "@deco/deco/htmx"; import type { ComponentType } from "preact"; @@ -51,6 +51,7 @@ export interface InitOptions { site?: SiteInfo; deco?: Deco; middlewares?: PluginMiddleware[]; + visibilityOverrides?: DecoContext["visibilityOverrides"]; } export type Options = @@ -89,6 +90,7 @@ export default function decoPlugin( site: opt?.site?.name, namespace: opt?.site?.namespace, bindings: { framework: opt?.htmx ? htmxFramework : framework }, + visibilityOverrides: opt.visibilityOverrides, }); const catchAll: PluginRoute = { diff --git a/runtime/mod.ts b/runtime/mod.ts index 94069f765..7e7e6dff8 100644 --- a/runtime/mod.ts +++ b/runtime/mod.ts @@ -57,6 +57,8 @@ export interface DecoOptions { manifest?: TAppManifest; decofile?: DecofileProvider; bindings?: Bindings; + + visibilityOverrides?: DecoContext["visibilityOverrides"]; } const NOOP_CALL = () => {}; @@ -86,6 +88,7 @@ export class Deco { crypto.randomUUID(), site, opts?.namespace, + opts?.visibilityOverrides, ) ); Context.setDefault(decoContext);