@k2b/cloud 0.33.0 → 0.35.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/package.json +3 -3
- package/src/_internal/app-presentation.ts +40 -11
- package/src/_internal/define-app.ts +1 -0
- package/src/_internal/registry.ts +10 -1
- package/src/_internal/widget-declarations.ts +16 -3
- package/src/_internal/widget-settings.ts +93 -0
- package/src/ai/capabilities.ts +130 -16
- package/src/ai/firecrawl-tools.ts +3 -30
- package/src/ai/resource-refs.ts +35 -3
- package/src/ai/system-prompt.ts +3 -2
- package/src/api/index.ts +2 -0
- package/src/api/mcp.ts +48 -17
- package/src/api/pwa.ts +7 -2
- package/src/api/resource-links.ts +43 -0
- package/src/api/widgets.ts +163 -82
- package/src/browser/CloudResourceSearch.tsx +6 -6
- package/src/browser/command-shortcuts.ts +2 -11
- package/src/browser/resource-picker.tsx +1 -0
- package/src/browser/resource-search.ts +7 -2
- package/src/browser/widgets.ts +23 -5
- package/src/capabilities/testing.ts +13 -1
- package/src/cli/capabilities.ts +61 -17
- package/src/cli/index.ts +2 -0
- package/src/cli/resource-links.ts +13 -0
- package/src/contracts/app.ts +7 -1
- package/src/contracts/capabilities.ts +25 -5
- package/src/contracts/registry.ts +3 -1
- package/src/contracts/resource-href.ts +7 -0
- package/src/contracts/widgets.ts +80 -6
- package/src/desktop/index.ts +3 -2
- package/src/desktop/solid.tsx +7 -5
- package/src/server/widget-request.ts +34 -5
- package/src/services/app-approval.ts +4 -2
- package/src/services/approvals/index.ts +40 -11
- package/src/services/approvals/list.ts +1 -0
- package/src/services/approvals/migrate.ts +10 -0
- package/src/services/network-security.ts +33 -9
- package/src/services/pdf/markdown.ts +11 -7
- package/src/services/pwa-devices.ts +68 -15
- package/src/shared/app-presentation.ts +49 -1
- package/src/shared/index.ts +1 -0
- package/src/shared/markdown/callout-sanitizer.ts +37 -0
- package/src/shared/markdown/index.ts +8 -2
- package/src/shared/network-address.ts +46 -45
- package/src/ssr/Layout.tsx +3 -15
- package/src/ssr/LayoutHelp.browser-harness.tsx +43 -0
- package/src/ssr/LayoutHelp.tsx +320 -183
- package/src/ssr/LayoutHelpTrigger.tsx +1 -3
- package/src/ssr/LayoutPreferences.tsx +15 -5
- package/src/ssr/MinimalLayoutPreferences.island.tsx +3 -9
- package/src/ssr/MobileProfileActions.tsx +3 -8
- package/src/ssr/ProfilePreferences.tsx +8 -10
- package/src/ssr/ShellRuntime.island.tsx +1 -5
- package/src/ssr/ThemeCycleButton.tsx +46 -0
- package/src/ssr/app-appearance.ts +6 -1
- package/src/ssr/layout-help-url.ts +39 -0
- package/src/ssr/platform-messages.ts +0 -8
- package/src/ssr/preference-controller.ts +53 -19
- package/src/ssr/profile-actions.ts +12 -16
- package/src/ssr/profile-preferences-messages.ts +4 -2
- package/src/ssr/rail-navigation.ts +3 -0
- package/src/ssr/shell-data.tsx +2 -2
- package/src/styles/effects.css +13 -3
- package/src/styles/global.css +47 -12
- package/src/styles/input.css +0 -8
- package/src/styles/tokens.css +0 -2
- package/src/styles/utilities-navigation.css +10 -13
- package/src/ssr/GlobalSearchHelpDialog.tsx +0 -104
|
@@ -32,22 +32,12 @@ const UNSAFE_IPV4_RANGES: ReadonlyArray<readonly [string, number]> = [
|
|
|
32
32
|
["240.0.0.0", 4],
|
|
33
33
|
];
|
|
34
34
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
if (address.includes(".")) {
|
|
38
|
-
const separator = address.lastIndexOf(":");
|
|
39
|
-
const ipv4 = ipv4ToNumber(address.slice(separator + 1));
|
|
40
|
-
if (separator < 0 || ipv4 === null) return null;
|
|
41
|
-
address = `${address.slice(0, separator)}:${(ipv4 >>> 16).toString(16)}:${(ipv4 & 0xffff).toString(16)}`;
|
|
42
|
-
}
|
|
35
|
+
// Only canonical IPv6 from the WHATWG parser reaches this conversion.
|
|
36
|
+
const ipv6ToBigInt = (address: string): bigint => {
|
|
43
37
|
const halves = address.split("::");
|
|
44
|
-
if (halves.length > 2) return null;
|
|
45
38
|
const left = halves[0] ? halves[0].split(":") : [];
|
|
46
39
|
const right = halves[1] ? halves[1].split(":") : [];
|
|
47
|
-
const
|
|
48
|
-
if (missing < 0 || (halves.length === 1 && left.length !== 8)) return null;
|
|
49
|
-
const groups = [...left, ...Array.from({ length: missing }, () => "0"), ...right];
|
|
50
|
-
if (groups.length !== 8 || groups.some((group) => !/^[0-9a-f]{1,4}$/.test(group))) return null;
|
|
40
|
+
const groups = [...left, ...Array.from({ length: 8 - left.length - right.length }, () => "0"), ...right];
|
|
51
41
|
return groups.reduce((value, group) => (value << 16n) | BigInt(`0x${group}`), 0n);
|
|
52
42
|
};
|
|
53
43
|
|
|
@@ -57,51 +47,62 @@ const ipv6InRange = (address: bigint, base: bigint, bits: number): boolean => {
|
|
|
57
47
|
};
|
|
58
48
|
|
|
59
49
|
const UNSAFE_IPV6_RANGES: ReadonlyArray<readonly [bigint, number]> = [
|
|
60
|
-
[ipv6ToBigInt("::")
|
|
61
|
-
[ipv6ToBigInt("
|
|
62
|
-
[ipv6ToBigInt("
|
|
63
|
-
[ipv6ToBigInt("
|
|
64
|
-
[ipv6ToBigInt("2001::")!, 32],
|
|
65
|
-
[ipv6ToBigInt("2001:2::")!, 48],
|
|
66
|
-
[ipv6ToBigInt("2001:db8::")!, 32],
|
|
67
|
-
[ipv6ToBigInt("2002::")!, 16],
|
|
68
|
-
[ipv6ToBigInt("3fff::")!, 20],
|
|
69
|
-
[ipv6ToBigInt("fc00::")!, 7],
|
|
70
|
-
[ipv6ToBigInt("fe80::")!, 10],
|
|
71
|
-
[ipv6ToBigInt("fec0::")!, 10],
|
|
72
|
-
[ipv6ToBigInt("ff00::")!, 8],
|
|
50
|
+
[ipv6ToBigInt("2001::"), 23],
|
|
51
|
+
[ipv6ToBigInt("2001:db8::"), 32],
|
|
52
|
+
[ipv6ToBigInt("2002::"), 16],
|
|
53
|
+
[ipv6ToBigInt("3fff::"), 20],
|
|
73
54
|
];
|
|
74
55
|
|
|
75
|
-
export const normalizeNetworkHostname = (hostname: string): string =>
|
|
76
|
-
hostname
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
56
|
+
export const normalizeNetworkHostname = (hostname: string): string => {
|
|
57
|
+
const lower = hostname.toLowerCase();
|
|
58
|
+
if (lower.startsWith("[") && lower.endsWith("]") && lower.includes(":")) return lower.slice(1, -1);
|
|
59
|
+
return lower.includes(":") ? lower : lower.replace(/\.$/, "");
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
// WHATWG's "ends in a number" rule: these hosts must parse as IPv4,
|
|
63
|
+
// rather than falling back to DNS (including invalid octal and overflow).
|
|
64
|
+
const endsInNumber = (hostname: string): boolean => {
|
|
65
|
+
const last = hostname.split(".").at(-1) ?? "";
|
|
66
|
+
return /^\d+$/.test(last) || /^0x[0-9a-f]*$/i.test(last);
|
|
67
|
+
};
|
|
80
68
|
|
|
81
|
-
|
|
69
|
+
/** Parse all URL IP literal forms without DNS or server-only dependencies. */
|
|
70
|
+
export const parseNetworkAddress = (rawAddress: string): { address: string; family: 4 | 6 } | null => {
|
|
82
71
|
const address = normalizeNetworkHostname(rawAddress);
|
|
83
|
-
|
|
84
|
-
|
|
72
|
+
const ipv6 = address.includes(":");
|
|
73
|
+
if (!address || /[\s/@\\?#%\[\]]/.test(address) || (!ipv6 && !endsInNumber(address))) return null;
|
|
74
|
+
try {
|
|
75
|
+
// URL implements legacy IPv4 (decimal, octal, hex and shortened forms),
|
|
76
|
+
// as well as IPv6 compression and dotted tails. Reject zone IDs above.
|
|
77
|
+
const parsed = new URL(`http://${ipv6 ? `[${address}]` : address}/`).hostname;
|
|
78
|
+
if (ipv6) return { address: normalizeNetworkHostname(parsed), family: 6 };
|
|
79
|
+
return ipv4ToNumber(parsed) !== null ? { address: parsed, family: 4 } : null;
|
|
80
|
+
} catch {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
85
83
|
};
|
|
86
84
|
|
|
85
|
+
export const networkAddressFamily = (rawAddress: string): 4 | 6 | null => parseNetworkAddress(rawAddress)?.family ?? null;
|
|
86
|
+
|
|
87
87
|
export const isUnsafeNetworkAddress = (rawAddress: string): boolean => {
|
|
88
|
-
const
|
|
89
|
-
|
|
90
|
-
if (family === 4) return UNSAFE_IPV4_RANGES.some(([base, bits]) => ipv4InRange(address, base, bits));
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
if (value === null) return true;
|
|
94
|
-
if (value >> 32n === 0xffffn) {
|
|
88
|
+
const literal = parseNetworkAddress(rawAddress);
|
|
89
|
+
if (!literal) return true;
|
|
90
|
+
if (literal.family === 4) return UNSAFE_IPV4_RANGES.some(([base, bits]) => ipv4InRange(literal.address, base, bits));
|
|
91
|
+
const value = ipv6ToBigInt(literal.address);
|
|
92
|
+
if (value >> 32n === 0xffffn || ipv6InRange(value, ipv6ToBigInt("64:ff9b::"), 96)) {
|
|
95
93
|
const mapped = Number(value & 0xffffffffn);
|
|
96
|
-
|
|
97
|
-
return isUnsafeNetworkAddress(ipv4);
|
|
94
|
+
return isUnsafeNetworkAddress([mapped >>> 24, (mapped >>> 16) & 255, (mapped >>> 8) & 255, mapped & 255].join("."));
|
|
98
95
|
}
|
|
99
|
-
return UNSAFE_IPV6_RANGES.some(([base, bits]) => ipv6InRange(value, base, bits));
|
|
96
|
+
return !ipv6InRange(value, ipv6ToBigInt("2000::"), 3) || UNSAFE_IPV6_RANGES.some(([base, bits]) => ipv6InRange(value, base, bits));
|
|
100
97
|
};
|
|
101
98
|
|
|
102
99
|
export const isUnsafeNetworkHostname = (rawHostname: string): boolean => {
|
|
103
100
|
const hostname = normalizeNetworkHostname(rawHostname);
|
|
104
101
|
return (
|
|
105
|
-
hostname
|
|
102
|
+
!hostname ||
|
|
103
|
+
["localhost", "localhost.localdomain", "metadata", "metadata.google.internal", "instance-data"].includes(hostname) ||
|
|
104
|
+
[".localhost", ".internal", ".local", ".home.arpa"].some((suffix) => hostname.endsWith(suffix)) ||
|
|
105
|
+
/[%\[\]]/.test(hostname) ||
|
|
106
|
+
((endsInNumber(hostname) || hostname.includes(":")) && !parseNetworkAddress(hostname))
|
|
106
107
|
);
|
|
107
108
|
};
|
package/src/ssr/Layout.tsx
CHANGED
|
@@ -11,9 +11,8 @@ import { getDateConfig } from "../server/time";
|
|
|
11
11
|
import { dates } from "../shared";
|
|
12
12
|
import { readThemePreferenceFromCookieHeader } from "../shared/theme";
|
|
13
13
|
import AnnouncementBanners from "./AnnouncementBanners";
|
|
14
|
-
import { appAppearanceStyle } from "./app-appearance";
|
|
14
|
+
import { appAppearanceStyle, resolveCurrentApp } from "./app-appearance";
|
|
15
15
|
import { resolveRuntimeRoute, visibleNavigationApps } from "./app-navigation";
|
|
16
|
-
import type { GlobalSearchHelpApp } from "./GlobalSearchHelpDialog";
|
|
17
16
|
import { getLayoutHelp } from "./help";
|
|
18
17
|
import LayoutFooter from "./LayoutFooter";
|
|
19
18
|
import LayoutHeader from "./LayoutHeader";
|
|
@@ -151,7 +150,7 @@ export default function Layout(props: LayoutProps) {
|
|
|
151
150
|
const user = c.get("user");
|
|
152
151
|
const pathname = new URL(c.req.raw.url).pathname;
|
|
153
152
|
const currentRoute = resolveRuntimeRoute(runtime.apps, pathname);
|
|
154
|
-
const currentApp =
|
|
153
|
+
const currentApp = resolveCurrentApp(runtime.apps, pathname);
|
|
155
154
|
const registeredHelp = getLayoutHelp(c);
|
|
156
155
|
const runtimeHelp = registeredHelp ? { documents: registeredHelp.documents, pageBase: registeredHelp.pageBase } : undefined;
|
|
157
156
|
const workspaceLayout = resolveAppWorkspaceLayoutForSidebar(
|
|
@@ -164,17 +163,6 @@ export default function Layout(props: LayoutProps) {
|
|
|
164
163
|
const searchLinks = Object.fromEntries(
|
|
165
164
|
runtime.apps.flatMap((app) => (visibleAppIds.has(app.id) && app.searchLinks?.length ? [[app.id, app.searchLinks] as const] : [])),
|
|
166
165
|
);
|
|
167
|
-
const searchHelpApps: GlobalSearchHelpApp[] = runtime.apps
|
|
168
|
-
.filter((app) => (app.searchTags?.length ?? 0) > 0)
|
|
169
|
-
.map((app) => ({
|
|
170
|
-
appId: app.id,
|
|
171
|
-
appName: app.name,
|
|
172
|
-
appIcon: app.icon,
|
|
173
|
-
help: app.searchHelp,
|
|
174
|
-
tags: [...new Set((app.searchTags ?? []).map((tag) => tag.toLowerCase()))],
|
|
175
|
-
tagHelp: [...(app.searchTagHelp ?? [])],
|
|
176
|
-
}))
|
|
177
|
-
.sort((a, b) => a.appName.localeCompare(b.appName));
|
|
178
166
|
const settings = c.get("settings");
|
|
179
167
|
const announcements = c.get("announcements");
|
|
180
168
|
const appName = settings?.app?.name || "Cloud";
|
|
@@ -223,7 +211,7 @@ export default function Layout(props: LayoutProps) {
|
|
|
223
211
|
theme,
|
|
224
212
|
accent: currentApp?.appearance?.accent,
|
|
225
213
|
searchLinks,
|
|
226
|
-
|
|
214
|
+
appId: currentApp?.id,
|
|
227
215
|
};
|
|
228
216
|
if (focusMode) {
|
|
229
217
|
return (
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { LocaleProvider } from "@k2b/ui";
|
|
2
|
+
import { render } from "solid-js/web";
|
|
3
|
+
import type { HelpDocumentManifest, HelpDocumentPayload } from "../shared/help";
|
|
4
|
+
import { LayoutHelpPage } from "./LayoutHelp";
|
|
5
|
+
|
|
6
|
+
const documents: HelpDocumentManifest[] = Array.from({ length: 8 }, (_, index) => ({
|
|
7
|
+
id: `topic-${index}`,
|
|
8
|
+
title: `Topic ${index}`,
|
|
9
|
+
description: "Help browser fixture",
|
|
10
|
+
order: index,
|
|
11
|
+
url: `/api/help/topic-${index}`,
|
|
12
|
+
searchUrl: "/api/help/search",
|
|
13
|
+
}));
|
|
14
|
+
const html = Array.from(
|
|
15
|
+
{ length: 8 },
|
|
16
|
+
(_, index) =>
|
|
17
|
+
`<h2 id="section-${index}" class="help-section-title" data-help-icon="ti ti-point">Section ${index}</h2>${Array.from({ length: 18 }, () => "<p>Read this guidance to finish the task and understand how the app works.</p>").join("")}`,
|
|
18
|
+
).join("");
|
|
19
|
+
window.fetch = Object.assign(
|
|
20
|
+
async (input: URL | RequestInfo) => {
|
|
21
|
+
await new Promise((resolve) => setTimeout(resolve, 80));
|
|
22
|
+
const path = String(input);
|
|
23
|
+
if (path.includes("/search")) return Response.json({ locale: "en", ids: ["topic-0", "topic-1"] });
|
|
24
|
+
const id = path.split("/").at(-1) ?? "topic-0";
|
|
25
|
+
const payload: HelpDocumentPayload = { id, locale: "en", title: id, markdown: "Help", html };
|
|
26
|
+
return Response.json(payload);
|
|
27
|
+
},
|
|
28
|
+
{ preconnect: () => {} },
|
|
29
|
+
);
|
|
30
|
+
const main = document.querySelector<HTMLElement>(".layout-content-main");
|
|
31
|
+
if (!main) throw new Error("Missing Layout main");
|
|
32
|
+
const embedded = new URL(location.href).searchParams.has("embedded");
|
|
33
|
+
const host = document.createElement("div");
|
|
34
|
+
if (embedded) host.style.cssText = "width:360px;height:700px";
|
|
35
|
+
main.append(host);
|
|
36
|
+
render(
|
|
37
|
+
() => (
|
|
38
|
+
<LocaleProvider locale="en">
|
|
39
|
+
<LayoutHelpPage documents={documents} embedded={embedded} pageBase="/app/spaces/help" />
|
|
40
|
+
</LocaleProvider>
|
|
41
|
+
),
|
|
42
|
+
host,
|
|
43
|
+
);
|