@k2b/cloud 0.40.0 → 0.42.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 +9 -5
- package/src/_internal/bounded-json.ts +14 -2
- package/src/_internal/capabilities.ts +87 -8
- package/src/_internal/capability-reviews.ts +6 -0
- package/src/_internal/capability-sentences.ts +2 -1
- package/src/_internal/capability-timeout.ts +71 -0
- package/src/_internal/define-app.ts +158 -92
- package/src/_internal/heartbeat.ts +203 -14
- package/src/_internal/http-drain.ts +56 -0
- package/src/_internal/registry-validation.ts +3 -0
- package/src/_internal/registry.ts +28 -1
- package/src/_internal/runtime-context.ts +1 -0
- package/src/_internal/setup-lock.ts +103 -0
- package/src/_internal/status-preserving-ssr.ts +2 -0
- package/src/access/PermissionEditor.tsx +16 -17
- package/src/access/PrincipalPicker.tsx +45 -10
- package/src/access/messages.ts +8 -0
- package/src/access/principal-label.ts +18 -0
- package/src/access/ui.ts +1 -0
- package/src/account/EntitySearch.tsx +5 -4
- package/src/ai/action-review.ts +2 -1
- package/src/ai/app-skill-store.ts +1 -1
- package/src/ai/capabilities.ts +21 -3
- package/src/ai/capability-execution.ts +14 -12
- package/src/ai/chat/action-card.ts +76 -4
- package/src/ai/chat/action-messages.ts +14 -0
- package/src/ai/chat/action-question.browser-harness.tsx +12 -0
- package/src/ai/chat/action-question.tsx +214 -23
- package/src/ai/chat/action-text.ts +288 -0
- package/src/ai/chat/tool-action-words.ts +31 -0
- package/src/ai/chat/tool-action.tsx +1 -26
- package/src/ai/chat-tasks.ts +2 -1
- package/src/ai/client/controller.ts +150 -32
- package/src/ai/client/transport.ts +14 -2
- package/src/ai/code-capability-routes.ts +27 -23
- package/src/ai/code-runtime-tools.ts +14 -16
- package/src/ai/code-source-tools.ts +16 -18
- package/src/ai/executor.ts +64 -21
- package/src/ai/fetch-file-tool.ts +67 -76
- package/src/ai/firecrawl-tools.ts +15 -2
- package/src/ai/http.ts +1 -0
- package/src/ai/index.ts +1 -0
- package/src/ai/migrate.ts +28 -0
- package/src/ai/open-questions.ts +168 -0
- package/src/ai/projects.ts +1 -2
- package/src/ai/protocol.ts +2 -2
- package/src/ai/question-text.ts +8 -0
- package/src/ai/recent-resources.ts +12 -5
- package/src/ai/review-content.ts +77 -0
- package/src/ai/routes.ts +119 -35
- package/src/ai/runtime.ts +1 -0
- package/src/ai/skill-tool.ts +17 -6
- package/src/ai/skills-routes.ts +72 -27
- package/src/ai/skills.ts +139 -60
- package/src/ai/store.ts +184 -31
- package/src/ai/testing.ts +36 -0
- package/src/ai/tool-sentences.ts +152 -34
- package/src/ai/tool-wording.ts +12 -6
- package/src/ai/tools.ts +8 -3
- package/src/ai/types.ts +25 -4
- package/src/ai/usage.ts +2 -2
- package/src/api/admin-core-settings.ts +15 -2
- package/src/api/admin-geo.ts +54 -0
- package/src/api/admin-linux-identities.ts +6 -3
- package/src/api/admin-outgoing-mail.ts +29 -15
- package/src/api/app-approval-actions.ts +31 -7
- package/src/api/approvals-loader.ts +3 -3
- package/src/api/approvals.ts +51 -0
- package/src/api/apps.ts +4 -2
- package/src/api/auth/schemas.ts +3 -0
- package/src/api/auth.ts +54 -18
- package/src/api/capabilities.ts +258 -181
- package/src/api/capability-streams.ts +21 -13
- package/src/api/cli-plugins.ts +4 -1
- package/src/api/geo-glyphs.ts +11 -0
- package/src/api/geo.ts +128 -0
- package/src/api/help.ts +12 -4
- package/src/api/index.ts +5 -0
- package/src/api/mcp.ts +73 -41
- package/src/api/me-panel-consent.ts +8 -5
- package/src/api/me.ts +79 -3
- package/src/api/search.ts +21 -14
- package/src/api/sync-ops.ts +19 -13
- package/src/api/widgets.ts +42 -18
- package/src/approvals/Approvals.tsx +30 -66
- package/src/approvals/approval-card.ts +68 -0
- package/src/browser/CloudResourceSearch.browser-harness.tsx +53 -1
- package/src/browser/CloudResourceSearch.tsx +3 -13
- package/src/browser/PanelLeaveNotice.tsx +18 -0
- package/src/browser/navigation-search.ts +78 -25
- package/src/browser/panel-leave.ts +69 -0
- package/src/browser/panel-messages.ts +12 -0
- package/src/browser/panel-slot.ts +173 -24
- package/src/browser/panel.ts +3 -0
- package/src/browser/resource-search-messages.ts +2 -0
- package/src/capabilities/client.ts +28 -1
- package/src/capabilities/executions.ts +22 -3
- package/src/capabilities/geo-client.ts +57 -0
- package/src/capabilities/response.ts +2 -1
- package/src/capabilities/review-storage.ts +24 -0
- package/src/capabilities/reviews.ts +17 -3
- package/src/capabilities/server.ts +36 -16
- package/src/cli/access.ts +15 -23
- package/src/cli/admin/linux.ts +13 -2
- package/src/cli/admin/outgoing-mail.ts +26 -4
- package/src/cli/capabilities.ts +19 -0
- package/src/config/env.ts +0 -6
- package/src/contracts/app-access.ts +48 -0
- package/src/contracts/app-approval.ts +3 -3
- package/src/contracts/app.ts +4 -1
- package/src/contracts/approvals.ts +2 -2
- package/src/contracts/avatar.ts +186 -0
- package/src/contracts/capabilities.ts +149 -55
- package/src/contracts/geo-admin.ts +127 -0
- package/src/contracts/geo-map.ts +19 -0
- package/src/contracts/geo-route.ts +168 -0
- package/src/contracts/geo.ts +248 -0
- package/src/contracts/index.ts +4 -0
- package/src/contracts/outgoing-mail.ts +17 -4
- package/src/contracts/registry.ts +2 -0
- package/src/contracts/shared.ts +3 -0
- package/src/events/app-access.ts +68 -0
- package/src/events/live-delivery.ts +128 -0
- package/src/events/live.ts +14 -130
- package/src/geo/CloudMap.tsx +95 -0
- package/src/geo/map-messages.ts +17 -0
- package/src/geo/ui.ts +1 -0
- package/src/index.ts +2 -0
- package/src/server/client-ip.ts +36 -0
- package/src/server/index.ts +3 -4
- package/src/server/middleware/app-access.ts +312 -0
- package/src/server/middleware/auth.ts +5 -3
- package/src/server/middleware/invocation.ts +11 -0
- package/src/server/middleware/rate-limit.ts +4 -4
- package/src/server/services/access.ts +13 -9
- package/src/server/services/index.ts +0 -2
- package/src/server/services/services.ts +0 -2
- package/src/services/accounts/app.ts +23 -3
- package/src/services/accounts/posix.ts +21 -7
- package/src/services/accounts/users.ts +12 -1
- package/src/services/allowed-network-fetch.ts +16 -10
- package/src/services/app-access-metrics.ts +24 -0
- package/src/services/app-access.ts +123 -0
- package/src/services/app-discovery.ts +27 -0
- package/src/services/approvals/content.ts +40 -0
- package/src/services/approvals/device.ts +49 -33
- package/src/services/approvals/index.ts +15 -6
- package/src/services/approvals/list.ts +6 -11
- package/src/services/approvals/question.ts +72 -0
- package/src/services/audit/index.ts +1 -0
- package/src/services/auth-flows/ipa.ts +39 -2
- package/src/services/auth-flows/password-reset.ts +32 -6
- package/src/services/downstream-budget.ts +87 -0
- package/src/services/geo/admin.ts +262 -0
- package/src/services/geo/bucket.ts +67 -0
- package/src/services/geo/cache.ts +57 -0
- package/src/services/geo/costs.ts +5 -0
- package/src/services/geo/errors.ts +41 -0
- package/src/services/geo/fixtures/README.md +17 -0
- package/src/services/geo/fixtures/along.json +355 -0
- package/src/services/geo/fixtures/autocomplete.json +218 -0
- package/src/services/geo/fixtures/geocode.json +129 -0
- package/src/services/geo/fixtures/places.json +355 -0
- package/src/services/geo/fixtures/reverse.json +61 -0
- package/src/services/geo/fixtures/routing.json +57 -0
- package/src/services/geo/geoapify-route.ts +93 -0
- package/src/services/geo/geoapify.ts +281 -0
- package/src/services/geo/ledger.ts +122 -0
- package/src/services/geo/map.ts +160 -0
- package/src/services/geo/migrate.ts +25 -0
- package/src/services/geo/polyline.ts +184 -0
- package/src/services/geo/route-chain.ts +145 -0
- package/src/services/geo/route-costs.ts +9 -0
- package/src/services/geo/route-geometry.ts +39 -0
- package/src/services/geo/route-plan.ts +172 -0
- package/src/services/geo/route-service.ts +433 -0
- package/src/services/geo/service.ts +300 -0
- package/src/services/geo/settings.ts +49 -0
- package/src/services/help/index.ts +16 -7
- package/src/services/help/types.ts +6 -1
- package/src/services/identity/invocation-actor.ts +20 -2
- package/src/services/identity/issue-invocation.ts +67 -0
- package/src/services/identity/key-ring.ts +5 -3
- package/src/services/identity/runtime-config.ts +23 -0
- package/src/services/index.ts +11 -0
- package/src/services/mandates/index.ts +40 -3
- package/src/services/network-security.ts +6 -3
- package/src/services/network-transport.ts +99 -0
- package/src/services/notifications/catalog.ts +3 -2
- package/src/services/notifications/runtime.ts +1 -0
- package/src/services/notifications/web-push-transport.ts +45 -38
- package/src/services/outgoing-mail/dispatcher.ts +6 -1
- package/src/services/outgoing-mail/index.ts +55 -1
- package/src/services/outgoing-mail/messages.ts +82 -3
- package/src/services/outgoing-mail/resolve-host.ts +2 -2
- package/src/services/outgoing-mail/retention.ts +13 -1
- package/src/services/outgoing-mail/store.ts +36 -6
- package/src/services/outgoing-mail/test-send.ts +34 -4
- package/src/services/outgoing-mail/transport.ts +1 -1
- package/src/services/pdf/gotenberg.ts +11 -38
- package/src/services/pdf/markdown.ts +160 -2
- package/src/services/pdf/template-preview.ts +1 -1
- package/src/services/public-http.ts +28 -40
- package/src/services/session/index.ts +203 -29
- package/src/services/settings/app.ts +4 -1
- package/src/services/settings/core-settings.ts +88 -0
- package/src/services/settings/defaults.ts +8 -2
- package/src/services/settings/errors.ts +11 -0
- package/src/services/settings/geo-budget.ts +40 -0
- package/src/services/settings/index.ts +4 -3
- package/src/services/settings/store.ts +25 -17
- package/src/services/weather/geo.ts +29 -111
- package/src/services/weather/index.ts +4 -3
- package/src/services/weather/locations.ts +8 -5
- package/src/services/weather/migrate.ts +1 -0
- package/src/services/weather/settings.ts +0 -16
- package/src/shared/__snapshots__/avatar-art.test.ts.snap +5 -0
- package/src/shared/avatar-art.ts +400 -0
- package/src/shared/index.ts +2 -0
- package/src/shared/network-address.ts +22 -3
- package/src/shared/template-rendering.ts +36 -4
- package/src/shared-text/documents.ts +1 -1
- package/src/shared-text/index.ts +2 -0
- package/src/shared-text/log.ts +51 -3
- package/src/shared-text/snapshots.ts +340 -0
- package/src/ssr/AdminLayout.tsx +4 -2
- package/src/ssr/CloudShell.island.tsx +6 -0
- package/src/ssr/HelpPanelArticle.island.tsx +308 -0
- package/src/ssr/HelpPanelBack.island.tsx +32 -0
- package/src/ssr/HelpPanelMenu.island.tsx +133 -0
- package/src/ssr/HelpPanelSearch.island.tsx +123 -0
- package/src/ssr/HelpPanelShortcuts.island.tsx +22 -0
- package/src/ssr/HelpPanelView.island.tsx +27 -0
- package/src/ssr/Layout.tsx +65 -15
- package/src/ssr/LayoutHelp.browser-harness.tsx +1 -1
- package/src/ssr/LayoutHelp.tsx +185 -542
- package/src/ssr/LayoutHelpPanel.browser-harness.tsx +6 -7
- package/src/ssr/LayoutHelpPanel.server.browser-harness.tsx +131 -0
- package/src/ssr/LayoutHelpPanel.server.tsx +115 -0
- package/src/ssr/PanelRuntime.island.tsx +11 -7
- package/src/ssr/PwaLayout.tsx +2 -2
- package/src/ssr/RailApps.tsx +6 -0
- package/src/ssr/ShellRuntime.island.tsx +23 -7
- package/src/ssr/__snapshots__/Layout.unchanged.render.test.tsx.snap +151 -0
- package/src/ssr/__snapshots__/LayoutHelp.ssr.render.test.tsx.snap +29 -0
- package/src/ssr/admin-navigation.ts +1 -0
- package/src/ssr/app-badges.ts +6 -1
- package/src/ssr/compact-shell.browser-harness.ts +44 -5
- package/src/ssr/help.ts +7 -5
- package/src/ssr/index.ts +2 -1
- package/src/ssr/islands/index.ts +3 -4
- package/src/ssr/layout-context.ts +2 -0
- package/src/ssr/layout-help-panel-controls.tsx +43 -0
- package/src/ssr/layout-help-panel-document.ts +67 -0
- package/src/ssr/layout-help-panel-lifecycle.ts +32 -0
- package/src/ssr/layout-help-panel-menu.tsx +113 -0
- package/src/ssr/layout-help-panel-modules.ts +7 -0
- package/src/ssr/layout-help-panel-reader.tsx +159 -0
- package/src/ssr/layout-help-panel-search.tsx +69 -0
- package/src/ssr/layout-help-panel-shortcuts.ts +7 -0
- package/src/ssr/layout-help-panel-state.ts +141 -0
- package/src/ssr/layout-help-panel-types.ts +19 -0
- package/src/ssr/layout-help-views.tsx +484 -0
- package/src/ssr/mobile-menu-history.ts +4 -0
- package/src/ssr/page-swap-fixture/Column.island.tsx +15 -0
- package/src/ssr/page-swap-fixture/ProviderProbe.island.tsx +31 -0
- package/src/ssr/page-swap.browser-harness.tsx +177 -0
- package/src/ssr/page-swap.ts +28 -0
- package/src/ssr/page-trail-runtime.ts +32 -13
- package/src/ssr/panel-consent.ts +2 -2
- package/src/ssr/panel-idle.ts +9 -0
- package/src/ssr/panel-slot.browser-harness.tsx +3 -0
- package/src/ssr/platform-messages.ts +8 -0
- package/src/ssr/runtime.ts +17 -0
- package/src/ssr/shell-data.tsx +10 -2
- package/src/styles/approvals.css +6 -0
- package/src/styles/effects.css +15 -0
- package/src/styles/global.css +70 -6
- package/src/server/services/geo.ts +0 -215
|
@@ -15,9 +15,10 @@ import {
|
|
|
15
15
|
capabilityResultSchema,
|
|
16
16
|
} from "../contracts/capabilities";
|
|
17
17
|
import type { RequestAuthority } from "../server";
|
|
18
|
+
import { AppAccessDeniedError, appAccessDeniedBody } from "../server/middleware/app-access";
|
|
18
19
|
import { invocationAuthorityFromRequest } from "../services/identity/invocation-authority";
|
|
19
20
|
import { capabilityInvocationOperation } from "../services/identity/invocation-operations";
|
|
20
|
-
import { signInvocationToken } from "../services/identity/invocation
|
|
21
|
+
import { issueInvocationToken as signInvocationToken } from "../services/identity/issue-invocation";
|
|
21
22
|
import { withActiveIdentitySigner } from "../services/identity/key-ring";
|
|
22
23
|
import { logger } from "../services/logging";
|
|
23
24
|
import { decryptSecret, encryptSecret } from "../services/secrets";
|
|
@@ -142,17 +143,20 @@ export async function dispatchCapabilityStream(
|
|
|
142
143
|
destructive = "destructive" in operation && operation.destructive;
|
|
143
144
|
const signed = await (dependencies.withActiveSigner ?? withActiveIdentitySigner)(
|
|
144
145
|
"invocation",
|
|
145
|
-
(signer) =>
|
|
146
|
-
(dependencies.signInvocation ?? signInvocationToken)(
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
146
|
+
(signer, db) =>
|
|
147
|
+
(dependencies.signInvocation ?? signInvocationToken)(
|
|
148
|
+
{
|
|
149
|
+
targetAppId: grant.appId,
|
|
150
|
+
callingAppId: "core",
|
|
151
|
+
operation: capabilityInvocationOperation(grant.kind, grant.capabilityId),
|
|
152
|
+
schemaHash: operation.schemaHash,
|
|
153
|
+
authority: invocationAuthorityFromRequest(authority),
|
|
154
|
+
requestId: grant.requestId,
|
|
155
|
+
signer,
|
|
156
|
+
issuer: signer.issuer,
|
|
157
|
+
},
|
|
158
|
+
{ db, groupsAdmin: signer.groupsAdmin },
|
|
159
|
+
),
|
|
156
160
|
{ signal, timeoutMs: 30_000 },
|
|
157
161
|
);
|
|
158
162
|
const headers = new Headers({
|
|
@@ -265,7 +269,11 @@ export async function dispatchCapabilityStream(
|
|
|
265
269
|
if (!error.success || response.status === 401) return denied(status, "STREAM_FAILED");
|
|
266
270
|
} else await record(response.status);
|
|
267
271
|
return Response.json(receipt.data, { status: response.status, headers: { "cache-control": "no-store" } });
|
|
268
|
-
} catch {
|
|
272
|
+
} catch (error) {
|
|
273
|
+
if (error instanceof AppAccessDeniedError) {
|
|
274
|
+
await record(403, error.code);
|
|
275
|
+
return Response.json(appAccessDeniedBody(error.app), { status: 403 });
|
|
276
|
+
}
|
|
269
277
|
const status = request.signal.aborted ? 499 : signal.aborted ? 504 : 502;
|
|
270
278
|
const code = status === 499 ? "REQUEST_CANCELLED" : status === 504 ? "DEADLINE_EXCEEDED" : "STREAM_FAILED";
|
|
271
279
|
await record(status, code);
|
package/src/api/cli-plugins.ts
CHANGED
|
@@ -4,13 +4,16 @@ import { listApps } from "../_internal/registry";
|
|
|
4
4
|
import type { CloudCliPluginSummary } from "../cli/plugin";
|
|
5
5
|
import type { AuthContext } from "../server/middleware/auth";
|
|
6
6
|
|
|
7
|
+
import { appAccessAuthority } from "../services/app-access";
|
|
8
|
+
import { discoverableApps } from "../services/app-discovery";
|
|
9
|
+
|
|
7
10
|
/**
|
|
8
11
|
* `GET /cli/plugins`: the `cld` plugins that live applications serve, under
|
|
9
12
|
* the same access rule as the plugin downloads. Core mounts this list; each
|
|
10
13
|
* application serves its own plugin files.
|
|
11
14
|
*/
|
|
12
15
|
export const cliPluginListRoutes = new Hono<AuthContext>().get("/", requireCliPluginAccess, async (c) => {
|
|
13
|
-
const plugins: CloudCliPluginSummary[] = (await listApps())
|
|
16
|
+
const plugins: CloudCliPluginSummary[] = (await discoverableApps(appAccessAuthority(c), await listApps()))
|
|
14
17
|
.flatMap((app) => (app.cliModules ?? []).map((name) => ({ name, app: app.id, version: app.runtime?.version ?? "unknown" })))
|
|
15
18
|
.sort((left, right) => (left.name < right.name ? -1 : left.name > right.name ? 1 : 0));
|
|
16
19
|
return c.json({ plugins }, 200, { "cache-control": "private, no-store" });
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { appAssetPath } from "../server/app-assets";
|
|
2
|
+
|
|
3
|
+
declare const __CLOUD_APP_ASSETS__: string | undefined;
|
|
4
|
+
|
|
5
|
+
/** The route validates the font and range before resolving a bundled asset. */
|
|
6
|
+
export const geoGlyphFile = (font: string, range: string) =>
|
|
7
|
+
Bun.file(
|
|
8
|
+
typeof __CLOUD_APP_ASSETS__ === "string"
|
|
9
|
+
? appAssetPath("map-glyphs", font, range)
|
|
10
|
+
: new URL(`./map-glyphs/${encodeURIComponent(font)}/${range}`, import.meta.resolve("@k2b/ui/package.json")),
|
|
11
|
+
);
|
package/src/api/geo.ts
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { mapStyle } from "@k2b/ui";
|
|
2
|
+
import { Hono, type MiddlewareHandler } from "hono";
|
|
3
|
+
import { describeRoute } from "hono-openapi";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { GEO_MAP_MAX_ZOOM } from "../contracts/geo-map";
|
|
6
|
+
import { type AuthContext, auth, getLocale, requiresAuth, v } from "../server";
|
|
7
|
+
import { REQUEST_IDLE_TIMEOUT_SECONDS, setRequestTimeout } from "../server/request-timeout";
|
|
8
|
+
import { GEO_TIMEOUT_MS } from "../services/geo/bucket";
|
|
9
|
+
import { GeoError } from "../services/geo/errors";
|
|
10
|
+
import { coreGeoMap } from "../services/geo/map";
|
|
11
|
+
|
|
12
|
+
import { geoGlyphFile } from "./geo-glyphs";
|
|
13
|
+
|
|
14
|
+
const styleQuery = z.object({
|
|
15
|
+
theme: z.enum(["light", "dark"]).default("light"),
|
|
16
|
+
locale: z
|
|
17
|
+
.string()
|
|
18
|
+
.max(100)
|
|
19
|
+
.refine((locale) => {
|
|
20
|
+
try {
|
|
21
|
+
new Intl.Locale(locale);
|
|
22
|
+
return true;
|
|
23
|
+
} catch {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
})
|
|
27
|
+
.optional(),
|
|
28
|
+
});
|
|
29
|
+
const tileParams = z.object({
|
|
30
|
+
z: z.coerce.number().int().min(0).max(GEO_MAP_MAX_ZOOM),
|
|
31
|
+
x: z.coerce.number().int().nonnegative(),
|
|
32
|
+
y: z.string().regex(/^\d+\.pbf$/),
|
|
33
|
+
});
|
|
34
|
+
const glyphParams = z.object({ font: z.literal("IBM Plex Sans Medium"), range: z.string().regex(/^\d+-\d+\.pbf$/) });
|
|
35
|
+
|
|
36
|
+
export const createGeoRoutes = (
|
|
37
|
+
authenticate: MiddlewareHandler<AuthContext> = auth.requireRole("authenticated"),
|
|
38
|
+
service: typeof coreGeoMap = coreGeoMap,
|
|
39
|
+
) =>
|
|
40
|
+
new Hono<AuthContext>()
|
|
41
|
+
.use("*", authenticate)
|
|
42
|
+
.use("*", auth.requireOAuthScope("read", "admin"))
|
|
43
|
+
.onError((cause, c) => {
|
|
44
|
+
const error = cause instanceof GeoError ? cause : new GeoError("GEO_PROVIDER_FAILED");
|
|
45
|
+
c.header("Cache-Control", "private, no-store");
|
|
46
|
+
if (error.code === "GEO_BUSY") c.header("Retry-After", String(error.details.retryAfterSeconds ?? 1));
|
|
47
|
+
return Response.json(
|
|
48
|
+
{ code: error.code, details: error.details },
|
|
49
|
+
{
|
|
50
|
+
status: error.status,
|
|
51
|
+
headers: {
|
|
52
|
+
"Cache-Control": "private, no-store",
|
|
53
|
+
...(error.code === "GEO_BUSY" ? { "Retry-After": String(error.details.retryAfterSeconds ?? 1) } : {}),
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
);
|
|
57
|
+
})
|
|
58
|
+
.get(
|
|
59
|
+
"/style",
|
|
60
|
+
describeRoute({ ...requiresAuth, tags: ["Places and maps"], summary: "Get the Cloud map style with same-origin tiles and glyphs" }),
|
|
61
|
+
v("query", styleQuery),
|
|
62
|
+
async (c) => {
|
|
63
|
+
const query = c.req.valid("query");
|
|
64
|
+
const locale = query.locale ?? getLocale(c);
|
|
65
|
+
await service.style({ theme: query.theme }, { actor: c.get("actor"), accessSubject: c.get("accessSubject"), locale });
|
|
66
|
+
c.header("Cache-Control", "private, no-store");
|
|
67
|
+
return c.json(
|
|
68
|
+
mapStyle({
|
|
69
|
+
theme: query.theme,
|
|
70
|
+
locale,
|
|
71
|
+
tiles: "/api/geo/tiles/{z}/{x}/{y}.pbf",
|
|
72
|
+
glyphs: "/api/geo/glyphs/{fontstack}/{range}.pbf",
|
|
73
|
+
maxzoom: GEO_MAP_MAX_ZOOM,
|
|
74
|
+
}),
|
|
75
|
+
);
|
|
76
|
+
},
|
|
77
|
+
)
|
|
78
|
+
.get(
|
|
79
|
+
"/tiles/:z/:x/:y",
|
|
80
|
+
describeRoute({ ...requiresAuth, tags: ["Places and maps"], summary: "Load one budgeted Geoapify EU vector tile" }),
|
|
81
|
+
v("param", tileParams),
|
|
82
|
+
async (c) => {
|
|
83
|
+
const params = c.req.valid("param");
|
|
84
|
+
setRequestTimeout(c, Math.ceil(GEO_TIMEOUT_MS.tiles / 1000) + REQUEST_IDLE_TIMEOUT_SECONDS);
|
|
85
|
+
try {
|
|
86
|
+
const bytes = await service.tile(
|
|
87
|
+
{ z: params.z, x: params.x, y: Number(params.y.slice(0, -4)) },
|
|
88
|
+
{
|
|
89
|
+
actor: c.get("actor"),
|
|
90
|
+
accessSubject: c.get("accessSubject"),
|
|
91
|
+
locale: getLocale(c),
|
|
92
|
+
signal: c.req.raw.signal,
|
|
93
|
+
},
|
|
94
|
+
);
|
|
95
|
+
return new Response(bytes, {
|
|
96
|
+
headers: {
|
|
97
|
+
"Content-Type": "application/vnd.mapbox-vector-tile",
|
|
98
|
+
"Cache-Control": "private, max-age=604800",
|
|
99
|
+
"X-Content-Type-Options": "nosniff",
|
|
100
|
+
},
|
|
101
|
+
});
|
|
102
|
+
} finally {
|
|
103
|
+
setRequestTimeout(c, REQUEST_IDLE_TIMEOUT_SECONDS);
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
)
|
|
107
|
+
.get(
|
|
108
|
+
"/glyphs/:font/:range",
|
|
109
|
+
describeRoute({ ...requiresAuth, tags: ["Places and maps"], summary: "Load locally bundled IBM Plex map labels" }),
|
|
110
|
+
v("param", glyphParams),
|
|
111
|
+
async (c) => {
|
|
112
|
+
const { font, range } = c.req.valid("param");
|
|
113
|
+
const [start, end] = range.slice(0, -4).split("-").map(Number);
|
|
114
|
+
if (start === undefined || end === undefined || start < 0 || end > 65535 || start % 256 !== 0 || end !== start + 255)
|
|
115
|
+
return c.notFound();
|
|
116
|
+
const file = geoGlyphFile(font, range);
|
|
117
|
+
if (!(await file.exists())) return c.notFound();
|
|
118
|
+
return new Response(file, {
|
|
119
|
+
headers: {
|
|
120
|
+
"Content-Type": "application/x-protobuf",
|
|
121
|
+
"Cache-Control": "private, max-age=604800",
|
|
122
|
+
"X-Content-Type-Options": "nosniff",
|
|
123
|
+
},
|
|
124
|
+
});
|
|
125
|
+
},
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
export default createGeoRoutes();
|
package/src/api/help.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Hono, type MiddlewareHandler } from "hono";
|
|
2
2
|
import { type AuthContext, auth, getLocale } from "../server";
|
|
3
3
|
import { buildHelpDocumentPayload } from "../server/help-payload";
|
|
4
|
+
import { appAccessAuthority } from "../services/app-access";
|
|
4
5
|
import { createHelpReader, type HelpReaderFactory } from "../services/help";
|
|
5
6
|
import type { HelpSearchPayload } from "../shared/help";
|
|
6
7
|
import type { renderHelpMarkdown } from "../shared/markdown";
|
|
@@ -24,16 +25,23 @@ export const createHelpRoutes = (dependencies: HelpRouteDependencies = {}) => {
|
|
|
24
25
|
const query = c.req.query("q")?.trim().slice(0, 200) ?? "";
|
|
25
26
|
const payload: HelpSearchPayload = {
|
|
26
27
|
locale,
|
|
27
|
-
ids: (
|
|
28
|
-
(
|
|
29
|
-
|
|
28
|
+
ids: (
|
|
29
|
+
await reader(locale, c.get("actor"), { authority: appAccessAuthority(c) }).search({
|
|
30
|
+
query,
|
|
31
|
+
appId: c.req.param("appId"),
|
|
32
|
+
limit: 25,
|
|
33
|
+
})
|
|
34
|
+
).map((document) => document.documentId),
|
|
30
35
|
};
|
|
31
36
|
return c.json(payload);
|
|
32
37
|
})
|
|
33
38
|
.get("/help/v1/:appId/documents/:documentId", async (c) => {
|
|
34
39
|
const locale = getLocale(c);
|
|
35
40
|
const { t } = helpApiMessages.resolve([locale]);
|
|
36
|
-
const document = await reader(locale, c.get("actor")
|
|
41
|
+
const document = await reader(locale, c.get("actor"), { authority: appAccessAuthority(c) }).read({
|
|
42
|
+
appId: c.req.param("appId"),
|
|
43
|
+
documentId: c.req.param("documentId"),
|
|
44
|
+
});
|
|
37
45
|
if (!document) return c.json({ code: "HELP_NOT_FOUND", message: t.notFound }, 404);
|
|
38
46
|
const payload = buildHelpDocumentPayload(document, c.req.header("User-Agent") ?? "", dependencies.renderMarkdown);
|
|
39
47
|
return c.json(payload);
|
package/src/api/index.ts
CHANGED
|
@@ -26,6 +26,7 @@ import adminAiQuotaRoutes from "./admin-ai-quotas";
|
|
|
26
26
|
import adminAiSkillsRoutes from "./admin-ai-skills";
|
|
27
27
|
import adminAiUsageRoutes from "./admin-ai-usage";
|
|
28
28
|
import adminCoreSettingsRoutes from "./admin-core-settings";
|
|
29
|
+
import adminGeoRoutes from "./admin-geo";
|
|
29
30
|
import adminGeoIpRoutes from "./admin-geoip";
|
|
30
31
|
import adminIdentityRoutes from "./admin-identity";
|
|
31
32
|
import adminLifecycleRoutes from "./admin-lifecycle";
|
|
@@ -38,6 +39,7 @@ import { approvalRoutes } from "./approvals";
|
|
|
38
39
|
import { appDiscoveryRoutes } from "./apps";
|
|
39
40
|
import { createAuthRoutes } from "./auth";
|
|
40
41
|
import { createCapabilityRoutes } from "./capabilities";
|
|
42
|
+
import geoRoutes from "./geo";
|
|
41
43
|
import { createHelpRoutes } from "./help";
|
|
42
44
|
import { createMcpRoutes } from "./mcp";
|
|
43
45
|
import meRoutes from "./me";
|
|
@@ -46,6 +48,7 @@ import { createSearchRoutes } from "./search";
|
|
|
46
48
|
import { createSyncOpsProxyRoutes } from "./sync-ops";
|
|
47
49
|
import { createWidgetRoutes } from "./widgets";
|
|
48
50
|
|
|
51
|
+
export { withCapabilityRequestTimeout } from "../_internal/capability-timeout";
|
|
49
52
|
export { createAiOpenAiRoutes } from "../ai/openai-api";
|
|
50
53
|
export { type CapabilityDispatchDependencies, dispatchCapability } from "./capabilities";
|
|
51
54
|
export { cliPluginListRoutes } from "./cli-plugins";
|
|
@@ -78,6 +81,7 @@ const buildCoreApi = (options: CoreApiOptions) => {
|
|
|
78
81
|
.route("/auth", createAuthRoutes(options.notifications))
|
|
79
82
|
.route("/me", meRoutes)
|
|
80
83
|
.route("/accounts", accountsEntitiesRoutes)
|
|
84
|
+
.route("/geo", geoRoutes)
|
|
81
85
|
.route("/apps", appDiscoveryRoutes)
|
|
82
86
|
.route("/approvals", approvalRoutes)
|
|
83
87
|
.route("/announcements", announcementRoutes)
|
|
@@ -91,6 +95,7 @@ const buildCoreApi = (options: CoreApiOptions) => {
|
|
|
91
95
|
.route("/admin/core/rail", adminRailRoutes)
|
|
92
96
|
.route("/admin/core/linux-identities", adminLinuxIdentityRoutes)
|
|
93
97
|
.route("/admin/core/geoip", adminGeoIpRoutes)
|
|
98
|
+
.route("/admin/core/geo", adminGeoRoutes)
|
|
94
99
|
.route("/admin/identity", adminIdentityRoutes)
|
|
95
100
|
.route("/admin/sync", createSyncOpsProxyRoutes())
|
|
96
101
|
.route("/admin/lifecycle", adminLifecycleRoutes)
|
package/src/api/mcp.ts
CHANGED
|
@@ -15,6 +15,8 @@ import {
|
|
|
15
15
|
import { Hono, type MiddlewareHandler } from "hono";
|
|
16
16
|
import { z } from "zod";
|
|
17
17
|
import { readBoundedJson } from "../_internal/bounded-json";
|
|
18
|
+
import { capabilitySentenceText } from "../_internal/capability-sentences";
|
|
19
|
+
import { withCapabilityRequestTimeout } from "../_internal/capability-timeout";
|
|
18
20
|
import {
|
|
19
21
|
HELP_SEARCH_MAX_LIMIT,
|
|
20
22
|
helpAgentMarkdown,
|
|
@@ -31,6 +33,7 @@ import {
|
|
|
31
33
|
CAPABILITY_MAX_RESULT_BYTES,
|
|
32
34
|
CAPABILITY_PROTOCOL_VERSION,
|
|
33
35
|
type CapabilityActionManifest,
|
|
36
|
+
CapabilityActionReceiptSchema,
|
|
34
37
|
type CapabilityQueryManifest,
|
|
35
38
|
CapabilitySemanticLinkSchema,
|
|
36
39
|
type CloudResourceRef,
|
|
@@ -42,12 +45,15 @@ import {
|
|
|
42
45
|
} from "../contracts/capabilities";
|
|
43
46
|
import type { AppRegistryEntry, CapabilityRegistryEntry } from "../contracts/registry";
|
|
44
47
|
import { type AuthContext, auth, type RequestAuthority, rateLimit, rejectReservedWorkloadCredential, resolveLocale } from "../server";
|
|
48
|
+
import { preferredLocale } from "../server/locale";
|
|
45
49
|
import { noTransform } from "../server/middleware/no-transform";
|
|
50
|
+
import { discoverableApps } from "../services/app-discovery";
|
|
46
51
|
import { createHelpReader, type HelpArticle, type HelpMetadata, type HelpReader, type HelpReaderFactory } from "../services/help";
|
|
47
52
|
import { normalizeInvocationRequestId } from "../services/identity/invocation-token";
|
|
48
53
|
import { logger } from "../services/logging";
|
|
49
54
|
import { get } from "../services/settings";
|
|
50
55
|
import { cloudMcpResourceUri, publicCloudOrigin } from "../shared/app-url";
|
|
56
|
+
import { normalizeTimeZone } from "../shared/time";
|
|
51
57
|
import { type CapabilityDispatchDependencies, dispatchCapability } from "./capabilities";
|
|
52
58
|
|
|
53
59
|
const MCP_TOOL_PAGE_SIZE = 100;
|
|
@@ -67,6 +73,7 @@ type McpRouteDependencies = CapabilityDispatchDependencies & {
|
|
|
67
73
|
listApps?: () => Promise<AppRegistryEntry[]>;
|
|
68
74
|
help?: HelpReaderFactory;
|
|
69
75
|
getOperatorLocale?: () => Promise<string | undefined>;
|
|
76
|
+
getOperatorTimeZone?: () => Promise<string | undefined>;
|
|
70
77
|
getAppUrl?: () => Promise<string>;
|
|
71
78
|
authenticate?: MiddlewareHandler<AuthContext>;
|
|
72
79
|
limit?: MiddlewareHandler<AuthContext>;
|
|
@@ -488,7 +495,7 @@ const createMcpServer = (
|
|
|
488
495
|
oauthScopes: string[] | null,
|
|
489
496
|
authority?: RequestAuthority,
|
|
490
497
|
): Server => {
|
|
491
|
-
const registry = dependencies.listApps ?? listApps;
|
|
498
|
+
const registry = async () => discoverableApps(authority ?? null, await (dependencies.listApps ?? listApps)());
|
|
492
499
|
const capabilityLookup = dependencies.getCapability ?? getCapability;
|
|
493
500
|
let helpLocale: Promise<string> | undefined;
|
|
494
501
|
const resolveHelpLocale = () =>
|
|
@@ -496,9 +503,10 @@ const createMcpServer = (
|
|
|
496
503
|
resolveLocale(request.headers, operatorDefault),
|
|
497
504
|
));
|
|
498
505
|
const helpReader = async () =>
|
|
499
|
-
(dependencies.help ?? ((locale, viewer) => createHelpReader(locale, viewer, { listApps: registry })))(
|
|
506
|
+
(dependencies.help ?? ((locale, viewer) => createHelpReader(locale, viewer, { listApps: registry, authority: authority ?? null })))(
|
|
500
507
|
await resolveHelpLocale(),
|
|
501
508
|
authority?.actor,
|
|
509
|
+
{ authority: authority ?? null },
|
|
502
510
|
);
|
|
503
511
|
const hasScope = (scope: "read" | "write"): boolean =>
|
|
504
512
|
oauthScopes === null || oauthScopes.includes(scope) || oauthScopes.includes("admin");
|
|
@@ -682,14 +690,33 @@ const createMcpServer = (
|
|
|
682
690
|
origin,
|
|
683
691
|
);
|
|
684
692
|
}
|
|
685
|
-
const
|
|
693
|
+
const receipt = response.ok && selected.kind === "actions" ? CapabilityActionReceiptSchema.safeParse(body.receipt) : null;
|
|
694
|
+
let receiptText: string | undefined;
|
|
695
|
+
if (receipt?.success) {
|
|
696
|
+
// The receipt line is extra presentation: if Cloud cannot word it, the completed Action's result still goes out.
|
|
697
|
+
try {
|
|
698
|
+
const locale = preferredLocale(request.headers) ?? (await resolveHelpLocale());
|
|
699
|
+
const timeZone = normalizeTimeZone(
|
|
700
|
+
await (dependencies.getOperatorTimeZone ?? (() => get<string>("app.timezone")))().catch(() => "UTC"),
|
|
701
|
+
);
|
|
702
|
+
receiptText = capabilitySentenceText(receipt.data.text, receipt.data.slots ?? {}, { locale, timeZone });
|
|
703
|
+
} catch (error) {
|
|
704
|
+
log.warn("MCP receipt text unavailable", { error: error instanceof Error ? error.message : String(error) });
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
const resultFor = (value: Record<string, unknown>, withReceipt = true): CallToolResult => {
|
|
686
708
|
const result = toolResult(value, !response.ok);
|
|
687
709
|
if (origin) result.content.push(...semanticResourceLinks(value, origin));
|
|
710
|
+
if (withReceipt && receiptText) result.content.unshift({ type: "text", text: receiptText });
|
|
688
711
|
return result;
|
|
689
712
|
};
|
|
690
|
-
//
|
|
713
|
+
// Both enrichments are optional: neither turns a result that fits into RESPONSE_TOO_LARGE.
|
|
691
714
|
const result = resultFor(body);
|
|
692
|
-
|
|
715
|
+
if (fitsToolResult(result)) return result;
|
|
716
|
+
const plain = resultFor(body, false);
|
|
717
|
+
if (fitsToolResult(plain)) return plain;
|
|
718
|
+
const original = resultFor(providerBody);
|
|
719
|
+
return ensureBoundedToolResult(fitsToolResult(original) ? original : resultFor(providerBody, false));
|
|
693
720
|
});
|
|
694
721
|
|
|
695
722
|
return server;
|
|
@@ -757,42 +784,47 @@ export const createMcpRoutes = (dependencies: McpRouteDependencies = {}) =>
|
|
|
757
784
|
.use("/mcp/v1", dependencies.authenticate ?? mcpAuthentication(dependencies.getAppUrl ?? defaultAppUrl))
|
|
758
785
|
.use("/mcp/v1", rejectReservedWorkloadCredential)
|
|
759
786
|
.use("/mcp/v1", dependencies.limit ?? rateLimit())
|
|
760
|
-
.all("/mcp/v1", async (c) =>
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
787
|
+
.all("/mcp/v1", async (c) =>
|
|
788
|
+
withCapabilityRequestTimeout(c, "query", async () => {
|
|
789
|
+
if (c.req.method !== "POST") {
|
|
790
|
+
return new Response(null, { status: 405, headers: { Allow: "POST" } });
|
|
791
|
+
}
|
|
792
|
+
let request = c.req.raw;
|
|
793
|
+
const parsed = await readBoundedJson(request, CAPABILITY_MAX_REQUEST_BYTES);
|
|
794
|
+
if (!parsed.ok) {
|
|
795
|
+
return c.json(
|
|
796
|
+
{
|
|
797
|
+
jsonrpc: "2.0",
|
|
798
|
+
id: null,
|
|
799
|
+
error: {
|
|
800
|
+
code: parsed.reason === "too_large" ? -32600 : -32700,
|
|
801
|
+
message: parsed.reason === "too_large" ? "MCP request is too large" : "MCP request body must be JSON",
|
|
802
|
+
},
|
|
774
803
|
},
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
804
|
+
parsed.reason === "too_large" ? 413 : 400,
|
|
805
|
+
);
|
|
806
|
+
}
|
|
807
|
+
const headers = new Headers(request.headers);
|
|
808
|
+
headers.delete("content-length");
|
|
809
|
+
request = new Request(request.url, {
|
|
810
|
+
method: "POST",
|
|
811
|
+
headers,
|
|
812
|
+
body: JSON.stringify(parsed.data),
|
|
813
|
+
signal: request.signal,
|
|
814
|
+
});
|
|
815
|
+
const transport = new WebStandardStreamableHTTPServerTransport({ sessionIdGenerator: undefined, enableJsonResponse: true });
|
|
816
|
+
const server = createMcpServer(request, dependencies, c.get("oauthScopes") ?? null, auth.getAuthority(c));
|
|
817
|
+
try {
|
|
818
|
+
await server.connect(transport);
|
|
819
|
+
return await transport.handleRequest(request);
|
|
820
|
+
} finally {
|
|
821
|
+
try {
|
|
822
|
+
await transport.close();
|
|
823
|
+
} finally {
|
|
824
|
+
await server.close();
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
}),
|
|
828
|
+
);
|
|
797
829
|
|
|
798
830
|
export type McpApiType = ReturnType<typeof createMcpRoutes>;
|
|
@@ -5,6 +5,8 @@ import { z } from "zod";
|
|
|
5
5
|
import { CapabilityAppIdSchema } from "../contracts/capabilities";
|
|
6
6
|
import { panelProviderNeedsConsent } from "../contracts/panel-context-plain";
|
|
7
7
|
import { type AuthContext, jsonResponse, requiresAuth, v } from "../server";
|
|
8
|
+
import { appAccessAuthority } from "../services/app-access";
|
|
9
|
+
import { discoverableApps } from "../services/app-discovery";
|
|
8
10
|
import { PanelConsentCancelled, panelContextConsent } from "../services/panel-context-consent";
|
|
9
11
|
import { getRuntimeContext } from "../ssr/runtime";
|
|
10
12
|
|
|
@@ -15,9 +17,9 @@ const SavedSchema = ProviderSchema.extend(AnswerSchema.shape).strict();
|
|
|
15
17
|
|
|
16
18
|
/** Mounted below /me's session and user guards. The live request registry owns provider membership. */
|
|
17
19
|
export const createMePanelConsentRoutes = (service = panelContextConsent) => {
|
|
18
|
-
const providers = (c:
|
|
19
|
-
getRuntimeContext(c)
|
|
20
|
-
.
|
|
20
|
+
const providers = async (c: import("hono").Context<AuthContext>) =>
|
|
21
|
+
(await discoverableApps(appAccessAuthority(c), getRuntimeContext(c).apps))
|
|
22
|
+
.filter((app) => app.panel && panelProviderNeedsConsent(app.id))
|
|
21
23
|
.map((app) => app.id);
|
|
22
24
|
return new Hono<AuthContext>()
|
|
23
25
|
.use("*", async (c, next) => {
|
|
@@ -33,7 +35,7 @@ export const createMePanelConsentRoutes = (service = panelContextConsent) => {
|
|
|
33
35
|
...requiresAuth,
|
|
34
36
|
responses: { 200: jsonResponse(AnswersSchema, "Answers for registered panel providers") },
|
|
35
37
|
}),
|
|
36
|
-
async (c) => c.json({ answers: await service.list(c.get("user").id, providers(c)) }),
|
|
38
|
+
async (c) => c.json({ answers: await service.list(c.get("user").id, await providers(c)) }),
|
|
37
39
|
)
|
|
38
40
|
.put(
|
|
39
41
|
"/panel-consent/:provider",
|
|
@@ -53,7 +55,8 @@ export const createMePanelConsentRoutes = (service = panelContextConsent) => {
|
|
|
53
55
|
v("json", AnswerSchema),
|
|
54
56
|
async (c) => {
|
|
55
57
|
const { provider } = c.req.valid("param");
|
|
56
|
-
if (!providers(c).includes(provider))
|
|
58
|
+
if (!(await providers(c)).includes(provider))
|
|
59
|
+
return c.json({ code: "PANEL_PROVIDER_NOT_FOUND", message: "Panel provider not found" }, 404);
|
|
57
60
|
const { allowed, revision } = c.req.valid("json");
|
|
58
61
|
let saved: Awaited<ReturnType<typeof service.set>>;
|
|
59
62
|
try {
|
package/src/api/me.ts
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import { err, fail, ok, type Result } from "@k2b/stdlib";
|
|
8
8
|
import { sql } from "bun";
|
|
9
9
|
import { type Context, Hono, type MiddlewareHandler } from "hono";
|
|
10
|
+
import { deleteCookie } from "hono/cookie";
|
|
10
11
|
import { createMiddleware } from "hono/factory";
|
|
11
12
|
import { describeRoute } from "hono-openapi";
|
|
12
13
|
import { z } from "zod";
|
|
@@ -27,6 +28,7 @@ import {
|
|
|
27
28
|
NotificationQuietSettingsSchema,
|
|
28
29
|
RegisterBrowserNotificationEndpointSchema,
|
|
29
30
|
ServiceAccountCredentialSchema,
|
|
31
|
+
SignOutOthersResponseSchema,
|
|
30
32
|
UpdateAvatarResponseSchema,
|
|
31
33
|
UpdateAvatarSchema,
|
|
32
34
|
UpdateNotificationQuietSettingsSchema,
|
|
@@ -47,6 +49,7 @@ import {
|
|
|
47
49
|
accountsAppService as accountsService,
|
|
48
50
|
audit,
|
|
49
51
|
browserNotifications,
|
|
52
|
+
logger,
|
|
50
53
|
mandates,
|
|
51
54
|
notifications,
|
|
52
55
|
serviceAccountCredentials,
|
|
@@ -61,6 +64,8 @@ import { createMePanelConsentRoutes } from "./me-panel-consent";
|
|
|
61
64
|
import { createMeRailRoutes } from "./me-rail";
|
|
62
65
|
import { createMeWebVitalsRoutes } from "./me-web-vitals";
|
|
63
66
|
|
|
67
|
+
const log = logger("api:me");
|
|
68
|
+
|
|
64
69
|
const toAccountsActor = (user: AuthContext["Variables"]["user"]) => ({
|
|
65
70
|
userId: user.id,
|
|
66
71
|
uid: user.uid,
|
|
@@ -747,25 +752,96 @@ const app = new Hono<AuthContext>()
|
|
|
747
752
|
describeRoute({
|
|
748
753
|
tags: ["Me"],
|
|
749
754
|
summary: "Change current user password",
|
|
750
|
-
description:
|
|
755
|
+
description:
|
|
756
|
+
"Change the authenticated user's password and end every other sign-in. Requires the current password. The current browser stays signed in with a new cookie and its original sign-in time. signInAgain is true when the caller must sign in again: this browser could not be kept, or the request did not come from a browser sign-in.",
|
|
751
757
|
...requiresAuth,
|
|
752
758
|
responses: {
|
|
753
|
-
200: jsonResponse(
|
|
759
|
+
200: jsonResponse(SignOutOthersResponseSchema, "Password changed"),
|
|
754
760
|
400: jsonResponse(ErrorResponseSchema, "Failed to change password"),
|
|
755
761
|
401: jsonResponse(ErrorResponseSchema, "Current password is incorrect"),
|
|
762
|
+
500: jsonResponse(
|
|
763
|
+
ErrorResponseSchema,
|
|
764
|
+
"Password changed, but signing out other sign-ins failed (SIGN_OUT_OTHERS_FAILED). Retry POST /api/me/sign-ins/sign-out-others.",
|
|
765
|
+
),
|
|
756
766
|
403: jsonResponse(ErrorResponseSchema, "Not permitted for API keys or other credentials"),
|
|
757
767
|
},
|
|
758
768
|
}),
|
|
759
769
|
v("json", ChangePasswordSchema),
|
|
760
770
|
async (c) =>
|
|
761
771
|
respond(c, async () => {
|
|
772
|
+
const keep = await auth.browserSignIn(c);
|
|
762
773
|
const result = await accountsService.user.changeOwnPassword({
|
|
774
|
+
keep: keep && { sid: keep.sid, authEpoch: keep.authEpoch },
|
|
763
775
|
user: c.get("user"),
|
|
764
776
|
currentPassword: c.req.valid("json").currentPassword,
|
|
765
777
|
newPassword: c.req.valid("json").newPassword,
|
|
766
778
|
});
|
|
767
779
|
if (!result.ok) return result;
|
|
768
|
-
|
|
780
|
+
if (result.data.kept) auth.session.setWebCookie(c, result.data.kept.token, result.data.kept.expiresAt);
|
|
781
|
+
const signInAgain = !result.data.kept;
|
|
782
|
+
if (keep && signInAgain) deleteCookie(c, "session_token", { path: "/" });
|
|
783
|
+
return ok({
|
|
784
|
+
message: signInAgain
|
|
785
|
+
? keep
|
|
786
|
+
? "Password changed. You are signed out everywhere, including this browser. Sign in again."
|
|
787
|
+
: "Password changed. You are signed out everywhere. Sign in again."
|
|
788
|
+
: "Password changed successfully.",
|
|
789
|
+
signInAgain,
|
|
790
|
+
});
|
|
791
|
+
}),
|
|
792
|
+
)
|
|
793
|
+
|
|
794
|
+
.post(
|
|
795
|
+
"/sign-ins/sign-out-others",
|
|
796
|
+
auth.rejectAppSession,
|
|
797
|
+
requireBrowserSignIn,
|
|
798
|
+
describeRoute({
|
|
799
|
+
tags: ["Me"],
|
|
800
|
+
summary: "Sign out every other sign-in",
|
|
801
|
+
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 phones stay paired. API keys, passkeys and mandates stay valid.",
|
|
803
|
+
...requiresAuth,
|
|
804
|
+
responses: {
|
|
805
|
+
200: jsonResponse(SignOutOthersResponseSchema, "Other sign-ins ended"),
|
|
806
|
+
401: jsonResponse(ErrorResponseSchema, "Authentication required"),
|
|
807
|
+
403: jsonResponse(
|
|
808
|
+
ErrorResponseSchema,
|
|
809
|
+
"Browser sign-in required (BROWSER_SIGN_IN_REQUIRED); mobile app sessions receive FORBIDDEN",
|
|
810
|
+
),
|
|
811
|
+
500: jsonResponse(ErrorResponseSchema, "Signing out other sign-ins failed (SIGN_OUT_OTHERS_FAILED)"),
|
|
812
|
+
},
|
|
813
|
+
}),
|
|
814
|
+
async (c) =>
|
|
815
|
+
respond(c, async () => {
|
|
816
|
+
const signIn = await auth.browserSignIn(c);
|
|
817
|
+
if (!signIn) throw new Error("Browser sign-in gate was not applied");
|
|
818
|
+
try {
|
|
819
|
+
const { kept } = await auth.session.endOtherSignIns(c.get("user").id, {
|
|
820
|
+
keep: { sid: signIn.sid, authEpoch: signIn.authEpoch },
|
|
821
|
+
reason: "sign_out_others",
|
|
822
|
+
});
|
|
823
|
+
const user = c.get("user");
|
|
824
|
+
try {
|
|
825
|
+
await audit.record({
|
|
826
|
+
action: "auth.session.sign_out_others",
|
|
827
|
+
outcome: "allowed",
|
|
828
|
+
actor: toAccountsActor(user),
|
|
829
|
+
target: { type: "user", id: user.id, label: user.uid, provider: user.provider },
|
|
830
|
+
metadata: { keptThisBrowser: !!kept },
|
|
831
|
+
});
|
|
832
|
+
} catch {
|
|
833
|
+
log.warn("Other sign-ins ended, but recording the audit event failed", { userId: user.id });
|
|
834
|
+
}
|
|
835
|
+
if (kept) auth.session.setWebCookie(c, kept.token, kept.expiresAt);
|
|
836
|
+
const signInAgain = !kept;
|
|
837
|
+
if (signInAgain) deleteCookie(c, "session_token", { path: "/" });
|
|
838
|
+
return ok({
|
|
839
|
+
message: signInAgain ? "Other sign-ins ended. Sign in again in this browser." : "Other sign-ins ended.",
|
|
840
|
+
signInAgain,
|
|
841
|
+
});
|
|
842
|
+
} catch {
|
|
843
|
+
return fail({ code: "SIGN_OUT_OTHERS_FAILED", status: 500, message: "Signing out your other sign-ins did not work. Try again." });
|
|
844
|
+
}
|
|
769
845
|
}),
|
|
770
846
|
)
|
|
771
847
|
|