@k2b/cloud 0.22.0 → 0.24.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.
Files changed (117) hide show
  1. package/package.json +4 -3
  2. package/scripts/README.md +1 -1
  3. package/scripts/build-canvas-workers.ts +58 -0
  4. package/scripts/build.ts +4 -5
  5. package/src/_internal/canvas-worker.ts +24 -0
  6. package/src/_internal/capabilities.ts +230 -75
  7. package/src/_internal/define-app.ts +90 -7
  8. package/src/_internal/fixtures/filesv2-manifest-cloud-v0.29.0.json +887 -0
  9. package/src/_internal/page-responses.ts +20 -4
  10. package/src/_internal/registry-validation.ts +6 -0
  11. package/src/_internal/registry.ts +78 -3
  12. package/src/_internal/runtime-context.ts +1 -0
  13. package/src/_internal/static-assets.ts +18 -3
  14. package/src/access/ResourceApiKeys.tsx +30 -30
  15. package/src/ai/capability-execution.ts +5 -0
  16. package/src/ai/chat/blocks.tsx +2 -1
  17. package/src/ai/chat/messages.ts +6 -0
  18. package/src/ai/client/controller.ts +80 -21
  19. package/src/ai/client/transport.ts +47 -3
  20. package/src/ai/code-mode-skill.ts +2 -2
  21. package/src/ai/index.ts +0 -7
  22. package/src/ai/live-events.ts +1 -262
  23. package/src/ai/live.ts +31 -2
  24. package/src/ai/migrate.ts +35 -16
  25. package/src/ai/pdf-render.ts +2 -22
  26. package/src/ai/routes.ts +5 -4
  27. package/src/ai/runtime.ts +1 -6
  28. package/src/ai/solid.ts +0 -5
  29. package/src/ai/stream.ts +89 -65
  30. package/src/api/app-approval.ts +5 -1
  31. package/src/api/help.ts +2 -2
  32. package/src/api/index.ts +3 -0
  33. package/src/api/me.ts +19 -5
  34. package/src/api/pwa-phone.ts +168 -0
  35. package/src/api/pwa.ts +179 -0
  36. package/src/api/search/schemas.ts +34 -0
  37. package/src/api/search.ts +118 -50
  38. package/src/browser/CloudResourceSearch.browser-harness.tsx +11 -0
  39. package/src/browser/CloudResourceSearch.tsx +208 -80
  40. package/src/browser/app-session.ts +69 -0
  41. package/src/browser/live-connection.ts +211 -0
  42. package/src/browser/live-websocket.ts +31 -11
  43. package/src/browser/live.ts +2 -0
  44. package/src/browser/resource-search-messages.ts +15 -2
  45. package/src/browser/search-stream.ts +151 -0
  46. package/src/contracts/app.ts +10 -0
  47. package/src/contracts/capabilities.ts +89 -24
  48. package/src/contracts/capability-compatibility.ts +28 -14
  49. package/src/contracts/file-provider.ts +158 -0
  50. package/src/contracts/index.ts +2 -0
  51. package/src/contracts/pwa-paths.ts +6 -0
  52. package/src/contracts/pwa.ts +156 -0
  53. package/src/contracts/registry.ts +7 -0
  54. package/src/contracts/shared.ts +7 -1
  55. package/src/events/index.ts +2 -0
  56. package/src/events/live-engine.ts +830 -0
  57. package/src/events/live-protocol.ts +31 -0
  58. package/src/events/live.ts +298 -0
  59. package/src/server/actor.ts +7 -0
  60. package/src/server/api-client.ts +23 -2
  61. package/src/server/help.ts +3 -3
  62. package/src/server/index.ts +3 -1
  63. package/src/server/middleware/auth.ts +80 -8
  64. package/src/server/middleware/openapi.ts +2 -1
  65. package/src/server/services/access.ts +49 -0
  66. package/src/server/services/index.ts +1 -0
  67. package/src/services/app-approval.ts +6 -25
  68. package/src/services/audit/index.ts +2 -0
  69. package/src/services/branding/app-icon-source.ts +40 -0
  70. package/src/services/branding/app-icons.ts +112 -0
  71. package/src/services/branding/icon-render-worker.ts +69 -0
  72. package/src/services/branding/icon-svg.ts +22 -0
  73. package/src/services/identity/invocation-actor.ts +8 -5
  74. package/src/services/identity/invocation-authority.ts +1 -0
  75. package/src/services/identity/invocation-token.ts +6 -0
  76. package/src/services/index.ts +1 -0
  77. package/src/services/outbox.ts +114 -80
  78. package/src/services/pairing-secret.ts +16 -0
  79. package/src/services/pdf/markdown.ts +22 -2
  80. package/src/services/pwa-devices.ts +640 -0
  81. package/src/services/session/index.ts +212 -57
  82. package/src/services/session/recent.ts +42 -0
  83. package/src/services/session/user.ts +15 -5
  84. package/src/shared/index.ts +1 -0
  85. package/src/{browser → shared}/locale-preference.ts +1 -1
  86. package/src/shared/markdown/formula.ts +122 -26
  87. package/src/shared/markdown/index.ts +27 -23
  88. package/src/ssr/AppLaunchpad.island.tsx +8 -1
  89. package/src/ssr/AppLaunchpadPanel.tsx +1 -1
  90. package/src/ssr/GlobalAnnouncements.island.tsx +1 -1
  91. package/src/ssr/Layout.tsx +10 -12
  92. package/src/ssr/LayoutHeader.tsx +14 -2
  93. package/src/ssr/LayoutHelp.tsx +2 -4
  94. package/src/ssr/LayoutRail.tsx +13 -4
  95. package/src/ssr/MinimalLayoutPreferences.island.tsx +1 -1
  96. package/src/ssr/MobileProfileActions.tsx +19 -13
  97. package/src/ssr/PageError.tsx +23 -1
  98. package/src/ssr/ProfilePreferences.island.tsx +9 -1
  99. package/src/ssr/PwaLayout.tsx +70 -0
  100. package/src/ssr/PwaRuntime.island.tsx +105 -0
  101. package/src/ssr/TimezoneCookie.island.tsx +7 -1
  102. package/src/ssr/app-navigation.ts +25 -10
  103. package/src/ssr/index.ts +8 -1
  104. package/src/ssr/layout-context.ts +1 -1
  105. package/src/ssr/preference-controller.ts +2 -2
  106. package/src/ssr/profile-actions.ts +3 -1
  107. package/src/ssr/profile-preferences-messages.ts +2 -0
  108. package/src/ssr/pwa-messages.ts +24 -0
  109. package/src/styles/global.css +6 -0
  110. package/src/styles/resource-search.css +19 -0
  111. package/src/styles/utilities-feedback.css +2 -2
  112. package/scripts/build-pdf-renderer.ts +0 -39
  113. package/src/ai/client/live-connection.ts +0 -141
  114. package/src/ai/live-messages.ts +0 -45
  115. package/src/ai/live-outbox.ts +0 -102
  116. package/src/ai/live-routes.ts +0 -412
  117. package/src/shared/markdown/extensions/info-blocks.ts +0 -108
@@ -1,3 +1,4 @@
1
+ import { startLiveOutbox } from "../events/live";
1
2
  import { registerHelp } from "../services/help";
2
3
  import { preloadLayoutHelp } from "../ssr/help";
3
4
  import { invokeCapabilityStream } from "./capability-streams";
@@ -26,6 +27,7 @@ import type {
26
27
  AppLifecycle,
27
28
  AppMeta,
28
29
  AppPresentationCatalog,
30
+ AppPwaPart,
29
31
  AppSearchLink,
30
32
  CloudLifecycleContext,
31
33
  WidgetEndpoint,
@@ -38,6 +40,7 @@ import {
38
40
  CapabilityOriginSchema,
39
41
  } from "../contracts/capabilities";
40
42
  import { type BoundNotificationMap, bindNotificationDefinitions, type NotificationDefinitionMap } from "../contracts/notification-types";
43
+ import { isPwaPartId, PWA_AUTH_PATH, PWA_CANVAS_COLORS, PWA_MANIFEST_PATH, PWA_SHELL_APP_ID } from "../contracts/pwa";
41
44
  import type { AppRegistryEntry } from "../contracts/registry";
42
45
  import type { AppSettingsMap, KindToType } from "../contracts/settings-types";
43
46
  import type { Role } from "../contracts/shared";
@@ -59,6 +62,7 @@ import { normalizeInvocationRequestId } from "../services/identity/invocation-to
59
62
  import { logger } from "../services/logging";
60
63
  import { startNotificationDefinitionRegistration } from "../services/notifications/catalog";
61
64
  import { readRailSnapshot } from "../services/rail-snapshot";
65
+ import { isAppPagePath } from "../services/session";
62
66
  import { get, loadCache as loadSettingsCache, set } from "../services/settings";
63
67
  import { createSettingsAPI, type SettingsAPI } from "../services/settings/api";
64
68
  import { registerSettings, toLegacySettingDefs } from "../services/settings/defaults";
@@ -95,6 +99,8 @@ export type PageOptions = {
95
99
  theme?: "light" | "dark";
96
100
  /** Framework-resolved BCP 47 document language — always the canonical request locale, never a page override. */
97
101
  lang?: string;
102
+ /** Renders the document as a page of the installable mobile app; set by `PwaLayout`. */
103
+ pwa?: { name: string };
98
104
  };
99
105
 
100
106
  type ResolvedPageOptions = PageOptions & { performanceRoute?: string };
@@ -162,6 +168,16 @@ export type AppOptions<S extends AppSettingsMap = {}, N extends NotificationDefi
162
168
  legalLinks?: ReadonlyArray<{ label: string; href: string; icon?: string }>;
163
169
  /** Static destinations for the global search UI; no capabilities are created. */
164
170
  searchLinks?: readonly AppSearchLink[];
171
+ /**
172
+ * Preview: pages of this app in the installable mobile app. Declaring it adds
173
+ * `/pwa/<id>` to the gateway routes and lists the app in the mobile app,
174
+ * with its name, icon and description. Serve the pages below `/pwa/<id>`
175
+ * with `PwaLayout` and `ssr.pwaAccess`. `routes` never lists `/pwa` paths.
176
+ */
177
+ pwa?: {
178
+ /** Coarse visibility in the mobile app, like `nav.requiresRoles`. Routes and services still authorize. */
179
+ requiresRoles?: Role[];
180
+ };
165
181
  /**
166
182
  * Dashboard widget endpoints this app exposes. Each entry references an
167
183
  * HTTP path on this app that returns a `WidgetResponse`. The dashboard
@@ -312,6 +328,32 @@ export type AppDefinition<S extends AppSettingsMap = {}, N extends NotificationD
312
328
 
313
329
  // ── Implementation ──────────────────────────────────────────────────────────
314
330
 
331
+ const isPwaPath = (route: string): boolean => route === "/pwa" || route.startsWith("/pwa/");
332
+ /** A route as the gateway reads it: trimmed and without empty segments, so `//pwa/x/` is `/pwa/x`. */
333
+ const canonicalRoute = (route: string): string => {
334
+ const trimmed = route.trim();
335
+ return trimmed.startsWith("/") ? `/${trimmed.split("/").filter(Boolean).join("/")}` : trimmed;
336
+ };
337
+
338
+ /**
339
+ * `/pwa` belongs to the mobile app shell, `/pwa/_auth` to Core, and `/pwa/<id>`
340
+ * to the app that declares `pwa`. A part never claims a path by hand, so no app
341
+ * can take the shell's pages or another app's part.
342
+ */
343
+ const validateAppPwaPart = (opts: Pick<AppOptions, "id" | "routes" | "pwa">): AppPwaPart | undefined => {
344
+ for (const route of opts.routes) {
345
+ const path = canonicalRoute(route);
346
+ if (!isPwaPath(path)) continue;
347
+ if ((path === "/pwa" && opts.id === PWA_SHELL_APP_ID) || (path === PWA_AUTH_PATH && opts.id === "core")) continue;
348
+ throw new Error(`App "${opts.id}" lists route "${route}"; /pwa is reserved for the mobile app, declare pwa: {} to get /pwa/${opts.id}`);
349
+ }
350
+ if (!opts.pwa) return undefined;
351
+ if (!isPwaPartId(opts.id)) {
352
+ throw new Error(`App "${opts.id}" cannot declare pwa; a part needs a plain id other than pwa, settings and offline`);
353
+ }
354
+ return { href: `/pwa/${opts.id}`, requiresRoles: opts.pwa.requiresRoles ? [...opts.pwa.requiresRoles] : undefined };
355
+ };
356
+
315
357
  export const defineApp = <
316
358
  const S extends AppSettingsMap = {},
317
359
  const N extends NotificationDefinitionMap = {},
@@ -323,6 +365,7 @@ export const defineApp = <
323
365
  const isDevelopment = env.IS_DEVELOPMENT;
324
366
  const notifications = bindNotificationDefinitions(opts.id, opts.notifications);
325
367
  const cliModules = validateAppCliModules(opts.id, opts.cli);
368
+ const pwaPart = validateAppPwaPart(opts);
326
369
 
327
370
  // ── 0. Register declared settings into the runtime registry ──────────
328
371
  // SETTINGS_MAP is the single source of truth for validation in store.ts
@@ -341,19 +384,43 @@ export const defineApp = <
341
384
  rootDir,
342
385
  componentRoots: [resolve(env.APP_DIR ?? rootDir, "src"), fileURLToPath(new URL("../", import.meta.url))],
343
386
  basePath: opts.basePath,
344
- template: ({ body, scripts, title, description, theme, lang, performanceRoute }) => {
387
+ template: ({ body, scripts, title, description, theme, lang, performanceRoute, pwa }) => {
345
388
  const themeFixed = theme !== undefined;
389
+ // App pages paint the canvas before any stylesheet and give it to the
390
+ // status bar. No status-bar style: `black-translucent` lays a blur band
391
+ // over the top edge on iOS 26.
392
+ const canvas = PWA_CANVAS_COLORS[theme ?? "light"];
393
+ const head = pwa
394
+ ? {
395
+ html: ` style="background-color:${canvas}"`,
396
+ viewport: "width=device-width, initial-scale=1, viewport-fit=cover",
397
+ app: `<meta name="theme-color" content="${canvas}">
398
+ <link rel="manifest" href="${PWA_MANIFEST_PATH}">
399
+ <link rel="apple-touch-icon" href="/branding/apple-touch-icon.png">
400
+ <meta name="apple-mobile-web-app-title" content="${escapeHtml(pwa.name)}">`,
401
+ // The web cross-fades between pages; an app page swaps at once. The browser ignores taps while a
402
+ // cross-document transition runs, so a tab tap right after the last one would be lost. After
403
+ // global.css, whose `navigation: auto` it overrides.
404
+ styles: `
405
+ <style data-cloud-app-navigation>@view-transition{navigation:none}</style>`,
406
+ }
407
+ : {
408
+ html: "",
409
+ viewport: "width=device-width, initial-scale=1.0",
410
+ app: `<meta name="theme-color" content="#09090b">
411
+ <meta name="mobile-web-app-capable" content="yes">`,
412
+ styles: "",
413
+ };
346
414
  // The inline layer statement fixes the cascade order before any
347
415
  // stylesheet declares a layer. Tailwind's `properties` layer resets
348
416
  // `--tw-*` in browsers without `@property` and must stay lowest.
349
417
  return `<!DOCTYPE html>
350
- <html lang="${normalizeLocale(lang)}" class="${theme ?? "light"}"${themeFixed ? " data-theme-fixed" : ""}>
351
- <head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0">
418
+ <html lang="${normalizeLocale(lang)}" class="${theme ?? "light"}"${themeFixed ? " data-theme-fixed" : ""}${head.html}>
419
+ <head><meta charset="UTF-8"><meta name="viewport" content="${head.viewport}">
352
420
  <meta name="view-transition" content="same-origin">
353
421
  <title>${escapeHtml(title ?? "Cloud")}</title>
354
422
  <meta name="description" content="${escapeHtml(description ?? "Cloud workspace")}">
355
- <meta name="theme-color" content="#09090b">
356
- <meta name="mobile-web-app-capable" content="yes">
423
+ ${head.app}
357
424
  <link rel="icon" href="${appFaviconHref(opts.id, v)}">
358
425
  <style data-cloud-css-layers>@layer properties, theme, base, components, utilities;</style>
359
426
  <link rel="preload" href="/public/tabler-icons.woff2" as="font" type="font/woff2" crossorigin>
@@ -361,7 +428,7 @@ export const defineApp = <
361
428
  <link rel="stylesheet" href="/public/fonts.css?v=${v}">
362
429
  <link rel="stylesheet" href="/public/tabler-icons.css?v=${v}">
363
430
  <link rel="stylesheet" href="/public/${opts.id}/app.css?v=${v}">
364
- <link rel="stylesheet" href="/public/global.css?v=${v}">
431
+ <link rel="stylesheet" href="/public/global.css?v=${v}">${head.styles}
365
432
  <script>${themeBootstrapScript}</script>
366
433
  </head>
367
434
  <body class="k2b-ui" data-k2b-app-workspace-controller="global">
@@ -389,6 +456,9 @@ export const defineApp = <
389
456
  const user = c.get("user");
390
457
  c.get("page").performanceRoute =
391
458
  user && c.get("settings")?.observability?.web_vitals?.enabled === true ? (matchedRouteTemplate(c) ?? undefined) : undefined;
459
+ // Pages of the mobile app render in PwaLayout, which shows no announcements, Help, or rail; reading them would
460
+ // only delay every tab switch. Core's pages below /pwa/_auth, such as its not-found page, keep the web layout.
461
+ if (isAppPagePath(c.req.path)) return;
392
462
  await Promise.all([
393
463
  preloadLayoutAnnouncements(c),
394
464
  preloadLayoutHelp(c),
@@ -414,10 +484,11 @@ export const defineApp = <
414
484
  links: group.links.map((link) => ({ ...link })),
415
485
  }),
416
486
  ),
417
- routes: [...opts.routes, ...cliPluginRoutePrefixes(cliModules)],
487
+ routes: [...opts.routes, ...cliPluginRoutePrefixes(cliModules), ...(pwaPart ? [pwaPart.href] : [])],
418
488
  nav: opts.nav,
419
489
  legalLinks: opts.legalLinks ? [...opts.legalLinks] : undefined,
420
490
  searchLinks: opts.searchLinks?.map((link) => ({ ...link, keywords: link.keywords ? [...link.keywords] : undefined })),
491
+ pwa: pwaPart,
421
492
  widgets: opts.widgets ? opts.widgets.map((w) => ({ ...w })) : undefined,
422
493
  settingKeys: opts.settings ? Object.keys(opts.settings) : undefined,
423
494
  openapi: opts.openapi,
@@ -512,6 +583,9 @@ export const defineApp = <
512
583
  help: compiledHelp?.summary,
513
584
  legalLinks: meta.legalLinks ? meta.legalLinks.map((l) => ({ ...l })) : undefined,
514
585
  searchLinks: meta.searchLinks,
586
+ pwa: meta.pwa
587
+ ? { href: meta.pwa.href, requiresRoles: meta.pwa.requiresRoles ? [...meta.pwa.requiresRoles] : undefined }
588
+ : undefined,
515
589
  widgets: meta.widgets ? meta.widgets.map((w) => ({ ...w })) : undefined,
516
590
  settingKeys: meta.settingKeys ? [...meta.settingKeys] : undefined,
517
591
  openapi: advertiseOpenapi ? opts.openapi : undefined,
@@ -812,10 +886,19 @@ export const defineApp = <
812
886
  const stopLifecycle = startOpts.lifecycle?.stop;
813
887
  if (stopLifecycle) cleanup.push(() => stopLifecycle(cloudCtx));
814
888
 
889
+ const startOutbox = async () => {
890
+ const stopLiveOutbox = await startLiveOutbox(meta.id);
891
+ if (stopLiveOutbox) cleanup.push(stopLiveOutbox);
892
+ };
893
+ // An application fails before any setup when Core has not created the platform outbox yet.
894
+ // Core creates it in its own setup, so it starts publishing after that.
895
+ if (meta.id !== "core") await startOutbox();
896
+
815
897
  if (!startOpts.skipSetup && startOpts.lifecycle?.setup) {
816
898
  log.info(`Setup: ${meta.id}`);
817
899
  await startOpts.lifecycle.setup(cloudCtx);
818
900
  }
901
+ if (meta.id === "core") await startOutbox();
819
902
 
820
903
  stopNotificationRegistration = await startNotificationDefinitionRegistration(meta.id, notifications);
821
904