@agent-native/core 0.198.3 → 0.198.5

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.
Files changed (94) hide show
  1. package/dist/a2a/types.d.ts +5 -0
  2. package/dist/agent/production-agent.js +26 -1
  3. package/dist/catalog.json +1 -0
  4. package/dist/client/chat/agentkit-agent-native.js +15 -15
  5. package/dist/client/chat/runtime.js +5 -3
  6. package/dist/client/feature-flags/types.d.ts +2 -0
  7. package/dist/client/i18n.d.ts +2 -0
  8. package/dist/client/i18n.js +2 -0
  9. package/dist/client/integrations/index.d.ts +1 -0
  10. package/dist/client/integrations/index.js +1 -0
  11. package/dist/client/labs/index.d.ts +1 -1
  12. package/dist/client/labs/use-lab.d.ts +5 -0
  13. package/dist/client/labs/use-lab.js +7 -3
  14. package/dist/client/resources/mcp-connection-resume.d.ts +5 -3
  15. package/dist/client/resources/mcp-connection-resume.js +150 -13
  16. package/dist/client/sse-event-processor.d.ts +6 -0
  17. package/dist/client/uploads/index.d.ts +1 -0
  18. package/dist/client/uploads/index.js +1 -0
  19. package/dist/client/uploads/private-icon.d.ts +10 -0
  20. package/dist/client/uploads/private-icon.js +52 -0
  21. package/dist/collab/routes.d.ts +2 -2
  22. package/dist/feature-flags/actions/get-feature-flags.js +20 -0
  23. package/dist/feature-flags/actions/list-feature-flags.d.ts +2 -0
  24. package/dist/feature-flags/actions/list-feature-flags.js +5 -0
  25. package/dist/feature-flags/actions/set-feature-flag.js +5 -0
  26. package/dist/file-upload/actions/upload-image.d.ts +3 -3
  27. package/dist/framework-tools.js +2 -0
  28. package/dist/icon-assets/index.d.ts +3 -0
  29. package/dist/icon-assets/index.js +3 -0
  30. package/dist/icon-assets/multipart.d.ts +9 -0
  31. package/dist/icon-assets/multipart.js +60 -0
  32. package/dist/icon-assets/schema.d.ts +194 -0
  33. package/dist/icon-assets/schema.js +16 -0
  34. package/dist/icon-assets/store.d.ts +38 -0
  35. package/dist/icon-assets/store.js +187 -0
  36. package/dist/icon-assets/svg.d.ts +2 -0
  37. package/dist/icon-assets/svg.js +146 -0
  38. package/dist/icon-assets/workspace-transport.d.ts +20 -0
  39. package/dist/icon-assets/workspace-transport.js +122 -0
  40. package/dist/identity/rekey.js +1 -0
  41. package/dist/jobs/actions/manage-recurring-job.d.ts +1 -1
  42. package/dist/labs/actions/get-lab-states.d.ts +2 -0
  43. package/dist/labs/actions/get-lab-states.js +14 -0
  44. package/dist/labs/actions/get-labs.js +2 -2
  45. package/dist/labs/actions/set-lab.js +4 -2
  46. package/dist/labs/index.d.ts +1 -1
  47. package/dist/labs/index.js +1 -1
  48. package/dist/labs/registry.d.ts +3 -0
  49. package/dist/labs/registry.js +14 -0
  50. package/dist/labs/server.d.ts +1 -1
  51. package/dist/labs/server.js +1 -1
  52. package/dist/labs/store.d.ts +13 -2
  53. package/dist/labs/store.js +124 -7
  54. package/dist/localization/default-messages.d.ts +2 -0
  55. package/dist/localization/default-messages.js +1 -1
  56. package/dist/localization/icon-picker-messages.d.ts +44 -0
  57. package/dist/localization/icon-picker-messages.js +22 -0
  58. package/dist/mcp-client/oauth-client.d.ts +1 -0
  59. package/dist/mcp-client/oauth-client.js +42 -4
  60. package/dist/mcp-client/oauth-routes.js +15 -2
  61. package/dist/notifications/routes.d.ts +1 -1
  62. package/dist/observability/routes.d.ts +9 -9
  63. package/dist/org/actions/list-workspace-icons.d.ts +7 -0
  64. package/dist/org/actions/list-workspace-icons.js +41 -0
  65. package/dist/org/handlers.d.ts +1 -1
  66. package/dist/org/handlers.js +28 -1
  67. package/dist/org/index.d.ts +1 -1
  68. package/dist/org/migrations.js +21 -0
  69. package/dist/org/plugin.js +42 -0
  70. package/dist/org/private-icon-handlers.d.ts +6 -0
  71. package/dist/org/private-icon-handlers.js +212 -0
  72. package/dist/progress/routes.d.ts +1 -1
  73. package/dist/provider-api/actions/custom-provider-registration.d.ts +8 -8
  74. package/dist/provider-api/actions/provider-api.d.ts +5 -5
  75. package/dist/provider-api/corpus-jobs.d.ts +2 -2
  76. package/dist/provider-api/index.d.ts +1 -1
  77. package/dist/provider-api/index.js +26 -6
  78. package/dist/secrets/routes.d.ts +4 -4
  79. package/dist/server/action-discovery.js +5 -0
  80. package/dist/server/agent-chat-plugin.js +14 -0
  81. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  82. package/dist/server/agent-engine-default-model-route.d.ts +1 -1
  83. package/dist/server/agent-engine-ollama-models-route.d.ts +1 -1
  84. package/dist/server/core-routes-plugin.d.ts +1 -1
  85. package/dist/server/core-routes-plugin.js +18 -3
  86. package/dist/server/index.d.ts +1 -1
  87. package/dist/server/index.js +1 -1
  88. package/dist/server/workspace-provider-oauth.d.ts +1 -0
  89. package/dist/server/workspace-provider-oauth.js +4 -2
  90. package/dist/templates/chat/app/components/chat/ChatRouteContent.tsx +9 -16
  91. package/dist/triggers/actions/manage-automation.d.ts +5 -5
  92. package/dist/vite/action-types-plugin.js +4 -0
  93. package/package.json +7 -9
  94. package/src/templates/chat/app/components/chat/ChatRouteContent.tsx +9 -16
@@ -0,0 +1,146 @@
1
+ import { XMLParser, XMLValidator } from "fast-xml-parser";
2
+ const ELEMENTS = new Set([
3
+ "svg",
4
+ "g",
5
+ "path",
6
+ "rect",
7
+ "circle",
8
+ "ellipse",
9
+ "line",
10
+ "polyline",
11
+ "polygon",
12
+ "defs",
13
+ "linearGradient",
14
+ "radialGradient",
15
+ "stop",
16
+ "clipPath",
17
+ ]);
18
+ const ATTRIBUTES = new Set([
19
+ "xmlns",
20
+ "viewBox",
21
+ "width",
22
+ "height",
23
+ "x",
24
+ "y",
25
+ "x1",
26
+ "x2",
27
+ "y1",
28
+ "y2",
29
+ "cx",
30
+ "cy",
31
+ "r",
32
+ "rx",
33
+ "ry",
34
+ "d",
35
+ "points",
36
+ "fill",
37
+ "stroke",
38
+ "stroke-width",
39
+ "stroke-linecap",
40
+ "stroke-linejoin",
41
+ "stroke-miterlimit",
42
+ "stroke-dasharray",
43
+ "stroke-dashoffset",
44
+ "fill-rule",
45
+ "clip-rule",
46
+ "opacity",
47
+ "fill-opacity",
48
+ "stroke-opacity",
49
+ "transform",
50
+ "id",
51
+ "gradientUnits",
52
+ "gradientTransform",
53
+ "offset",
54
+ "stop-color",
55
+ "stop-opacity",
56
+ "clip-path",
57
+ "preserveAspectRatio",
58
+ ]);
59
+ const COLOR = /^(?:none|currentColor|transparent|#[0-9a-fA-F]{3,8}|[a-zA-Z]+|url\(#[A-Za-z][\w.-]*\))$/;
60
+ const NUMERIC = /^[\d\s.,+\-eE%]*$/;
61
+ const PATH = /^[\d\s.,+\-eEa-zA-Z]*$/;
62
+ const TRANSFORM = /^(?:\s*(?:matrix|translate|scale|rotate|skewX|skewY)\([\d\s.,+\-eE]+\)\s*)+$/;
63
+ function escapeAttribute(value) {
64
+ return value
65
+ .replaceAll("&", "&")
66
+ .replaceAll('"', """)
67
+ .replaceAll("<", "&lt;");
68
+ }
69
+ function safeAttribute(name, value) {
70
+ if (!ATTRIBUTES.has(name) || value.length > 100_000)
71
+ return false;
72
+ if (name === "xmlns")
73
+ return value === "http://www.w3.org/2000/svg";
74
+ if (name === "id")
75
+ return /^[A-Za-z][\w.-]*$/.test(value);
76
+ if (["fill", "stroke", "stop-color"].includes(name))
77
+ return COLOR.test(value);
78
+ if (name === "clip-path")
79
+ return /^url\(#[A-Za-z][\w.-]*\)$/.test(value);
80
+ if (name === "transform" || name === "gradientTransform")
81
+ return TRANSFORM.test(value);
82
+ if ([
83
+ "stroke-linecap",
84
+ "stroke-linejoin",
85
+ "fill-rule",
86
+ "clip-rule",
87
+ "gradientUnits",
88
+ "preserveAspectRatio",
89
+ ].includes(name)) {
90
+ return /^[A-Za-z\s]+$/.test(value);
91
+ }
92
+ if (name === "d")
93
+ return PATH.test(value);
94
+ return NUMERIC.test(value);
95
+ }
96
+ function serializeNode(node) {
97
+ const entries = Object.entries(node).filter(([name]) => name !== ":@");
98
+ if (entries.length !== 1)
99
+ throw new Error("SVG contains unsupported markup");
100
+ const [name, children] = entries[0];
101
+ if (!ELEMENTS.has(name) || !Array.isArray(children)) {
102
+ throw new Error("SVG contains unsupported markup");
103
+ }
104
+ const attributes = (node[":@"] ?? {});
105
+ let opening = `<${name}`;
106
+ for (const [rawName, value] of Object.entries(attributes)) {
107
+ const attribute = rawName.startsWith("@_") ? rawName.slice(2) : rawName;
108
+ if (typeof value !== "string" || !safeAttribute(attribute, value)) {
109
+ throw new Error("SVG contains an unsafe attribute");
110
+ }
111
+ opening += ` ${attribute}="${escapeAttribute(value)}"`;
112
+ }
113
+ const body = children
114
+ .map((child) => {
115
+ if ("#text" in child) {
116
+ if (typeof child["#text"] !== "string" || child["#text"].trim()) {
117
+ throw new Error("SVG contains unsupported text");
118
+ }
119
+ return "";
120
+ }
121
+ return serializeNode(child);
122
+ })
123
+ .join("");
124
+ return `${opening}>${body}</${name}>`;
125
+ }
126
+ /** Canonicalize the small SVG icon vocabulary before bytes reach storage. */
127
+ export function sanitizeIconSvg(data) {
128
+ const source = new TextDecoder("utf-8", { fatal: true }).decode(data).trim();
129
+ if (!source.startsWith("<svg") || /<!|<\?/.test(source)) {
130
+ throw new Error("Invalid SVG icon");
131
+ }
132
+ if (XMLValidator.validate(source) !== true)
133
+ throw new Error("Invalid SVG icon");
134
+ const nodes = new XMLParser({
135
+ ignoreAttributes: false,
136
+ preserveOrder: true,
137
+ processEntities: false,
138
+ parseTagValue: false,
139
+ parseAttributeValue: false,
140
+ trimValues: false,
141
+ }).parse(source);
142
+ if (nodes.length !== 1 || !Object.hasOwn(nodes[0], "svg")) {
143
+ throw new Error("Invalid SVG icon");
144
+ }
145
+ return new TextEncoder().encode(serializeNode(nodes[0]));
146
+ }
@@ -0,0 +1,20 @@
1
+ import type { H3Event } from "h3";
2
+ export interface WorkspaceIconAuthority {
3
+ identityAuthority: string | null;
4
+ identityId: string | null;
5
+ allowedDomain: string | null;
6
+ }
7
+ export declare function uploadFederatedWorkspaceIcon(event: H3Event, authority: WorkspaceIconAuthority, email: string, file: {
8
+ data: Uint8Array;
9
+ mimeType: string;
10
+ filename: string;
11
+ }): Promise<string>;
12
+ export declare function verifyFederatedWorkspaceIconOwner(event: H3Event, authority: WorkspaceIconAuthority, email: string, assetId: string): Promise<boolean>;
13
+ export declare function readFederatedWorkspaceIcon(event: H3Event, authority: WorkspaceIconAuthority, email: string, assetId: string): Promise<{
14
+ data: Uint8Array;
15
+ mimeType: string;
16
+ } | null>;
17
+ export declare function listFederatedWorkspaceIconsForOwner(authority: WorkspaceIconAuthority, email: string): Promise<Array<{
18
+ id: string;
19
+ filename?: string;
20
+ }>>;
@@ -0,0 +1,122 @@
1
+ import { canonicalA2AAudience, signA2AToken } from "../a2a/index.js";
2
+ import { resolveVercelDeploymentProtectionHeaders } from "../server/credential-provider.js";
3
+ import { CANONICAL_IDENTITY_SSO_HUB_URL, NETLIFY_PREVIEW_IDENTITY_SSO_HUB_URL, getIdentityHubUrl, } from "../server/identity-sso-store.js";
4
+ import { resolveIdentityHubUrl } from "../server/identity-sso.js";
5
+ function dispatchUrl(event, authority) {
6
+ if (!authority.identityId || !authority.identityAuthority) {
7
+ throw new Error("Workspace icon identity authority is unavailable.");
8
+ }
9
+ const linked = new URL(authority.identityAuthority);
10
+ const hubs = event
11
+ ? [resolveIdentityHubUrl(event)]
12
+ : [
13
+ getIdentityHubUrl(),
14
+ CANONICAL_IDENTITY_SSO_HUB_URL,
15
+ NETLIFY_PREVIEW_IDENTITY_SSO_HUB_URL,
16
+ ];
17
+ const hub = hubs.find((candidate) => {
18
+ if (!candidate)
19
+ return false;
20
+ const expected = new URL(candidate);
21
+ return (linked.origin === expected.origin &&
22
+ linked.pathname.replace(/\/+$/, "") ===
23
+ expected.pathname.replace(/\/+$/, ""));
24
+ });
25
+ if (!hub) {
26
+ throw new Error("Workspace icon identity authority does not match this deployment.");
27
+ }
28
+ return hub.replace(/\/+$/, "");
29
+ }
30
+ async function dispatchRequest(event, authority, email, scope, assetId, file) {
31
+ const hub = dispatchUrl(event, authority);
32
+ const token = await signA2AToken(email, authority.allowedDomain ?? undefined, undefined, {
33
+ preferGlobalSecret: true,
34
+ audience: canonicalA2AAudience(hub, "/private-icon"),
35
+ expiresIn: "2m",
36
+ extraClaims: {
37
+ scope,
38
+ org_id: authority.identityId,
39
+ ...(assetId ? { asset_id: assetId } : {}),
40
+ },
41
+ });
42
+ const body = file ? new FormData() : undefined;
43
+ if (file) {
44
+ body.set("file", new Blob([new Uint8Array(file.data)], { type: file.mimeType }), file.filename);
45
+ }
46
+ const url = `${hub}/_agent-native/private-icons${assetId ? `/${encodeURIComponent(assetId)}` : ""}`;
47
+ return fetch(url, {
48
+ method: file
49
+ ? "POST"
50
+ : scope === "private-icon:verify-owner"
51
+ ? "HEAD"
52
+ : "GET",
53
+ headers: {
54
+ Authorization: `Bearer ${token}`,
55
+ ...resolveVercelDeploymentProtectionHeaders(url),
56
+ },
57
+ body,
58
+ redirect: "manual",
59
+ signal: AbortSignal.timeout(10_000),
60
+ });
61
+ }
62
+ export async function uploadFederatedWorkspaceIcon(event, authority, email, file) {
63
+ const response = await dispatchRequest(event, authority, email, "private-icon:upload", undefined, file);
64
+ if (!response.ok)
65
+ throw new Error(`Workspace icon upload failed (${response.status}).`);
66
+ const result = await response.json();
67
+ if (!result ||
68
+ typeof result !== "object" ||
69
+ !("id" in result) ||
70
+ typeof result.id !== "string") {
71
+ throw new Error("Workspace icon upload returned an invalid asset ID.");
72
+ }
73
+ return result.id;
74
+ }
75
+ export async function verifyFederatedWorkspaceIconOwner(event, authority, email, assetId) {
76
+ const response = await dispatchRequest(event, authority, email, "private-icon:verify-owner", assetId);
77
+ if (response.status === 404)
78
+ return false;
79
+ if (!response.ok)
80
+ throw new Error(`Workspace icon ownership check failed (${response.status}).`);
81
+ return true;
82
+ }
83
+ export async function readFederatedWorkspaceIcon(event, authority, email, assetId) {
84
+ const response = await dispatchRequest(event, authority, email, "private-icon:read", assetId);
85
+ if (response.status === 404)
86
+ return null;
87
+ if (!response.ok)
88
+ throw new Error(`Workspace icon read failed (${response.status}).`);
89
+ const mimeType = response.headers
90
+ .get("content-type")
91
+ ?.split(";", 1)[0]
92
+ ?.trim();
93
+ if (!mimeType?.startsWith("image/"))
94
+ throw new Error("Workspace icon response is not an image.");
95
+ return { data: new Uint8Array(await response.arrayBuffer()), mimeType };
96
+ }
97
+ export async function listFederatedWorkspaceIconsForOwner(authority, email) {
98
+ const response = await dispatchRequest(undefined, authority, email, "private-icon:list");
99
+ if (!response.ok)
100
+ throw new Error(`Workspace icon list failed (${response.status}).`);
101
+ const body = await response.json();
102
+ if (!body ||
103
+ typeof body !== "object" ||
104
+ !("assets" in body) ||
105
+ !Array.isArray(body.assets)) {
106
+ throw new Error("Workspace icon list returned an invalid response.");
107
+ }
108
+ return body.assets.map((asset) => {
109
+ if (!asset ||
110
+ typeof asset !== "object" ||
111
+ !("id" in asset) ||
112
+ typeof asset.id !== "string") {
113
+ throw new Error("Workspace icon list returned an invalid asset.");
114
+ }
115
+ return {
116
+ id: asset.id,
117
+ filename: "filename" in asset && typeof asset.filename === "string"
118
+ ? asset.filename
119
+ : undefined,
120
+ };
121
+ });
122
+ }
@@ -55,6 +55,7 @@ export const IDENTITY_REKEY_COLUMNS = [
55
55
  { table: "integration_remote_devices", column: "owner_email" },
56
56
  { table: "agent_harness_sessions", column: "owner_email" },
57
57
  { table: "workspace_apps", column: "owner_email" },
58
+ { table: "private_icon_assets", column: "owner_email" },
58
59
  { table: "application_state", column: "session_id", mode: "state-session" },
59
60
  { table: "oauth_tokens", column: "owner", mode: "unsupported-oauth" },
60
61
  { table: "workspace_connections", column: "owner_email" },
@@ -11,9 +11,9 @@ declare const _default: import("../../action.js").ActionDefinition<{
11
11
  enabled?: undefined;
12
12
  nextRun?: undefined;
13
13
  } | {
14
+ deleted?: undefined;
14
15
  name: string;
15
16
  enabled: boolean;
16
17
  nextRun: string;
17
- deleted?: undefined;
18
18
  }>;
19
19
  export default _default;
@@ -0,0 +1,2 @@
1
+ declare const _default: import("../../action.js").ActionDefinition<Record<string, never>, Record<string, import("../store.js").UserLabState>>;
2
+ export default _default;
@@ -0,0 +1,14 @@
1
+ import { z } from "zod";
2
+ import { defineAction, fail } from "../../action.js";
3
+ import { getUserLabStates } from "../store.js";
4
+ export default defineAction({
5
+ description: "Return each registered lab's effective state, including whether it comes from a saved choice or inherited feature flags.",
6
+ schema: z.object({}),
7
+ http: { method: "GET" },
8
+ run: async (_args, ctx) => {
9
+ const email = ctx?.userEmail;
10
+ if (!email)
11
+ fail("Not authenticated.", { statusCode: 401 });
12
+ return getUserLabStates(email, { orgId: ctx?.orgId });
13
+ },
14
+ });
@@ -2,13 +2,13 @@ import { z } from "zod";
2
2
  import { defineAction, fail } from "../../action.js";
3
3
  import { getUserLabs } from "../store.js";
4
4
  export default defineAction({
5
- description: "Return every registered lab and the current user's enabled state. Unset preferences use each lab's app-defined default.",
5
+ description: "Return every registered lab and the current user's effective enabled state. Unset preferences inherit declared legacy flags or use the app-defined default.",
6
6
  schema: z.object({}),
7
7
  http: { method: "GET" },
8
8
  run: async (_args, ctx) => {
9
9
  const email = ctx?.userEmail;
10
10
  if (!email)
11
11
  fail("Not authenticated.", { statusCode: 401 });
12
- return getUserLabs(email);
12
+ return getUserLabs(email, { orgId: ctx?.orgId });
13
13
  },
14
14
  });
@@ -7,7 +7,7 @@ const schema = z.object({
7
7
  enabled: z.boolean().describe("Whether the current user opts into it."),
8
8
  });
9
9
  export default defineAction({
10
- description: "Opt the current user into or out of one registered lab. Unset preferences use the app-defined default; labs may expose new or unstable features.",
10
+ description: "Opt the current user into or out of one registered lab. Unset preferences inherit declared legacy flags or use the app-defined default; labs may expose new or unstable features.",
11
11
  schema,
12
12
  http: { method: "POST" },
13
13
  run: async (args, ctx) => {
@@ -17,7 +17,9 @@ export default defineAction({
17
17
  if (!getLabDefinition(args.key)) {
18
18
  fail(`Unknown lab: ${args.key}`, { statusCode: 404 });
19
19
  }
20
- const values = await setUserLab(email, args.key, args.enabled);
20
+ const values = await setUserLab(email, args.key, args.enabled, {
21
+ orgId: ctx?.orgId,
22
+ });
21
23
  return { key: args.key, enabled: values[args.key] === true, values };
22
24
  },
23
25
  });
@@ -1 +1 @@
1
- export { defineLab, defineLabs, getLabDefinition, listLabs, registerLabs, type LabDefinition, } from "./registry.js";
1
+ export { defineLab, defineLabs, getLabDefinition, getLabForLegacyFlag, listLabs, registerLabs, type LabDefinition, } from "./registry.js";
@@ -1 +1 @@
1
- export { defineLab, defineLabs, getLabDefinition, listLabs, registerLabs, } from "./registry.js";
1
+ export { defineLab, defineLabs, getLabDefinition, getLabForLegacyFlag, listLabs, registerLabs, } from "./registry.js";
@@ -1,6 +1,8 @@
1
1
  export interface LabDefinition {
2
2
  key: string;
3
3
  defaultEnabled?: boolean;
4
+ legacyFlagKeys?: readonly string[];
5
+ inheritedMixedDescription?: string;
4
6
  displayName?: string;
5
7
  description?: string;
6
8
  keywords?: string;
@@ -10,4 +12,5 @@ export declare function defineLabs(definitions: readonly LabDefinition[]): reado
10
12
  export declare function registerLabs(definitions: readonly LabDefinition[]): void;
11
13
  export declare function listLabs(): readonly LabDefinition[];
12
14
  export declare function getLabDefinition(key: string): LabDefinition | null;
15
+ export declare function getLabForLegacyFlag(key: string): LabDefinition | null;
13
16
  export declare function _resetLabRegistryForTests(): void;
@@ -12,6 +12,12 @@ function normalizeDefinition(definition) {
12
12
  ...(definition.defaultEnabled !== undefined && {
13
13
  defaultEnabled: definition.defaultEnabled,
14
14
  }),
15
+ ...(definition.legacyFlagKeys?.length && {
16
+ legacyFlagKeys: Object.freeze([...new Set(definition.legacyFlagKeys)]),
17
+ }),
18
+ ...(definition.inheritedMixedDescription?.trim() && {
19
+ inheritedMixedDescription: definition.inheritedMixedDescription.trim(),
20
+ }),
15
21
  ...(definition.displayName?.trim() && {
16
22
  displayName: definition.displayName.trim(),
17
23
  }),
@@ -46,6 +52,10 @@ export function registerLabs(definitions) {
46
52
  continue;
47
53
  }
48
54
  if (existing.defaultEnabled !== definition.defaultEnabled ||
55
+ JSON.stringify(existing.legacyFlagKeys) !==
56
+ JSON.stringify(definition.legacyFlagKeys) ||
57
+ existing.inheritedMixedDescription !==
58
+ definition.inheritedMixedDescription ||
49
59
  existing.displayName !== definition.displayName ||
50
60
  existing.description !== definition.description ||
51
61
  existing.keywords !== definition.keywords) {
@@ -59,6 +69,10 @@ export function listLabs() {
59
69
  export function getLabDefinition(key) {
60
70
  return registry.get(key) ?? null;
61
71
  }
72
+ export function getLabForLegacyFlag(key) {
73
+ return ([...registry.values()].find((lab) => lab.legacyFlagKeys?.includes(key)) ??
74
+ null);
75
+ }
62
76
  export function _resetLabRegistryForTests() {
63
77
  registry.clear();
64
78
  }
@@ -1,2 +1,2 @@
1
1
  export { createLabsPlugin } from "./plugin.js";
2
- export { getUserLabs } from "./store.js";
2
+ export { getUserLabEnabled, getUserLabState, getUserLabStates, getUserLabs, type UserLabState, } from "./store.js";
@@ -1,2 +1,2 @@
1
1
  export { createLabsPlugin } from "./plugin.js";
2
- export { getUserLabs } from "./store.js";
2
+ export { getUserLabEnabled, getUserLabState, getUserLabStates, getUserLabs, } from "./store.js";
@@ -1,4 +1,15 @@
1
+ import { type FeatureFlagScope } from "../feature-flags/store.js";
2
+ import { type LabDefinition } from "./registry.js";
1
3
  export declare const LABS_SETTING_KEY = "labs";
4
+ export interface UserLabState {
5
+ enabled: boolean;
6
+ source: "choice" | "legacy" | "default";
7
+ legacyValues?: Record<string, boolean>;
8
+ mixed: boolean;
9
+ }
10
+ export declare function getUserLabStates(email: string, scope?: FeatureFlagScope): Promise<Record<string, UserLabState>>;
11
+ export declare function getUserLabState(email: string, lab: string | LabDefinition, scope?: FeatureFlagScope): Promise<UserLabState>;
12
+ export declare function getUserLabEnabled(email: string, lab: string | LabDefinition, scope?: FeatureFlagScope): Promise<boolean>;
2
13
  export declare function normalizeLabValues(stored: Record<string, unknown> | null | undefined): Record<string, boolean>;
3
- export declare function getUserLabs(email: string): Promise<Record<string, boolean>>;
4
- export declare function setUserLab(email: string, key: string, enabled: boolean): Promise<Record<string, boolean>>;
14
+ export declare function getUserLabs(email: string, scope?: FeatureFlagScope): Promise<Record<string, boolean>>;
15
+ export declare function setUserLab(email: string, key: string, enabled: boolean, scope?: FeatureFlagScope): Promise<Record<string, boolean>>;
@@ -1,13 +1,123 @@
1
+ import { defaultFeatureFlagRules, evaluateFeatureFlagRules, normalizeFeatureFlagRules, } from "../feature-flags/store.js";
2
+ import { getOrgSetting } from "../settings/org-settings.js";
3
+ import { getSetting } from "../settings/store.js";
1
4
  import { getUserSetting, mutateUserSetting, } from "../settings/user-settings.js";
2
5
  import { getLabDefinition, listLabs } from "./registry.js";
3
6
  export const LABS_SETTING_KEY = "labs";
4
7
  const LEGACY_LABS_SETTING_KEY = "experiments";
8
+ function validLabSetting(key, value) {
9
+ if (value !== null && (typeof value !== "object" || Array.isArray(value))) {
10
+ throw new Error(`Invalid saved lab setting: ${key}`);
11
+ }
12
+ return value;
13
+ }
5
14
  async function getStoredLabs(email) {
6
15
  const [labs, legacy] = await Promise.all([
7
16
  getUserSetting(email, LABS_SETTING_KEY),
8
17
  getUserSetting(email, LEGACY_LABS_SETTING_KEY),
9
18
  ]);
10
- return labs || legacy ? { ...(legacy ?? {}), ...(labs ?? {}) } : null;
19
+ const current = validLabSetting(LABS_SETTING_KEY, labs);
20
+ const previous = validLabSetting(LEGACY_LABS_SETTING_KEY, legacy);
21
+ return current || previous
22
+ ? { ...(previous ?? {}), ...(current ?? {}) }
23
+ : null;
24
+ }
25
+ function parseLegacyRules(key, value) {
26
+ if (value === null)
27
+ return defaultFeatureFlagRules();
28
+ if (typeof value !== "object" ||
29
+ Array.isArray(value) ||
30
+ !["off", "on", "rules"].includes(value.mode) ||
31
+ (value.emails !== undefined &&
32
+ (!Array.isArray(value.emails) ||
33
+ !value.emails.every((item) => typeof item === "string"))) ||
34
+ (value.orgIds !== undefined &&
35
+ (!Array.isArray(value.orgIds) ||
36
+ !value.orgIds.every((item) => typeof item === "string"))) ||
37
+ (value.percentage !== undefined &&
38
+ (typeof value.percentage !== "number" ||
39
+ !Number.isFinite(value.percentage) ||
40
+ value.percentage < 0 ||
41
+ value.percentage > 100))) {
42
+ throw new Error(`Invalid legacy feature flag rules: ${key}`);
43
+ }
44
+ return normalizeFeatureFlagRules(value);
45
+ }
46
+ async function getLegacyRules(key, scope) {
47
+ const settingKey = `feature-flag:${key}`;
48
+ const orgId = scope.orgId?.trim();
49
+ const [global, org] = await Promise.all([
50
+ getSetting(settingKey, { transaction: scope.transaction }),
51
+ orgId
52
+ ? getOrgSetting(orgId, settingKey, { transaction: scope.transaction })
53
+ : Promise.resolve(null),
54
+ ]);
55
+ return parseLegacyRules(key, org ?? global);
56
+ }
57
+ export async function getUserLabStates(email, scope = {}) {
58
+ const stored = await getStoredLabs(email);
59
+ return resolveStoredLabStates(email, stored, scope);
60
+ }
61
+ async function resolveStoredLabStates(email, stored, scope) {
62
+ const definitions = listLabs();
63
+ for (const { key } of definitions) {
64
+ if (stored &&
65
+ Object.hasOwn(stored, key) &&
66
+ typeof stored[key] !== "boolean") {
67
+ throw new Error(`Invalid saved lab choice: ${key}`);
68
+ }
69
+ }
70
+ const unresolved = definitions.filter(({ key, legacyFlagKeys }) => typeof stored?.[key] !== "boolean" && legacyFlagKeys?.length);
71
+ const legacyKeys = [
72
+ ...new Set(unresolved.flatMap((lab) => lab.legacyFlagKeys ?? [])),
73
+ ];
74
+ const evaluationScope = { ...scope, userEmail: email };
75
+ const rules = new Map(await Promise.all(legacyKeys.map(async (key) => [key, await getLegacyRules(key, evaluationScope)])));
76
+ return Object.fromEntries(definitions.map((lab) => {
77
+ const choice = stored?.[lab.key];
78
+ if (typeof choice === "boolean") {
79
+ return [lab.key, { enabled: choice, source: "choice", mixed: false }];
80
+ }
81
+ if (lab.legacyFlagKeys?.length) {
82
+ const legacyValues = Object.fromEntries(lab.legacyFlagKeys.map((key) => {
83
+ const flagRules = rules.get(key);
84
+ if (!flagRules)
85
+ throw new Error(`Unknown legacy feature flag: ${key}`);
86
+ return [
87
+ key,
88
+ evaluateFeatureFlagRules(key, flagRules, evaluationScope),
89
+ ];
90
+ }));
91
+ const values = Object.values(legacyValues);
92
+ return [
93
+ lab.key,
94
+ {
95
+ enabled: values.every(Boolean),
96
+ source: "legacy",
97
+ legacyValues,
98
+ mixed: values.some(Boolean) && !values.every(Boolean),
99
+ },
100
+ ];
101
+ }
102
+ return [
103
+ lab.key,
104
+ {
105
+ enabled: lab.defaultEnabled === true,
106
+ source: "default",
107
+ mixed: false,
108
+ },
109
+ ];
110
+ }));
111
+ }
112
+ export async function getUserLabState(email, lab, scope = {}) {
113
+ const key = typeof lab === "string" ? lab : lab.key;
114
+ const state = (await getUserLabStates(email, scope))[key];
115
+ if (!state)
116
+ throw new Error(`Unknown lab: ${key}`);
117
+ return state;
118
+ }
119
+ export async function getUserLabEnabled(email, lab, scope = {}) {
120
+ return (await getUserLabState(email, lab, scope)).enabled;
11
121
  }
12
122
  export function normalizeLabValues(stored) {
13
123
  return Object.fromEntries(listLabs().map(({ key, defaultEnabled }) => [
@@ -16,18 +126,25 @@ export function normalizeLabValues(stored) {
16
126
  (stored?.[key] === undefined && defaultEnabled === true),
17
127
  ]));
18
128
  }
19
- export async function getUserLabs(email) {
20
- return normalizeLabValues(await getStoredLabs(email));
129
+ export async function getUserLabs(email, scope = {}) {
130
+ const states = await getUserLabStates(email, scope);
131
+ return Object.fromEntries(Object.entries(states).map(([key, state]) => [key, state.enabled]));
21
132
  }
22
- export async function setUserLab(email, key, enabled) {
133
+ export async function setUserLab(email, key, enabled, scope = {}) {
23
134
  if (!getLabDefinition(key)) {
24
135
  throw new Error(`Unknown lab: ${key}`);
25
136
  }
26
- const stored = await mutateUserSetting(email, LABS_SETTING_KEY, async (current) => {
137
+ let effectiveValues;
138
+ await mutateUserSetting(email, LABS_SETTING_KEY, async (current) => {
139
+ validLabSetting(LABS_SETTING_KEY, current);
27
140
  const legacy = current === null
28
141
  ? await getUserSetting(email, LEGACY_LABS_SETTING_KEY)
29
142
  : null;
30
- return { ...(legacy ?? {}), ...(current ?? {}), [key]: enabled };
143
+ validLabSetting(LEGACY_LABS_SETTING_KEY, legacy);
144
+ const next = { ...(legacy ?? {}), ...(current ?? {}), [key]: enabled };
145
+ const states = await resolveStoredLabStates(email, next, scope);
146
+ effectiveValues = Object.fromEntries(Object.entries(states).map(([labKey, state]) => [labKey, state.enabled]));
147
+ return next;
31
148
  });
32
- return normalizeLabValues(stored);
149
+ return effectiveValues;
33
150
  }
@@ -3,7 +3,9 @@ declare const messages: {
3
3
  allCategories: string;
4
4
  loadError: string;
5
5
  saveError: string;
6
+ uploadFailed: string;
6
7
  uploadTooLarge: string;
8
+ uploadUnsupportedType: string;
7
9
  retry: string;
8
10
  uploadHint: string;
9
11
  colorNames: {
@@ -1416,7 +1416,7 @@ const messages = {
1416
1416
  gong: {
1417
1417
  description: "Search Gong calls and generate account and deal insights.",
1418
1418
  useCase: "Sales calls, transcripts, deal insights, account summaries",
1419
- setupNote: "Gong requires a tech admin to create an MCP integration and choose personal or shared authorization. The generated client ID and secret must be configured before connecting.",
1419
+ setupNote: "A Gong tech admin must create an MCP integration with personal or shared authorization. For Manual registration, save the generated client ID and secret as workspace secrets `GONG_MCP_CLIENT_ID` and `GONG_MCP_CLIENT_SECRET`; Automatic registration needs no client credentials.",
1420
1420
  },
1421
1421
  semgrep: {
1422
1422
  description: "Scan code for security findings.",