@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.
- package/dist/a2a/types.d.ts +5 -0
- package/dist/agent/production-agent.js +26 -1
- package/dist/catalog.json +1 -0
- package/dist/client/chat/agentkit-agent-native.js +15 -15
- package/dist/client/chat/runtime.js +5 -3
- package/dist/client/feature-flags/types.d.ts +2 -0
- package/dist/client/i18n.d.ts +2 -0
- package/dist/client/i18n.js +2 -0
- package/dist/client/integrations/index.d.ts +1 -0
- package/dist/client/integrations/index.js +1 -0
- package/dist/client/labs/index.d.ts +1 -1
- package/dist/client/labs/use-lab.d.ts +5 -0
- package/dist/client/labs/use-lab.js +7 -3
- package/dist/client/resources/mcp-connection-resume.d.ts +5 -3
- package/dist/client/resources/mcp-connection-resume.js +150 -13
- package/dist/client/sse-event-processor.d.ts +6 -0
- package/dist/client/uploads/index.d.ts +1 -0
- package/dist/client/uploads/index.js +1 -0
- package/dist/client/uploads/private-icon.d.ts +10 -0
- package/dist/client/uploads/private-icon.js +52 -0
- package/dist/collab/routes.d.ts +2 -2
- package/dist/feature-flags/actions/get-feature-flags.js +20 -0
- package/dist/feature-flags/actions/list-feature-flags.d.ts +2 -0
- package/dist/feature-flags/actions/list-feature-flags.js +5 -0
- package/dist/feature-flags/actions/set-feature-flag.js +5 -0
- package/dist/file-upload/actions/upload-image.d.ts +3 -3
- package/dist/framework-tools.js +2 -0
- package/dist/icon-assets/index.d.ts +3 -0
- package/dist/icon-assets/index.js +3 -0
- package/dist/icon-assets/multipart.d.ts +9 -0
- package/dist/icon-assets/multipart.js +60 -0
- package/dist/icon-assets/schema.d.ts +194 -0
- package/dist/icon-assets/schema.js +16 -0
- package/dist/icon-assets/store.d.ts +38 -0
- package/dist/icon-assets/store.js +187 -0
- package/dist/icon-assets/svg.d.ts +2 -0
- package/dist/icon-assets/svg.js +146 -0
- package/dist/icon-assets/workspace-transport.d.ts +20 -0
- package/dist/icon-assets/workspace-transport.js +122 -0
- package/dist/identity/rekey.js +1 -0
- package/dist/jobs/actions/manage-recurring-job.d.ts +1 -1
- package/dist/labs/actions/get-lab-states.d.ts +2 -0
- package/dist/labs/actions/get-lab-states.js +14 -0
- package/dist/labs/actions/get-labs.js +2 -2
- package/dist/labs/actions/set-lab.js +4 -2
- package/dist/labs/index.d.ts +1 -1
- package/dist/labs/index.js +1 -1
- package/dist/labs/registry.d.ts +3 -0
- package/dist/labs/registry.js +14 -0
- package/dist/labs/server.d.ts +1 -1
- package/dist/labs/server.js +1 -1
- package/dist/labs/store.d.ts +13 -2
- package/dist/labs/store.js +124 -7
- package/dist/localization/default-messages.d.ts +2 -0
- package/dist/localization/default-messages.js +1 -1
- package/dist/localization/icon-picker-messages.d.ts +44 -0
- package/dist/localization/icon-picker-messages.js +22 -0
- package/dist/mcp-client/oauth-client.d.ts +1 -0
- package/dist/mcp-client/oauth-client.js +42 -4
- package/dist/mcp-client/oauth-routes.js +15 -2
- package/dist/notifications/routes.d.ts +1 -1
- package/dist/observability/routes.d.ts +9 -9
- package/dist/org/actions/list-workspace-icons.d.ts +7 -0
- package/dist/org/actions/list-workspace-icons.js +41 -0
- package/dist/org/handlers.d.ts +1 -1
- package/dist/org/handlers.js +28 -1
- package/dist/org/index.d.ts +1 -1
- package/dist/org/migrations.js +21 -0
- package/dist/org/plugin.js +42 -0
- package/dist/org/private-icon-handlers.d.ts +6 -0
- package/dist/org/private-icon-handlers.js +212 -0
- package/dist/progress/routes.d.ts +1 -1
- package/dist/provider-api/actions/custom-provider-registration.d.ts +8 -8
- package/dist/provider-api/actions/provider-api.d.ts +5 -5
- package/dist/provider-api/corpus-jobs.d.ts +2 -2
- package/dist/provider-api/index.d.ts +1 -1
- package/dist/provider-api/index.js +26 -6
- package/dist/secrets/routes.d.ts +4 -4
- package/dist/server/action-discovery.js +5 -0
- package/dist/server/agent-chat-plugin.js +14 -0
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/agent-engine-default-model-route.d.ts +1 -1
- package/dist/server/agent-engine-ollama-models-route.d.ts +1 -1
- package/dist/server/core-routes-plugin.d.ts +1 -1
- package/dist/server/core-routes-plugin.js +18 -3
- package/dist/server/index.d.ts +1 -1
- package/dist/server/index.js +1 -1
- package/dist/server/workspace-provider-oauth.d.ts +1 -0
- package/dist/server/workspace-provider-oauth.js +4 -2
- package/dist/templates/chat/app/components/chat/ChatRouteContent.tsx +9 -16
- package/dist/triggers/actions/manage-automation.d.ts +5 -5
- package/dist/vite/action-types-plugin.js +4 -0
- package/package.json +7 -9
- 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("<", "<");
|
|
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
|
+
}
|
package/dist/identity/rekey.js
CHANGED
|
@@ -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,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
|
|
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
|
});
|
package/dist/labs/index.d.ts
CHANGED
|
@@ -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";
|
package/dist/labs/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export { defineLab, defineLabs, getLabDefinition, listLabs, registerLabs, } from "./registry.js";
|
|
1
|
+
export { defineLab, defineLabs, getLabDefinition, getLabForLegacyFlag, listLabs, registerLabs, } from "./registry.js";
|
package/dist/labs/registry.d.ts
CHANGED
|
@@ -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;
|
package/dist/labs/registry.js
CHANGED
|
@@ -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
|
}
|
package/dist/labs/server.d.ts
CHANGED
|
@@ -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";
|
package/dist/labs/server.js
CHANGED
|
@@ -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";
|
package/dist/labs/store.d.ts
CHANGED
|
@@ -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>>;
|
package/dist/labs/store.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
149
|
+
return effectiveValues;
|
|
33
150
|
}
|
|
@@ -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
|
|
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.",
|