From 9f3c4ff5b598e44baf43f8235fb6ea7027981b7b Mon Sep 17 00:00:00 2001 From: Andrew Jiang Date: Thu, 10 Oct 2024 01:08:19 -0400 Subject: [PATCH] feat: audience filters --- fern/apis/fdr/definition/commons.yml | 2 + .../navigation/latest/__package__.yml | 17 ++++- .../api/resources/commons/types/AudienceId.ts | 13 ++++ .../api/resources/commons/types/index.ts | 1 + .../latest/types/WithNodeMetadata.ts | 11 +++ .../src/navigation/migrators/v1ToV2.ts | 16 ++++ .../__test__/pruneNavigationTree.test.ts | 23 ++++++ .../navigation/utils/pruneNavigationTree.ts | 44 +++++------ .../fdr-sdk/src/utils/traversers/prunetree.ts | 4 +- .../ui/docs-bundle/src/server/DocsLoader.ts | 9 ++- .../withBasicTokenViewAllowed.test.ts | 47 +++++++++++- .../src/server/withBasicTokenAnonymous.ts | 75 ++++++++++++++++--- packages/ui/fern-docs-auth/src/types.ts | 6 ++ .../resources/commons/types/AudienceId.d.ts | 8 ++ .../api/resources/commons/types/AudienceId.js | 6 ++ .../api/resources/commons/types/index.d.ts | 1 + .../api/resources/commons/types/index.js | 1 + .../latest/types/WithNodeMetadata.d.ts | 11 +++ 18 files changed, 255 insertions(+), 40 deletions(-) create mode 100644 packages/fdr-sdk/src/client/generated/api/resources/commons/types/AudienceId.ts create mode 100644 servers/fdr/src/api/generated/api/resources/commons/types/AudienceId.d.ts create mode 100644 servers/fdr/src/api/generated/api/resources/commons/types/AudienceId.js diff --git a/fern/apis/fdr/definition/commons.yml b/fern/apis/fdr/definition/commons.yml index 6bb8431c46..8be958bad3 100644 --- a/fern/apis/fdr/definition/commons.yml +++ b/fern/apis/fdr/definition/commons.yml @@ -43,6 +43,8 @@ types: PropertyKey: string + AudienceId: string + EndpointIdentifier: properties: path: EndpointPathLiteral diff --git a/fern/apis/fdr/definition/navigation/latest/__package__.yml b/fern/apis/fdr/definition/navigation/latest/__package__.yml index 23bec19799..1b39192128 100644 --- a/fern/apis/fdr/definition/navigation/latest/__package__.yml +++ b/fern/apis/fdr/definition/navigation/latest/__package__.yml @@ -334,8 +334,21 @@ types: type: optional docs: The slug that should be used in the canonical URL rel. If not provided, the `slug` will be used. icon: optional - hidden: optional - authed: optional + hidden: + type: optional + docs: If true, this node will not be displayed in the sidebar, and noindex will be considered true. + authed: + type: optional + docs: | + If true, this node is only visible to authenticated users. + If false, this node is only visible to all users (including anonymous). + audience: + type: optional> + availability: in-development + docs: | + The audience(s) that this node is intended for. If not provided, the node is intended for all audiences. + If provided, the node is only intended for the specified audience(s). OR logic is used for multiple audiences on a single node. + AND logic is used when evaluating audiences up the tree. WithPage: properties: diff --git a/packages/fdr-sdk/src/client/generated/api/resources/commons/types/AudienceId.ts b/packages/fdr-sdk/src/client/generated/api/resources/commons/types/AudienceId.ts new file mode 100644 index 0000000000..a48acc77b7 --- /dev/null +++ b/packages/fdr-sdk/src/client/generated/api/resources/commons/types/AudienceId.ts @@ -0,0 +1,13 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +import * as FernRegistry from "../../../index"; + +export type AudienceId = string & { + AudienceId: void; +}; + +export function AudienceId(value: string): FernRegistry.AudienceId { + return value as unknown as FernRegistry.AudienceId; +} diff --git a/packages/fdr-sdk/src/client/generated/api/resources/commons/types/index.ts b/packages/fdr-sdk/src/client/generated/api/resources/commons/types/index.ts index 11cb260750..401177ffa5 100644 --- a/packages/fdr-sdk/src/client/generated/api/resources/commons/types/index.ts +++ b/packages/fdr-sdk/src/client/generated/api/resources/commons/types/index.ts @@ -14,6 +14,7 @@ export * from "./FileId"; export * from "./Url"; export * from "./JqString"; export * from "./PropertyKey"; +export * from "./AudienceId"; export * from "./EndpointIdentifier"; export * from "./EndpointPathLiteral"; export * from "./HttpMethod"; diff --git a/packages/fdr-sdk/src/client/generated/api/resources/navigation/resources/latest/types/WithNodeMetadata.ts b/packages/fdr-sdk/src/client/generated/api/resources/navigation/resources/latest/types/WithNodeMetadata.ts index de4ca78219..8654b97802 100644 --- a/packages/fdr-sdk/src/client/generated/api/resources/navigation/resources/latest/types/WithNodeMetadata.ts +++ b/packages/fdr-sdk/src/client/generated/api/resources/navigation/resources/latest/types/WithNodeMetadata.ts @@ -22,6 +22,17 @@ export interface WithNodeMetadata extends FernRegistry.navigation.latest.WithNod /** The slug that should be used in the canonical URL rel. If not provided, the `slug` will be used. */ canonicalSlug: FernRegistry.navigation.latest.Slug | undefined; icon: string | undefined; + /** If true, this node will not be displayed in the sidebar, and noindex will be considered true. */ hidden: boolean | undefined; + /** + * If true, this node is only visible to authenticated users. + * If false, this node is only visible to all users (including anonymous). + */ authed: boolean | undefined; + /** + * The audience(s) that this node is intended for. If not provided, the node is intended for all audiences. + * If provided, the node is only intended for the specified audience(s). OR logic is used for multiple audiences on a single node. + * AND logic is used when evaluating audiences up the tree. + */ + audience: FernRegistry.AudienceId[] | undefined; } diff --git a/packages/fdr-sdk/src/navigation/migrators/v1ToV2.ts b/packages/fdr-sdk/src/navigation/migrators/v1ToV2.ts index 2b40fa7dd5..643e0ead2d 100644 --- a/packages/fdr-sdk/src/navigation/migrators/v1ToV2.ts +++ b/packages/fdr-sdk/src/navigation/migrators/v1ToV2.ts @@ -31,6 +31,7 @@ export class FernNavigationV1ToLatest { icon: node.icon, hidden: node.hidden, authed: undefined, + audience: undefined, }; return latest; @@ -100,6 +101,7 @@ export class FernNavigationV1ToLatest { authed: undefined, id: FernNavigation.NodeId(node.id), pointsTo: node.pointsTo ? FernNavigation.Slug(node.pointsTo) : undefined, + audience: undefined, }; return latest; }; @@ -124,6 +126,7 @@ export class FernNavigationV1ToLatest { id: FernNavigation.NodeId(node.id), pageId: FernNavigation.PageId(node.pageId), noindex: node.noindex, + audience: undefined, }; return latest; }; @@ -161,6 +164,7 @@ export class FernNavigationV1ToLatest { authed: undefined, id: FernNavigation.NodeId(node.id), pointsTo: node.pointsTo ? FernNavigation.Slug(node.pointsTo) : undefined, + audience: undefined, }; return latest; }; @@ -261,6 +265,7 @@ export class FernNavigationV1ToLatest { versioned: (value) => this.versioned(value, [...parents, node]), }), subtitle: node.subtitle, + audience: undefined, }; return latest; }; @@ -315,6 +320,7 @@ export class FernNavigationV1ToLatest { authed: undefined, pageId: FernNavigation.PageId(node.pageId), noindex: node.noindex, + audience: undefined, }; return latest; }; @@ -346,6 +352,7 @@ export class FernNavigationV1ToLatest { collapsed: node.collapsed, overviewPageId, noindex: node.noindex, + audience: undefined, }; return latest; }; @@ -383,6 +390,7 @@ export class FernNavigationV1ToLatest { apiDefinitionId: node.apiDefinitionId, availability: this.#availability(node.availability), pointsTo: node.pointsTo ? FernNavigation.Slug(node.pointsTo) : undefined, + audience: undefined, }; return latest; }; @@ -412,6 +420,7 @@ export class FernNavigationV1ToLatest { authed: undefined, overviewPageId, noindex: node.noindex, + audience: undefined, }; return latest; }; @@ -431,6 +440,7 @@ export class FernNavigationV1ToLatest { hidden: node.hidden, authed: undefined, year: node.year, + audience: undefined, }; return latest; }; @@ -450,6 +460,7 @@ export class FernNavigationV1ToLatest { hidden: node.hidden, authed: undefined, month: node.month, + audience: undefined, }; return latest; }; @@ -475,6 +486,7 @@ export class FernNavigationV1ToLatest { date: node.date, pageId: FernNavigation.PageId(node.pageId), noindex: node.noindex, + audience: undefined, }; return latest; }; @@ -508,6 +520,7 @@ export class FernNavigationV1ToLatest { noindex: node.noindex, apiDefinitionId: node.apiDefinitionId, availability: this.#availability(node.availability), + audience: undefined, }; return latest; }; @@ -539,6 +552,7 @@ export class FernNavigationV1ToLatest { method: node.method, endpointId: node.endpointId, isResponseStream: node.isResponseStream, + audience: undefined, }; return latest; }; @@ -581,6 +595,7 @@ export class FernNavigationV1ToLatest { apiDefinitionId: node.apiDefinitionId, availability: this.#availability(node.availability), webSocketId: node.webSocketId, + audience: undefined, }; return latest; }; @@ -610,6 +625,7 @@ export class FernNavigationV1ToLatest { availability: this.#availability(node.availability), method: node.method, webhookId: node.webhookId, + audience: undefined, }; return latest; }; diff --git a/packages/fdr-sdk/src/navigation/utils/__test__/pruneNavigationTree.test.ts b/packages/fdr-sdk/src/navigation/utils/__test__/pruneNavigationTree.test.ts index 11fd326b6c..c0e87786f7 100644 --- a/packages/fdr-sdk/src/navigation/utils/__test__/pruneNavigationTree.test.ts +++ b/packages/fdr-sdk/src/navigation/utils/__test__/pruneNavigationTree.test.ts @@ -20,6 +20,7 @@ describe("pruneNavigationTree", () => { hidden: undefined, authed: undefined, noindex: undefined, + audience: undefined, }, ], collapsed: undefined, @@ -30,6 +31,7 @@ describe("pruneNavigationTree", () => { overviewPageId: undefined, noindex: undefined, pointsTo: undefined, + audience: undefined, }; const result = Pruner.from(root) @@ -56,6 +58,7 @@ describe("pruneNavigationTree", () => { hidden: undefined, authed: undefined, noindex: undefined, + audience: undefined, }, ], collapsed: undefined, @@ -66,6 +69,7 @@ describe("pruneNavigationTree", () => { overviewPageId: undefined, noindex: undefined, pointsTo: FernNavigation.Slug("root/page"), + audience: undefined, }); }); @@ -87,6 +91,7 @@ describe("pruneNavigationTree", () => { hidden: undefined, authed: undefined, noindex: undefined, + audience: undefined, }, ], collapsed: undefined, @@ -97,6 +102,7 @@ describe("pruneNavigationTree", () => { overviewPageId: undefined, noindex: undefined, pointsTo: FernNavigation.Slug("root/page"), + audience: undefined, }; const result = Pruner.from(root) @@ -125,6 +131,7 @@ describe("pruneNavigationTree", () => { hidden: undefined, authed: undefined, noindex: undefined, + audience: undefined, }, ], collapsed: undefined, @@ -134,6 +141,7 @@ describe("pruneNavigationTree", () => { authed: undefined, noindex: undefined, pointsTo: undefined, + audience: undefined, }; const result = Pruner.from(root) @@ -161,6 +169,7 @@ describe("pruneNavigationTree", () => { hidden: undefined, authed: undefined, noindex: undefined, + audience: undefined, }, ], collapsed: undefined, @@ -170,6 +179,7 @@ describe("pruneNavigationTree", () => { authed: undefined, noindex: undefined, pointsTo: FernNavigation.Slug("root/page"), + audience: undefined, }); }); @@ -192,6 +202,7 @@ describe("pruneNavigationTree", () => { hidden: undefined, authed: undefined, noindex: undefined, + audience: undefined, }, ], collapsed: undefined, @@ -201,6 +212,7 @@ describe("pruneNavigationTree", () => { authed: undefined, noindex: undefined, pointsTo: undefined, + audience: undefined, }; const result = Pruner.from(root) @@ -224,6 +236,7 @@ describe("pruneNavigationTree", () => { authed: undefined, noindex: undefined, pointsTo: undefined, + audience: undefined, }); }); @@ -246,6 +259,7 @@ describe("pruneNavigationTree", () => { hidden: undefined, authed: undefined, noindex: undefined, + audience: undefined, }, ], collapsed: undefined, @@ -255,6 +269,7 @@ describe("pruneNavigationTree", () => { authed: undefined, noindex: undefined, pointsTo: undefined, + audience: undefined, }; const result = Pruner.from(root) @@ -282,6 +297,7 @@ describe("pruneNavigationTree", () => { hidden: undefined, authed: undefined, noindex: undefined, + audience: undefined, }, ], collapsed: undefined, @@ -291,6 +307,7 @@ describe("pruneNavigationTree", () => { authed: undefined, noindex: undefined, pointsTo: FernNavigation.Slug("root/page"), + audience: undefined, }); }); @@ -320,6 +337,7 @@ describe("pruneNavigationTree", () => { hidden: undefined, authed: undefined, noindex: undefined, + audience: undefined, }, ], collapsed: undefined, @@ -329,6 +347,7 @@ describe("pruneNavigationTree", () => { authed: undefined, noindex: undefined, pointsTo: undefined, + audience: undefined, }, { type: "page", @@ -341,6 +360,7 @@ describe("pruneNavigationTree", () => { hidden: undefined, authed: undefined, noindex: undefined, + audience: undefined, }, ], collapsed: undefined, @@ -350,6 +370,7 @@ describe("pruneNavigationTree", () => { authed: undefined, noindex: undefined, pointsTo: undefined, + audience: undefined, }; const result = Pruner.from(root) @@ -377,6 +398,7 @@ describe("pruneNavigationTree", () => { hidden: undefined, authed: undefined, noindex: undefined, + audience: undefined, }, ], collapsed: undefined, @@ -385,6 +407,7 @@ describe("pruneNavigationTree", () => { hidden: undefined, authed: undefined, noindex: undefined, + audience: undefined, // NOTE: points to is updated! pointsTo: "root/page", diff --git a/packages/fdr-sdk/src/navigation/utils/pruneNavigationTree.ts b/packages/fdr-sdk/src/navigation/utils/pruneNavigationTree.ts index f361b0f96e..3fd99d9ab6 100644 --- a/packages/fdr-sdk/src/navigation/utils/pruneNavigationTree.ts +++ b/packages/fdr-sdk/src/navigation/utils/pruneNavigationTree.ts @@ -1,10 +1,14 @@ import structuredClone from "@ungap/structured-clone"; import { FernNavigation } from "../.."; -import { bfs } from "../../utils/traversers/bfs"; import { prunetree } from "../../utils/traversers/prunetree"; import { mutableDeleteChild } from "./deleteChild"; import { mutableUpdatePointsTo } from "./updatePointsTo"; +type Predicate = ( + node: T, + parents: readonly FernNavigation.NavigationNodeParent[], +) => boolean; + export class Pruner { public static from(tree: ROOT): Pruner { return new Pruner(tree); @@ -15,7 +19,7 @@ export class Pruner { this.tree = structuredClone(tree) as ROOT; } - public keep(predicate: (node: FernNavigation.NavigationNode) => boolean): this { + public keep(predicate: Predicate): this { if (this.tree == null) { return this; } @@ -29,35 +33,31 @@ export class Pruner { return this; } - public hide(predicate: (node: FernNavigation.NavigationNodeWithMetadata) => boolean): this { + public remove(predicate: Predicate): this { + return this.keep((node, parents) => !predicate(node, parents)); + } + + public hide(predicate: Predicate): this { if (this.tree == null) { return this; } - bfs( - this.tree, - (node) => { - if (FernNavigation.hasMarkdown(node) && predicate(node)) { - node.hidden = true; - } - }, - FernNavigation.getChildren, - ); + FernNavigation.traverseBF(this.tree, (node, parents) => { + if (FernNavigation.hasMetadata(node)) { + node.hidden = predicate(node, parents) ? true : undefined; + } + }); return this; } - public authed(predicate: (node: FernNavigation.NavigationNodeWithMetadata) => boolean): this { + public authed(predicate: Predicate): this { if (this.tree == null) { return this; } - bfs( - this.tree, - (node) => { - if (FernNavigation.hasMarkdown(node) && predicate(node)) { - node.authed = true; - } - }, - FernNavigation.getChildren, - ); + FernNavigation.traverseBF(this.tree, (node, parents) => { + if (FernNavigation.hasMetadata(node)) { + node.authed = predicate(node, parents) ? true : undefined; + } + }); return this; } diff --git a/packages/fdr-sdk/src/utils/traversers/prunetree.ts b/packages/fdr-sdk/src/utils/traversers/prunetree.ts index d34aad46dc..bb6fbb80fd 100644 --- a/packages/fdr-sdk/src/utils/traversers/prunetree.ts +++ b/packages/fdr-sdk/src/utils/traversers/prunetree.ts @@ -6,7 +6,7 @@ interface PruneTreeOptions { * @param node the node to check * @returns **false** if the node SHOULD be deleted */ - predicate: (node: NODE) => boolean; + predicate: (node: NODE, parents: readonly PARENT[]) => boolean; getChildren: (node: PARENT) => readonly NODE[]; /** @@ -53,7 +53,7 @@ export function prunetree { it("should deny the request if the allowlist is empty", () => { @@ -27,7 +27,9 @@ describe("withBasicTokenAnonymous", () => { it("shouuld respect denylist before allowlist", () => { expect(withBasicTokenAnonymous({ allowlist: ["/public"], denylist: ["/public"] }, "/public")).toBe(true); }); +}); +describe("withBasicTokenAnonymousCheck", () => { it("should never deny external links", () => { expect( withBasicTokenAnonymousCheck({ denylist: ["/(.*)"] })({ @@ -56,6 +58,7 @@ describe("withBasicTokenAnonymous", () => { overviewPageId: undefined, noindex: undefined, pointsTo: undefined, + audience: undefined, }), ).toBe(false); }); @@ -76,7 +79,49 @@ describe("withBasicTokenAnonymous", () => { overviewPageId: PageId("1.mdx"), noindex: undefined, pointsTo: undefined, + audience: undefined, }), ).toBe(false); }); }); + +describe("matchAudience", () => { + it("should return true if the audience is empty", () => { + expect(matchAudience([], [])).toBe(true); + expect(matchAudience([], [[], []])).toBe(true); + }); + + it("should return false if an audience filter exists", () => { + expect(matchAudience([], [["a"]])).toBe(false); + }); + + it("should return true if the audience matches the filter", () => { + expect(matchAudience(["a"], [["a"]])).toBe(true); + }); + + it("should return true if the audience matches any of the filters", () => { + expect(matchAudience(["a"], [["b", "a"]])).toBe(true); + }); + + it("should return false if the audience does not match any of the filters", () => { + expect(matchAudience(["a"], [["b"]])).toBe(false); + }); + + it("should return false if the audience does not match all filters across all nodes", () => { + expect(matchAudience(["a"], [["a"], ["b"]])).toBe(false); + expect(matchAudience(["b"], [["a"], ["a", "b"]])).toBe(false); + }); + + it("should return true if the audience matches all filters across all nodes", () => { + expect(matchAudience(["a"], [["a"], ["a"]])).toBe(true); + expect(matchAudience(["a"], [["a"], ["a", "b"]])).toBe(true); + expect(matchAudience(["a", "b"], [["a"], ["a", "b"]])).toBe(true); + expect(matchAudience(["a", "b"], [["a"], ["b"]])).toBe(true); + }); + + it("should return true if the user has more audiences than the filter", () => { + expect(matchAudience(["a", "b"], [])).toBe(true); + expect(matchAudience(["a", "b"], [[]])).toBe(true); + expect(matchAudience(["a", "b"], [["a"]])).toBe(true); + }); +}); diff --git a/packages/ui/docs-bundle/src/server/withBasicTokenAnonymous.ts b/packages/ui/docs-bundle/src/server/withBasicTokenAnonymous.ts index ee7abd0c36..ea15b7fb78 100644 --- a/packages/ui/docs-bundle/src/server/withBasicTokenAnonymous.ts +++ b/packages/ui/docs-bundle/src/server/withBasicTokenAnonymous.ts @@ -1,16 +1,37 @@ -import { Pruner, isPage, type NavigationNode, type RootNode } from "@fern-api/fdr-sdk/navigation"; +import { FernNavigation } from "@fern-api/fdr-sdk"; +import { + NavigationNodeParent, + Pruner, + hasMetadata, + isPage, + type NavigationNode, + type RootNode, +} from "@fern-api/fdr-sdk/navigation"; +import { EMPTY_ARRAY } from "@fern-api/ui-core-utils"; import type { AuthEdgeConfigBasicTokenVerification } from "@fern-ui/fern-docs-auth"; import { matchPath } from "@fern-ui/fern-docs-utils"; +interface AuthRulesPathName { + /** + * List of paths that should be allowed to pass through without authentication + */ + allowlist?: string[]; + + /** + * List of paths that should be denied access without authentication + */ + denylist?: string[]; + + /** + * List of paths that should be allowed to pass through without authentication, but should be hidden when the user is authenticated + */ + anonymous?: string[]; +} + /** - * @param auth Basic token verification configuration - * @param pathname pathname of the request to check * @returns true if the request should should be marked as authed */ -export function withBasicTokenAnonymous( - auth: Pick, - pathname: string, -): boolean { +export function withBasicTokenAnonymous(auth: AuthRulesPathName, pathname: string): boolean { // if the path is in the denylist, deny the request if (auth.denylist?.find((path) => matchPath(path, pathname))) { return true; @@ -32,9 +53,16 @@ export function withBasicTokenAnonymous( * @internal visibleForTesting */ export function withBasicTokenAnonymousCheck( - auth: Pick, -): (node: NavigationNode) => boolean { - return (node: NavigationNode) => { + auth: AuthRulesPathName, +): (node: NavigationNode, parents?: readonly NavigationNodeParent[]) => boolean { + const hasAudience = (audiences: string[] | undefined) => { + return audiences != null && audiences.length > 0; + }; + return (node, parents = EMPTY_ARRAY) => { + if ([...parents, node].some((n) => hasMetadata(n) && (n.authed || hasAudience(n.audience)))) { + return true; + } + if (isPage(node)) { return withBasicTokenAnonymous(auth, `/${node.slug}`); } @@ -57,12 +85,35 @@ export function pruneWithBasicTokenAnonymous(auth: AuthEdgeConfigBasicTokenVerif return result; } -export function pruneWithBasicTokenAuthed(auth: AuthEdgeConfigBasicTokenVerification, node: RootNode): RootNode { +function getAudienceFilters(...node: FernNavigation.NavigationNode[]): string[][] { + return node.map((n) => (hasMetadata(n) ? n.audience ?? [] : [])).filter((audience) => audience.length > 0); +} + +/** + * @internal + * @param audience current viewer's audience + * @param filters audience filters for the current node + * @returns true if the audience matches the filters (i.e. the viewer is allowed to view the node) + */ +export function matchAudience(audience: string[], filters: string[][]): boolean { + if (filters.length === 0 || filters.every((filter) => filter.length === 0)) { + return true; + } + + return filters.every((filter) => filter.some((aud) => audience.includes(aud))); +} + +export function pruneWithBasicTokenAuthed(auth: AuthRulesPathName, node: RootNode, audience: string[] = []): RootNode { const result = Pruner.from(node) + // apply audience filters + .keep((n, parents) => !hasMetadata(n) || matchAudience(audience, getAudienceFilters(...parents, n))) // hide nodes that are not authed - .hide((n) => auth.anonymous?.find((path) => matchPath(path, `/${n.slug}`)) != null) + .hide((n) => node.hidden || auth.anonymous?.find((path) => matchPath(path, `/${n.slug}`)) != null) + // mark all nodes as unauthed since we are currently authenticated + .authed(() => false) .get(); + // TODO: handle this more gracefully if (result == null) { throw new Error("Failed to prune navigation tree"); } diff --git a/packages/ui/fern-docs-auth/src/types.ts b/packages/ui/fern-docs-auth/src/types.ts index fbf568ec06..b89409b855 100644 --- a/packages/ui/fern-docs-auth/src/types.ts +++ b/packages/ui/fern-docs-auth/src/types.ts @@ -3,6 +3,12 @@ import { z } from "zod"; export const FernUserSchema = z.object({ name: z.string().optional(), email: z.string().optional(), + audience: z + .union([z.string(), z.array(z.string())], { + description: + "The audience of the token (can be a string or an array of strings) which limits what content users can access", + }) + .optional(), }); export type FernUser = z.infer; diff --git a/servers/fdr/src/api/generated/api/resources/commons/types/AudienceId.d.ts b/servers/fdr/src/api/generated/api/resources/commons/types/AudienceId.d.ts new file mode 100644 index 0000000000..a42585d353 --- /dev/null +++ b/servers/fdr/src/api/generated/api/resources/commons/types/AudienceId.d.ts @@ -0,0 +1,8 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +import * as FernRegistry from "../../../index"; +export declare type AudienceId = string & { + AudienceId: void; +}; +export declare function AudienceId(value: string): FernRegistry.AudienceId; diff --git a/servers/fdr/src/api/generated/api/resources/commons/types/AudienceId.js b/servers/fdr/src/api/generated/api/resources/commons/types/AudienceId.js new file mode 100644 index 0000000000..935cbae44b --- /dev/null +++ b/servers/fdr/src/api/generated/api/resources/commons/types/AudienceId.js @@ -0,0 +1,6 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +export function AudienceId(value) { + return value; +} diff --git a/servers/fdr/src/api/generated/api/resources/commons/types/index.d.ts b/servers/fdr/src/api/generated/api/resources/commons/types/index.d.ts index 11cb260750..401177ffa5 100644 --- a/servers/fdr/src/api/generated/api/resources/commons/types/index.d.ts +++ b/servers/fdr/src/api/generated/api/resources/commons/types/index.d.ts @@ -14,6 +14,7 @@ export * from "./FileId"; export * from "./Url"; export * from "./JqString"; export * from "./PropertyKey"; +export * from "./AudienceId"; export * from "./EndpointIdentifier"; export * from "./EndpointPathLiteral"; export * from "./HttpMethod"; diff --git a/servers/fdr/src/api/generated/api/resources/commons/types/index.js b/servers/fdr/src/api/generated/api/resources/commons/types/index.js index 11cb260750..401177ffa5 100644 --- a/servers/fdr/src/api/generated/api/resources/commons/types/index.js +++ b/servers/fdr/src/api/generated/api/resources/commons/types/index.js @@ -14,6 +14,7 @@ export * from "./FileId"; export * from "./Url"; export * from "./JqString"; export * from "./PropertyKey"; +export * from "./AudienceId"; export * from "./EndpointIdentifier"; export * from "./EndpointPathLiteral"; export * from "./HttpMethod"; diff --git a/servers/fdr/src/api/generated/api/resources/navigation/resources/latest/types/WithNodeMetadata.d.ts b/servers/fdr/src/api/generated/api/resources/navigation/resources/latest/types/WithNodeMetadata.d.ts index 3a71ff6b8b..208ac6cadd 100644 --- a/servers/fdr/src/api/generated/api/resources/navigation/resources/latest/types/WithNodeMetadata.d.ts +++ b/servers/fdr/src/api/generated/api/resources/navigation/resources/latest/types/WithNodeMetadata.d.ts @@ -20,6 +20,17 @@ export interface WithNodeMetadata extends FernRegistry.navigation.latest.WithNod /** The slug that should be used in the canonical URL rel. If not provided, the `slug` will be used. */ canonicalSlug: FernRegistry.navigation.latest.Slug | undefined; icon: string | undefined; + /** If true, this node will not be displayed in the sidebar, and noindex will be considered true. */ hidden: boolean | undefined; + /** + * If true, this node is only visible to authenticated users. + * If false, this node is only visible to all users (including anonymous). + */ authed: boolean | undefined; + /** + * The audience(s) that this node is intended for. If not provided, the node is intended for all audiences. + * If provided, the node is only intended for the specified audience(s). OR logic is used for multiple audiences on a single node. + * AND logic is used when evaluating audiences up the tree. + */ + audience: FernRegistry.AudienceId[] | undefined; }