@k2b/cloud 0.43.0 → 0.45.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 +11 -2
- 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 +158 -12
- package/src/_internal/capability-origin.ts +4 -0
- package/src/_internal/capability-streams.ts +7 -5
- package/src/_internal/define-app.ts +57 -6
- 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/process-identity.ts +7 -2
- package/src/_internal/registry-validation.ts +11 -1
- package/src/_internal/registry.ts +8 -1
- package/src/_internal/runtime-context.ts +8 -1
- package/src/access/AppAccessEditor.tsx +2 -0
- package/src/access/GroupCoverage.tsx +20 -5
- package/src/access/PermissionEditor.tsx +20 -5
- package/src/access/PrincipalAvatar.tsx +30 -21
- package/src/access/PrincipalPicker.tsx +14 -4
- package/src/access/ResourceApiKeys.tsx +3 -4
- package/src/access/app-access-messages.ts +6 -0
- package/src/access/messages.ts +4 -4
- package/src/account/EntitySearch.tsx +12 -0
- package/src/account/ProfilePictureDialog.tsx +699 -0
- package/src/account/profile-picture-messages.ts +269 -0
- package/src/account/ui.ts +7 -0
- 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/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/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 +10 -6
- package/src/ai/enrich.ts +2 -2
- package/src/ai/executor.ts +40 -25
- package/src/ai/http.ts +17 -29
- package/src/ai/index.ts +12 -5
- package/src/ai/inference-calls.ts +5 -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.ts +26 -9
- package/src/ai/personal-agent.ts +1 -1
- package/src/ai/prefs.ts +12 -10
- package/src/ai/project-tool.ts +4 -4
- package/src/ai/projects-authorization.ts +53 -0
- package/src/ai/projects-routes.ts +58 -47
- package/src/ai/projects.ts +110 -88
- package/src/ai/protocol.ts +2 -2
- package/src/ai/provider-connections.ts +161 -0
- package/src/ai/provider-errors.ts +55 -6
- package/src/ai/provider-fetch.ts +4 -0
- package/src/ai/provider.ts +1 -1
- package/src/ai/quota-limits.ts +36 -0
- package/src/ai/quota-provider.ts +4 -2
- package/src/ai/quota-report.ts +7 -4
- package/src/ai/quotas.ts +19 -8
- package/src/ai/recent-resources.ts +9 -3
- package/src/ai/request-headers.ts +14 -87
- package/src/ai/resource-refs.ts +6 -2
- package/src/ai/routes.ts +47 -48
- package/src/ai/runtime.ts +50 -22
- package/src/ai/settings.ts +93 -580
- package/src/ai/skill-tool.ts +4 -4
- package/src/ai/skills-routes.ts +27 -26
- package/src/ai/skills.ts +120 -81
- 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 +21 -44
- 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 +27 -22
- 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 +22 -3
- package/src/api/admin-ai-quotas.ts +34 -56
- package/src/api/admin-ai-skills.ts +4 -1
- 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/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 +35 -5
- package/src/api/capabilities.ts +28 -4
- package/src/api/index.ts +7 -1
- package/src/api/mcp.ts +175 -2
- package/src/api/me.ts +6 -4
- package/src/api/pwa-phone.ts +75 -26
- 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/app-pwa/contracts.ts +4 -0
- package/src/app-pwa/core-query-error.ts +20 -0
- package/src/app-pwa/index.ts +1 -0
- package/src/app-pwa/service.ts +91 -0
- package/src/app-pwa/transport.ts +22 -0
- package/src/approvals/messages.ts +2 -2
- package/src/browser/cloud-id.ts +86 -0
- package/src/browser/resource-clipboard.ts +4 -0
- package/src/browser/resource-links.ts +52 -0
- package/src/browser/session.ts +39 -0
- package/src/browser/shared-text-history-validation.ts +151 -0
- package/src/browser/shared-text-history.ts +464 -0
- package/src/browser/shared-text-offline-authorized.ts +29 -0
- package/src/browser/shared-text-offline.ts +351 -0
- package/src/browser/shared-text-provider.ts +714 -0
- package/src/browser/shared-text-table-contract.ts +150 -0
- package/src/browser/shared-text.ts +19 -0
- package/src/capabilities/server-invoke.ts +2 -11
- package/src/capabilities/testing.ts +5 -2
- package/src/cli/access.ts +6 -5
- 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 +5 -2
- package/src/cli/apps.ts +89 -26
- package/src/cli/capabilities.ts +22 -1
- package/src/cli/events.ts +320 -0
- package/src/config/env.ts +40 -0
- package/src/contracts/app-access.ts +26 -2
- package/src/contracts/app-details.ts +89 -0
- package/src/contracts/app-events.ts +51 -0
- package/src/contracts/app.ts +21 -5
- package/src/contracts/capabilities.ts +143 -3
- package/src/contracts/index.ts +2 -0
- package/src/contracts/notification-types.ts +36 -9
- package/src/contracts/pwa.ts +17 -1
- package/src/contracts/registry.ts +6 -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 +6 -1
- 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 +1 -1
- package/src/server/api/respond.ts +17 -4
- package/src/server/index.ts +2 -0
- package/src/server/middleware/app-access.ts +1 -1
- package/src/server/middleware/auth.ts +21 -7
- package/src/server/middleware/index.ts +1 -0
- package/src/server/middleware/invocation.ts +2 -0
- package/src/server/public-storage.ts +106 -0
- package/src/server/services/access.ts +31 -10
- package/src/server/services/index.ts +1 -0
- package/src/services/accounts/app.ts +36 -13
- package/src/services/accounts/entities.ts +43 -19
- package/src/services/accounts/group-sql.ts +23 -0
- package/src/services/accounts/groups.ts +19 -14
- package/src/services/accounts/identities.ts +70 -11
- package/src/services/accounts/identity-reconciliation.ts +3 -1
- package/src/services/accounts/local-groups.ts +69 -4
- package/src/services/accounts/users.ts +45 -14
- package/src/services/app-access.ts +25 -7
- 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/ledger.ts +28 -10
- package/src/services/geo/service.ts +43 -24
- package/src/services/identity/core-origin.ts +12 -0
- package/src/services/identity/invocation-actor.ts +20 -2
- package/src/services/identity/invocation-authority.ts +4 -0
- package/src/services/identity/invocation-token.ts +238 -56
- package/src/services/identity/issue-invocation.ts +6 -1
- package/src/services/image-metadata.ts +65 -1
- package/src/services/index.ts +13 -1
- package/src/services/ipa/sync.ts +6 -2
- package/src/services/ipa/users.ts +4 -1
- package/src/services/mandates/index.ts +2 -2
- package/src/services/mandates/policy.ts +20 -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/session/index.ts +26 -3
- package/src/services/settings/core-settings.ts +71 -43
- 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/email-html.ts +1 -0
- package/src/shared/index.ts +2 -0
- package/src/shared/markdown/extensions/katex.ts +8 -1
- package/src/shared/markdown/extensions/tables.ts +14 -1
- package/src/shared/markdown/index.ts +22 -11
- package/src/shared-text/anchors.ts +29 -0
- package/src/shared-text/awareness.ts +87 -0
- package/src/shared-text/changes.ts +27 -0
- package/src/shared-text/documents.ts +433 -65
- package/src/shared-text/events.ts +12 -2
- package/src/shared-text/index.ts +16 -1
- package/src/shared-text/log.ts +24 -5
- package/src/shared-text/snapshots.ts +69 -23
- package/src/shared-text/structural-history.ts +377 -0
- package/src/shared-text/table-actions.ts +432 -0
- package/src/shared-text/text-diff.ts +75 -0
- package/src/shared-text/types.ts +24 -5
- package/src/ssr/AdminArea.tsx +131 -0
- package/src/ssr/AdminLayout.tsx +9 -3
- package/src/ssr/AdminSidebar.tsx +7 -7
- package/src/ssr/AppAccessDenied.tsx +2 -0
- package/src/ssr/AppBadge.tsx +14 -0
- package/src/ssr/AppLaunchpadPanel.tsx +2 -2
- package/src/ssr/CloudShell.island.tsx +44 -7
- package/src/ssr/Layout.tsx +9 -7
- package/src/ssr/MobileProfileActions.tsx +2 -2
- package/src/ssr/ProfilePreferences.tsx +2 -1
- package/src/ssr/PwaLayout.tsx +16 -1
- package/src/ssr/PwaPartCount.island.tsx +23 -0
- package/src/ssr/SettingsSaveOutcome.tsx +58 -0
- package/src/ssr/SettingsSaveStatus.island.tsx +16 -0
- package/src/ssr/__snapshots__/Layout.unchanged.render.test.tsx.snap +3 -3
- package/src/ssr/admin-active-link.ts +16 -0
- package/src/ssr/admin-navigation.ts +26 -31
- package/src/ssr/app-badges.ts +31 -7
- package/src/ssr/app-launchpad.tsx +6 -1
- package/src/ssr/index.ts +8 -0
- package/src/ssr/page-trail-runtime.ts +3 -1
- package/src/ssr/platform-messages.ts +11 -1
- package/src/ssr/profile-actions.ts +3 -6
- package/src/ssr/pwa-messages.ts +2 -2
- package/src/ssr/rail-navigation.ts +5 -1
- package/src/ssr/settings-draft.ts +153 -0
- package/src/ssr/shell-data.tsx +3 -1
- 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/language/expressions.ts +3 -2
- package/src/workflows/runtime/executor.ts +134 -34
- 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
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { i18n } from "@k2b/stdlib";
|
|
2
|
+
import type { Context } from "hono";
|
|
3
|
+
import { getLocale } from "../../server/locale";
|
|
4
|
+
|
|
5
|
+
const messages = i18n.define({
|
|
6
|
+
baseLocale: "en",
|
|
7
|
+
messages: {
|
|
8
|
+
en: {
|
|
9
|
+
recovery: "Use another sign-in method to confirm your identity. To recover an account, sign out first.",
|
|
10
|
+
mismatch: "Sign in with the account already open in this browser. If your session expired, open the sign-in page and start again.",
|
|
11
|
+
},
|
|
12
|
+
de: {
|
|
13
|
+
recovery: "Bestätige deine Identität mit einer anderen Anmeldemethode. Zur Kontowiederherstellung melde dich zuerst ab.",
|
|
14
|
+
mismatch:
|
|
15
|
+
"Melde dich mit dem Konto an, das in diesem Browser geöffnet ist. Falls deine Sitzung abgelaufen ist, öffne die Anmeldeseite und beginne erneut.",
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
export const accountMismatchResponse = (c: Context) =>
|
|
21
|
+
c.json({ code: "ACCOUNT_MISMATCH", message: messages.resolve([getLocale(c)]).t.mismatch }, 403);
|
|
22
|
+
|
|
23
|
+
export const confirmationRecoveryResponse = (c: Context) =>
|
|
24
|
+
c.json({ code: "CONFIRMATION_METHOD_UNAVAILABLE", message: messages.resolve([getLocale(c)]).t.recovery }, 403);
|
package/src/api/auth.ts
CHANGED
|
@@ -4,15 +4,18 @@ import { describeRoute } from "hono-openapi";
|
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
import { env } from "../config";
|
|
6
6
|
import { ChangeExpiredPasswordSchema } from "../contracts";
|
|
7
|
+
import { coreUserEventPublisher } from "../events/core-user-created";
|
|
7
8
|
import { type AuthContext, auth, getLocale, jsonResponse, rateLimit, respond, v } from "../server";
|
|
8
9
|
import { accounts, audit, authFlows, getFreeIpaConfig, logger, webauthn } from "../services";
|
|
9
10
|
import { isAccountCategoryAllowed } from "../services/account-category-policy";
|
|
10
11
|
import type { AuthNotificationSender } from "../services/auth-flows/notification-sender";
|
|
11
12
|
import { legalConsent } from "../services/legal-consent";
|
|
12
|
-
import { SessionEpochChangedError } from "../services/session";
|
|
13
|
+
import { SessionAccountMismatchError, SessionEpochChangedError } from "../services/session";
|
|
13
14
|
import * as categorySettings from "../services/settings";
|
|
14
15
|
import { publicCloudOrigin } from "../shared/app-url";
|
|
15
16
|
|
|
17
|
+
import { accountMismatchResponse, confirmationRecoveryResponse } from "./auth/account-mismatch";
|
|
18
|
+
|
|
16
19
|
const log = logger("auth");
|
|
17
20
|
|
|
18
21
|
import { ErrorResponseSchema, MessageResponseSchema } from "../contracts";
|
|
@@ -33,7 +36,19 @@ const jsonError = (c: Context, message: string, status: 400 | 401 | 500 | 503) =
|
|
|
33
36
|
/** Authentication routes: login, logout. */
|
|
34
37
|
export const createAuthRoutes = (notificationSender: AuthNotificationSender) =>
|
|
35
38
|
new Hono<AuthContext>()
|
|
39
|
+
.onError((error, c) => {
|
|
40
|
+
if (error instanceof SessionAccountMismatchError) return accountMismatchResponse(c);
|
|
41
|
+
throw error;
|
|
42
|
+
})
|
|
36
43
|
.use(rateLimit())
|
|
44
|
+
.use(async (c, next) => {
|
|
45
|
+
if (
|
|
46
|
+
c.req.header("X-Cloud-Reauthenticate") === "1" &&
|
|
47
|
+
["/admin-login", "/change-expired-password", "/password-reset/complete"].some((path) => c.req.path.endsWith(path))
|
|
48
|
+
)
|
|
49
|
+
return confirmationRecoveryResponse(c);
|
|
50
|
+
return next();
|
|
51
|
+
})
|
|
37
52
|
.post(
|
|
38
53
|
"/legal-consent",
|
|
39
54
|
describeRoute({ tags: ["Auth"], summary: "Explicitly accept terms after sign-in; unlock the pending session" }),
|
|
@@ -147,13 +162,20 @@ export const createAuthRoutes = (notificationSender: AuthNotificationSender) =>
|
|
|
147
162
|
tags: ["Auth"],
|
|
148
163
|
summary: "Logout",
|
|
149
164
|
description:
|
|
150
|
-
"Idempotent: clears the session cookie and revokes the current credential if present. No authentication required
|
|
165
|
+
"Idempotent: clears the session cookie and revokes the current credential if present. No authentication required. An optional X-Cloud-Logout-User header binds revocation to that user UUID (or none) without writing the shared browser cookie; a changed account returns 409 without revocation.",
|
|
166
|
+
parameters: [{ in: "header", name: "X-Cloud-Logout-User", required: false, schema: { type: "string" } }],
|
|
151
167
|
responses: {
|
|
152
168
|
200: jsonResponse(MessageResponseSchema, "Session invalidated"),
|
|
169
|
+
400: jsonResponse(ErrorResponseSchema, "Invalid expected account"),
|
|
170
|
+
409: jsonResponse(ErrorResponseSchema, "Browser account changed"),
|
|
153
171
|
},
|
|
154
172
|
}),
|
|
155
173
|
async (c) => {
|
|
156
|
-
|
|
174
|
+
const expected = c.req.header("x-cloud-logout-user");
|
|
175
|
+
if (expected !== undefined && expected !== "none" && !z.string().uuid().safeParse(expected).success)
|
|
176
|
+
return c.json({ message: "Invalid expected account" }, 400);
|
|
177
|
+
if (!(await auth.session.delete(c, expected === "none" ? null : expected)))
|
|
178
|
+
return c.json({ message: "Browser account changed; reload before signing out" }, 409);
|
|
157
179
|
log.info("Logout");
|
|
158
180
|
return c.json({ message: "Logged out" });
|
|
159
181
|
},
|
|
@@ -301,7 +323,7 @@ export const createAuthRoutes = (notificationSender: AuthNotificationSender) =>
|
|
|
301
323
|
tags: ["Auth"],
|
|
302
324
|
summary: "Complete password reset",
|
|
303
325
|
description:
|
|
304
|
-
"Use a one-time reset token to first sign the account out everywhere, including unpairing Cloud Login and mobile app
|
|
326
|
+
"Use a one-time reset token to first sign the account out everywhere, including unpairing Cloud Login and mobile app devices, then set the password, sign out everywhere again and create the new browser session. If signing out everywhere fails before the change, the password stays unchanged; if it fails after the change, no session is issued. Both answer 500, and a new reset link is required. signOutOthersFailed is false whenever a session is issued.",
|
|
305
327
|
responses: {
|
|
306
328
|
200: jsonResponse(PasswordChangeAuthResponseSchema, "Password reset completed and session created"),
|
|
307
329
|
400: jsonResponse(ErrorResponseSchema, "Password reset failed"),
|
|
@@ -399,14 +421,22 @@ export const createAuthRoutes = (notificationSender: AuthNotificationSender) =>
|
|
|
399
421
|
// prior `DO NOTHING` could log into a pre-existing `uid='admin'` row that
|
|
400
422
|
// was locally demoted (non-admin local user, or even an IPA row named
|
|
401
423
|
// admin), silently bypassing the intent of the emergency endpoint.
|
|
402
|
-
await sql
|
|
424
|
+
await sql.begin(async (tx) => {
|
|
425
|
+
const [created] = await tx<{ id: string; inserted: boolean }[]>`
|
|
403
426
|
INSERT INTO auth.users (uid, provider, profile, admin, given_name, sn, display_name)
|
|
404
427
|
VALUES ('admin', 'local', 'user', true, 'Admin', '', 'Admin')
|
|
405
428
|
ON CONFLICT (uid) DO UPDATE SET
|
|
406
429
|
provider = 'local',
|
|
407
430
|
profile = 'user',
|
|
408
431
|
admin = true
|
|
432
|
+
RETURNING id, (xmax = 0) AS inserted
|
|
409
433
|
`;
|
|
434
|
+
if (created?.inserted)
|
|
435
|
+
await coreUserEventPublisher().publish(tx, "user.created", {
|
|
436
|
+
subject: created.id,
|
|
437
|
+
data: { userId: created.id, provider: "local" },
|
|
438
|
+
});
|
|
439
|
+
});
|
|
410
440
|
|
|
411
441
|
const user = await accounts.users.get({ uid: "admin" });
|
|
412
442
|
if (!user) return jsonError(c, "Failed to resolve admin user.", 500);
|
package/src/api/capabilities.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Hono, type MiddlewareHandler } from "hono";
|
|
2
2
|
import { describeRoute } from "hono-openapi";
|
|
3
3
|
import { z } from "zod";
|
|
4
|
+
import { type CatalogSubscription, projectAppEventCatalog, readCatalogSubscriptions } from "../_internal/app-event-catalog";
|
|
4
5
|
import { readBoundedJson } from "../_internal/bounded-json";
|
|
5
6
|
import { resolveCapabilityManifestPresentation } from "../_internal/capabilities";
|
|
6
7
|
import {
|
|
@@ -112,6 +113,7 @@ const CapabilityCatalogQuerySchema = z
|
|
|
112
113
|
.strict();
|
|
113
114
|
export type CapabilityRouteDependencies = {
|
|
114
115
|
listApps?: () => Promise<AppRegistryEntry[]>;
|
|
116
|
+
eventSubscriptions?: () => Promise<CatalogSubscription[]>;
|
|
115
117
|
getCapability?: (appId: string) => Promise<CapabilityRegistryEntry | null>;
|
|
116
118
|
fetch?: (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
|
|
117
119
|
authenticate?: MiddlewareHandler<AuthContext>;
|
|
@@ -165,7 +167,7 @@ const invocationAuthorityFromMandate = (authority: {
|
|
|
165
167
|
|
|
166
168
|
export const loadCapabilityCatalogPage = async (
|
|
167
169
|
query: { cursor?: string; limit: number },
|
|
168
|
-
dependencies: Pick<CapabilityRouteDependencies, "listApps" | "getCapability"> = {},
|
|
170
|
+
dependencies: Pick<CapabilityRouteDependencies, "listApps" | "getCapability" | "eventSubscriptions"> = {},
|
|
169
171
|
locale?: string,
|
|
170
172
|
authority: RequestAuthority | null = null,
|
|
171
173
|
): Promise<CapabilityCatalog> => {
|
|
@@ -178,6 +180,21 @@ export const loadCapabilityCatalogPage = async (
|
|
|
178
180
|
const liveApps = entries
|
|
179
181
|
.filter((entry) => entry.capabilities?.protocolVersion === CAPABILITY_PROTOCOL_VERSION)
|
|
180
182
|
.sort((left, right) => left.id.localeCompare(right.id));
|
|
183
|
+
const resolved = new Map<string, CapabilityRegistryEntry | null>();
|
|
184
|
+
const lookup = async (appId: string) => {
|
|
185
|
+
if (!resolved.has(appId)) resolved.set(appId, await capabilityLookup(appId));
|
|
186
|
+
return resolved.get(appId) ?? null;
|
|
187
|
+
};
|
|
188
|
+
let subscriptions: CatalogSubscription[] | undefined;
|
|
189
|
+
const loadSubscribers = async () => {
|
|
190
|
+
if (!subscriptions) {
|
|
191
|
+
const visibleApps = new Set(liveApps.map((entry) => entry.id));
|
|
192
|
+
subscriptions = (await (dependencies.eventSubscriptions ?? readCatalogSubscriptions)()).filter((subscription) =>
|
|
193
|
+
visibleApps.has(subscription.appId),
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
return subscriptions;
|
|
197
|
+
};
|
|
181
198
|
const start = query.cursor ? liveApps.findIndex((entry) => entry.id > query.cursor!) : 0;
|
|
182
199
|
const offset = start < 0 ? liveApps.length : start;
|
|
183
200
|
const apps: CapabilityCatalog["apps"] = [];
|
|
@@ -192,22 +209,29 @@ export const loadCapabilityCatalogPage = async (
|
|
|
192
209
|
).byteLength;
|
|
193
210
|
for (const entry of liveApps.slice(offset)) {
|
|
194
211
|
if (apps.length === query.limit) break;
|
|
195
|
-
const capability = await
|
|
212
|
+
const capability = await lookup(entry.id);
|
|
196
213
|
if (!capability || capability.manifest.manifestHash !== entry.capabilities?.manifestHash) {
|
|
197
214
|
lastScannedAppId = entry.id;
|
|
198
215
|
continue;
|
|
199
216
|
}
|
|
200
217
|
const presentedEntry = locale ? resolveAppPresentation(entry, locale) : entry;
|
|
218
|
+
const manifest = locale
|
|
219
|
+
? resolveCapabilityManifestPresentation(capability.manifest, capability.presentation, locale)
|
|
220
|
+
: capability.manifest;
|
|
201
221
|
const projected = {
|
|
202
222
|
appId: capability.appId,
|
|
203
223
|
appName: presentedEntry.name,
|
|
204
224
|
appIcon: capability.appIcon,
|
|
205
225
|
appDescription: presentedEntry.description,
|
|
206
|
-
manifest
|
|
226
|
+
manifest,
|
|
227
|
+
...(manifest.events ? { events: projectAppEventCatalog(manifest, manifest.events.length ? await loadSubscribers() : []) } : {}),
|
|
207
228
|
};
|
|
208
229
|
const projectedBytes = new TextEncoder().encode(JSON.stringify(projected)).byteLength;
|
|
209
230
|
const separatingCommas = apps.length;
|
|
210
|
-
if (
|
|
231
|
+
if (envelopeBytes + bytes + separatingCommas + projectedBytes > CAPABILITY_MAX_CATALOG_BYTES) {
|
|
232
|
+
if (!apps.length) throw new RangeError("An app's event catalog exceeds the catalog response byte limit");
|
|
233
|
+
break;
|
|
234
|
+
}
|
|
211
235
|
apps.push(projected);
|
|
212
236
|
bytes += projectedBytes;
|
|
213
237
|
lastScannedAppId = entry.id;
|
package/src/api/index.ts
CHANGED
|
@@ -21,6 +21,7 @@ import { aiProjectsRoutes } from "../ai/projects-routes";
|
|
|
21
21
|
import { aiRoutes } from "../ai/routes";
|
|
22
22
|
import { aiSkillsRoutes } from "../ai/skills-routes";
|
|
23
23
|
import accountsEntitiesRoutes from "./accounts-entities";
|
|
24
|
+
import adminAiRoutes from "./admin-ai";
|
|
24
25
|
import adminAiProjectsRoutes from "./admin-ai-projects";
|
|
25
26
|
import adminAiQuotaRoutes from "./admin-ai-quotas";
|
|
26
27
|
import adminAiSkillsRoutes from "./admin-ai-skills";
|
|
@@ -36,6 +37,7 @@ import adminOutgoingMailRoutes from "./admin-outgoing-mail";
|
|
|
36
37
|
import adminRailRoutes from "./admin-rail";
|
|
37
38
|
import { adminAnnouncementRoutes, announcementRoutes } from "./announcements";
|
|
38
39
|
import { createAppApprovalRoutes } from "./app-approval";
|
|
40
|
+
import { createAppEventRoutes } from "./app-events";
|
|
39
41
|
import { approvalRoutes } from "./approvals";
|
|
40
42
|
import { appDiscoveryRoutes } from "./apps";
|
|
41
43
|
import { createAuthRoutes } from "./auth";
|
|
@@ -45,6 +47,7 @@ import { createHelpRoutes } from "./help";
|
|
|
45
47
|
import { createMcpRoutes } from "./mcp";
|
|
46
48
|
import meRoutes from "./me";
|
|
47
49
|
import { createPwaRoutes } from "./pwa";
|
|
50
|
+
import { createResourceResolveRoutes } from "./resource-resolve";
|
|
48
51
|
import { createSearchRoutes } from "./search";
|
|
49
52
|
import { createSyncOpsProxyRoutes } from "./sync-ops";
|
|
50
53
|
import { createWidgetRoutes } from "./widgets";
|
|
@@ -85,8 +88,11 @@ const buildCoreApi = (options: CoreApiOptions) => {
|
|
|
85
88
|
.route("/accounts", accountsEntitiesRoutes)
|
|
86
89
|
.route("/geo", geoRoutes)
|
|
87
90
|
.route("/apps", appDiscoveryRoutes)
|
|
91
|
+
.route("/app-events", createAppEventRoutes())
|
|
92
|
+
.route("/resources", createResourceResolveRoutes())
|
|
88
93
|
.route("/approvals", approvalRoutes)
|
|
89
94
|
.route("/announcements", announcementRoutes)
|
|
95
|
+
.route("/admin/core/ai", adminAiRoutes)
|
|
90
96
|
.route("/admin/core/ai-usage", adminAiUsageRoutes)
|
|
91
97
|
.route("/admin/core/ai-quotas", adminAiQuotaRoutes)
|
|
92
98
|
.route("/admin/core/ai-projects", adminAiProjectsRoutes)
|
|
@@ -136,4 +142,4 @@ export {
|
|
|
136
142
|
createMcpRoutes,
|
|
137
143
|
publicCloudOrigin,
|
|
138
144
|
} from "./mcp";
|
|
139
|
-
export { resolveCloudResourceHref } from "./resource-links";
|
|
145
|
+
export { resolveCloudResourceHref, resolveCloudResourceUrl } from "./resource-links";
|
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,
|
|
@@ -64,6 +70,27 @@ const IDEMPOTENCY_KEY_FIELD = "idempotencyKey";
|
|
|
64
70
|
const RESOURCE_READ_TOOL = "cloud__resource__read";
|
|
65
71
|
const HELP_SEARCH_TOOL = "cloud__help__search";
|
|
66
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
|
+
});
|
|
67
94
|
export const CLOUD_MCP_PROTECTED_RESOURCE_PATH = "/.well-known/oauth-protected-resource/api/mcp/v1";
|
|
68
95
|
const MCP_TOOL_NAME_MAX_LENGTH = 128;
|
|
69
96
|
const MCP_INSTRUCTIONS =
|
|
@@ -73,6 +100,7 @@ const log = logger("mcp");
|
|
|
73
100
|
type McpRouteDependencies = CapabilityDispatchDependencies & {
|
|
74
101
|
listApps?: () => Promise<AppRegistryEntry[]>;
|
|
75
102
|
help?: HelpReaderFactory;
|
|
103
|
+
eventSubscriptions?: (type: string, version: number) => Promise<CatalogSubscription[]>;
|
|
76
104
|
getOperatorLocale?: () => Promise<string | undefined>;
|
|
77
105
|
getOperatorTimeZone?: () => Promise<string | undefined>;
|
|
78
106
|
getAppUrl?: () => Promise<string>;
|
|
@@ -206,6 +234,38 @@ const platformTools: Tool[] = [
|
|
|
206
234
|
annotations: { title: "Read Cloud Help", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
207
235
|
_meta: { "cloud/kind": "help-read" },
|
|
208
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
|
+
},
|
|
209
269
|
];
|
|
210
270
|
|
|
211
271
|
const projectTool = (
|
|
@@ -343,10 +403,12 @@ const parseResponse = async (response: Response): Promise<Record<string, unknown
|
|
|
343
403
|
: { code: CAPABILITY_FRAMEWORK_ERROR_CODES.invalidAppResponse, message: "Capability returned a non-object result" };
|
|
344
404
|
};
|
|
345
405
|
|
|
346
|
-
const responseTooLargeToolResult = (
|
|
406
|
+
const responseTooLargeToolResult = (
|
|
407
|
+
message = "Capability result is too large for an MCP tool response; narrow the query and retry",
|
|
408
|
+
): CallToolResult => {
|
|
347
409
|
const error = {
|
|
348
410
|
code: CAPABILITY_FRAMEWORK_ERROR_CODES.responseTooLarge,
|
|
349
|
-
message
|
|
411
|
+
message,
|
|
350
412
|
};
|
|
351
413
|
return { isError: true, structuredContent: error, content: [{ type: "text", text: JSON.stringify(error) }] };
|
|
352
414
|
};
|
|
@@ -490,6 +552,97 @@ const callHelpTool = async (name: string, argsValue: unknown, reader: HelpReader
|
|
|
490
552
|
return null;
|
|
491
553
|
};
|
|
492
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
|
+
|
|
493
646
|
const createMcpServer = (
|
|
494
647
|
request: Request,
|
|
495
648
|
dependencies: McpRouteDependencies,
|
|
@@ -582,6 +735,26 @@ const createMcpServer = (
|
|
|
582
735
|
});
|
|
583
736
|
|
|
584
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
|
+
}
|
|
585
758
|
if (message.params.name === HELP_SEARCH_TOOL || message.params.name === HELP_READ_TOOL) {
|
|
586
759
|
if (!hasScope("read")) {
|
|
587
760
|
return boundedToolResult({ code: "FORBIDDEN", message: "OAuth scope read is required" }, true);
|
package/src/api/me.ts
CHANGED
|
@@ -324,7 +324,8 @@ const app = new Hono<AuthContext>()
|
|
|
324
324
|
describeRoute({
|
|
325
325
|
tags: ["Me"],
|
|
326
326
|
summary: "List current user notification preferences",
|
|
327
|
-
description:
|
|
327
|
+
description:
|
|
328
|
+
"List active user-facing notification kinds, app recommendations, required and excluded channels, and explicit preferences.",
|
|
328
329
|
...requiresAuth,
|
|
329
330
|
responses: {
|
|
330
331
|
200: jsonResponse(UserNotificationPreferencesResponseSchema, "Notification preferences"),
|
|
@@ -343,11 +344,12 @@ const app = new Hono<AuthContext>()
|
|
|
343
344
|
describeRoute({
|
|
344
345
|
tags: ["Me"],
|
|
345
346
|
summary: "Set current user notification preference",
|
|
346
|
-
description:
|
|
347
|
+
description:
|
|
348
|
+
"Replace optional channels for one notification kind. Required channels remain enforced; excluded channels are rejected.",
|
|
347
349
|
...requiresAuth,
|
|
348
350
|
responses: {
|
|
349
351
|
200: jsonResponse(UserNotificationPreferenceSchema, "Updated notification preference"),
|
|
350
|
-
400: jsonResponse(ErrorResponseSchema, "Invalid or
|
|
352
|
+
400: jsonResponse(ErrorResponseSchema, "Invalid, unavailable, or excluded channel"),
|
|
351
353
|
401: jsonResponse(ErrorResponseSchema, "Authentication required"),
|
|
352
354
|
404: jsonResponse(ErrorResponseSchema, "Notification preference not found"),
|
|
353
355
|
},
|
|
@@ -818,7 +820,7 @@ const app = new Hono<AuthContext>()
|
|
|
818
820
|
tags: ["Me"],
|
|
819
821
|
summary: "Sign out every other sign-in",
|
|
820
822
|
description:
|
|
821
|
-
"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.",
|
|
822
824
|
...requiresAuth,
|
|
823
825
|
responses: {
|
|
824
826
|
200: jsonResponse(SignOutOthersResponseSchema, "Other sign-ins ended"),
|
package/src/api/pwa-phone.ts
CHANGED
|
@@ -1,10 +1,21 @@
|
|
|
1
1
|
import { type Context, Hono } from "hono";
|
|
2
2
|
import { deleteCookie, getCookie, setCookie } from "hono/cookie";
|
|
3
|
+
import { describeRoute } from "hono-openapi";
|
|
3
4
|
import { registeredPwaBuilds } from "../_internal/runtime-watcher";
|
|
4
5
|
import { env } from "../config/env";
|
|
5
|
-
import {
|
|
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";
|
|
6
17
|
import { launchTarget } from "../contracts/pwa-location";
|
|
7
|
-
import { type AuthContext, auth, getLocale, rateLimit, v } from "../server";
|
|
18
|
+
import { type AuthContext, auth, getLocale, jsonResponse, rateLimit, v } from "../server";
|
|
8
19
|
import { type PwaCredentials, type PwaDeviceNotifier, PwaError, pwaDevices } from "../services/pwa-devices";
|
|
9
20
|
import { defaultShellAvailable, handlePwaError, type PwaRouteOptions, pwaErrorResponse, pwaInvalidRequest, pwaTransport } from "./pwa";
|
|
10
21
|
|
|
@@ -83,30 +94,68 @@ export const createPwaPhoneRoutes = (options: PwaPhoneRouteOptions) => {
|
|
|
83
94
|
});
|
|
84
95
|
return c.json({ code: claimed.code, account: claimed.account, expiresAt: claimed.expiresAt });
|
|
85
96
|
})
|
|
86
|
-
.post(
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
+
)
|
|
110
159
|
.get("/session/launch", async (c) => {
|
|
111
160
|
const to = launchTarget(c.req.query("to"));
|
|
112
161
|
if (!shellAvailable(c)) return c.redirect(stateUrl("unavailable"), 302);
|