@k2b/cloud 0.6.0-rc.0 → 0.7.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/README.md +2 -2
- package/package.json +30 -17
- package/scripts/browser-performance.ts +14 -0
- package/scripts/build-pdf-renderer.ts +39 -0
- package/scripts/build.ts +15 -35
- package/scripts/preload.ts +9 -12
- package/scripts/sync-recovery-smoke.ts +2 -1
- package/src/_internal/app-presentation.ts +14 -2
- package/src/_internal/capabilities.ts +108 -7
- package/src/_internal/capability-streams.ts +81 -0
- package/src/_internal/define-app.ts +62 -36
- package/src/_internal/heartbeat.ts +4 -0
- package/src/_internal/help-catalog.ts +27 -172
- package/src/_internal/help.ts +13 -76
- package/src/_internal/page-responses.ts +2 -0
- package/src/_internal/postgres-application-name.ts +30 -0
- package/src/_internal/process-identity.ts +15 -0
- package/src/_internal/process-sync.ts +1 -1
- package/src/_internal/registry-validation.ts +29 -25
- package/src/_internal/registry.ts +2 -53
- package/src/_internal/runtime-context.ts +5 -16
- package/src/_internal/server-timing.ts +13 -0
- package/src/_internal/status-preserving-ssr.ts +18 -6
- package/src/_internal/web-vitals-asset.ts +10 -0
- package/src/access/PermissionEditor.tsx +14 -153
- package/src/access/PrincipalPicker.tsx +104 -0
- package/src/access/ui.ts +2 -0
- package/src/ai/admin.ts +14 -13
- package/src/ai/assistant-models.ts +12 -1
- package/src/ai/audio-format.ts +34 -0
- package/src/ai/audio-tool.ts +90 -0
- package/src/ai/browser-code-contracts.ts +79 -0
- package/src/ai/browser.ts +2 -0
- package/src/ai/capabilities.ts +55 -36
- package/src/ai/capability-execution.ts +47 -12
- package/src/ai/chat/blocks.tsx +157 -95
- package/src/ai/chat/capability-result.ts +15 -0
- package/src/ai/chat/capability-table.tsx +2 -10
- package/src/ai/chat/composer-adapter.ts +48 -3
- package/src/ai/chat/message-actions.tsx +8 -7
- package/src/ai/chat/message-utils.ts +12 -1
- package/src/ai/chat/messages.ts +2 -0
- package/src/ai/chat/presentation.tsx +19 -48
- package/src/ai/chat/primitives.tsx +43 -1
- package/src/ai/chat/tool-disclosure.tsx +17 -2
- package/src/ai/chat/tool-groups.ts +52 -0
- package/src/ai/chat-quotas.ts +25 -0
- package/src/ai/chat-task-contracts.ts +9 -1
- package/src/ai/chat-tasks.ts +276 -43
- package/src/ai/client/controller.ts +110 -33
- package/src/ai/client/file-source.ts +12 -6
- package/src/ai/client/live-connection.ts +1 -0
- package/src/ai/client/projection.ts +22 -11
- package/src/ai/client/transport.ts +4 -0
- package/src/ai/code-capability-routes.ts +110 -0
- package/src/ai/code-capability-transport.ts +38 -0
- package/src/ai/code-mode-skill.ts +114 -0
- package/src/ai/code-runtime-tools.ts +166 -0
- package/src/ai/code-source-contracts.ts +202 -0
- package/src/ai/code-source-tools.ts +145 -0
- package/src/ai/compaction.ts +27 -6
- package/src/ai/data-analysis-skill.ts +10 -0
- package/src/ai/default-tools.ts +25 -2
- package/src/ai/dictation-runtime.ts +171 -0
- package/src/ai/dictations.ts +188 -0
- package/src/ai/draft-content.ts +34 -0
- package/src/ai/enrich.ts +6 -0
- package/src/ai/executor.ts +191 -31
- package/src/ai/file-content-version.ts +22 -0
- package/src/ai/file-context.ts +2 -1
- package/src/ai/file-media-type.ts +12 -0
- package/src/ai/file-reference-contracts.ts +15 -0
- package/src/ai/file-tools.ts +2 -1
- package/src/ai/files-store.ts +174 -71
- package/src/ai/fixtures/vision-encrypted.pdf +0 -0
- package/src/ai/grids-skill.ts +40 -10
- package/src/ai/http.ts +15 -8
- package/src/ai/index.ts +26 -2
- package/src/ai/inference-calls.ts +198 -0
- package/src/ai/live-events.ts +5 -1
- package/src/ai/memory-learning.ts +60 -28
- package/src/ai/message-queue.ts +167 -0
- package/src/ai/migrate.ts +187 -11
- package/src/ai/model-pricing.ts +43 -0
- package/src/ai/pdf-render-worker.ts +74 -0
- package/src/ai/pdf-render.fixture.ts +28 -0
- package/src/ai/pdf-render.ts +77 -0
- package/src/ai/projects-routes.ts +3 -3
- package/src/ai/projects.ts +20 -6
- package/src/ai/protocol.ts +3 -0
- package/src/ai/provider.ts +1 -8
- package/src/ai/quota-provider.ts +191 -0
- package/src/ai/quota-report.ts +152 -0
- package/src/ai/quotas-migrate.ts +45 -0
- package/src/ai/quotas.ts +168 -0
- package/src/ai/routes.ts +215 -11
- package/src/ai/run-timeout.ts +10 -0
- package/src/ai/runtime-tools.ts +3 -0
- package/src/ai/runtime.ts +55 -16
- package/src/ai/settings.ts +36 -8
- package/src/ai/short-id.ts +2 -2
- package/src/ai/skill-catalog.ts +0 -10
- package/src/ai/skill-search.ts +47 -0
- package/src/ai/skill-seeds.ts +105 -20
- package/src/ai/skill-tool.ts +16 -5
- package/src/ai/skills-routes.ts +32 -4
- package/src/ai/skills.ts +324 -155
- package/src/ai/store.ts +373 -191
- package/src/ai/stream.ts +5 -0
- package/src/ai/structured-runs.ts +4 -3
- package/src/ai/structured.ts +8 -1
- package/src/ai/system-prompt.ts +12 -2
- package/src/ai/timeline.ts +2 -5
- package/src/ai/todo-contracts.ts +20 -0
- package/src/ai/todo-tool.ts +16 -0
- package/src/ai/tool-audit.ts +30 -40
- package/src/ai/tools.ts +6 -0
- package/src/ai/transcription.ts +202 -0
- package/src/ai/turn-timing.ts +114 -0
- package/src/ai/types.ts +58 -10
- package/src/ai/ui.tsx +2 -0
- package/src/ai/usage.ts +118 -118
- package/src/ai/vision-tool.ts +39 -10
- package/src/api/admin-ai-projects.ts +2 -2
- package/src/api/admin-ai-quotas.ts +74 -0
- package/src/api/admin-ai-skills.ts +48 -5
- package/src/api/admin-ai-usage.ts +2 -2
- package/src/api/admin-core-settings.ts +25 -0
- package/src/api/admin-identity.ts +91 -51
- package/src/api/admin-rail.ts +58 -0
- package/src/api/announcements.ts +14 -0
- package/src/api/capabilities.ts +292 -43
- package/src/api/capability-streams.ts +262 -0
- package/src/api/help.ts +8 -44
- package/src/api/index.ts +6 -0
- package/src/api/mcp.ts +44 -26
- package/src/api/me-web-vitals.ts +35 -0
- package/src/api/me.ts +2 -0
- package/src/api/search/schemas.ts +26 -15
- package/src/api/search.bench.ts +1 -1
- package/src/api/search.ts +54 -17
- package/src/browser/CloudResourceSearch.tsx +640 -702
- package/src/browser/app-approval-vault.ts +19 -7
- package/src/browser/command-bridge.ts +109 -0
- package/src/browser/command-shortcuts.ts +76 -0
- package/src/browser/commands.ts +66 -0
- package/src/browser/navigation-search.ts +48 -0
- package/src/browser/notifications.ts +1 -1
- package/src/browser/resource-picker.tsx +5 -18
- package/src/browser/resource-search-dialog.ts +9 -0
- package/src/browser/resource-search-input.ts +46 -0
- package/src/browser/resource-search-messages.ts +76 -40
- package/src/browser/resource-search.ts +7 -0
- package/src/browser/search-bridge.ts +102 -0
- package/src/browser/search-commands.ts +59 -0
- package/src/browser/search.ts +13 -0
- package/src/browser/spotlight-position.ts +208 -0
- package/src/browser/testing.ts +5 -0
- package/src/browser/web-vitals.ts +25 -0
- package/src/capabilities/claims.ts +179 -0
- package/src/capabilities/client.ts +34 -0
- package/src/capabilities/command-link.ts +16 -0
- package/src/capabilities/executions.ts +298 -0
- package/src/capabilities/migrate.ts +81 -0
- package/src/capabilities/server.ts +114 -34
- package/src/capabilities/stream-body.ts +16 -0
- package/src/capabilities/streams.ts +28 -0
- package/src/cli/admin/ai-quotas.ts +161 -0
- package/src/cli/admin/ai-skills.ts +64 -0
- package/src/cli/admin/ai-usage.ts +53 -30
- package/src/cli/admin/app-credentials.ts +79 -0
- package/src/cli/admin/data.ts +17 -0
- package/src/cli/admin/index.ts +9 -0
- package/src/cli/capabilities.ts +29 -0
- package/src/cli/capability-streams.ts +25 -0
- package/src/cli/index.ts +3 -2
- package/src/config/env.ts +11 -8
- package/src/contracts/app.ts +23 -3
- package/src/contracts/capabilities.ts +68 -5
- package/src/contracts/capability-streams.ts +46 -0
- package/src/contracts/commands.ts +49 -0
- package/src/contracts/index.ts +6 -1
- package/src/contracts/rail-admin.ts +38 -0
- package/src/contracts/rail-preferences.ts +16 -0
- package/src/contracts/registry.ts +3 -27
- package/src/contracts/settings-types.ts +1 -1
- package/src/contracts/shared.ts +9 -5
- package/src/contracts/web-vitals.ts +27 -0
- package/src/desktop/solid.tsx +3 -3
- package/src/index.ts +0 -4
- package/src/server/index.ts +3 -0
- package/src/server/middleware/auth.ts +3 -2
- package/src/server/middleware/route-template.ts +9 -3
- package/src/server/middleware/runtime.ts +2 -1
- package/src/server/middleware/settings.ts +17 -11
- package/src/server/services/access-revision.ts +10 -0
- package/src/server/services/access.ts +1 -1
- package/src/server/services/index.ts +1 -0
- package/src/services/account-category-policy.ts +9 -0
- package/src/services/account-lifecycle/audit.ts +1 -1
- package/src/services/accounts/app.ts +12 -2
- package/src/services/accounts/email-write.ts +23 -0
- package/src/services/accounts/groups.ts +15 -4
- package/src/services/accounts/identities.ts +249 -0
- package/src/services/accounts/identity-reconciliation.ts +87 -0
- package/src/services/accounts/ipa-data.ts +2 -2
- package/src/services/accounts/local-groups.ts +2 -2
- package/src/services/accounts/posix.ts +45 -29
- package/src/services/accounts/users.ts +2 -2
- package/src/services/announcements/index.ts +69 -8
- package/src/services/audit/index.ts +1 -1
- package/src/services/cache-fill.ts +35 -0
- package/src/services/help/index.ts +109 -0
- package/src/services/help/maintenance.ts +25 -0
- package/src/services/help/store.ts +180 -0
- package/src/services/help/types.ts +46 -0
- package/src/services/identity/invocation-actor.ts +2 -2
- package/src/services/identity/key-config.ts +2 -1
- package/src/services/identity/key-ring.ts +1 -1
- package/src/services/index.ts +15 -0
- package/src/services/ipa/users.ts +268 -221
- package/src/services/legal-consent.ts +1 -1
- package/src/services/logging/index.ts +15 -13
- package/src/services/logging/trace.ts +2 -1
- package/src/services/logging/web-vitals.ts +81 -0
- package/src/services/mandates/index.ts +18 -4
- package/src/services/mandates/policy.ts +65 -2
- package/src/services/notifications/batches.ts +124 -57
- package/src/services/notifications/browser.ts +12 -0
- package/src/services/notifications/index.ts +0 -2
- package/src/services/notifications/platform.ts +0 -4
- package/src/services/oauth-tokens.ts +3 -3
- package/src/services/pdf/gotenberg.ts +107 -84
- package/src/services/pdf/index.ts +3 -0
- package/src/services/providers/local/users.ts +25 -16
- package/src/services/public-http.ts +101 -0
- package/src/services/rail-shortcuts.ts +95 -0
- package/src/services/rail-snapshot.ts +53 -0
- package/src/services/request-cache-redis.ts +28 -0
- package/src/services/session/index.ts +15 -2
- package/src/services/session/user.ts +62 -37
- package/src/services/settings/core-settings.ts +21 -28
- package/src/services/settings/defaults.ts +5 -0
- package/src/services/settings/store.ts +58 -84
- package/src/shared/ai-costs.ts +42 -0
- package/src/shared/ai-platform-prompt.ts +1 -1
- package/src/shared/ai-quotas.ts +136 -0
- package/src/shared/ai-usage.ts +6 -16
- package/src/shared/app-presentation.ts +21 -15
- package/src/shared/capability-messages.ts +14 -0
- package/src/shared/help.ts +6 -42
- package/src/shared/index.ts +6 -0
- package/src/ssr/AdminLayout.tsx +7 -5
- package/src/ssr/AdminSidebar.tsx +29 -15
- package/src/ssr/AppLaunchpad.island.tsx +24 -137
- package/src/ssr/AppLaunchpadPanel.tsx +181 -0
- package/src/ssr/BrowserPushRegistration.island.tsx +12 -0
- package/src/ssr/GlobalSearchDialog.tsx +167 -20
- package/src/ssr/GlobalSearchTrigger.island.tsx +33 -22
- package/src/ssr/Layout.tsx +17 -6
- package/src/ssr/LayoutBreadcrumbs.island.tsx +3 -5
- package/src/ssr/LayoutFooter.tsx +1 -1
- package/src/ssr/LayoutHeader.tsx +46 -14
- package/src/ssr/LayoutHelp.tsx +23 -22
- package/src/ssr/{HotkeysHelpRail.island.tsx → LayoutHelpTrigger.island.tsx} +18 -12
- package/src/ssr/LayoutRail.tsx +17 -9
- package/src/ssr/MobileNavigation.tsx +71 -0
- package/src/ssr/MobileProfileActions.tsx +49 -0
- package/src/ssr/ProfilePreferences.island.tsx +3 -36
- package/src/ssr/RailApps.island.tsx +2 -2
- package/src/ssr/RailEditor.tsx +17 -2
- package/src/ssr/WorkspaceNavigation.island.tsx +7 -0
- package/src/ssr/WorkspaceNavigationProvider.tsx +16 -0
- package/src/ssr/admin-navigation.ts +7 -1
- package/src/ssr/help.ts +29 -0
- package/src/ssr/islands/SearchBar.tsx +67 -0
- package/src/ssr/islands/index.ts +3 -1
- package/src/ssr/layout-context.ts +2 -2
- package/src/ssr/layout-help-search.ts +15 -0
- package/src/ssr/mobile-menu-history.ts +70 -0
- package/src/ssr/platform-messages.ts +16 -8
- package/src/ssr/profile-actions.ts +42 -0
- package/src/ssr/rail-context.ts +8 -4
- package/src/ssr/rail-messages.ts +2 -0
- package/src/ssr/rail-navigation.ts +3 -3
- package/src/ssr/workspace-navigation.ts +75 -0
- package/src/styles/effects.css +6 -6
- package/src/styles/global.css +54 -0
- package/src/styles/input.css +43 -41
- package/src/styles/resource-search.css +505 -0
- package/src/styles/utilities-feedback.css +13 -0
- package/src/styles/utilities-layout.css +1 -5
- package/src/styles/utilities-navigation.css +3 -39
- package/src/types/ambient.d.ts +5 -0
- package/src/workflows/ai/runtime.ts +5 -1
- package/src/workflows/ai/store.ts +2 -0
- package/src/workflows/ai-actions.ts +16 -13
- package/src/workflows/definition.ts +10 -5
- package/src/workflows/store/actions.ts +44 -20
- package/src/workflows/store/index.ts +2 -0
- package/src/workflows/store/runs.ts +36 -0
- package/src/workflows/store/worker-pool.ts +55 -0
- package/src/workflows/store/worker-runtime.ts +90 -0
- package/scripts/app-favicon.test.ts +0 -49
- package/src/ai/chat/blocks.render.test.tsx +0 -1108
- package/src/ai/kit-skill.ts +0 -22
- package/src/ai/usage-migrate.ts +0 -81
- package/src/browser/CloudResourceSearch.behavior.test.tsx +0 -74
- package/src/browser/notification-target.ts +0 -27
- package/src/contracts/notification-live.ts +0 -66
- package/src/services/notifications/live.ts +0 -50
- package/src/ssr/BrowserNotifications.island.tsx +0 -135
- package/src/ssr/GlobalSearchDialog.behavior.test.tsx +0 -96
- package/src/ssr/Layout.render.test.tsx +0 -148
- package/src/ssr/islands/SearchBar.island.tsx +0 -80
|
@@ -11,6 +11,8 @@ import {
|
|
|
11
11
|
type CapabilityActionReviewResult,
|
|
12
12
|
CapabilityActionReviewSchema,
|
|
13
13
|
type CapabilityDefinitions,
|
|
14
|
+
type CapabilityCommandManifest,
|
|
15
|
+
CapabilityCommandManifestSchema,
|
|
14
16
|
type CapabilityError,
|
|
15
17
|
CapabilityErrorSchema,
|
|
16
18
|
type CapabilityExecutionContext,
|
|
@@ -190,12 +192,14 @@ const compileSchemaPresentation = (
|
|
|
190
192
|
|
|
191
193
|
const compileOperationPresentation = (
|
|
192
194
|
value: unknown,
|
|
193
|
-
operation: CapabilityQueryManifest | CapabilityActionManifest,
|
|
195
|
+
operation: CapabilityQueryManifest | CapabilityActionManifest | CapabilityCommandManifest,
|
|
194
196
|
label: string,
|
|
195
197
|
): CapabilityOperationPresentationTranslation => {
|
|
196
198
|
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`${label} must be an object`);
|
|
197
199
|
const translation = value as Record<string, unknown>;
|
|
198
|
-
const allowed = new Set(
|
|
200
|
+
const allowed = new Set(
|
|
201
|
+
"path" in operation ? ["title", "description", "input"] : ["title", "description", "input", "data", "searchTags"],
|
|
202
|
+
);
|
|
199
203
|
const extra = Object.keys(translation).find((key) => !allowed.has(key));
|
|
200
204
|
if (extra) throw new Error(`${label} contains unsupported field "${extra}"`);
|
|
201
205
|
if (translation.title !== undefined) {
|
|
@@ -238,7 +242,7 @@ const compileOperationPresentation = (
|
|
|
238
242
|
);
|
|
239
243
|
}
|
|
240
244
|
const input = compileSchemaPresentation(translation.input, operation.inputSchema, `${label}.input`);
|
|
241
|
-
const data = compileSchemaPresentation(translation.data, operation.dataSchema, `${label}.data`);
|
|
245
|
+
const data = "dataSchema" in operation ? compileSchemaPresentation(translation.data, operation.dataSchema, `${label}.data`) : undefined;
|
|
242
246
|
return {
|
|
243
247
|
...(typeof translation.title === "string" ? { title: translation.title.trim() } : {}),
|
|
244
248
|
...(typeof translation.description === "string" ? { description: translation.description.trim() } : {}),
|
|
@@ -263,6 +267,7 @@ export const compileCapabilityPresentation = (manifest: CapabilityManifest, valu
|
|
|
263
267
|
const types = new Map(manifest.types.map((type) => [type.localId, type]));
|
|
264
268
|
const queries = new Map(manifest.queries.map((operation) => [operation.localId, operation]));
|
|
265
269
|
const actions = new Map(manifest.actions.map((operation) => [operation.localId, operation]));
|
|
270
|
+
const commands = new Map(manifest.commands.map((operation) => [operation.localId, operation]));
|
|
266
271
|
const translations: Record<string, CapabilityPresentationTranslation> = {};
|
|
267
272
|
for (const [locale, rawTranslation] of Object.entries(catalog.translations as Record<string, unknown>)) {
|
|
268
273
|
const canonical = canonicalLocale(locale);
|
|
@@ -273,7 +278,7 @@ export const compileCapabilityPresentation = (manifest: CapabilityManifest, valu
|
|
|
273
278
|
throw new Error(`Capability presentation translation ${canonical} must be an object`);
|
|
274
279
|
}
|
|
275
280
|
const raw = rawTranslation as Record<string, unknown>;
|
|
276
|
-
const extra = Object.keys(raw).find((key) => key !== "types" && key !== "queries" && key !== "actions");
|
|
281
|
+
const extra = Object.keys(raw).find((key) => key !== "types" && key !== "queries" && key !== "actions" && key !== "commands");
|
|
277
282
|
if (extra) throw new Error(`Capability presentation translation ${canonical} contains unsupported field "${extra}"`);
|
|
278
283
|
const compileGroup = <T>(
|
|
279
284
|
group: unknown,
|
|
@@ -311,10 +316,12 @@ export const compileCapabilityPresentation = (manifest: CapabilityManifest, valu
|
|
|
311
316
|
});
|
|
312
317
|
const translatedQueries = compileGroup(raw.queries, queries, "Query", compileOperationPresentation);
|
|
313
318
|
const translatedActions = compileGroup(raw.actions, actions, "Action", compileOperationPresentation);
|
|
319
|
+
const translatedCommands = compileGroup(raw.commands, commands, "Command", compileOperationPresentation);
|
|
314
320
|
translations[canonical] = {
|
|
315
321
|
...(translatedTypes ? { types: translatedTypes } : {}),
|
|
316
322
|
...(translatedQueries ? { queries: translatedQueries } : {}),
|
|
317
323
|
...(translatedActions ? { actions: translatedActions } : {}),
|
|
324
|
+
...(translatedCommands ? { commands: translatedCommands } : {}),
|
|
318
325
|
};
|
|
319
326
|
}
|
|
320
327
|
return { baseLocale, translations };
|
|
@@ -406,7 +413,7 @@ const normalizeSearchTags = (definition: CapabilityQueryDefinition, label: strin
|
|
|
406
413
|
...(aliases.length > 0 ? { aliases } : {}),
|
|
407
414
|
};
|
|
408
415
|
});
|
|
409
|
-
return { tags };
|
|
416
|
+
return { tags, ...(definition.universalSearch.scopeTypes ? { scopeTypes: [...new Set(definition.universalSearch.scopeTypes)] } : {}) };
|
|
410
417
|
};
|
|
411
418
|
|
|
412
419
|
const compileOperationSchemas = (definition: CapabilityQueryDefinition | CapabilityActionDefinition, label: string) => {
|
|
@@ -426,7 +433,7 @@ const compileOperationSchemas = (definition: CapabilityQueryDefinition | Capabil
|
|
|
426
433
|
inputSchema,
|
|
427
434
|
dataSchema,
|
|
428
435
|
resultZodSchema,
|
|
429
|
-
schemaHash: capabilityHash({ inputSchema, dataSchema }),
|
|
436
|
+
schemaHash: capabilityHash({ inputSchema, dataSchema, ...(definition.stream ? { stream: { direction: definition.stream.direction, maxBytes: definition.stream.maxBytes } } : {}) }),
|
|
430
437
|
};
|
|
431
438
|
};
|
|
432
439
|
|
|
@@ -462,6 +469,8 @@ export const compileCapabilities = (appId: string, definitions: CapabilityDefini
|
|
|
462
469
|
for (const [localId, definition] of Object.entries(definitions.queries ?? {}).sort(([left], [right]) => left.localeCompare(right))) {
|
|
463
470
|
registerLocalId(localId, "Query");
|
|
464
471
|
const label = `Query ${localId}`;
|
|
472
|
+
if (definition.stream && ((definition.stream.direction === "write" && !("idempotency" in definition)) || definition.stream.maxBytes <= 0)) throw new Error(`${label}: write streams require an Action and a positive byte budget`);
|
|
473
|
+
if (definition.stream?.direction === "write" && "idempotency" in definition && definition.idempotency !== "required") throw new Error(`${label}: write streams require idempotency`);
|
|
465
474
|
const schemas = compileOperationSchemas(definition, label);
|
|
466
475
|
if (definition.universalSearch) {
|
|
467
476
|
const expectedInput = projectSchema(UniversalSearchInputSchema, "Universal Search input", "input");
|
|
@@ -480,9 +489,13 @@ export const compileCapabilities = (appId: string, definitions: CapabilityDefini
|
|
|
480
489
|
inputSchema: schemas.inputSchema,
|
|
481
490
|
dataSchema: schemas.dataSchema,
|
|
482
491
|
schemaHash: schemas.schemaHash,
|
|
492
|
+
...(definition.stream ? { stream: { direction: definition.stream.direction, maxBytes: definition.stream.maxBytes } } : {}),
|
|
483
493
|
openWorld: definition.openWorld,
|
|
484
494
|
universalSearch: normalizeSearchTags(definition, label),
|
|
485
495
|
} satisfies CapabilityQueryManifest;
|
|
496
|
+
for (const type of manifest.universalSearch?.scopeTypes ?? []) {
|
|
497
|
+
if (!typeIds.has(qualifiedId(appId, type))) throw new Error(`${label} declares an unknown search scope type ${type}`);
|
|
498
|
+
}
|
|
486
499
|
queries.set(localId, {
|
|
487
500
|
definition,
|
|
488
501
|
manifest,
|
|
@@ -499,12 +512,36 @@ export const compileCapabilities = (appId: string, definitions: CapabilityDefini
|
|
|
499
512
|
for (const [localId, definition] of Object.entries(definitions.actions ?? {}).sort(([left], [right]) => left.localeCompare(right))) {
|
|
500
513
|
registerLocalId(localId, "Action");
|
|
501
514
|
const label = `Action ${localId}`;
|
|
515
|
+
// Declaration rule, documented in docs/platform/capabilities.md:
|
|
516
|
+
// destructive means irreversible or externally visible. An irreversible or
|
|
517
|
+
// open-world effect must always be reviewable and safely repeatable, and
|
|
518
|
+
// must never be approved once and then replayed silently.
|
|
519
|
+
if (definition.destructive && !definition.review) {
|
|
520
|
+
throw new Error(`${label} is destructive and must declare review()`);
|
|
521
|
+
}
|
|
522
|
+
if (definition.destructive && definition.idempotency !== "required") {
|
|
523
|
+
throw new Error(`${label} is destructive and must declare idempotency: "required"`);
|
|
524
|
+
}
|
|
525
|
+
if (definition.openWorld && !definition.review) {
|
|
526
|
+
throw new Error(`${label} is open-world and must declare review()`);
|
|
527
|
+
}
|
|
528
|
+
if (definition.openWorld && definition.idempotency !== "required") {
|
|
529
|
+
throw new Error(`${label} is open-world and must declare idempotency: "required"`);
|
|
530
|
+
}
|
|
502
531
|
if (definition.approval === "rememberable" && definition.openWorld) {
|
|
503
532
|
throw new Error(`${label} cannot remember approval for an open-world effect`);
|
|
504
533
|
}
|
|
534
|
+
if (definition.approval === "rememberable" && definition.destructive) {
|
|
535
|
+
throw new Error(`${label} cannot remember approval for a destructive effect`);
|
|
536
|
+
}
|
|
537
|
+
if (definition.approval === "none" && (definition.destructive || definition.openWorld)) {
|
|
538
|
+
throw new Error(`${label} cannot skip approval for destructive or open-world effects`);
|
|
539
|
+
}
|
|
505
540
|
if (definition.approval === "rememberable" && !definition.review) {
|
|
506
541
|
throw new Error(`${label} must provide a review before approval can be remembered`);
|
|
507
542
|
}
|
|
543
|
+
if (definition.stream && ((definition.stream.direction === "write" && !("idempotency" in definition)) || definition.stream.maxBytes <= 0)) throw new Error(`${label}: write streams require an Action and a positive byte budget`);
|
|
544
|
+
if (definition.stream?.direction === "write" && "idempotency" in definition && definition.idempotency !== "required") throw new Error(`${label}: write streams require idempotency`);
|
|
508
545
|
const schemas = compileOperationSchemas(definition, label);
|
|
509
546
|
const manifest = {
|
|
510
547
|
localId,
|
|
@@ -513,6 +550,7 @@ export const compileCapabilities = (appId: string, definitions: CapabilityDefini
|
|
|
513
550
|
inputSchema: schemas.inputSchema,
|
|
514
551
|
dataSchema: schemas.dataSchema,
|
|
515
552
|
schemaHash: schemas.schemaHash,
|
|
553
|
+
...(definition.stream ? { stream: { direction: definition.stream.direction, maxBytes: definition.stream.maxBytes } } : {}),
|
|
516
554
|
destructive: definition.destructive,
|
|
517
555
|
openWorld: definition.openWorld,
|
|
518
556
|
idempotency: definition.idempotency,
|
|
@@ -526,12 +564,30 @@ export const compileCapabilities = (appId: string, definitions: CapabilityDefini
|
|
|
526
564
|
});
|
|
527
565
|
}
|
|
528
566
|
|
|
567
|
+
const commands = Object.entries(definitions.commands ?? {})
|
|
568
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
569
|
+
.map(([localId, definition]) => {
|
|
570
|
+
registerLocalId(localId, "Command");
|
|
571
|
+
const inputSchema = projectSchema(definition.input, `Command ${localId} input`, "input");
|
|
572
|
+
assertClosedObjectInput(inputSchema, `Command ${localId} input`);
|
|
573
|
+
return CapabilityCommandManifestSchema.parse({
|
|
574
|
+
localId,
|
|
575
|
+
title: definition.title.trim(),
|
|
576
|
+
description: definition.description.trim(),
|
|
577
|
+
icon: definition.icon,
|
|
578
|
+
keywords: [...(definition.keywords ?? [])],
|
|
579
|
+
inputSchema,
|
|
580
|
+
path: definition.path,
|
|
581
|
+
});
|
|
582
|
+
});
|
|
583
|
+
|
|
529
584
|
const manifestBase = {
|
|
530
585
|
protocolVersion: CAPABILITY_PROTOCOL_VERSION,
|
|
531
586
|
appId,
|
|
532
587
|
types,
|
|
533
588
|
queries: [...queries.values()].map((entry) => entry.manifest),
|
|
534
589
|
actions: [...actions.values()].map((entry) => entry.manifest),
|
|
590
|
+
commands,
|
|
535
591
|
};
|
|
536
592
|
const manifest = CapabilityManifestSchema.parse({
|
|
537
593
|
...manifestBase,
|
|
@@ -614,6 +670,7 @@ export const resolveCapabilityManifestPresentation = (
|
|
|
614
670
|
...(operation.universalSearch
|
|
615
671
|
? {
|
|
616
672
|
universalSearch: {
|
|
673
|
+
...operation.universalSearch,
|
|
617
674
|
tags: operation.universalSearch.tags.map((tag) => {
|
|
618
675
|
const tagCopy = copy.searchTags?.[tag.tag];
|
|
619
676
|
return tagCopy
|
|
@@ -625,6 +682,17 @@ export const resolveCapabilityManifestPresentation = (
|
|
|
625
682
|
: {}),
|
|
626
683
|
};
|
|
627
684
|
}),
|
|
685
|
+
commands: current.commands.map((operation) => {
|
|
686
|
+
const copy = translation.commands?.[operation.localId];
|
|
687
|
+
return copy
|
|
688
|
+
? {
|
|
689
|
+
...operation,
|
|
690
|
+
title: copy.title ?? operation.title,
|
|
691
|
+
description: copy.description ?? operation.description,
|
|
692
|
+
inputSchema: applySchemaPresentation(operation.inputSchema, copy.input),
|
|
693
|
+
}
|
|
694
|
+
: operation;
|
|
695
|
+
}),
|
|
628
696
|
actions: current.actions.map((operation) => {
|
|
629
697
|
const copy = translation.actions?.[operation.localId];
|
|
630
698
|
return copy
|
|
@@ -668,10 +736,19 @@ export const parseCapabilityManifest = (value: unknown, expectedAppId: string):
|
|
|
668
736
|
);
|
|
669
737
|
}
|
|
670
738
|
assertClosedObjectInput(inputSchema, `Operation ${operation.localId} input`);
|
|
671
|
-
|
|
739
|
+
if (
|
|
740
|
+
operation.stream?.direction === "write" &&
|
|
741
|
+
(!("idempotency" in operation) || operation.idempotency !== "required")
|
|
742
|
+
) {
|
|
743
|
+
throw new Error(`Operation ${operation.localId} write streams require an idempotent Action`);
|
|
744
|
+
}
|
|
745
|
+
const expectedSchemaHash = capabilityHash({ inputSchema: operation.inputSchema, dataSchema: operation.dataSchema, ...(operation.stream ? { stream: operation.stream } : {}) });
|
|
672
746
|
if (operation.schemaHash !== expectedSchemaHash)
|
|
673
747
|
throw new Error(`Operation ${operation.localId} schemaHash does not match its schemas`);
|
|
674
748
|
if ("universalSearch" in operation && operation.universalSearch) {
|
|
749
|
+
for (const type of operation.universalSearch.scopeTypes ?? []) {
|
|
750
|
+
if (!manifest.types.some((entry) => entry.localId === type)) throw new Error(`Unknown search scope type ${type}`);
|
|
751
|
+
}
|
|
675
752
|
const expectedInput = projectSchema(UniversalSearchInputSchema, "Universal Search input", "input");
|
|
676
753
|
const expectedData = projectSchema(UniversalSearchDataSchema, "Universal Search data", "output");
|
|
677
754
|
if (
|
|
@@ -682,6 +759,11 @@ export const parseCapabilityManifest = (value: unknown, expectedAppId: string):
|
|
|
682
759
|
}
|
|
683
760
|
}
|
|
684
761
|
}
|
|
762
|
+
for (const command of manifest.commands) {
|
|
763
|
+
registerLocalId(command.localId, "Command");
|
|
764
|
+
z.fromJSONSchema(structuredClone(command.inputSchema));
|
|
765
|
+
assertClosedObjectInput(command.inputSchema, `Command ${command.localId} input`);
|
|
766
|
+
}
|
|
685
767
|
const queries = new Map(manifest.queries.map((query) => [query.localId, query]));
|
|
686
768
|
for (const type of manifest.types) {
|
|
687
769
|
if (!type.reader) continue;
|
|
@@ -787,6 +869,12 @@ export const capabilityManifestEvolutionIssues = (previous: CapabilityManifest,
|
|
|
787
869
|
else if (type.reader && current.reader !== type.reader) issues.push(`Type ${type.localId} reader changed`);
|
|
788
870
|
}
|
|
789
871
|
|
|
872
|
+
const nextCommands = new Map(next.commands.map((command) => [command.localId, command]));
|
|
873
|
+
for (const command of previous.commands) {
|
|
874
|
+
const current = nextCommands.get(command.localId);
|
|
875
|
+
if (!current) issues.push(`Command ${command.localId} was removed`);
|
|
876
|
+
else issues.push(...schemaEvolutionIssues(command.inputSchema, current.inputSchema, `Command ${command.localId} input`, "input"));
|
|
877
|
+
}
|
|
790
878
|
const nextQueries = new Map(next.queries.map((operation) => [operation.localId, operation]));
|
|
791
879
|
const nextActions = new Map(next.actions.map((operation) => [operation.localId, operation]));
|
|
792
880
|
for (const operation of previous.queries) {
|
|
@@ -982,11 +1070,24 @@ export const invokeCompiledCapability = async (params: {
|
|
|
982
1070
|
},
|
|
983
1071
|
};
|
|
984
1072
|
}
|
|
1073
|
+
if ("universalSearch" in operation.manifest && operation.manifest.universalSearch) {
|
|
1074
|
+
const search = UniversalSearchInputSchema.parse(input.data);
|
|
1075
|
+
if (
|
|
1076
|
+
search.scope &&
|
|
1077
|
+
!(operation.manifest.universalSearch.scopeTypes ?? []).some(
|
|
1078
|
+
(type) => qualifiedId(params.compiled.manifest.appId, type) === search.scope!.type,
|
|
1079
|
+
)
|
|
1080
|
+
)
|
|
1081
|
+
return { ok: false, error: { code: "VALIDATION_FAILED", message: "Unsupported search scope", status: 400 } };
|
|
1082
|
+
}
|
|
985
1083
|
try {
|
|
986
1084
|
const invoked = await operation.definition.run(input.data, context);
|
|
987
1085
|
if (!invoked.ok) {
|
|
988
1086
|
return normalizeProviderError(invoked.error, "Capability returned an invalid error", params.onUnexpectedError);
|
|
989
1087
|
}
|
|
1088
|
+
const stream = invoked.data.stream;
|
|
1089
|
+
if (stream && (Buffer.byteLength(stream.id) > 2048 || !operation.definition.stream || stream.direction !== operation.definition.stream.direction || stream.size > operation.definition.stream.maxBytes || Date.parse(stream.expiresAt) <= Date.now() || Date.parse(stream.expiresAt) > Date.now() + 86_400_000))
|
|
1090
|
+
return invalidProviderError("Stream is outside its declared policy or 24 hour lifetime");
|
|
990
1091
|
const validated = validateCapabilityResult(params.compiled, operation, invoked.data, params.onUnexpectedError);
|
|
991
1092
|
return validated.ok
|
|
992
1093
|
? ({
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { isServiceError } from "@k2b/stdlib";
|
|
2
|
+
import { exactStream } from "../capabilities/stream-body";
|
|
3
|
+
import type { CapabilityExecutionContext } from "../contracts/capabilities";
|
|
4
|
+
import {
|
|
5
|
+
CAPABILITY_MAX_RESULT_BYTES,
|
|
6
|
+
CapabilityStreamSchema,
|
|
7
|
+
CapabilityStreamStatusSchema,
|
|
8
|
+
capabilityResultSchema,
|
|
9
|
+
} from "../contracts/capabilities";
|
|
10
|
+
import type { CompiledCapabilities } from "./capabilities";
|
|
11
|
+
|
|
12
|
+
const failure = (status: number, code: string, message: string) => Response.json({ code, message }, { status });
|
|
13
|
+
export async function invokeCapabilityStream(params: {
|
|
14
|
+
compiled: CompiledCapabilities;
|
|
15
|
+
kind: "queries" | "actions";
|
|
16
|
+
localId: string;
|
|
17
|
+
verb: string;
|
|
18
|
+
request: Request;
|
|
19
|
+
context: CapabilityExecutionContext;
|
|
20
|
+
}): Promise<Response> {
|
|
21
|
+
const operation = params.compiled[params.kind].get(params.localId);
|
|
22
|
+
const definition = operation?.definition.stream;
|
|
23
|
+
if (!operation || !definition) return failure(404, "STREAM_NOT_FOUND", "Stream operation not found");
|
|
24
|
+
const offer = params.request.headers.get("x-cloud-stream-offer");
|
|
25
|
+
if (!offer || offer.length > 24000) return failure(400, "STREAM_INVALID", "Invalid stream offer");
|
|
26
|
+
const receipt = (value: unknown) => {
|
|
27
|
+
const parsed = capabilityResultSchema(operation.definition.data).parse(value);
|
|
28
|
+
if (parsed.stream) throw new Error("Stream receipts cannot issue another stream");
|
|
29
|
+
return parsed;
|
|
30
|
+
};
|
|
31
|
+
try {
|
|
32
|
+
const stream = CapabilityStreamSchema.parse(JSON.parse(Buffer.from(offer, "base64url").toString()));
|
|
33
|
+
if (stream.direction !== definition.direction || stream.size > definition.maxBytes || Date.parse(stream.expiresAt) <= Date.now())
|
|
34
|
+
return failure(410, "STREAM_EXPIRED", "Stream expired or outside its byte budget");
|
|
35
|
+
if (definition.direction === "read") {
|
|
36
|
+
if (params.verb !== "read") return failure(403, "STREAM_DENIED", "Read-only stream");
|
|
37
|
+
const response = await definition.read(stream, params.context);
|
|
38
|
+
if (!response.ok || !response.body) {
|
|
39
|
+
await response.body?.cancel();
|
|
40
|
+
return failure(502, "STREAM_FAILED", "Source could not be read");
|
|
41
|
+
}
|
|
42
|
+
return new Response(exactStream(response.body, stream.size), {
|
|
43
|
+
headers: { "content-type": stream.mediaType, "cache-control": "no-store" },
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
let result: unknown;
|
|
47
|
+
if (params.verb === "write") {
|
|
48
|
+
const body = exactStream(
|
|
49
|
+
params.request.body ??
|
|
50
|
+
new ReadableStream({
|
|
51
|
+
start(c) {
|
|
52
|
+
c.close();
|
|
53
|
+
},
|
|
54
|
+
}),
|
|
55
|
+
stream.size,
|
|
56
|
+
);
|
|
57
|
+
try {
|
|
58
|
+
const value = await definition.write(stream, body, params.context);
|
|
59
|
+
result = receipt(value);
|
|
60
|
+
} finally {
|
|
61
|
+
if (!body.locked) await body.cancel().catch(() => undefined);
|
|
62
|
+
}
|
|
63
|
+
} else if (params.verb === "status") {
|
|
64
|
+
const status = CapabilityStreamStatusSchema.parse(await definition.status(stream, params.context));
|
|
65
|
+
if (status.state === "completed") receipt(status.result);
|
|
66
|
+
result = status;
|
|
67
|
+
} else if (params.verb === "abort") {
|
|
68
|
+
await definition.abort(stream, params.context);
|
|
69
|
+
const status = CapabilityStreamStatusSchema.parse(await definition.status(stream, params.context));
|
|
70
|
+
if (status.state === "completed") receipt(status.result);
|
|
71
|
+
result = status;
|
|
72
|
+
} else return failure(400, "STREAM_INVALID", "Unknown stream operation");
|
|
73
|
+
const json = JSON.stringify(result);
|
|
74
|
+
if (Buffer.byteLength(json) > CAPABILITY_MAX_RESULT_BYTES)
|
|
75
|
+
return failure(502, "STREAM_FAILED", "Stream receipt is too large; inspect status before retrying");
|
|
76
|
+
return new Response(json, { headers: { "content-type": "application/json", "cache-control": "no-store" } });
|
|
77
|
+
} catch (error) {
|
|
78
|
+
if (isServiceError(error)) return failure(error.status, error.code, error.message);
|
|
79
|
+
return failure(409, "STREAM_FAILED", "Transfer failed; inspect status before retrying a write");
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import { invokeCapabilityStream } from "./capability-streams";
|
|
2
|
+
import { registerHelp } from "../services/help";
|
|
3
|
+
import { preloadLayoutHelp } from "../ssr/help";
|
|
4
|
+
import { bindProcessApplicationId, clearProcessApplicationId } from "./process-identity";
|
|
1
5
|
/**
|
|
2
6
|
* defineApp() — The single entry point for every cloud app.
|
|
3
7
|
*
|
|
@@ -5,6 +9,8 @@
|
|
|
5
9
|
* Returns `{ ssr, plugin, config, meta, start }`.
|
|
6
10
|
*/
|
|
7
11
|
|
|
12
|
+
import { resolve } from "node:path";
|
|
13
|
+
import { fileURLToPath } from "node:url";
|
|
8
14
|
import type { SsrConfig } from "@k2b/ssr";
|
|
9
15
|
import { createConfig as createSsrConfig } from "@k2b/ssr";
|
|
10
16
|
import { routes } from "@k2b/ssr/hono";
|
|
@@ -18,10 +24,17 @@ import type {
|
|
|
18
24
|
AppLifecycle,
|
|
19
25
|
AppMeta,
|
|
20
26
|
AppPresentationCatalog,
|
|
27
|
+
AppSearchLink,
|
|
21
28
|
CloudLifecycleContext,
|
|
22
29
|
WidgetEndpoint,
|
|
23
30
|
} from "../contracts/app";
|
|
24
|
-
import {
|
|
31
|
+
import {
|
|
32
|
+
CAPABILITY_FRAMEWORK_ERROR_CODES,
|
|
33
|
+
CAPABILITY_MAX_REQUEST_BYTES,
|
|
34
|
+
CAPABILITY_ORIGIN_HEADER,
|
|
35
|
+
type CapabilityDefinitions,
|
|
36
|
+
CapabilityOriginSchema,
|
|
37
|
+
} from "../contracts/capabilities";
|
|
25
38
|
import { type BoundNotificationMap, bindNotificationDefinitions, type NotificationDefinitionMap } from "../contracts/notification-types";
|
|
26
39
|
import type { AppRegistryEntry } from "../contracts/registry";
|
|
27
40
|
import type { AppSettingsMap, KindToType } from "../contracts/settings-types";
|
|
@@ -30,18 +43,19 @@ import type { HelpDefinition } from "../server/help";
|
|
|
30
43
|
import { getLocale, resolveLocale } from "../server/locale";
|
|
31
44
|
import { type AuthContext, auth } from "../server/middleware/auth";
|
|
32
45
|
import { requireInvocation } from "../server/middleware/invocation";
|
|
33
|
-
import { routeTemplate } from "../server/middleware/route-template";
|
|
46
|
+
import { matchedRouteTemplate, routeTemplate } from "../server/middleware/route-template";
|
|
34
47
|
import { runtime as runtimeMiddleware } from "../server/middleware/runtime";
|
|
35
|
-
import { settings as settingsMiddleware } from "../server/middleware/settings";
|
|
48
|
+
import { preloadLayoutAnnouncements, settings as settingsMiddleware } from "../server/middleware/settings";
|
|
36
49
|
import {
|
|
37
50
|
capabilityInvocationOperation,
|
|
38
51
|
searchInvocationOperation,
|
|
39
52
|
syncInvocationOperation,
|
|
40
53
|
widgetInvocationOperation,
|
|
41
54
|
} from "../services/identity/invocation-operations";
|
|
55
|
+
import { normalizeInvocationRequestId } from "../services/identity/invocation-token";
|
|
42
56
|
import { logger } from "../services/logging";
|
|
43
57
|
import { startNotificationDefinitionRegistration } from "../services/notifications/catalog";
|
|
44
|
-
import {
|
|
58
|
+
import { readRailSnapshot } from "../services/rail-snapshot";
|
|
45
59
|
import { get, loadCache as loadSettingsCache, set } from "../services/settings";
|
|
46
60
|
import { createSettingsAPI, type SettingsAPI } from "../services/settings/api";
|
|
47
61
|
import { registerSettings, toLegacySettingDefs } from "../services/settings/defaults";
|
|
@@ -59,11 +73,13 @@ import { createHeartbeat } from "./heartbeat";
|
|
|
59
73
|
import { compileHelp } from "./help";
|
|
60
74
|
import { createPageResponses } from "./page-responses";
|
|
61
75
|
import { getProcessSync, startProcessSync } from "./process-sync";
|
|
76
|
+
import { configurePostgresApplicationName } from "./postgres-application-name";
|
|
62
77
|
import { APP_READINESS_PATH, appReadinessResponse } from "./readiness";
|
|
63
|
-
import { appRegistry, type CapabilityRegistryRecord, capabilityRegistry
|
|
78
|
+
import { appRegistry, type CapabilityRegistryRecord, capabilityRegistry } from "./registry";
|
|
64
79
|
import { ensureRuntimeWatcher, getCurrentRuntime, stopRuntimeWatcher } from "./runtime-watcher";
|
|
65
80
|
import { servePublicAsset } from "./static-assets";
|
|
66
81
|
import { createStatusPreservingSsrHandler } from "./status-preserving-ssr";
|
|
82
|
+
import { WEB_VITALS_ASSET_HREF } from "./web-vitals-asset";
|
|
67
83
|
|
|
68
84
|
/** Cache-busting version stamp — changes on every server start / rebuild. */
|
|
69
85
|
const v = Date.now();
|
|
@@ -76,6 +92,8 @@ export type PageOptions = {
|
|
|
76
92
|
lang?: string;
|
|
77
93
|
};
|
|
78
94
|
|
|
95
|
+
type ResolvedPageOptions = PageOptions & { performanceRoute?: string };
|
|
96
|
+
|
|
79
97
|
// ── Public types ────────────────────────────────────────────────────────────
|
|
80
98
|
|
|
81
99
|
/**
|
|
@@ -103,6 +121,7 @@ export type AppOptions<S extends AppSettingsMap = {}, N extends NotificationDefi
|
|
|
103
121
|
adminHref?: string;
|
|
104
122
|
/** Multi-link admin navigation contributed by this app. */
|
|
105
123
|
adminNav?: ReadonlyArray<{
|
|
124
|
+
section?: "ai";
|
|
106
125
|
id?: string;
|
|
107
126
|
label: string;
|
|
108
127
|
links: ReadonlyArray<{ label: string; href: string; icon: string }>;
|
|
@@ -136,6 +155,8 @@ export type AppOptions<S extends AppSettingsMap = {}, N extends NotificationDefi
|
|
|
136
155
|
* always open in a new tab from the login footer.
|
|
137
156
|
*/
|
|
138
157
|
legalLinks?: ReadonlyArray<{ label: string; href: string; icon?: string }>;
|
|
158
|
+
/** Static destinations for the global search UI; no capabilities are created. */
|
|
159
|
+
searchLinks?: readonly AppSearchLink[];
|
|
139
160
|
/**
|
|
140
161
|
* Dashboard widget endpoints this app exposes. Each entry references an
|
|
141
162
|
* HTTP path on this app that returns a `WidgetResponse`. The dashboard
|
|
@@ -177,9 +198,10 @@ export type AppOptions<S extends AppSettingsMap = {}, N extends NotificationDefi
|
|
|
177
198
|
*/
|
|
178
199
|
openapi?: string;
|
|
179
200
|
/**
|
|
180
|
-
* Project root used
|
|
201
|
+
* Project root used for SSR component IDs and development assets.
|
|
181
202
|
* Defaults to `process.cwd()`. Override only if you run the entrypoint
|
|
182
|
-
* from a directory other than the project root.
|
|
203
|
+
* from a directory other than the project root. Discovery uses APP_DIR/src
|
|
204
|
+
* (or appRoot/src) plus the installed Cloud framework source.
|
|
183
205
|
*/
|
|
184
206
|
appRoot?: string;
|
|
185
207
|
};
|
|
@@ -280,6 +302,7 @@ export const defineApp = <
|
|
|
280
302
|
>(
|
|
281
303
|
opts: AppOptions<S, N, AppId>,
|
|
282
304
|
): AppDefinition<S, N, AppId> => {
|
|
305
|
+
configurePostgresApplicationName(opts.id);
|
|
283
306
|
const isDevelopment = process.env.NODE_ENV === "development";
|
|
284
307
|
const notifications = bindNotificationDefinitions(opts.id, opts.notifications);
|
|
285
308
|
|
|
@@ -293,12 +316,14 @@ export const defineApp = <
|
|
|
293
316
|
}
|
|
294
317
|
|
|
295
318
|
// ── 1. SSR config ─────────────────────────────────────────────────────
|
|
296
|
-
const
|
|
319
|
+
const rootDir = opts.appRoot ?? process.cwd();
|
|
320
|
+
const { config, plugin, html } = createSsrConfig<ResolvedPageOptions>({
|
|
297
321
|
dev: isDevelopment,
|
|
298
322
|
verbose: true,
|
|
299
|
-
rootDir
|
|
323
|
+
rootDir,
|
|
324
|
+
componentRoots: [resolve(process.env.APP_DIR ?? rootDir, "src"), fileURLToPath(new URL("../", import.meta.url))],
|
|
300
325
|
basePath: opts.basePath,
|
|
301
|
-
template: ({ body, scripts, title, description, theme, lang }) => {
|
|
326
|
+
template: ({ body, scripts, title, description, theme, lang, performanceRoute }) => {
|
|
302
327
|
const themeFixed = theme !== undefined;
|
|
303
328
|
return `<!DOCTYPE html>
|
|
304
329
|
<html lang="${normalizeLocale(lang)}" class="${theme ?? "light"}"${themeFixed ? " data-theme-fixed" : ""}>
|
|
@@ -321,6 +346,7 @@ export const defineApp = <
|
|
|
321
346
|
${body}
|
|
322
347
|
</body>
|
|
323
348
|
${scripts}
|
|
349
|
+
${performanceRoute ? `<script type="module" src="${WEB_VITALS_ASSET_HREF}" data-cloud-web-vitals data-app-id="${escapeHtml(opts.id)}" data-route-template="${escapeHtml(performanceRoute)}"></script>` : ""}
|
|
324
350
|
</html>`;
|
|
325
351
|
},
|
|
326
352
|
});
|
|
@@ -336,10 +362,16 @@ export const defineApp = <
|
|
|
336
362
|
// `getDateConfig` share one canonical `getLocale(c)` that page handlers
|
|
337
363
|
// cannot override.
|
|
338
364
|
const ssr = Object.assign(
|
|
339
|
-
createStatusPreservingSsrHandler<
|
|
365
|
+
createStatusPreservingSsrHandler<ResolvedPageOptions>(html, async (c) => {
|
|
340
366
|
c.get("page").lang = getLocale(c);
|
|
341
367
|
const user = c.get("user");
|
|
342
|
-
|
|
368
|
+
c.get("page").performanceRoute =
|
|
369
|
+
user && c.get("settings")?.observability?.web_vitals?.enabled === true ? (matchedRouteTemplate(c) ?? undefined) : undefined;
|
|
370
|
+
await Promise.all([
|
|
371
|
+
preloadLayoutAnnouncements(c),
|
|
372
|
+
preloadLayoutHelp(c),
|
|
373
|
+
user ? readRailSnapshot(user).then((snapshot) => c.set("railPreferences", snapshot)) : undefined,
|
|
374
|
+
]);
|
|
343
375
|
}),
|
|
344
376
|
createPageResponses(html),
|
|
345
377
|
);
|
|
@@ -354,6 +386,7 @@ export const defineApp = <
|
|
|
354
386
|
adminHref: opts.adminHref,
|
|
355
387
|
adminNav: opts.adminNav?.map(
|
|
356
388
|
(group): AppAdminNavigationGroup => ({
|
|
389
|
+
...(group.section ? { section: group.section } : {}),
|
|
357
390
|
id: group.id,
|
|
358
391
|
label: group.label,
|
|
359
392
|
links: group.links.map((link) => ({ ...link })),
|
|
@@ -362,6 +395,7 @@ export const defineApp = <
|
|
|
362
395
|
routes: [...opts.routes],
|
|
363
396
|
nav: opts.nav,
|
|
364
397
|
legalLinks: opts.legalLinks ? [...opts.legalLinks] : undefined,
|
|
398
|
+
searchLinks: opts.searchLinks?.map((link) => ({ ...link, keywords: link.keywords ? [...link.keywords] : undefined })),
|
|
365
399
|
widgets: opts.widgets ? opts.widgets.map((w) => ({ ...w })) : undefined,
|
|
366
400
|
settingKeys: opts.settings ? Object.keys(opts.settings) : undefined,
|
|
367
401
|
openapi: opts.openapi,
|
|
@@ -400,6 +434,8 @@ export const defineApp = <
|
|
|
400
434
|
}
|
|
401
435
|
})());
|
|
402
436
|
try {
|
|
437
|
+
bindProcessApplicationId(meta.id);
|
|
438
|
+
cleanup.unshift(clearProcessApplicationId);
|
|
403
439
|
const startedAt = Date.now();
|
|
404
440
|
|
|
405
441
|
// OpenAPI advertised in the registry only when there's a router to
|
|
@@ -410,8 +446,6 @@ export const defineApp = <
|
|
|
410
446
|
const compiledHelp = startOpts.help
|
|
411
447
|
? compileHelp({
|
|
412
448
|
appId: meta.id,
|
|
413
|
-
appName: meta.name,
|
|
414
|
-
appIcon: meta.icon,
|
|
415
449
|
basePath: opts.basePath,
|
|
416
450
|
definition: startOpts.help,
|
|
417
451
|
})
|
|
@@ -441,6 +475,7 @@ export const defineApp = <
|
|
|
441
475
|
}
|
|
442
476
|
: undefined,
|
|
443
477
|
adminNav: meta.adminNav?.map((group) => ({
|
|
478
|
+
...(group.section ? { section: group.section } : {}),
|
|
444
479
|
id: group.id,
|
|
445
480
|
label: group.label,
|
|
446
481
|
links: group.links.map((link) => ({ ...link })),
|
|
@@ -453,6 +488,7 @@ export const defineApp = <
|
|
|
453
488
|
: undefined,
|
|
454
489
|
help: compiledHelp?.summary,
|
|
455
490
|
legalLinks: meta.legalLinks ? meta.legalLinks.map((l) => ({ ...l })) : undefined,
|
|
491
|
+
searchLinks: meta.searchLinks,
|
|
456
492
|
widgets: meta.widgets ? meta.widgets.map((w) => ({ ...w })) : undefined,
|
|
457
493
|
settingKeys: meta.settingKeys ? [...meta.settingKeys] : undefined,
|
|
458
494
|
openapi: advertiseOpenapi ? opts.openapi : undefined,
|
|
@@ -461,6 +497,7 @@ export const defineApp = <
|
|
|
461
497
|
// Heartbeat
|
|
462
498
|
const heartbeat = createHeartbeat(meta.id, entry, {
|
|
463
499
|
registry: appRegistry(),
|
|
500
|
+
beforeWrite: compiledHelp ? () => registerHelp(compiledHelp.corpus) : undefined,
|
|
464
501
|
onError: (error) =>
|
|
465
502
|
log.error("Registry heartbeat failed", {
|
|
466
503
|
appId: meta.id,
|
|
@@ -499,27 +536,6 @@ export const defineApp = <
|
|
|
499
536
|
},
|
|
500
537
|
})
|
|
501
538
|
: undefined;
|
|
502
|
-
const helpHeartbeat = compiledHelp
|
|
503
|
-
? createHeartbeat(meta.id, compiledHelp.registryEntry, {
|
|
504
|
-
key: `help/${meta.id}`,
|
|
505
|
-
registry: helpRegistry(),
|
|
506
|
-
onError: (error) =>
|
|
507
|
-
log.error("Help registry heartbeat failed", {
|
|
508
|
-
appId: meta.id,
|
|
509
|
-
error: error instanceof Error ? error.message : String(error),
|
|
510
|
-
}),
|
|
511
|
-
onStale: (error) => {
|
|
512
|
-
log.error("Help registry lease expired; restarting", {
|
|
513
|
-
appId: meta.id,
|
|
514
|
-
error: error instanceof Error ? error.message : String(error),
|
|
515
|
-
});
|
|
516
|
-
process.exit(1);
|
|
517
|
-
},
|
|
518
|
-
})
|
|
519
|
-
: undefined;
|
|
520
|
-
cleanup.push(async () => {
|
|
521
|
-
await helpHeartbeat?.stop();
|
|
522
|
-
});
|
|
523
539
|
cleanup.push(async () => {
|
|
524
540
|
await capabilityHeartbeat?.stop();
|
|
525
541
|
});
|
|
@@ -614,6 +630,8 @@ export const defineApp = <
|
|
|
614
630
|
user,
|
|
615
631
|
idempotencyKey,
|
|
616
632
|
locale: requestLocale,
|
|
633
|
+
requestId: normalizeInvocationRequestId(c.req.header("x-request-id")) ?? crypto.randomUUID(),
|
|
634
|
+
origin: CapabilityOriginSchema.catch("http").parse(c.req.header(CAPABILITY_ORIGIN_HEADER)),
|
|
617
635
|
signal: c.req.raw.signal,
|
|
618
636
|
},
|
|
619
637
|
onUnexpectedError: (error: unknown) =>
|
|
@@ -654,6 +672,15 @@ export const defineApp = <
|
|
|
654
672
|
schemaHash: operation.manifest.schemaHash,
|
|
655
673
|
};
|
|
656
674
|
});
|
|
675
|
+
for (const kind of ["queries", "actions"] as const) {
|
|
676
|
+
server.post(`/api/_internal/capabilities/v1/streams/${kind}/:capabilityId/:verb`, capabilityAuth(kind), auth.requireOAuthScope(kind === "queries" ? "read" : "write", "admin"), async c => {
|
|
677
|
+
const actor = c.get("actor");
|
|
678
|
+
return invokeCapabilityStream({ compiled: compiledCapabilities, kind, localId: c.req.param("capabilityId") ?? "", verb: c.req.param("verb") ?? "", request: c.req.raw,
|
|
679
|
+
context: { actor, accessSubject: c.get("accessSubject"), user: actor.kind === "user" ? actor.user : actor.delegatedUser,
|
|
680
|
+
locale: resolveLocale(c.req.raw.headers, await get<string>("app.locale")),
|
|
681
|
+
requestId: normalizeInvocationRequestId(c.req.header("x-request-id")) ?? crypto.randomUUID(), origin: CapabilityOriginSchema.catch("http").parse(c.req.header(CAPABILITY_ORIGIN_HEADER)), signal: c.req.raw.signal } });
|
|
682
|
+
});
|
|
683
|
+
}
|
|
657
684
|
const capabilityReadScope = auth.requireOAuthScope("read", "admin");
|
|
658
685
|
const capabilityWriteScope = auth.requireOAuthScope("write", "admin");
|
|
659
686
|
server.post("/api/_internal/capabilities/v1/queries/:capabilityId", capabilityAuth("queries"), capabilityReadScope, (c) =>
|
|
@@ -759,7 +786,6 @@ export const defineApp = <
|
|
|
759
786
|
|
|
760
787
|
// Only advertise after setup, workers, and their declared resources are ready.
|
|
761
788
|
// Publish supporting entries first; the app entry makes them discoverable.
|
|
762
|
-
await helpHeartbeat?.start();
|
|
763
789
|
await capabilityHeartbeat?.start();
|
|
764
790
|
await heartbeat.start();
|
|
765
791
|
log.info(`Registered "${meta.id}"`, { baseUrl });
|
|
@@ -18,6 +18,8 @@ type HeartbeatRegistry<T> = {
|
|
|
18
18
|
type HeartbeatOptions<T> = {
|
|
19
19
|
registry: HeartbeatRegistry<T>;
|
|
20
20
|
key?: string;
|
|
21
|
+
/** Publish dependent data before advertising or renewing the app. */
|
|
22
|
+
beforeWrite?: () => Promise<void>;
|
|
21
23
|
intervalMs?: number;
|
|
22
24
|
retryMs?: number;
|
|
23
25
|
staleAfterMs?: number;
|
|
@@ -53,6 +55,7 @@ export const createHeartbeat = <T>(appId: string, entry: T, options: HeartbeatOp
|
|
|
53
55
|
};
|
|
54
56
|
|
|
55
57
|
const register = async (): Promise<void> => {
|
|
58
|
+
await options.beforeWrite?.();
|
|
56
59
|
await registry.upsert({ key, value: entry });
|
|
57
60
|
noteSuccess();
|
|
58
61
|
};
|
|
@@ -61,6 +64,7 @@ export const createHeartbeat = <T>(appId: string, entry: T, options: HeartbeatOp
|
|
|
61
64
|
// Touch renews the existing entry (and emits a registry invalidation).
|
|
62
65
|
// Avoid rebuilding the manifest here. A missing entry is repaired
|
|
63
66
|
// immediately, preserving restart-free registry recovery.
|
|
67
|
+
await options.beforeWrite?.();
|
|
64
68
|
const touched = await registry.touch({ key });
|
|
65
69
|
if (!touched) await registry.upsert({ key, value: entry });
|
|
66
70
|
noteSuccess();
|