@k2b/cloud 0.42.0 → 0.44.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 +13 -5
- package/scripts/app-favicon.ts +7 -6
- package/scripts/build.ts +3 -0
- package/scripts/installable-assets.ts +61 -0
- package/scripts/preload.ts +3 -0
- package/src/_internal/app-access-path.ts +12 -0
- package/src/_internal/app-access-request.ts +13 -0
- package/src/_internal/app-access-resources.ts +141 -0
- package/src/_internal/app-event-catalog.ts +40 -0
- package/src/_internal/app-event-schemas.ts +127 -0
- package/src/_internal/application-package.ts +40 -0
- package/src/_internal/built-in-app-ids.json +27 -0
- package/src/_internal/capabilities.ts +185 -12
- package/src/_internal/capability-origin.ts +4 -0
- package/src/_internal/capability-streams.ts +10 -5
- package/src/_internal/define-app.ts +159 -45
- package/src/_internal/installable-assets.ts +90 -0
- package/src/_internal/installable-head.ts +22 -0
- package/src/_internal/object-store-area.ts +9 -0
- package/src/_internal/object-store-config.ts +101 -0
- package/src/_internal/object-store-key.ts +14 -0
- package/src/_internal/page-responses.ts +34 -3
- package/src/_internal/process-identity.ts +7 -2
- package/src/_internal/registry-validation.ts +15 -1
- package/src/_internal/registry.ts +44 -1
- package/src/_internal/runtime-context.ts +9 -1
- package/src/_internal/runtime-watcher.ts +8 -0
- package/src/_internal/status-preserving-ssr.ts +12 -1
- package/src/access/AppAccessEditor.browser-fixture.ts +429 -0
- package/src/access/AppAccessEditor.browser-frame.tsx +68 -0
- package/src/access/AppAccessEditor.browser-harness.tsx +65 -0
- package/src/access/AppAccessEditor.tsx +380 -0
- package/src/access/PermissionEditor.browser-harness.tsx +22 -2
- package/src/access/PermissionEditor.tsx +386 -77
- package/src/access/PrincipalAvatar.tsx +63 -0
- package/src/access/PrincipalPicker.tsx +29 -8
- package/src/access/ResourceApiKeys.tsx +3 -4
- package/src/access/app-access-adapter.ts +51 -0
- package/src/access/app-access-messages.ts +172 -0
- package/src/access/messages.ts +14 -0
- package/src/access/ui.ts +4 -1
- package/src/account/Avatar.tsx +21 -3
- package/src/account/ProfilePictureDialog.tsx +699 -0
- package/src/account/profile-picture-messages.ts +269 -0
- package/src/account/ui.ts +8 -1
- package/src/ai/admin.ts +3 -0
- package/src/ai/app-frame.ts +12 -0
- package/src/ai/application-tasks.ts +101 -0
- package/src/ai/assistant-models.ts +126 -64
- package/src/ai/capabilities.ts +19 -4
- package/src/ai/capability-execution.ts +5 -2
- package/src/ai/chart-block.ts +63 -30
- package/src/ai/chat/chart-block.tsx +12 -3
- package/src/ai/chat/composer-adapter.ts +8 -7
- package/src/ai/chat/message-actions.tsx +1 -1
- package/src/ai/chat/message-utils.ts +2 -2
- package/src/ai/chat/presentation.tsx +22 -1
- package/src/ai/chat-quotas.ts +5 -4
- package/src/ai/chat-tasks.ts +2 -2
- package/src/ai/client/controller.ts +11 -10
- package/src/ai/client/projection.ts +3 -3
- package/src/ai/code-capability-routes.ts +1 -0
- package/src/ai/compaction.ts +3 -3
- package/src/ai/credentials.ts +13 -114
- package/src/ai/default-tool-contracts.ts +4 -3
- package/src/ai/dictation-runtime.ts +4 -4
- package/src/ai/enrich.ts +2 -2
- package/src/ai/executor.ts +38 -27
- package/src/ai/http.ts +17 -29
- package/src/ai/index.ts +11 -5
- package/src/ai/inference-calls.ts +6 -4
- package/src/ai/maintenance.ts +1 -1
- package/src/ai/memory-learning-runs.ts +5 -5
- package/src/ai/memory-learning.ts +3 -3
- package/src/ai/message-queue.ts +1 -2
- package/src/ai/migrate.ts +75 -42
- package/src/ai/model-access-migrate.ts +3 -17
- package/src/ai/model-access.ts +44 -85
- package/src/ai/model-card.tsx +134 -0
- package/src/ai/model-catalog.ts +359 -0
- package/src/ai/model-chooser-messages.ts +88 -0
- package/src/ai/model-chooser.tsx +577 -0
- package/src/ai/model-delete.ts +234 -0
- package/src/ai/model-fixture.ts +39 -0
- package/src/ai/model-icon.tsx +36 -0
- package/src/ai/model-overview.ts +53 -0
- package/src/ai/model-usage.ts +45 -0
- package/src/ai/openai-api-messages.ts +0 -4
- package/src/ai/openai-api-slots.ts +10 -2
- package/src/ai/openai-api.ts +83 -16
- package/src/ai/personal-agent.ts +1 -1
- package/src/ai/prefs.ts +12 -10
- package/src/ai/projects-routes.ts +2 -1
- package/src/ai/projects.ts +13 -12
- package/src/ai/protocol.ts +2 -2
- package/src/ai/provider-connections.ts +161 -0
- package/src/ai/provider-errors.ts +138 -0
- package/src/ai/provider-fetch.ts +4 -0
- package/src/ai/provider-retry.ts +1 -1
- package/src/ai/provider.ts +2 -1
- package/src/ai/quota-limits.ts +36 -0
- package/src/ai/quota-provider.ts +11 -2
- package/src/ai/quota-report.ts +7 -4
- package/src/ai/quotas-migrate.ts +1 -0
- package/src/ai/quotas.ts +19 -8
- package/src/ai/recent-resources.ts +6 -0
- package/src/ai/request-headers.ts +14 -87
- package/src/ai/resource-refs.ts +6 -2
- package/src/ai/routes.ts +17 -18
- package/src/ai/runtime.ts +50 -22
- package/src/ai/settings.ts +93 -580
- package/src/ai/store.ts +45 -23
- package/src/ai/stream.ts +1 -1
- package/src/ai/structured-runs.ts +2 -2
- package/src/ai/structured.ts +29 -46
- package/src/ai/task-contracts.ts +25 -5
- package/src/ai/task-execution.ts +15 -6
- package/src/ai/task-model.ts +9 -0
- package/src/ai/transcription.ts +37 -21
- package/src/ai/turn-failure.ts +24 -2
- package/src/ai/types.ts +50 -62
- package/src/ai/ui.tsx +13 -0
- package/src/ai/usage.ts +8 -8
- package/src/ai/validate.ts +8 -7
- package/src/ai/view-block.ts +2 -2
- package/src/api/accounts-entities.ts +40 -16
- package/src/api/admin-ai-quotas.ts +34 -56
- package/src/api/admin-ai-usage.ts +1 -1
- package/src/api/admin-ai.ts +173 -0
- package/src/api/admin-core-settings.ts +68 -244
- package/src/api/admin-directory-shares.ts +55 -0
- package/src/api/admin-identity.ts +81 -2
- package/src/api/app-access.ts +85 -0
- package/src/api/app-approval.ts +3 -1
- package/src/api/app-event-delivery.ts +51 -0
- package/src/api/app-events.ts +123 -0
- package/src/api/app-resource-access.ts +48 -0
- package/src/api/auth/account-mismatch.ts +24 -0
- package/src/api/auth.ts +26 -3
- package/src/api/capabilities.ts +127 -15
- package/src/api/capability-streams.ts +2 -5
- package/src/api/identity-child-invocation.ts +145 -0
- package/src/api/index.ts +11 -2
- package/src/api/mcp.ts +180 -5
- package/src/api/me.ts +38 -17
- package/src/api/pwa-phone.ts +87 -48
- package/src/api/pwa.ts +34 -8
- package/src/api/resource-links.ts +83 -6
- package/src/api/resource-resolve.ts +50 -0
- package/src/api/search/schemas.ts +3 -1
- package/src/api/search.ts +9 -7
- package/src/api/widgets.ts +1 -0
- package/src/app-pwa/authority.ts +75 -0
- package/src/app-pwa/contracts.ts +177 -0
- package/src/app-pwa/core-query-error.ts +20 -0
- package/src/app-pwa/index.ts +23 -0
- package/src/app-pwa/protocol.ts +27 -0
- package/src/app-pwa/schema.ts +59 -0
- package/src/app-pwa/service.ts +861 -0
- package/src/app-pwa/transaction.ts +338 -0
- package/src/app-pwa/transport-input.ts +57 -0
- package/src/app-pwa/transport.ts +235 -0
- package/src/approvals/messages.ts +2 -2
- package/src/browser/app-access-recovery.ts +44 -0
- package/src/browser/app-session.ts +14 -3
- package/src/browser/cloud-id.ts +86 -0
- package/src/browser/live-websocket.ts +5 -0
- package/src/browser/pwa-drafts.ts +57 -0
- package/src/browser/resource-clipboard.ts +4 -0
- package/src/browser/resource-links.ts +52 -0
- package/src/browser/resource-picker.tsx +3 -2
- package/src/capabilities/calendar-sources.ts +302 -0
- package/src/capabilities/executions.ts +10 -3
- package/src/capabilities/migrate.ts +4 -0
- package/src/capabilities/nested.ts +43 -0
- package/src/capabilities/server-invoke.ts +202 -0
- package/src/capabilities/server.ts +26 -187
- package/src/capabilities/testing.ts +5 -2
- package/src/cli/access.ts +6 -5
- package/src/cli/account.ts +57 -0
- package/src/cli/admin/agents.ts +66 -0
- package/src/cli/admin/ai-models.ts +199 -0
- package/src/cli/admin/ai-quotas.ts +6 -102
- package/src/cli/admin/ai-usage.ts +4 -4
- package/src/cli/admin/app-credentials.ts +5 -3
- package/src/cli/admin/index.ts +7 -3
- package/src/cli/app-access.ts +60 -0
- package/src/cli/apps.ts +89 -26
- package/src/cli/avatar.ts +64 -0
- package/src/cli/capabilities.ts +22 -1
- package/src/cli/events.ts +320 -0
- package/src/cli/index.ts +1 -1
- package/src/config/env.ts +40 -0
- package/src/contracts/app-access.ts +52 -5
- package/src/contracts/app-approval.ts +8 -1
- package/src/contracts/app-details.ts +89 -0
- package/src/contracts/app-events.ts +51 -0
- package/src/contracts/app.ts +24 -5
- package/src/contracts/calendar-source.ts +274 -0
- package/src/contracts/calendar-view.ts +35 -0
- package/src/contracts/capabilities.ts +164 -3
- package/src/contracts/geo-admin.ts +2 -0
- package/src/contracts/geo-route.ts +0 -11
- package/src/contracts/geo.ts +14 -2
- package/src/contracts/index.ts +6 -0
- package/src/contracts/installable.ts +40 -0
- package/src/contracts/notification-types.ts +36 -9
- package/src/contracts/profile.ts +32 -0
- package/src/contracts/pwa-builds.ts +14 -0
- package/src/contracts/pwa-location.ts +38 -0
- package/src/contracts/pwa.ts +24 -3
- package/src/contracts/registry.ts +8 -0
- package/src/contracts/resource-clipboard.ts +35 -0
- package/src/contracts/resource-pages.ts +140 -0
- package/src/contracts/resource-resolution.ts +13 -0
- package/src/contracts/shared.ts +4 -0
- package/src/contracts/user-notifications.ts +2 -0
- package/src/events/app-event-delivery.ts +100 -0
- package/src/events/app-events.testing.ts +65 -0
- package/src/events/app-events.ts +134 -0
- package/src/events/core-user-created.ts +24 -0
- package/src/events/index.ts +9 -0
- package/src/events/testing.ts +2 -0
- package/src/index.ts +2 -1
- package/src/server/api/respond.ts +8 -3
- package/src/server/index.ts +1 -0
- package/src/server/middleware/app-access.ts +7 -6
- package/src/server/middleware/auth.ts +168 -66
- package/src/server/middleware/index.ts +1 -0
- package/src/server/middleware/invocation.ts +4 -2
- package/src/server/middleware/workload.ts +3 -3
- package/src/server/oauth-scope.ts +10 -0
- package/src/server/public-storage.ts +106 -0
- package/src/server/services/access.ts +8 -0
- package/src/services/accounts/app.ts +35 -3
- package/src/services/accounts/avatar.ts +342 -35
- package/src/services/accounts/base-group.ts +4 -5
- package/src/services/accounts/directory-shares.ts +157 -0
- package/src/services/accounts/entities.ts +50 -6
- package/src/services/accounts/groups.ts +3 -1
- package/src/services/accounts/identities.ts +64 -12
- package/src/services/accounts/local-groups.ts +1 -1
- package/src/services/accounts/users.ts +23 -15
- package/src/services/app-access-management.ts +519 -0
- package/src/services/app-access.ts +25 -7
- package/src/services/app-approval.ts +15 -4
- package/src/services/app-details.ts +224 -0
- package/src/services/app-discovery.ts +2 -1
- package/src/services/app-event-shapes.ts +139 -0
- package/src/services/app-events/accept.ts +116 -0
- package/src/services/app-events/admin.ts +250 -0
- package/src/services/app-events/declarations.ts +187 -0
- package/src/services/app-events/deliver.ts +218 -0
- package/src/services/app-events.ts +92 -0
- package/src/services/config-conflict.ts +11 -0
- package/src/services/core-user-events.ts +1 -0
- package/src/services/freeipa-config.ts +9 -0
- package/src/services/geo/admin.ts +4 -1
- package/src/services/geo/ledger.ts +47 -14
- package/src/services/geo/route-chain.ts +40 -18
- package/src/services/geo/route-estimate.ts +19 -0
- package/src/services/geo/route-plan.ts +0 -2
- package/src/services/geo/route-service.ts +57 -39
- package/src/services/geo/service.ts +51 -25
- package/src/services/identity/core-origin.ts +12 -0
- package/src/services/identity/index.ts +2 -1
- package/src/services/identity/invocation-actor.ts +50 -2
- package/src/services/identity/invocation-authority.ts +38 -1
- package/src/services/identity/invocation-token.ts +255 -50
- package/src/services/identity/issue-invocation.ts +9 -1
- package/src/services/identity/models-only-auth.ts +13 -4
- package/src/services/identity/nested-invocation.ts +128 -0
- package/src/services/image-metadata.ts +65 -1
- package/src/services/index.ts +24 -1
- package/src/services/ipa/sync.ts +6 -2
- package/src/services/ipa/users.ts +4 -1
- package/src/services/mandates/policy.ts +19 -12
- package/src/services/notifications/browser.ts +4 -0
- package/src/services/notifications/catalog.ts +5 -2
- package/src/services/notifications/platform.ts +7 -2
- package/src/services/notifications/user.ts +18 -7
- package/src/services/oauth-tokens.ts +28 -2
- package/src/services/object-store.ts +217 -0
- package/src/services/providers/local/users.ts +5 -0
- package/src/services/pwa-devices.ts +147 -37
- package/src/services/service-account-credentials.ts +5 -2
- package/src/services/session/index.ts +14 -0
- package/src/services/settings/core-settings.ts +91 -47
- package/src/services/settings/index.ts +8 -0
- package/src/services/settings/versions.ts +26 -0
- package/src/shared/ai-models.ts +274 -0
- package/src/shared/ai-quotas.ts +2 -2
- package/src/shared/ai-usage.ts +2 -2
- package/src/shared/index.ts +2 -0
- package/src/shared-text/documents.ts +25 -8
- package/src/shared-text/log.ts +1 -1
- package/src/shared-text/text-diff.ts +75 -0
- package/src/ssr/AdminArea.tsx +131 -0
- package/src/ssr/AdminLayout.tsx +11 -3
- package/src/ssr/AdminSidebar.tsx +7 -7
- package/src/ssr/AppAccessDenied.tsx +114 -0
- package/src/ssr/AppBadge.tsx +14 -0
- package/src/ssr/AppLaunchpadPanel.tsx +1 -1
- package/src/ssr/AppPwaRuntime.island.tsx +82 -0
- package/src/ssr/CloudShell.island.tsx +25 -4
- package/src/ssr/Layout.tsx +15 -7
- package/src/ssr/PwaLayout.tsx +73 -8
- package/src/ssr/PwaPartCount.island.tsx +23 -0
- package/src/ssr/PwaRuntime.island.tsx +85 -6
- package/src/ssr/SettingsSaveOutcome.tsx +58 -0
- package/src/ssr/SettingsSaveStatus.island.tsx +16 -0
- package/src/ssr/ShellRuntime.island.tsx +5 -0
- package/src/ssr/__snapshots__/Layout.unchanged.render.test.tsx.snap +3 -3
- package/src/ssr/__snapshots__/LayoutHelp.ssr.render.test.tsx.snap +1 -1
- package/src/ssr/admin-active-link.ts +16 -0
- package/src/ssr/admin-navigation.ts +26 -20
- package/src/ssr/app-badges.ts +31 -7
- package/src/ssr/app-pwa-document.tsx +16 -0
- package/src/ssr/app-pwa-messages.ts +17 -0
- package/src/ssr/index.ts +8 -0
- package/src/ssr/layout-context.ts +1 -1
- package/src/ssr/page-trail-runtime.ts +3 -1
- package/src/ssr/platform-messages.ts +9 -1
- package/src/ssr/pwa-messages.ts +10 -2
- package/src/ssr/rail-navigation.ts +5 -1
- package/src/ssr/settings-draft.ts +153 -0
- package/src/ssr/shell-data.tsx +2 -0
- package/src/styles/global.css +1 -0
- package/src/styles/profile-picture.css +270 -0
- package/src/styles/utilities-navigation.css +6 -0
- package/src/workflows/ai/policy.ts +49 -0
- package/src/workflows/ai/runtime.ts +131 -21
- package/src/workflows/ai/store.ts +283 -35
- package/src/workflows/ai/types.ts +8 -2
- package/src/workflows/ai-actions.ts +148 -120
- package/src/workflows/authoring.ts +31 -0
- package/src/workflows/contracts.ts +1 -0
- package/src/workflows/definition.ts +2 -0
- package/src/workflows/editor.ts +15 -0
- package/src/workflows/index.ts +2 -1
- package/src/workflows/language/compiler.ts +34 -3
- package/src/workflows/runtime/executor.ts +132 -33
- package/src/workflows/runtime/index.ts +2 -0
- package/src/workflows/runtime/ports.ts +5 -1
- package/src/workflows/store/actions.ts +1 -1
- package/src/workflows/store/definitions.ts +5 -0
- package/src/workflows/store/runs.ts +5 -3
- package/src/workflows/store/worker.ts +7 -2
- package/src/ai/model-pricing.ts +0 -43
- package/src/ai/model-request-settings.ts +0 -98
- package/src/ai/settings-messages.ts +0 -129
- package/src/shared/ai-model-request-settings.ts +0 -21
package/src/api/mcp.ts
CHANGED
|
@@ -14,7 +14,9 @@ import {
|
|
|
14
14
|
} from "@modelcontextprotocol/sdk/types.js";
|
|
15
15
|
import { Hono, type MiddlewareHandler } from "hono";
|
|
16
16
|
import { z } from "zod";
|
|
17
|
+
import { type CatalogSubscription, projectAppEventCatalog, readEventCatalogSubscriptions } from "../_internal/app-event-catalog";
|
|
17
18
|
import { readBoundedJson } from "../_internal/bounded-json";
|
|
19
|
+
import { resolveCapabilityManifestPresentation } from "../_internal/capabilities";
|
|
18
20
|
import { capabilitySentenceText } from "../_internal/capability-sentences";
|
|
19
21
|
import { withCapabilityRequestTimeout } from "../_internal/capability-timeout";
|
|
20
22
|
import {
|
|
@@ -32,8 +34,12 @@ import {
|
|
|
32
34
|
CAPABILITY_MAX_REQUEST_BYTES,
|
|
33
35
|
CAPABILITY_MAX_RESULT_BYTES,
|
|
34
36
|
CAPABILITY_PROTOCOL_VERSION,
|
|
37
|
+
CAPABILITY_QUALIFIED_ID_MAX_LENGTH,
|
|
35
38
|
type CapabilityActionManifest,
|
|
36
39
|
CapabilityActionReceiptSchema,
|
|
40
|
+
CapabilityAppIdSchema,
|
|
41
|
+
CapabilityEventCatalogEntrySchema,
|
|
42
|
+
CapabilityQualifiedIdSchema,
|
|
37
43
|
type CapabilityQueryManifest,
|
|
38
44
|
CapabilitySemanticLinkSchema,
|
|
39
45
|
type CloudResourceRef,
|
|
@@ -46,6 +52,7 @@ import {
|
|
|
46
52
|
import type { AppRegistryEntry, CapabilityRegistryEntry } from "../contracts/registry";
|
|
47
53
|
import { type AuthContext, auth, type RequestAuthority, rateLimit, rejectReservedWorkloadCredential, resolveLocale } from "../server";
|
|
48
54
|
import { preferredLocale } from "../server/locale";
|
|
55
|
+
import { oauthScopeByOperation } from "../server/middleware/auth";
|
|
49
56
|
import { noTransform } from "../server/middleware/no-transform";
|
|
50
57
|
import { discoverableApps } from "../services/app-discovery";
|
|
51
58
|
import { createHelpReader, type HelpArticle, type HelpMetadata, type HelpReader, type HelpReaderFactory } from "../services/help";
|
|
@@ -63,6 +70,27 @@ const IDEMPOTENCY_KEY_FIELD = "idempotencyKey";
|
|
|
63
70
|
const RESOURCE_READ_TOOL = "cloud__resource__read";
|
|
64
71
|
const HELP_SEARCH_TOOL = "cloud__help__search";
|
|
65
72
|
const HELP_READ_TOOL = "cloud__help__read";
|
|
73
|
+
const EVENT_LIST_TOOL = "cloud__events__list";
|
|
74
|
+
const EVENT_READ_TOOL = "cloud__events__read";
|
|
75
|
+
const eventListArguments = z.strictObject({
|
|
76
|
+
appId: CapabilityAppIdSchema.optional(),
|
|
77
|
+
automation: z.boolean().optional(),
|
|
78
|
+
cursor: z
|
|
79
|
+
.string()
|
|
80
|
+
.max(CAPABILITY_QUALIFIED_ID_MAX_LENGTH + 1 + String(Number.MAX_SAFE_INTEGER).length)
|
|
81
|
+
.regex(/^[a-z][a-z0-9-]*\.[a-z][a-z0-9]*(?:[._-][a-z0-9]+)*@[1-9]\d*$/)
|
|
82
|
+
.optional(),
|
|
83
|
+
limit: z.number().int().min(1).max(MCP_TOOL_PAGE_SIZE).default(MCP_TOOL_PAGE_SIZE),
|
|
84
|
+
});
|
|
85
|
+
const eventReadArguments = z.strictObject({ type: CapabilityQualifiedIdSchema, version: z.number().int().positive() });
|
|
86
|
+
const eventSummarySchema = CapabilityEventCatalogEntrySchema.omit({ dataSchema: true, subscribers: true }).extend({
|
|
87
|
+
appId: CapabilityAppIdSchema,
|
|
88
|
+
});
|
|
89
|
+
const eventDetailSchema = CapabilityEventCatalogEntrySchema.extend({ appId: CapabilityAppIdSchema });
|
|
90
|
+
const eventPageSchema = z.object({
|
|
91
|
+
events: z.array(eventSummarySchema),
|
|
92
|
+
nextCursor: z.string().optional(),
|
|
93
|
+
});
|
|
66
94
|
export const CLOUD_MCP_PROTECTED_RESOURCE_PATH = "/.well-known/oauth-protected-resource/api/mcp/v1";
|
|
67
95
|
const MCP_TOOL_NAME_MAX_LENGTH = 128;
|
|
68
96
|
const MCP_INSTRUCTIONS =
|
|
@@ -72,6 +100,7 @@ const log = logger("mcp");
|
|
|
72
100
|
type McpRouteDependencies = CapabilityDispatchDependencies & {
|
|
73
101
|
listApps?: () => Promise<AppRegistryEntry[]>;
|
|
74
102
|
help?: HelpReaderFactory;
|
|
103
|
+
eventSubscriptions?: (type: string, version: number) => Promise<CatalogSubscription[]>;
|
|
75
104
|
getOperatorLocale?: () => Promise<string | undefined>;
|
|
76
105
|
getOperatorTimeZone?: () => Promise<string | undefined>;
|
|
77
106
|
getAppUrl?: () => Promise<string>;
|
|
@@ -205,6 +234,38 @@ const platformTools: Tool[] = [
|
|
|
205
234
|
annotations: { title: "Read Cloud Help", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
206
235
|
_meta: { "cloud/kind": "help-read" },
|
|
207
236
|
},
|
|
237
|
+
{
|
|
238
|
+
name: EVENT_LIST_TOOL,
|
|
239
|
+
title: "List declared Cloud events",
|
|
240
|
+
description:
|
|
241
|
+
"Discover declared event types of apps you may discover. Returns publisher appId, type, version, title, description, subject and place types (place may be null), and automation metadata. Filter by appId or automation; pass nextCursor to continue. This reads declarations only.",
|
|
242
|
+
inputSchema: cloneObjectSchema(z.toJSONSchema(eventListArguments, { io: "input" })),
|
|
243
|
+
outputSchema: cloneObjectSchema(z.toJSONSchema(eventPageSchema)),
|
|
244
|
+
annotations: {
|
|
245
|
+
title: "List declared Cloud events",
|
|
246
|
+
readOnlyHint: true,
|
|
247
|
+
destructiveHint: false,
|
|
248
|
+
idempotentHint: true,
|
|
249
|
+
openWorldHint: false,
|
|
250
|
+
},
|
|
251
|
+
_meta: { "cloud/kind": "events-list" },
|
|
252
|
+
},
|
|
253
|
+
{
|
|
254
|
+
name: EVENT_READ_TOOL,
|
|
255
|
+
title: "Read declared Cloud event",
|
|
256
|
+
description:
|
|
257
|
+
"Read one exact event type and version returned by cloud__events__list, including its data JSON Schema and discoverable subscribers with persisted subscription states. This reads declarations only; it does not observe events or create subscriptions.",
|
|
258
|
+
inputSchema: cloneObjectSchema(z.toJSONSchema(eventReadArguments)),
|
|
259
|
+
outputSchema: cloneObjectSchema(z.toJSONSchema(z.object({ event: eventDetailSchema }))),
|
|
260
|
+
annotations: {
|
|
261
|
+
title: "Read declared Cloud event",
|
|
262
|
+
readOnlyHint: true,
|
|
263
|
+
destructiveHint: false,
|
|
264
|
+
idempotentHint: true,
|
|
265
|
+
openWorldHint: false,
|
|
266
|
+
},
|
|
267
|
+
_meta: { "cloud/kind": "events-read" },
|
|
268
|
+
},
|
|
208
269
|
];
|
|
209
270
|
|
|
210
271
|
const projectTool = (
|
|
@@ -342,10 +403,12 @@ const parseResponse = async (response: Response): Promise<Record<string, unknown
|
|
|
342
403
|
: { code: CAPABILITY_FRAMEWORK_ERROR_CODES.invalidAppResponse, message: "Capability returned a non-object result" };
|
|
343
404
|
};
|
|
344
405
|
|
|
345
|
-
const responseTooLargeToolResult = (
|
|
406
|
+
const responseTooLargeToolResult = (
|
|
407
|
+
message = "Capability result is too large for an MCP tool response; narrow the query and retry",
|
|
408
|
+
): CallToolResult => {
|
|
346
409
|
const error = {
|
|
347
410
|
code: CAPABILITY_FRAMEWORK_ERROR_CODES.responseTooLarge,
|
|
348
|
-
message
|
|
411
|
+
message,
|
|
349
412
|
};
|
|
350
413
|
return { isError: true, structuredContent: error, content: [{ type: "text", text: JSON.stringify(error) }] };
|
|
351
414
|
};
|
|
@@ -489,6 +552,97 @@ const callHelpTool = async (name: string, argsValue: unknown, reader: HelpReader
|
|
|
489
552
|
return null;
|
|
490
553
|
};
|
|
491
554
|
|
|
555
|
+
const eventCursor = (event: { type: string; version: number }): string => `${event.type}@${event.version}`;
|
|
556
|
+
|
|
557
|
+
type EventCatalogReader = {
|
|
558
|
+
registry: () => Promise<AppRegistryEntry[]>;
|
|
559
|
+
lookup: (appId: string) => Promise<CapabilityRegistryEntry | null>;
|
|
560
|
+
locale: () => Promise<string>;
|
|
561
|
+
subscriptions: (type: string, version: number) => Promise<CatalogSubscription[]>;
|
|
562
|
+
};
|
|
563
|
+
|
|
564
|
+
const listEvents = async (argsValue: unknown, reader: EventCatalogReader): Promise<CallToolResult> => {
|
|
565
|
+
const parsed = eventListArguments.safeParse(argsValue ?? {});
|
|
566
|
+
if (!parsed.success) {
|
|
567
|
+
const issues = parsed.error.issues.map((issue) => `${issue.path.join(".") || "input"}: ${issue.message}`).join("; ");
|
|
568
|
+
return validationToolError(`cloud__events__list accepts appId, automation (boolean), cursor and limit (1-100) (${issues})`);
|
|
569
|
+
}
|
|
570
|
+
const { appId, automation, cursor, limit } = parsed.data;
|
|
571
|
+
const cursorAppId = cursor?.split(".")[0];
|
|
572
|
+
if (appId && cursorAppId && appId !== cursorAppId) {
|
|
573
|
+
return validationToolError("cloud__events__list cursor must belong to the requested appId");
|
|
574
|
+
}
|
|
575
|
+
const summaries = (await reader.registry())
|
|
576
|
+
.filter((entry) => entry.capabilities?.protocolVersion === CAPABILITY_PROTOCOL_VERSION)
|
|
577
|
+
.sort((left, right) => (left.id < right.id ? -1 : left.id > right.id ? 1 : 0));
|
|
578
|
+
const locale = await reader.locale();
|
|
579
|
+
const events: z.infer<typeof eventSummarySchema>[] = [];
|
|
580
|
+
const encoder = new TextEncoder();
|
|
581
|
+
const emptyBytes = encoder.encode(JSON.stringify(toolResult({ events: [] }, false))).byteLength;
|
|
582
|
+
let bytes = emptyBytes;
|
|
583
|
+
for (const summary of summaries) {
|
|
584
|
+
if (appId && summary.id !== appId) continue;
|
|
585
|
+
if (cursorAppId && summary.id < cursorAppId) continue;
|
|
586
|
+
const app = await reader.lookup(summary.id);
|
|
587
|
+
if (!app || app.manifest.manifestHash !== summary.capabilities?.manifestHash) continue;
|
|
588
|
+
const manifest = resolveCapabilityManifestPresentation(app.manifest, app.presentation, locale);
|
|
589
|
+
const catalog = projectAppEventCatalog(manifest, []).sort((left, right) =>
|
|
590
|
+
eventCursor(left) < eventCursor(right) ? -1 : eventCursor(left) > eventCursor(right) ? 1 : 0,
|
|
591
|
+
);
|
|
592
|
+
for (const entry of catalog) {
|
|
593
|
+
if (automation !== undefined && entry.automation !== automation) continue;
|
|
594
|
+
if (cursor && summary.id === cursorAppId && eventCursor(entry) <= cursor) continue;
|
|
595
|
+
const last = events.at(-1);
|
|
596
|
+
if (last && events.length === limit) return boundedToolResult({ events, nextCursor: eventCursor(last) }, false);
|
|
597
|
+
const { dataSchema: _schema, subscribers: _subscribers, ...summaryEntry } = entry;
|
|
598
|
+
const next = { appId: app.appId, ...summaryEntry };
|
|
599
|
+
const serialized = JSON.stringify(next);
|
|
600
|
+
// Count both structuredContent and the JSON-escaped text copy, plus their array separators.
|
|
601
|
+
const entryBytes = encoder.encode(serialized).byteLength + encoder.encode(JSON.stringify(serialized)).byteLength - 2;
|
|
602
|
+
const nextBytes = bytes + entryBytes + (events.length ? 2 : 0);
|
|
603
|
+
const cursorBytes =
|
|
604
|
+
encoder.encode(JSON.stringify(toolResult({ events: [], nextCursor: eventCursor(next) }, false))).byteLength - emptyBytes;
|
|
605
|
+
if (nextBytes + cursorBytes > CAPABILITY_MAX_RESULT_BYTES) {
|
|
606
|
+
return last ? boundedToolResult({ events, nextCursor: eventCursor(last) }, false) : responseTooLargeToolResult();
|
|
607
|
+
}
|
|
608
|
+
events.push(next);
|
|
609
|
+
bytes = nextBytes;
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
return boundedToolResult({ events }, false);
|
|
613
|
+
};
|
|
614
|
+
|
|
615
|
+
const readEvent = async (argsValue: unknown, reader: EventCatalogReader): Promise<CallToolResult> => {
|
|
616
|
+
const parsed = eventReadArguments.safeParse(argsValue);
|
|
617
|
+
if (!parsed.success) {
|
|
618
|
+
const issues = parsed.error.issues.map((issue) => `${issue.path.join(".") || "input"}: ${issue.message}`).join("; ");
|
|
619
|
+
return validationToolError(`cloud__events__read requires an exact event type and positive integer version (${issues})`);
|
|
620
|
+
}
|
|
621
|
+
const { type, version } = parsed.data;
|
|
622
|
+
const summaries = (await reader.registry()).filter((entry) => entry.capabilities?.protocolVersion === CAPABILITY_PROTOCOL_VERSION);
|
|
623
|
+
const summary = summaries.find((entry) => entry.id === type.split(".")[0]);
|
|
624
|
+
if (summary) {
|
|
625
|
+
const app = await reader.lookup(summary.id);
|
|
626
|
+
if (app && app.manifest.manifestHash === summary.capabilities?.manifestHash) {
|
|
627
|
+
const manifest = resolveCapabilityManifestPresentation(app.manifest, app.presentation, await reader.locale());
|
|
628
|
+
const declaration = manifest.events?.find((event) => `${app.appId}.${event.localId}` === type && event.version === version);
|
|
629
|
+
if (declaration) {
|
|
630
|
+
const visibleApps = new Set(summaries.map((entry) => entry.id));
|
|
631
|
+
const subscribers = (await reader.subscriptions(type, version)).filter((row) => visibleApps.has(row.appId));
|
|
632
|
+
const [entry] = projectAppEventCatalog({ ...manifest, events: [declaration] }, subscribers);
|
|
633
|
+
const result = toolResult({ event: { appId: app.appId, ...entry } }, false);
|
|
634
|
+
return fitsToolResult(result)
|
|
635
|
+
? result
|
|
636
|
+
: responseTooLargeToolResult("Event schema and subscriber list exceed the MCP response size limit");
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
return boundedToolResult(
|
|
641
|
+
{ code: "EVENT_NOT_FOUND", message: "Event type and version are not in the current discoverable catalog" },
|
|
642
|
+
true,
|
|
643
|
+
);
|
|
644
|
+
};
|
|
645
|
+
|
|
492
646
|
const createMcpServer = (
|
|
493
647
|
request: Request,
|
|
494
648
|
dependencies: McpRouteDependencies,
|
|
@@ -581,6 +735,26 @@ const createMcpServer = (
|
|
|
581
735
|
});
|
|
582
736
|
|
|
583
737
|
server.setRequestHandler(CallToolRequestSchema, async (message) => {
|
|
738
|
+
if (message.params.name === EVENT_LIST_TOOL || message.params.name === EVENT_READ_TOOL) {
|
|
739
|
+
if (!hasScope("read")) return boundedToolResult({ code: "FORBIDDEN", message: "OAuth scope read is required" }, true);
|
|
740
|
+
const reader: EventCatalogReader = {
|
|
741
|
+
registry,
|
|
742
|
+
lookup: capabilityLookup,
|
|
743
|
+
locale: resolveHelpLocale,
|
|
744
|
+
subscriptions: dependencies.eventSubscriptions ?? readEventCatalogSubscriptions,
|
|
745
|
+
};
|
|
746
|
+
try {
|
|
747
|
+
return message.params.name === EVENT_LIST_TOOL
|
|
748
|
+
? await listEvents(message.params.arguments, reader)
|
|
749
|
+
: await readEvent(message.params.arguments, reader);
|
|
750
|
+
} catch (error) {
|
|
751
|
+
log.error("Failed to read MCP event catalog", { error: error instanceof Error ? error.message : String(error) });
|
|
752
|
+
return boundedToolResult(
|
|
753
|
+
{ code: CAPABILITY_FRAMEWORK_ERROR_CODES.appUnavailable, message: "Event catalog is currently unavailable" },
|
|
754
|
+
true,
|
|
755
|
+
);
|
|
756
|
+
}
|
|
757
|
+
}
|
|
584
758
|
if (message.params.name === HELP_SEARCH_TOOL || message.params.name === HELP_READ_TOOL) {
|
|
585
759
|
if (!hasScope("read")) {
|
|
586
760
|
return boundedToolResult({ code: "FORBIDDEN", message: "OAuth scope read is required" }, true);
|
|
@@ -783,8 +957,8 @@ export const createMcpRoutes = (dependencies: McpRouteDependencies = {}) =>
|
|
|
783
957
|
.use("/mcp/v1", validateMcpOrigin(dependencies.getAppUrl ?? defaultAppUrl))
|
|
784
958
|
.use("/mcp/v1", dependencies.authenticate ?? mcpAuthentication(dependencies.getAppUrl ?? defaultAppUrl))
|
|
785
959
|
.use("/mcp/v1", rejectReservedWorkloadCredential)
|
|
786
|
-
.use("/mcp/v1", dependencies.limit ?? rateLimit())
|
|
787
|
-
.
|
|
960
|
+
.use("/mcp/v1", auth.readOnlyMiddleware(dependencies.limit ?? rateLimit()))
|
|
961
|
+
.post("/mcp/v1", oauthScopeByOperation, async (c) =>
|
|
788
962
|
withCapabilityRequestTimeout(c, "query", async () => {
|
|
789
963
|
if (c.req.method !== "POST") {
|
|
790
964
|
return new Response(null, { status: 405, headers: { Allow: "POST" } });
|
|
@@ -825,6 +999,7 @@ export const createMcpRoutes = (dependencies: McpRouteDependencies = {}) =>
|
|
|
825
999
|
}
|
|
826
1000
|
}
|
|
827
1001
|
}),
|
|
828
|
-
)
|
|
1002
|
+
)
|
|
1003
|
+
.all("/mcp/v1", () => new Response(null, { status: 405, headers: { Allow: "POST" } }));
|
|
829
1004
|
|
|
830
1005
|
export type McpApiType = ReturnType<typeof createMcpRoutes>;
|
package/src/api/me.ts
CHANGED
|
@@ -13,6 +13,7 @@ import { describeRoute } from "hono-openapi";
|
|
|
13
13
|
import { z } from "zod";
|
|
14
14
|
import {
|
|
15
15
|
AccountActivityListResponseSchema,
|
|
16
|
+
AvatarStateSchema,
|
|
16
17
|
BrowserNotificationConfigurationSchema,
|
|
17
18
|
BrowserNotificationEndpointSchema,
|
|
18
19
|
BrowserPushSubscriptionSchema,
|
|
@@ -27,10 +28,10 @@ import {
|
|
|
27
28
|
NotificationDeliveryStatusSchema,
|
|
28
29
|
NotificationQuietSettingsSchema,
|
|
29
30
|
RegisterBrowserNotificationEndpointSchema,
|
|
31
|
+
SaveAvatarResponseSchema,
|
|
32
|
+
SaveAvatarSchema,
|
|
30
33
|
ServiceAccountCredentialSchema,
|
|
31
34
|
SignOutOthersResponseSchema,
|
|
32
|
-
UpdateAvatarResponseSchema,
|
|
33
|
-
UpdateAvatarSchema,
|
|
34
35
|
UpdateNotificationQuietSettingsSchema,
|
|
35
36
|
UpdateProfileSchema,
|
|
36
37
|
UpdateUserNotificationPreferenceSchema,
|
|
@@ -323,7 +324,8 @@ const app = new Hono<AuthContext>()
|
|
|
323
324
|
describeRoute({
|
|
324
325
|
tags: ["Me"],
|
|
325
326
|
summary: "List current user notification preferences",
|
|
326
|
-
description:
|
|
327
|
+
description:
|
|
328
|
+
"List active user-facing notification kinds, app recommendations, required and excluded channels, and explicit preferences.",
|
|
327
329
|
...requiresAuth,
|
|
328
330
|
responses: {
|
|
329
331
|
200: jsonResponse(UserNotificationPreferencesResponseSchema, "Notification preferences"),
|
|
@@ -342,11 +344,12 @@ const app = new Hono<AuthContext>()
|
|
|
342
344
|
describeRoute({
|
|
343
345
|
tags: ["Me"],
|
|
344
346
|
summary: "Set current user notification preference",
|
|
345
|
-
description:
|
|
347
|
+
description:
|
|
348
|
+
"Replace optional channels for one notification kind. Required channels remain enforced; excluded channels are rejected.",
|
|
346
349
|
...requiresAuth,
|
|
347
350
|
responses: {
|
|
348
351
|
200: jsonResponse(UserNotificationPreferenceSchema, "Updated notification preference"),
|
|
349
|
-
400: jsonResponse(ErrorResponseSchema, "Invalid or
|
|
352
|
+
400: jsonResponse(ErrorResponseSchema, "Invalid, unavailable, or excluded channel"),
|
|
350
353
|
401: jsonResponse(ErrorResponseSchema, "Authentication required"),
|
|
351
354
|
404: jsonResponse(ErrorResponseSchema, "Notification preference not found"),
|
|
352
355
|
},
|
|
@@ -492,32 +495,50 @@ const app = new Hono<AuthContext>()
|
|
|
492
495
|
},
|
|
493
496
|
)
|
|
494
497
|
|
|
498
|
+
.get(
|
|
499
|
+
"/avatar",
|
|
500
|
+
describeRoute({
|
|
501
|
+
tags: ["Me"],
|
|
502
|
+
summary: "Read current user profile picture",
|
|
503
|
+
description:
|
|
504
|
+
"Which picture others see (photo, built avatar, or initials), the stored built avatar, and whether a photo is stored. The image itself is served under the account's avatar URL.",
|
|
505
|
+
...requiresAuth,
|
|
506
|
+
responses: {
|
|
507
|
+
200: jsonResponse(AvatarStateSchema, "Profile picture"),
|
|
508
|
+
401: jsonResponse(ErrorResponseSchema, "Authentication required"),
|
|
509
|
+
404: jsonResponse(ErrorResponseSchema, "User not found"),
|
|
510
|
+
},
|
|
511
|
+
}),
|
|
512
|
+
async (c) =>
|
|
513
|
+
respond(c, async () => {
|
|
514
|
+
const state = await accountsService.user.getAvatarState({ id: c.get("user").id });
|
|
515
|
+
return state ? ok(state) : fail(err.notFound("User"));
|
|
516
|
+
}),
|
|
517
|
+
)
|
|
518
|
+
|
|
495
519
|
.put(
|
|
496
520
|
"/avatar",
|
|
497
521
|
describeRoute({
|
|
498
522
|
tags: ["Me"],
|
|
499
|
-
summary: "Update current user
|
|
500
|
-
description:
|
|
523
|
+
summary: "Update current user profile picture",
|
|
524
|
+
description:
|
|
525
|
+
"Store and show a photo (`dataUrl`) or a built avatar (`built`), or show a stored picture (`use`: photo, built, or initials). A stored photo stays until it is deleted; only the shown picture is served. Avatars are visible to user-backed authenticated actors.",
|
|
501
526
|
...requiresAuth,
|
|
502
527
|
responses: {
|
|
503
|
-
200: jsonResponse(
|
|
528
|
+
200: jsonResponse(SaveAvatarResponseSchema, "Avatar updated"),
|
|
504
529
|
400: jsonResponse(ErrorResponseSchema, "Failed to update avatar"),
|
|
505
530
|
401: jsonResponse(ErrorResponseSchema, "Authentication required"),
|
|
531
|
+
409: jsonResponse(ErrorResponseSchema, "The picture to show is not stored"),
|
|
506
532
|
422: jsonResponse(ErrorResponseSchema, "Malformed image container or EXIF"),
|
|
507
533
|
},
|
|
508
534
|
}),
|
|
509
|
-
v("json",
|
|
535
|
+
v("json", SaveAvatarSchema),
|
|
510
536
|
async (c) =>
|
|
511
537
|
respond(c, async () => {
|
|
512
538
|
const user = c.get("user");
|
|
513
|
-
const
|
|
514
|
-
const result = await accountsService.user.setAvatar({
|
|
515
|
-
actor: toAccountsActor(user),
|
|
516
|
-
id: user.id,
|
|
517
|
-
dataUrl: data.dataUrl,
|
|
518
|
-
});
|
|
539
|
+
const result = await accountsService.user.updateAvatar({ actor: toAccountsActor(user), id: user.id, change: c.req.valid("json") });
|
|
519
540
|
if (!result.ok) return result;
|
|
520
|
-
return ok({ message: "Avatar updated.",
|
|
541
|
+
return ok({ message: "Avatar updated.", ...result.data });
|
|
521
542
|
}),
|
|
522
543
|
)
|
|
523
544
|
|
|
@@ -799,7 +820,7 @@ const app = new Hono<AuthContext>()
|
|
|
799
820
|
tags: ["Me"],
|
|
800
821
|
summary: "Sign out every other sign-in",
|
|
801
822
|
description:
|
|
802
|
-
"End other sign-ins and rotate this browser's cookie while preserving its sign-in time and expiry; signInAgain is true if this browser could not be kept. Cloud Login
|
|
823
|
+
"End other sign-ins and rotate this browser's cookie while preserving its sign-in time and expiry; signInAgain is true if this browser could not be kept. Cloud Login devices stay paired. API keys, passkeys and mandates stay valid.",
|
|
803
824
|
...requiresAuth,
|
|
804
825
|
responses: {
|
|
805
826
|
200: jsonResponse(SignOutOthersResponseSchema, "Other sign-ins ended"),
|
package/src/api/pwa-phone.ts
CHANGED
|
@@ -1,11 +1,26 @@
|
|
|
1
1
|
import { type Context, Hono } from "hono";
|
|
2
2
|
import { deleteCookie, getCookie, setCookie } from "hono/cookie";
|
|
3
|
+
import { describeRoute } from "hono-openapi";
|
|
4
|
+
import { registeredPwaBuilds } from "../_internal/runtime-watcher";
|
|
3
5
|
import { env } from "../config/env";
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
+
import {
|
|
7
|
+
PWA_AUTH_PATH,
|
|
8
|
+
PWA_COOKIES,
|
|
9
|
+
PWA_LIMITS,
|
|
10
|
+
PWA_SCOPE,
|
|
11
|
+
PwaClaimSchema,
|
|
12
|
+
PwaCompleteResultSchema,
|
|
13
|
+
PwaDeviceDecisionSchema,
|
|
14
|
+
type PwaLaunchState,
|
|
15
|
+
PwaRenameSchema,
|
|
16
|
+
} from "../contracts/pwa";
|
|
17
|
+
import { launchTarget } from "../contracts/pwa-location";
|
|
18
|
+
import { type AuthContext, auth, getLocale, jsonResponse, rateLimit, v } from "../server";
|
|
6
19
|
import { type PwaCredentials, type PwaDeviceNotifier, PwaError, pwaDevices } from "../services/pwa-devices";
|
|
7
20
|
import { defaultShellAvailable, handlePwaError, type PwaRouteOptions, pwaErrorResponse, pwaInvalidRequest, pwaTransport } from "./pwa";
|
|
8
21
|
|
|
22
|
+
export { launchTarget } from "../contracts/pwa-location";
|
|
23
|
+
|
|
9
24
|
const PAIRING_COOKIE_PATH = `${PWA_AUTH_PATH}/pairings`;
|
|
10
25
|
const cookie = { httpOnly: true, secure: !env.IS_DEVELOPMENT } as const;
|
|
11
26
|
|
|
@@ -19,7 +34,9 @@ const setAppCredentials = (c: Context, credentials: PwaCredentials) => {
|
|
|
19
34
|
maxAge: PWA_LIMITS.idleDays * 86_400,
|
|
20
35
|
});
|
|
21
36
|
};
|
|
37
|
+
const clearLastAddress = (c: Context) => deleteCookie(c, PWA_COOKIES.lastAddress, { path: PWA_SCOPE, secure: true, sameSite: "Lax" });
|
|
22
38
|
const clearAppCredentials = (c: Context) => {
|
|
39
|
+
clearLastAddress(c);
|
|
23
40
|
deleteCookie(c, PWA_COOKIES.session, { ...cookie, path: "/" });
|
|
24
41
|
deleteCookie(c, PWA_COOKIES.device, { ...cookie, path: PWA_AUTH_PATH });
|
|
25
42
|
};
|
|
@@ -35,27 +52,6 @@ const webSession = async (c: Context) => {
|
|
|
35
52
|
return session?.data.kind === "web" ? session : null;
|
|
36
53
|
};
|
|
37
54
|
|
|
38
|
-
/**
|
|
39
|
-
* A path inside the app to return to after the launch bounce; anything else becomes `/pwa/`.
|
|
40
|
-
* The `pwa_launch` marker lets the page stop instead of bouncing again.
|
|
41
|
-
*/
|
|
42
|
-
export const launchTarget = (to: string | undefined): string => {
|
|
43
|
-
const fallback = `${PWA_SCOPE}?pwa_launch=1`;
|
|
44
|
-
if (!to || to.length > 2048 || !to.startsWith(PWA_SCOPE) || to.includes("//") || to.includes("\\")) return fallback;
|
|
45
|
-
const url = new URL(to, "http://launch.invalid");
|
|
46
|
-
// Judge the path the browser will request: dot segments and escapes are resolved by then.
|
|
47
|
-
let path: string;
|
|
48
|
-
try {
|
|
49
|
-
path = decodeURIComponent(url.pathname);
|
|
50
|
-
} catch {
|
|
51
|
-
return fallback;
|
|
52
|
-
}
|
|
53
|
-
const inside = (value: string) => value.startsWith(PWA_SCOPE) && value !== PWA_AUTH_PATH && !value.startsWith(`${PWA_AUTH_PATH}/`);
|
|
54
|
-
if (url.origin !== "http://launch.invalid" || !inside(url.pathname) || !inside(path) || path.includes("//")) return fallback;
|
|
55
|
-
url.searchParams.set("pwa_launch", "1");
|
|
56
|
-
return url.pathname + url.search;
|
|
57
|
-
};
|
|
58
|
-
|
|
59
55
|
const stateUrl = (state: PwaLaunchState) => `${PWA_SCOPE}?pwa=${state}`;
|
|
60
56
|
|
|
61
57
|
export type PwaPhoneRouteOptions = PwaRouteOptions & {
|
|
@@ -98,29 +94,68 @@ export const createPwaPhoneRoutes = (options: PwaPhoneRouteOptions) => {
|
|
|
98
94
|
});
|
|
99
95
|
return c.json({ code: claimed.code, account: claimed.account, expiresAt: claimed.expiresAt });
|
|
100
96
|
})
|
|
101
|
-
.post(
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
97
|
+
.post(
|
|
98
|
+
"/pairings/complete",
|
|
99
|
+
describeRoute({
|
|
100
|
+
tags: ["Cloud app"],
|
|
101
|
+
summary: "Poll or complete a claimed pairing",
|
|
102
|
+
description:
|
|
103
|
+
"Requires the HttpOnly completion cookie from claim. Administrative code confirmation returns confirmation_required with the target, administrator and a pairing-bound decision token; polling alone never completes it. Only a subsequent explicit device decision permits credentials. Self-pairing is unchanged.",
|
|
104
|
+
responses: {
|
|
105
|
+
200: jsonResponse(PwaCompleteResultSchema, "Paired"),
|
|
106
|
+
202: jsonResponse(PwaCompleteResultSchema, "Waiting or device decision required"),
|
|
107
|
+
},
|
|
108
|
+
}),
|
|
109
|
+
async (c) => {
|
|
110
|
+
requireShell(c);
|
|
111
|
+
const web = await webSession(c);
|
|
112
|
+
try {
|
|
113
|
+
const result = await service.completePairing(c, {
|
|
114
|
+
completionSecret: getCookie(c, PWA_COOKIES.pairing),
|
|
115
|
+
deviceKey: getCookie(c, PWA_COOKIES.device),
|
|
116
|
+
appSession: await appSession(c),
|
|
117
|
+
webUserId: web?.user.id ?? null,
|
|
118
|
+
locale: getLocale(c),
|
|
119
|
+
notify: options.notify,
|
|
120
|
+
});
|
|
121
|
+
if (result.state === "waiting" || result.state === "confirmation_required") return c.json(result, 202);
|
|
122
|
+
if (result.state === "rejected") {
|
|
123
|
+
clearPairing(c);
|
|
124
|
+
return c.json(result);
|
|
125
|
+
}
|
|
126
|
+
if (result.credentials) setAppCredentials(c, result.credentials);
|
|
127
|
+
clearLastAddress(c);
|
|
128
|
+
clearPairing(c);
|
|
129
|
+
// A Home Screen app on iOS has its own cookie jar; a web session there is only a copy from Safari.
|
|
130
|
+
if (result.platform === "ios" && auth.session.getWebToken(c)) deleteCookie(c, "session_token", { path: "/" });
|
|
131
|
+
return c.json({ state: "paired" as const });
|
|
132
|
+
} catch (error) {
|
|
133
|
+
if (error instanceof PwaError && error.code === "EXPIRED") clearPairing(c);
|
|
134
|
+
throw error;
|
|
135
|
+
}
|
|
136
|
+
},
|
|
137
|
+
)
|
|
138
|
+
.post(
|
|
139
|
+
"/pairings/decision",
|
|
140
|
+
describeRoute({
|
|
141
|
+
tags: ["Cloud app"],
|
|
142
|
+
summary: "Accept or reject administrative Cloud app pairing on the device",
|
|
143
|
+
description:
|
|
144
|
+
"Requires both the claim's HttpOnly completion cookie and the confirmationToken returned for this pairing after the administrator matched the code. decision is confirm or reject. Repeating the same decision is idempotent. Confirm permits a subsequent complete request; reject issues no credentials.",
|
|
145
|
+
responses: {
|
|
146
|
+
204: { description: "Device decision recorded" },
|
|
147
|
+
403: { description: "Invalid decision token" },
|
|
148
|
+
409: { description: "Pairing is not awaiting this decision" },
|
|
149
|
+
410: { description: "Pairing expired" },
|
|
150
|
+
},
|
|
151
|
+
}),
|
|
152
|
+
v("json", PwaDeviceDecisionSchema, pwaInvalidRequest),
|
|
153
|
+
async (c) => {
|
|
154
|
+
requireShell(c);
|
|
155
|
+
await service.decidePairing(getCookie(c, PWA_COOKIES.pairing), c.req.valid("json"));
|
|
156
|
+
return c.body(null, 204);
|
|
157
|
+
},
|
|
158
|
+
)
|
|
124
159
|
.get("/session/launch", async (c) => {
|
|
125
160
|
const to = launchTarget(c.req.query("to"));
|
|
126
161
|
if (!shellAvailable(c)) return c.redirect(stateUrl("unavailable"), 302);
|
|
@@ -129,7 +164,11 @@ export const createPwaPhoneRoutes = (options: PwaPhoneRouteOptions) => {
|
|
|
129
164
|
if (result.outcome === "renewed") setAppCredentials(c, result.credentials);
|
|
130
165
|
if (result.outcome === "renewed" || result.outcome === "current") return c.redirect(to, 302);
|
|
131
166
|
if (result.outcome === "blocked") return c.redirect(stateUrl("blocked"), 302);
|
|
132
|
-
if (result.outcome === "ended")
|
|
167
|
+
if (result.outcome === "ended") {
|
|
168
|
+
clearAppCredentials(c);
|
|
169
|
+
// Only this answer may delete the drafts: "?pwa=ended" alone is an address anyone can open.
|
|
170
|
+
setCookie(c, PWA_COOKIES.ended, "1", { ...cookie, sameSite: "Lax", path: PWA_SCOPE, maxAge: 300 });
|
|
171
|
+
}
|
|
133
172
|
return c.redirect(stateUrl(result.outcome === "missing" ? "new" : "ended"), 302);
|
|
134
173
|
} catch (error) {
|
|
135
174
|
// Logs an unexpected failure by name only; a navigation always gets a page, never JSON.
|
|
@@ -152,7 +191,7 @@ export const createPwaPhoneRoutes = (options: PwaPhoneRouteOptions) => {
|
|
|
152
191
|
if (result.outcome === "renewed") setAppCredentials(c, result.credentials);
|
|
153
192
|
// Android shares Chrome's cookies with the app: fetches may run as that other web account.
|
|
154
193
|
const otherAccount = web && web.user.id !== result.userId ? { name: web.user.displayName || web.user.uid } : undefined;
|
|
155
|
-
return c.json({ renewed: result.outcome === "renewed", ...(otherAccount ? { otherAccount } : {}) });
|
|
194
|
+
return c.json({ builds: registeredPwaBuilds(), renewed: result.outcome === "renewed", ...(otherAccount ? { otherAccount } : {}) });
|
|
156
195
|
})
|
|
157
196
|
.patch("/session", v("json", PwaRenameSchema, pwaInvalidRequest), async (c) => {
|
|
158
197
|
await service.rename(getCookie(c, PWA_COOKIES.device), c.req.valid("json").name);
|
package/src/api/pwa.ts
CHANGED
|
@@ -28,7 +28,7 @@ const log = logger("pwa");
|
|
|
28
28
|
|
|
29
29
|
const MESSAGES: Record<PwaError["code"], string> = {
|
|
30
30
|
UNAVAILABLE: "The mobile app is not available right now.",
|
|
31
|
-
REAUTHENTICATE: "Sign in again to pair a
|
|
31
|
+
REAUTHENTICATE: "Sign in again to pair a device.",
|
|
32
32
|
FORBIDDEN: "Use Cloud on the web for this.",
|
|
33
33
|
ACCOUNT_BLOCKED: "This account cannot use the mobile app right now.",
|
|
34
34
|
INVALID_REQUEST: "The request is not valid.",
|
|
@@ -40,7 +40,7 @@ const MESSAGES: Record<PwaError["code"], string> = {
|
|
|
40
40
|
ALREADY_PAIRED: "This app is paired with another account.",
|
|
41
41
|
ACCOUNT_MISMATCH: "This browser is signed in to another account.",
|
|
42
42
|
LIMIT_REACHED: "The limit for this account was reached.",
|
|
43
|
-
UNPAIRED: "This
|
|
43
|
+
UNPAIRED: "This device is not paired.",
|
|
44
44
|
};
|
|
45
45
|
|
|
46
46
|
/** `{ code, message }` with a safe English message; the UI maps codes to its own catalog. */
|
|
@@ -95,7 +95,7 @@ const webActor = async (c: Context) => {
|
|
|
95
95
|
const session = token ? await auth.session.authenticateRequest(c, token) : null;
|
|
96
96
|
if (!session) throw new PwaError("REAUTHENTICATE", 403);
|
|
97
97
|
if (session.data.kind !== "web") throw new PwaError("FORBIDDEN", 403);
|
|
98
|
-
return { userId: session.user.id, sid: session.data.sid };
|
|
98
|
+
return { userId: session.user.id, sid: session.data.sid, admin: session.user.roles.includes("admin") };
|
|
99
99
|
};
|
|
100
100
|
|
|
101
101
|
/** The phone this request also comes from (Chrome on a paired Android phone shares the app's cookies). */
|
|
@@ -119,7 +119,7 @@ export const createPwaRoutes = (options: PwaRouteOptions = {}) => {
|
|
|
119
119
|
"/pairings",
|
|
120
120
|
describeRoute({
|
|
121
121
|
tags,
|
|
122
|
-
summary: "Start pairing a
|
|
122
|
+
summary: "Start pairing a device",
|
|
123
123
|
description: "Needs a web sign-in from the last ten minutes. The link secret is returned once.",
|
|
124
124
|
responses: { 201: jsonResponse(PwaPairingStartResultSchema, "Pairing started") },
|
|
125
125
|
}),
|
|
@@ -129,13 +129,33 @@ export const createPwaRoutes = (options: PwaRouteOptions = {}) => {
|
|
|
129
129
|
return c.json(await service.startPairing(actor), 201);
|
|
130
130
|
},
|
|
131
131
|
)
|
|
132
|
+
.post(
|
|
133
|
+
"/users/:userId/pairings",
|
|
134
|
+
describeRoute({
|
|
135
|
+
tags,
|
|
136
|
+
summary: "Pair the Cloud app for another person",
|
|
137
|
+
description:
|
|
138
|
+
"Administrator web session only, signed in within ten minutes. App sessions and API keys are refused. The target account must be active. Uses the existing account pairing and device limits. After the administrator confirms the device code, the device must explicitly accept before credentials are issued.",
|
|
139
|
+
responses: {
|
|
140
|
+
201: jsonResponse(PwaPairingStartResultSchema, "Administrative pairing started"),
|
|
141
|
+
403: { description: "Administrator, fresh web sign-in or active account required" },
|
|
142
|
+
},
|
|
143
|
+
}),
|
|
144
|
+
v("param", z.object({ userId: z.string().uuid() }), pwaInvalidRequest),
|
|
145
|
+
async (c) => {
|
|
146
|
+
const actor = await webActor(c);
|
|
147
|
+
if (!actor.admin) throw new PwaError("FORBIDDEN", 403);
|
|
148
|
+
if (!shellAvailable(c)) throw new PwaError("UNAVAILABLE", 503);
|
|
149
|
+
return c.json(await service.startUserPairing(actor, c.req.valid("param").userId), 201);
|
|
150
|
+
},
|
|
151
|
+
)
|
|
132
152
|
.get(
|
|
133
153
|
"/pairings/:id",
|
|
134
154
|
describeRoute({
|
|
135
155
|
tags,
|
|
136
156
|
summary: "Read a pairing",
|
|
137
157
|
description:
|
|
138
|
-
"Only for the web session that started it. Never returns the code. A pending read is held for up to one poll interval and answers as soon as the
|
|
158
|
+
"Only for the web session that started it. Never returns the code. A pending read is held for up to one poll interval and answers as soon as the device claims the link.",
|
|
139
159
|
responses: { 200: jsonResponse(PwaPairingStatusSchema, "Pairing state") },
|
|
140
160
|
}),
|
|
141
161
|
v("param", IdParamSchema, pwaInvalidRequest),
|
|
@@ -147,7 +167,13 @@ export const createPwaRoutes = (options: PwaRouteOptions = {}) => {
|
|
|
147
167
|
)
|
|
148
168
|
.post(
|
|
149
169
|
"/pairings/:id/confirm",
|
|
150
|
-
describeRoute({
|
|
170
|
+
describeRoute({
|
|
171
|
+
tags,
|
|
172
|
+
summary: "Confirm a pairing with the code the device shows",
|
|
173
|
+
description:
|
|
174
|
+
"Administrative pairings enter awaiting_device; only the claiming device can then accept or reject. Self-pairing proceeds as before.",
|
|
175
|
+
responses: { 204: { description: "Confirmed" } },
|
|
176
|
+
}),
|
|
151
177
|
v("param", IdParamSchema, pwaInvalidRequest),
|
|
152
178
|
v("json", PwaPairingConfirmSchema, pwaInvalidRequest),
|
|
153
179
|
async (c) => {
|
|
@@ -166,7 +192,7 @@ export const createPwaRoutes = (options: PwaRouteOptions = {}) => {
|
|
|
166
192
|
)
|
|
167
193
|
.get(
|
|
168
194
|
"/devices",
|
|
169
|
-
describeRoute({ tags, summary: "List paired
|
|
195
|
+
describeRoute({ tags, summary: "List paired devices", responses: { 200: jsonResponse(PwaDeviceListSchema, "Active devices") } }),
|
|
170
196
|
async (c) => {
|
|
171
197
|
const actor = await webActor(c);
|
|
172
198
|
return c.json({ items: await service.list(actor, await currentDeviceId(c, actor.userId)) });
|
|
@@ -174,7 +200,7 @@ export const createPwaRoutes = (options: PwaRouteOptions = {}) => {
|
|
|
174
200
|
)
|
|
175
201
|
.delete(
|
|
176
202
|
"/devices/:id",
|
|
177
|
-
describeRoute({ tags, summary: "Remove a paired
|
|
203
|
+
describeRoute({ tags, summary: "Remove a paired device", responses: { 204: { description: "Removed; idempotent" } } }),
|
|
178
204
|
v("param", IdParamSchema, pwaInvalidRequest),
|
|
179
205
|
async (c) => {
|
|
180
206
|
await service.revoke(await webActor(c), c.req.valid("param").id);
|