@sentry/junior-plugin-api 0.198.0 → 0.199.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/hooks.ts DELETED
@@ -1,118 +0,0 @@
1
- import type {
2
- ConversationSidebarHookContext,
3
- ConversationSidebarResult,
4
- } from "./annotations";
5
- import type {
6
- EgressHookContext,
7
- EgressResponseHookContext,
8
- IssueCredentialHookContext,
9
- PluginCredentialResult,
10
- PluginGrant,
11
- PluginProviderAccount,
12
- ResolveOAuthAccountHookContext,
13
- } from "./credentials";
14
- import type {
15
- HeartbeatHookContext,
16
- HeartbeatResult,
17
- OperationalReportHookContext,
18
- ProfileReportHookContext,
19
- ApiRouteRegistrationHookContext,
20
- PluginOperationalReportContent,
21
- PluginRoute,
22
- PluginRouteApp,
23
- RouteRegistrationHookContext,
24
- SlackConversationLink,
25
- SlackConversationLinkHookContext,
26
- UnfinishedWorkHookContext,
27
- UnfinishedWorkResult,
28
- } from "./operations";
29
- import type {
30
- AfterMcpToolHookContext,
31
- BeforeToolExecuteHookContext,
32
- PluginToolDefinition,
33
- SandboxPrepareHookContext,
34
- ToolRegistrationHookContext,
35
- WorkspacePrepareHookContext,
36
- } from "./tools";
37
- import type {
38
- PromptMessage,
39
- SystemPromptContext,
40
- UserPromptContext,
41
- UserPromptContribution,
42
- } from "./prompt";
43
-
44
- /** Input for a pure Markdown rewrite before destination delivery formatting. */
45
- export interface FormatMarkdownHookContext {
46
- text: string;
47
- }
48
-
49
- export interface PluginHooks {
50
- conversationSidebar?(
51
- ctx: ConversationSidebarHookContext,
52
- ): Promise<ConversationSidebarResult> | ConversationSidebarResult;
53
- systemPrompt?(
54
- ctx: SystemPromptContext,
55
- ): Promise<PromptMessage[]> | PromptMessage[];
56
- userPrompt?(
57
- ctx: UserPromptContext,
58
- ):
59
- | Promise<UserPromptContribution[] | undefined>
60
- | UserPromptContribution[]
61
- | undefined;
62
- beforeToolExecute?(ctx: BeforeToolExecuteHookContext): Promise<void> | void;
63
- /**
64
- * Run after a successful hosted MCP tool call.
65
- *
66
- * Prefer this for junior-owned side effects such as conversation annotations.
67
- * Do not use it to invent a parallel tool contract for the provider tool.
68
- */
69
- afterMcpTool?(ctx: AfterMcpToolHookContext): Promise<void> | void;
70
- grantForEgress?(
71
- ctx: EgressHookContext,
72
- ): Promise<PluginGrant | undefined> | PluginGrant | undefined;
73
- heartbeat?(
74
- ctx: HeartbeatHookContext,
75
- ): Promise<HeartbeatResult | void> | HeartbeatResult | void;
76
- unfinishedWork?(
77
- ctx: UnfinishedWorkHookContext,
78
- ): Promise<UnfinishedWorkResult> | UnfinishedWorkResult;
79
- issueCredential?(
80
- ctx: IssueCredentialHookContext,
81
- ): Promise<PluginCredentialResult> | PluginCredentialResult;
82
- onEgressResponse?(ctx: EgressResponseHookContext): Promise<void> | void;
83
- operationalReport?(
84
- ctx: OperationalReportHookContext,
85
- ):
86
- | Promise<PluginOperationalReportContent | undefined>
87
- | PluginOperationalReportContent
88
- | undefined;
89
- /**
90
- * Return one person-scoped operational report for a profile page.
91
- * Omit or return undefined when the plugin has nothing to show for the subject.
92
- */
93
- profileReport?(
94
- ctx: ProfileReportHookContext,
95
- ):
96
- | Promise<PluginOperationalReportContent | undefined>
97
- | PluginOperationalReportContent
98
- | undefined;
99
- /** Return plugin-owned product API routes mounted under Junior's authenticated plugin namespace. */
100
- apiRoutes?(ctx: ApiRouteRegistrationHookContext): PluginRouteApp | undefined;
101
- resolveOAuthAccount?(
102
- ctx: ResolveOAuthAccountHookContext,
103
- ):
104
- | Promise<PluginProviderAccount | undefined>
105
- | PluginProviderAccount
106
- | undefined;
107
- routes?(ctx: RouteRegistrationHookContext): PluginRoute[];
108
- sandboxPrepare?(ctx: SandboxPrepareHookContext): Promise<void> | void;
109
- workspacePrepare?(ctx: WorkspacePrepareHookContext): Promise<void> | void;
110
- slackConversationLink?(
111
- ctx: SlackConversationLinkHookContext,
112
- ): SlackConversationLink | undefined;
113
- /** Pure Markdown rewrite. Emit ordinary Markdown only. */
114
- formatMarkdown?(ctx: FormatMarkdownHookContext): string;
115
- tools?(
116
- ctx: ToolRegistrationHookContext,
117
- ): Record<string, PluginToolDefinition>;
118
- }
package/src/index.ts DELETED
@@ -1,29 +0,0 @@
1
- export * from "./annotations";
2
- export * from "./code";
3
- export * from "./conversation-events";
4
- export * from "./schemas";
5
- export * from "./context";
6
- export * from "./state";
7
- export {
8
- definePromptContext,
9
- promptContextSchema,
10
- promptMessageSchema,
11
- type PromptContext,
12
- type PromptContextContribution,
13
- type PromptMessage,
14
- type SystemPromptContext,
15
- type UserPromptContribution,
16
- type UserPromptContext,
17
- } from "./prompt";
18
- export * from "./dispatch";
19
- export * from "./resource-events";
20
- export * from "./tasks";
21
- export * from "./tools";
22
- export * from "./operations";
23
- export * from "./credentials";
24
- export * from "./egress-policy";
25
- export * from "./hooks";
26
- export * from "./cli";
27
- export * from "./manifest";
28
- export * from "./registration";
29
- export * from "./user-pages";
package/src/manifest.ts DELETED
@@ -1,89 +0,0 @@
1
- export interface PluginOAuthConfig {
2
- authorizeEndpoint: string;
3
- authorizeParams?: Record<string, string>;
4
- clientIdEnv: string;
5
- clientSecretEnv: string;
6
- scope?: string;
7
- /**
8
- * Treat a provider token response with `scope: ""` like an omitted scope and
9
- * fall back to the requested scope string when storing the token.
10
- */
11
- treatEmptyScopeAsUnreported?: boolean;
12
- tokenAuthMethod?: "body" | "basic";
13
- tokenEndpoint: string;
14
- tokenExtraHeaders?: Record<string, string>;
15
- }
16
-
17
- export interface PluginOAuthBearerCredentials {
18
- apiHeaders?: Record<string, string>;
19
- authTokenEnv: string;
20
- authTokenPlaceholder?: string;
21
- domains: string[];
22
- type: "oauth-bearer";
23
- }
24
-
25
- export type PluginCredentials = PluginOAuthBearerCredentials;
26
-
27
- export interface PluginNpmRuntimeDependency {
28
- package: string;
29
- type: "npm";
30
- version: string;
31
- }
32
-
33
- export interface PluginSystemRuntimeDependency {
34
- package: string;
35
- type: "system";
36
- }
37
-
38
- export interface PluginSystemRuntimeDependencyFromUrl {
39
- sha256: string;
40
- type: "system";
41
- url: string;
42
- }
43
-
44
- export type PluginRuntimeDependency =
45
- | PluginNpmRuntimeDependency
46
- | PluginSystemRuntimeDependency
47
- | PluginSystemRuntimeDependencyFromUrl;
48
-
49
- export interface PluginRuntimePostinstallCommand {
50
- args?: string[];
51
- cmd: string;
52
- sudo?: boolean;
53
- }
54
-
55
- export interface PluginMcpConfig {
56
- /** Provider tools exposed directly to the model. */
57
- allowedTools?: string[];
58
- headers?: Record<string, string>;
59
- transport: "http";
60
- url: string;
61
- /** Provider tools hidden from the model and callable only by plugin-owned wrapper tools. */
62
- wrappedTools?: string[];
63
- }
64
-
65
- export interface PluginEnvVarDeclaration {
66
- default?: string;
67
- exposeToCommandEnv?: boolean;
68
- }
69
-
70
- export interface PluginManifest {
71
- apiHeaders?: Record<string, string>;
72
- commandEnv?: Record<string, string>;
73
- configKeys?: string[];
74
- credentials?: PluginCredentials;
75
- description: string;
76
- displayName: string;
77
- domains?: string[];
78
- envVars?: Record<string, PluginEnvVarDeclaration>;
79
- mcp?: PluginMcpConfig;
80
- name: string;
81
- oauth?: PluginOAuthConfig;
82
- runtimeDependencies?: PluginRuntimeDependency[];
83
- runtimePostinstall?: PluginRuntimePostinstallCommand[];
84
- target?: {
85
- commandFlags?: string[];
86
- configKey: string;
87
- type: string;
88
- };
89
- }
package/src/operations.ts DELETED
@@ -1,199 +0,0 @@
1
- import { z } from "zod";
2
- import type { PluginContext, User } from "./context";
3
- import type { Dispatch, DispatchOptions, DispatchResult } from "./dispatch";
4
- import { nonBlankStringSchema } from "./schemas";
5
- import type { PluginReadState, PluginState } from "./state";
6
- import type { ResourceEventPublisher } from "./resource-events";
7
- import type { PluginConversationAnnotations } from "./annotations";
8
- import type { PluginConversationEventStats } from "./conversation-events";
9
- import type { CodeChangePublisher } from "./code";
10
-
11
- export interface HeartbeatHookContext extends PluginContext {
12
- agent: {
13
- dispatch(options: DispatchOptions): Promise<DispatchResult>;
14
- get(id: string): Promise<Dispatch | undefined>;
15
- };
16
- nowMs: number;
17
- state: PluginState;
18
- }
19
-
20
- export interface HeartbeatResult {
21
- dispatchCount?: number;
22
- }
23
-
24
- export interface UnfinishedWorkHookContext extends PluginContext {
25
- /** Bounded conversation candidates selected by the host. */
26
- conversationIds: string[];
27
- }
28
-
29
- export interface UnfinishedWorkResult {
30
- /** Candidate conversations that have plugin-owned work to finish. */
31
- conversationIds: string[];
32
- /**
33
- * Candidate conversations that have any associated plugin-owned work,
34
- * finished or unfinished. Omit when the plugin cannot distinguish assignment.
35
- *
36
- * The host uses this with unfinished work and finish times to set feed
37
- * `isPriority`. Finished assigned work stays out of Priority unless the
38
- * conversation has activity after the finish time.
39
- */
40
- assignedConversationIds?: string[];
41
- /**
42
- * Latest time when all known work finished, keyed by conversation id.
43
- * ISO-8601 timestamps. Used with conversation activity to decide whether
44
- * finished work still belongs in Priority.
45
- */
46
- finishedWorkAtByConversationId?: Record<string, string>;
47
- }
48
-
49
- export type PluginOperationalTone = "danger" | "good" | "neutral" | "warning";
50
-
51
- export interface PluginOperationalMetric {
52
- label: string;
53
- tone?: PluginOperationalTone;
54
- value: string;
55
- }
56
-
57
- export interface PluginOperationalField {
58
- key: string;
59
- label: string;
60
- }
61
-
62
- export interface PluginOperationalRecord {
63
- id: string;
64
- tone?: PluginOperationalTone;
65
- values: Record<string, string>;
66
- }
67
-
68
- export interface PluginOperationalRecordSet {
69
- fields?: PluginOperationalField[];
70
- emptyText?: string;
71
- records?: PluginOperationalRecord[];
72
- title: string;
73
- }
74
-
75
- export interface PluginOperationalChartSeries {
76
- format?: "usd";
77
- key: string;
78
- label: string;
79
- tone?: PluginOperationalTone;
80
- }
81
-
82
- export interface PluginOperationalChartCategory {
83
- id: string;
84
- label: string;
85
- values: Record<string, number>;
86
- }
87
-
88
- export interface PluginOperationalBarChartWidget {
89
- categories: PluginOperationalChartCategory[];
90
- description?: string;
91
- emptyText?: string;
92
- id: string;
93
- series: PluginOperationalChartSeries[];
94
- timeRangeDays?: Array<1 | 7 | 30 | 90>;
95
- title: string;
96
- type: "bar_chart";
97
- }
98
-
99
- export interface PluginOperationalReportContent {
100
- generatedAt?: string;
101
- metrics?: PluginOperationalMetric[];
102
- recordSets?: PluginOperationalRecordSet[];
103
- title?: string;
104
- widgets?: PluginOperationalBarChartWidget[];
105
- }
106
-
107
- export interface PluginOperationalReport extends PluginOperationalReportContent {
108
- pluginName: string;
109
- }
110
-
111
- export interface OperationalReportHookContext extends PluginContext {
112
- eventStats: PluginConversationEventStats;
113
- nowMs: number;
114
- state: PluginReadState;
115
- }
116
-
117
- /** Read-only context for one person-scoped plugin report on a profile page. */
118
- export interface ProfileReportHookContext extends PluginContext {
119
- nowMs: number;
120
- state: PluginReadState;
121
- /** Canonical user for the profile being viewed. */
122
- subject: User;
123
- /** Canonical user for the authenticated viewer. */
124
- viewer: User;
125
- }
126
-
127
- export type PluginRouteMethod =
128
- | "GET"
129
- | "POST"
130
- | "PUT"
131
- | "PATCH"
132
- | "DELETE"
133
- | "HEAD"
134
- | "OPTIONS"
135
- | "ALL";
136
-
137
- export type PluginRouteHandler = {
138
- bivarianceHack(request: Request): Promise<Response> | Response;
139
- }["bivarianceHack"];
140
-
141
- export interface PluginRoute {
142
- handler: PluginRouteHandler;
143
- method?: PluginRouteMethod | PluginRouteMethod[];
144
- path: string;
145
- }
146
-
147
- /** Fetch-compatible plugin HTTP app mounted by Junior. */
148
- export type PluginRouteApp = {
149
- fetch(
150
- request: Request,
151
- context?: PluginApiRouteRequestContext,
152
- ): Promise<Response> | Response;
153
- };
154
-
155
- export interface RouteRegistrationHookContext extends PluginContext {
156
- annotations: PluginConversationAnnotations;
157
- /** Provider-neutral write boundary for Junior's native code index. */
158
- codeChanges: CodeChangePublisher;
159
- /** Core-owned delivery boundary for provider webhook events. */
160
- resourceEvents: ResourceEventPublisher;
161
- }
162
-
163
- export interface ApiRouteRegistrationHookContext extends PluginContext {
164
- eventStats: PluginConversationEventStats;
165
- users: {
166
- /** Resolve or create the canonical user for one verified email. */
167
- resolve(email: string): Promise<User | undefined>;
168
- };
169
- }
170
-
171
- /** Per-request context Junior passes to authenticated plugin product API routes. */
172
- export const pluginApiRouteRequestContextSchema = z
173
- .object({
174
- auth: z
175
- .object({
176
- user: z
177
- .object({
178
- email: z.string().nullable().optional(),
179
- emailVerified: z.boolean().optional(),
180
- name: z.string().nullable().optional(),
181
- })
182
- .strict(),
183
- })
184
- .strict(),
185
- pluginName: nonBlankStringSchema,
186
- })
187
- .strict();
188
-
189
- export type PluginApiRouteRequestContext = z.output<
190
- typeof pluginApiRouteRequestContextSchema
191
- >;
192
-
193
- export interface SlackConversationLink {
194
- url: string;
195
- }
196
-
197
- export interface SlackConversationLinkHookContext extends PluginContext {
198
- conversationId: string;
199
- }
package/src/prompt.ts DELETED
@@ -1,99 +0,0 @@
1
- import { z } from "zod";
2
- import type {
3
- Destination,
4
- Platform,
5
- PluginContext,
6
- PluginEmbedder,
7
- PluginModel,
8
- Actor,
9
- Identity,
10
- Source,
11
- User,
12
- } from "./context";
13
- import type { PluginState } from "./state";
14
- import type { PluginConversationEvents } from "./conversation-events";
15
-
16
- const promptContextKindSchema = z
17
- .string()
18
- .trim()
19
- .min(1)
20
- .max(64)
21
- .regex(/^[a-z][a-z0-9_-]*$/);
22
-
23
- export const promptMessageSchema = z
24
- .object({
25
- text: z.string().trim().min(1).max(8_000),
26
- })
27
- .strict();
28
-
29
- /** Small plugin-owned prompt text block rendered by Junior core. */
30
- export type PromptMessage = z.output<typeof promptMessageSchema>;
31
-
32
- export const promptContextSchema = z
33
- .object({
34
- kind: promptContextKindSchema,
35
- version: z.number().int().positive(),
36
- content: z.record(z.string(), z.unknown()),
37
- })
38
- .strict();
39
-
40
- /** Structured plugin context retained alongside its model-visible rendering. */
41
- export type PromptContext = z.output<typeof promptContextSchema>;
42
-
43
- /** Runtime contribution produced from one validated plugin context value. */
44
- export interface PromptContextContribution {
45
- context: PromptContext;
46
- renderPrompt(): string;
47
- }
48
-
49
- /** Define one typed, versioned plugin context contribution. */
50
- export function definePromptContext<
51
- TSchema extends z.ZodType<Record<string, unknown>>,
52
- >(definition: {
53
- kind: string;
54
- version: number;
55
- schema: TSchema;
56
- renderPrompt(content: z.output<TSchema>): string;
57
- }): (content: z.input<TSchema>) => PromptContextContribution {
58
- const identity = promptContextSchema
59
- .pick({ kind: true, version: true })
60
- .parse({ kind: definition.kind, version: definition.version });
61
- return (content) => {
62
- const parsed = definition.schema.parse(content);
63
- return {
64
- context: { ...identity, content: parsed },
65
- renderPrompt: () => definition.renderPrompt(parsed),
66
- };
67
- };
68
- }
69
-
70
- /** One request-scoped plugin contribution to the model-visible user prompt. */
71
- export type UserPromptContribution = PromptMessage | PromptContextContribution;
72
-
73
- /** Stable platform context for plugin system prompt guidance. */
74
- export type SystemPromptContext = Pick<
75
- PluginContext,
76
- "db" | "log" | "plugin"
77
- > & {
78
- platform: Platform;
79
- };
80
-
81
- /** Runtime facts available while building plugin user prompt context. */
82
- export type UserPromptContext = Pick<PluginContext, "db" | "log" | "plugin"> & {
83
- conversationId?: string;
84
- destination: Destination;
85
- embedder: PluginEmbedder;
86
- /** Conversation-bound event writer when the prompt belongs to a durable turn. */
87
- events?: PluginConversationEvents;
88
- model: PluginModel;
89
- /** Location associated with this Conversation. */
90
- locationId?: string;
91
- actor?: Actor;
92
- source: Source;
93
- state: PluginState;
94
- text: string;
95
- users: {
96
- /** Resolve the current Actor's Identity and User. */
97
- resolveActor(): Promise<{ identity: Identity; user?: User } | undefined>;
98
- };
99
- };
@@ -1,173 +0,0 @@
1
- import type { PluginCliDefinition } from "./cli";
2
- import type { PluginConversationEventDefinition } from "./conversation-events";
3
- import type { PluginHooks } from "./hooks";
4
- import type { PluginManifest } from "./manifest";
5
- import type { PluginTasks } from "./tasks";
6
- import type { PluginUserPageDefinition } from "./user-pages";
7
- import {
8
- pluginResourceEventsSchema,
9
- type PluginResourceEvents,
10
- } from "./resource-events";
11
-
12
- export interface PluginModelConfig {
13
- /** Host model family used when no explicit structured model id is configured. */
14
- structuredModel?: "default" | "fast";
15
- /** Host model id used for this plugin's structured model calls. */
16
- structuredModelId?: string;
17
- }
18
-
19
- export type PluginRegistrationInput = {
20
- cli?: PluginCliDefinition;
21
- conversationEvents?: PluginConversationEventDefinition[];
22
- hooks?: PluginHooks;
23
- manifest: PluginManifest;
24
- model?: PluginModelConfig;
25
- packageName?: string;
26
- resourceEvents?: PluginResourceEvents;
27
- tasks?: PluginTasks;
28
- userPages?: PluginUserPageDefinition[];
29
- };
30
-
31
- export interface PluginRegistration extends PluginRegistrationInput {}
32
-
33
- const PLUGIN_NAME_RE = /^[a-z][a-z0-9-]*$/;
34
- const USER_PAGE_ID_RE = /^[a-z][a-z0-9-]*$/;
35
-
36
- /** Define one Junior plugin registration for app and build-time wiring. */
37
- export function defineJuniorPlugin(
38
- plugin: PluginRegistrationInput,
39
- ): PluginRegistration {
40
- if ("pluginConfig" in plugin) {
41
- throw new Error(
42
- "pluginConfig is no longer supported. Put runtime metadata in manifest or plugin registration fields.",
43
- );
44
- }
45
- if ("name" in plugin) {
46
- throw new Error("defineJuniorPlugin() uses manifest.name for identity.");
47
- }
48
- const manifest = plugin.manifest;
49
- if (!manifest) {
50
- throw new Error(
51
- "defineJuniorPlugin() requires a manifest. Use a package name string in defineJuniorPlugins([...]) for plugin.yaml packages.",
52
- );
53
- }
54
- const name = manifest.name;
55
- if (!name) {
56
- throw new Error("Junior plugin manifest.name is required.");
57
- }
58
- if (!PLUGIN_NAME_RE.test(name)) {
59
- throw new Error(
60
- `Junior plugin registration name "${name}" must be a lowercase plugin identifier.`,
61
- );
62
- }
63
- if (name === "junior") {
64
- throw new Error(
65
- 'Junior plugin registration name "junior" is reserved for native host events.',
66
- );
67
- }
68
- if (
69
- typeof manifest.displayName !== "string" ||
70
- !manifest.displayName.trim()
71
- ) {
72
- throw new Error(
73
- `Junior plugin "${name}" manifest.displayName is required.`,
74
- );
75
- }
76
- if (
77
- typeof manifest.description !== "string" ||
78
- !manifest.description.trim()
79
- ) {
80
- throw new Error(
81
- `Junior plugin "${name}" manifest.description is required.`,
82
- );
83
- }
84
- if (plugin.userPages !== undefined && !Array.isArray(plugin.userPages)) {
85
- throw new Error(`Junior plugin "${name}" userPages must be an array.`);
86
- }
87
- if (
88
- plugin.conversationEvents !== undefined &&
89
- !Array.isArray(plugin.conversationEvents)
90
- ) {
91
- throw new Error(
92
- `Junior plugin "${name}" conversationEvents must be an array.`,
93
- );
94
- }
95
- if (plugin.resourceEvents !== undefined) {
96
- const parsed = pluginResourceEventsSchema.safeParse(plugin.resourceEvents);
97
- if (!parsed.success) {
98
- throw new Error(`Junior plugin "${name}" resourceEvents is invalid.`, {
99
- cause: parsed.error,
100
- });
101
- }
102
- }
103
- const conversationEventIds = new Set<string>();
104
- for (const event of plugin.conversationEvents ?? []) {
105
- if (
106
- !event ||
107
- (typeof event !== "object" && typeof event !== "function") ||
108
- typeof event.parse !== "function" ||
109
- typeof event.renderEvent !== "function"
110
- ) {
111
- throw new Error(
112
- `Junior plugin "${name}" conversation event definitions must be created with defineConversationEvent().`,
113
- );
114
- }
115
- const id = `${event.eventName}@${event.version}`;
116
- if (conversationEventIds.has(id)) {
117
- throw new Error(
118
- `Junior plugin "${name}" has duplicate conversation event "${id}".`,
119
- );
120
- }
121
- conversationEventIds.add(id);
122
- }
123
- const userPageIds = new Set<string>();
124
- for (const page of plugin.userPages ?? []) {
125
- if (!page || typeof page !== "object") {
126
- throw new Error(`Junior plugin "${name}" user pages must be objects.`);
127
- }
128
- if (
129
- typeof page.id !== "string" ||
130
- page.id.length > 64 ||
131
- !USER_PAGE_ID_RE.test(page.id)
132
- ) {
133
- throw new Error(
134
- `Junior plugin "${name}" user page id "${page.id}" must be a lowercase identifier.`,
135
- );
136
- }
137
- if (userPageIds.has(page.id)) {
138
- throw new Error(
139
- `Junior plugin "${name}" has duplicate user page id "${page.id}".`,
140
- );
141
- }
142
- if (
143
- typeof page.label !== "string" ||
144
- !page.label.trim() ||
145
- page.label.length > 80 ||
146
- typeof page.description !== "string" ||
147
- !page.description.trim() ||
148
- page.description.length > 500
149
- ) {
150
- throw new Error(
151
- `Junior plugin "${name}" user page "${page.id}" requires label and description.`,
152
- );
153
- }
154
- if (
155
- page.navigation !== undefined &&
156
- page.navigation !== "primary" &&
157
- page.navigation !== "profile"
158
- ) {
159
- throw new Error(
160
- `Junior plugin "${name}" user page "${page.id}" navigation must be "primary" or "profile".`,
161
- );
162
- }
163
- if (typeof page.read !== "function") {
164
- throw new Error(
165
- `Junior plugin "${name}" user page "${page.id}" requires a read function.`,
166
- );
167
- }
168
- userPageIds.add(page.id);
169
- }
170
- return {
171
- ...plugin,
172
- };
173
- }