@k2b/cloud 0.38.0 → 0.40.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -0
- package/package.json +12 -5
- package/scripts/build-canvas-workers.ts +109 -29
- package/scripts/build.ts +3 -5
- package/src/_internal/app-favicon.ts +4 -2
- package/src/_internal/app-path.ts +25 -0
- package/src/_internal/app-presentation.ts +4 -0
- package/src/_internal/canvas-worker.ts +5 -3
- package/src/_internal/capabilities.ts +72 -48
- package/src/_internal/capability-reviews.ts +81 -0
- package/src/_internal/capability-schemas.ts +29 -0
- package/src/_internal/capability-sentences.ts +216 -1
- package/src/_internal/capability-streams.ts +7 -0
- package/src/_internal/define-app.ts +63 -16
- package/src/_internal/font-preloads.ts +2 -1
- package/src/_internal/panel-cookie.ts +39 -0
- package/src/_internal/registry-validation.ts +21 -26
- package/src/_internal/runtime-context.ts +1 -0
- package/src/_internal/runtime-watcher.ts +6 -0
- package/src/_internal/static-assets.ts +52 -3
- package/src/_internal/status-preserving-ssr.ts +2 -0
- package/src/access/PermissionEditor.browser-harness.tsx +57 -38
- package/src/access/PermissionEditor.tsx +32 -7
- package/src/access/ResourceApiKeys.tsx +5 -1
- package/src/account/EntitySearch.tsx +1 -1
- package/src/ai/action-review.ts +101 -0
- package/src/ai/approval-routes.ts +5 -2
- package/src/ai/approvals.ts +139 -28
- package/src/ai/background-review.ts +72 -0
- package/src/ai/browser.ts +10 -0
- package/src/ai/capabilities.ts +29 -11
- package/src/ai/capability-execution.ts +76 -43
- package/src/ai/capability-result.ts +95 -0
- package/src/ai/chat/action-card.ts +398 -0
- package/src/ai/chat/action-messages.ts +129 -0
- package/src/ai/chat/action-question.browser-harness.tsx +245 -0
- package/src/ai/chat/action-question.fixtures.ts +733 -0
- package/src/ai/chat/action-question.tsx +669 -0
- package/src/ai/chat/action-sentences.ts +1 -44
- package/src/ai/chat/blocks.tsx +19 -25
- package/src/ai/chat/message-actions.tsx +6 -0
- package/src/ai/chat/tool-action.tsx +481 -0
- package/src/ai/chat/turn-view.tsx +45 -163
- package/src/ai/chat/view-block.tsx +22 -7
- package/src/ai/chat-task-contracts.ts +31 -3
- package/src/ai/chat-task-reasons-migrate.ts +75 -0
- package/src/ai/chat-task-reasons.ts +92 -0
- package/src/ai/chat-tasks.ts +438 -78
- package/src/ai/client/controller.ts +59 -0
- package/src/ai/default-tools.ts +9 -1
- package/src/ai/executor.ts +190 -39
- package/src/ai/index.ts +14 -0
- package/src/ai/migrate.ts +39 -4
- package/src/ai/projects-routes.ts +3 -0
- package/src/ai/protocol.ts +14 -2
- package/src/ai/recent-resources.ts +240 -0
- package/src/ai/recent-sent-refs.ts +38 -0
- package/src/ai/request-context.ts +7 -1
- package/src/ai/routes.ts +172 -2
- package/src/ai/runtime.ts +13 -27
- package/src/ai/skills-routes.ts +3 -0
- package/src/ai/skills.ts +10 -1
- package/src/ai/store.ts +232 -27
- package/src/ai/timeline.ts +9 -1
- package/src/ai/tool-audit.ts +8 -0
- package/src/ai/tool-wording.ts +67 -0
- package/src/ai/turn-abort.ts +33 -0
- package/src/ai/types.ts +40 -4
- package/src/api/accounts-entities.ts +2 -1
- package/src/api/admin-ai-projects.ts +2 -0
- package/src/api/admin-ai-skills.ts +2 -0
- package/src/api/admin-ai-usage.ts +2 -0
- package/src/api/admin-core-settings.ts +190 -139
- package/src/api/admin-geoip.ts +80 -0
- package/src/api/admin-identity.ts +3 -0
- package/src/api/admin-lifecycle.ts +3 -0
- package/src/api/admin-outgoing-mail.ts +6 -2
- package/src/api/app-approval.ts +54 -6
- package/src/api/approvals-loader.ts +23 -1
- package/src/api/approvals.ts +3 -1
- package/src/api/auth.ts +25 -6
- package/src/api/capabilities.ts +44 -11
- package/src/api/capability-streams.ts +12 -0
- package/src/api/help.ts +4 -28
- package/src/api/index.ts +7 -1
- package/src/api/lasting-access-messages.ts +17 -0
- package/src/api/mcp.ts +3 -0
- package/src/api/me-panel-consent.ts +71 -0
- package/src/api/me.ts +80 -33
- package/src/api/search.ts +3 -1
- package/src/api/sync-ops.ts +2 -0
- package/src/api/widgets.ts +3 -0
- package/src/approvals/Approvals.tsx +180 -238
- package/src/approvals/messages.ts +12 -24
- package/src/browser/CloudResourceSearch.browser-harness.tsx +10 -2
- package/src/browser/CloudResourceSearch.tsx +10 -6
- package/src/browser/app-approval.ts +60 -6
- package/src/browser/commands.ts +22 -6
- package/src/browser/composer-catalog.ts +145 -0
- package/src/browser/composer-insert-messages.ts +31 -0
- package/src/browser/composer-insert.ts +160 -0
- package/src/browser/composer-open-messages.ts +17 -0
- package/src/browser/composer-open.ts +110 -0
- package/src/browser/discoverable-command.ts +6 -0
- package/src/browser/hints.ts +6 -0
- package/src/browser/live-connection.ts +26 -2
- package/src/browser/navigation-apps.ts +7 -0
- package/src/browser/panel-context-bridge.ts +30 -0
- package/src/browser/panel-context-receiver.ts +53 -0
- package/src/browser/panel-menu.tsx +56 -0
- package/src/browser/panel-messages.ts +35 -0
- package/src/browser/panel-shortcuts.ts +31 -0
- package/src/browser/panel-slot.ts +756 -0
- package/src/browser/panel.ts +65 -0
- package/src/browser/resource-picker.tsx +12 -1
- package/src/browser/search-commands.ts +6 -3
- package/src/capabilities/client.ts +8 -2
- package/src/capabilities/reviews.ts +205 -0
- package/src/capabilities/server.ts +10 -3
- package/src/capabilities/testing.ts +47 -0
- package/src/capabilities/types.ts +2 -2
- package/src/cli/account.ts +13 -3
- package/src/cli/admin/geoip.ts +115 -0
- package/src/cli/admin/index.ts +3 -0
- package/src/contracts/app-approval.ts +159 -12
- package/src/contracts/app.ts +14 -0
- package/src/contracts/approvals.ts +2 -2
- package/src/contracts/capabilities.ts +410 -18
- package/src/contracts/geoip.ts +42 -0
- package/src/contracts/index.ts +3 -0
- package/src/contracts/panel-context-plain.ts +99 -0
- package/src/contracts/panel-context.ts +93 -0
- package/src/contracts/registry.ts +3 -1
- package/src/contracts/seen-hints.ts +35 -0
- package/src/contracts/shared.ts +2 -0
- package/src/server/client-ip.ts +5 -0
- package/src/server/help-payload.ts +38 -0
- package/src/server/middleware/auth.ts +11 -1
- package/src/server/middleware/middleware.ts +9 -0
- package/src/server/middleware/no-transform.ts +19 -0
- package/src/server/middleware/rate-limit.ts +4 -6
- package/src/server/request-context/address.ts +45 -0
- package/src/server/request-context/geoip.ts +380 -0
- package/src/server/request-context/index.ts +33 -0
- package/src/server/request-context/user-agent.ts +95 -0
- package/src/server/services/access.ts +5 -1
- package/src/services/accounts/posix.ts +2 -1
- package/src/services/app-approval.ts +533 -90
- package/src/services/approvals/device.ts +9 -5
- package/src/services/approvals/index.ts +6 -5
- package/src/services/approvals/list.ts +26 -5
- package/src/services/auth-flows/notification-sender.ts +3 -0
- package/src/services/geoip/index.ts +164 -0
- package/src/services/geoip/store.ts +30 -0
- package/src/services/geoip/upload.ts +150 -0
- package/src/services/identity/routes.ts +5 -1
- package/src/services/index.ts +32 -3
- package/src/services/known-browsers.ts +312 -0
- package/src/services/login-address-store.ts +51 -0
- package/src/services/login-context.ts +118 -0
- package/src/services/mandates/index.ts +3 -0
- package/src/services/mandates/policy.ts +15 -3
- package/src/services/notifications/browser.ts +16 -5
- package/src/services/notifications/email-frame.ts +1 -1
- package/src/services/oauth-tokens.ts +16 -2
- package/src/services/panel-context-consent.ts +58 -0
- package/src/services/pdf/image-protocol.ts +135 -0
- package/src/services/pdf/image-variants.ts +124 -0
- package/src/services/pdf/image-worker.ts +31 -0
- package/src/services/pdf/image.ts +109 -0
- package/src/services/pdf/index.ts +9 -0
- package/src/services/service-account-credentials.ts +21 -0
- package/src/services/session/index.ts +97 -16
- package/src/services/session/recent.ts +13 -0
- package/src/services/settings/api.ts +11 -6
- package/src/services/settings/app.ts +5 -4
- package/src/services/settings/core-settings.ts +4 -3
- package/src/services/settings/index.ts +19 -17
- package/src/services/settings/namespace.ts +2 -1
- package/src/services/settings/store.ts +83 -11
- package/src/services/webauthn.ts +12 -0
- package/src/shared/ai-platform-prompt.ts +1 -1
- package/src/shared/app-presentation.ts +1 -0
- package/src/shared/email-html.ts +2 -0
- package/src/shared/help.ts +3 -0
- package/src/shared/redirect.ts +12 -0
- package/src/shared-text/cursor.ts +22 -0
- package/src/shared-text/documents.ts +142 -0
- package/src/shared-text/events.ts +43 -0
- package/src/shared-text/index.ts +12 -0
- package/src/shared-text/log.ts +48 -0
- package/src/shared-text/text-diff.ts +23 -0
- package/src/shared-text/types.ts +41 -0
- package/src/ssr/AdminLayout.tsx +2 -0
- package/src/ssr/AppLaunchpadPanel.tsx +2 -2
- package/src/ssr/CloudShell.island.tsx +95 -10
- package/src/ssr/Layout.tsx +66 -10
- package/src/ssr/LayoutHeader.tsx +15 -9
- package/src/ssr/LayoutHelp.browser-harness.tsx +2 -0
- package/src/ssr/LayoutHelp.tsx +435 -274
- package/src/ssr/LayoutHelpPanel.browser-harness.tsx +136 -0
- package/src/ssr/LayoutHelpTrigger.tsx +27 -2
- package/src/ssr/LayoutRail.tsx +2 -2
- package/src/ssr/LayoutTabBar.tsx +29 -0
- package/src/ssr/MobileNavigation.tsx +2 -2
- package/src/ssr/PanelLayout.tsx +35 -0
- package/src/ssr/PanelRuntime.island.tsx +94 -0
- package/src/ssr/PanelSlot.tsx +158 -0
- package/src/ssr/PanelTrigger.tsx +49 -0
- package/src/ssr/ShellRuntime.island.tsx +69 -5
- package/src/ssr/app-appearance.ts +3 -2
- package/src/ssr/app-color.ts +105 -0
- package/src/ssr/app-launchpad.tsx +18 -1
- package/src/ssr/app-navigation.ts +28 -0
- package/src/ssr/compact-shell.baseline.json +5270 -0
- package/src/ssr/compact-shell.browser-harness.ts +439 -0
- package/src/ssr/dialog-app-colors.browser-harness.tsx +43 -0
- package/src/ssr/help-messages.ts +4 -0
- package/src/ssr/help.ts +20 -0
- package/src/ssr/hints.ts +25 -0
- package/src/ssr/index.ts +5 -0
- package/src/ssr/islands/index.ts +4 -3
- package/src/ssr/layout-context.ts +2 -0
- package/src/ssr/layout-help-article.server.ts +50 -0
- package/src/ssr/layout-help-start.ts +33 -7
- package/src/ssr/panel-consent.ts +29 -0
- package/src/ssr/panel-context.ts +10 -0
- package/src/ssr/panel-slot.browser-harness.tsx +159 -0
- package/src/ssr/platform-messages.ts +48 -0
- package/src/ssr/rail-navigation.ts +44 -0
- package/src/ssr/shell-data.tsx +5 -0
- package/src/ssr/shell-shortcuts.ts +13 -2
- package/src/styles/effects.css +201 -11
- package/src/styles/file-chooser.css +2 -1
- package/src/styles/global.css +314 -63
- package/src/styles/panel.css +416 -0
- package/src/styles/resource-search.css +6 -2
- package/src/styles/tokens.css +36 -13
- package/src/styles/utilities-buttons.css +3 -2
- package/src/styles/utilities-data.css +2 -1
- package/src/styles/utilities-feedback.css +2 -2
- package/src/styles/utilities-layout.css +3 -13
- package/src/styles/utilities-markdown-editor.css +6 -6
- package/src/styles/utilities-navigation.css +22 -43
|
@@ -4,6 +4,7 @@ import { describeRoute } from "hono-openapi";
|
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
import { createPagination, ErrorResponseSchema, MessageResponseSchema, PaginationQuerySchema, parsePagination } from "../contracts";
|
|
6
6
|
import { type AuthContext, auth, jsonResponse, requiresAdmin, respond, v } from "../server";
|
|
7
|
+
import { noTransform } from "../server/middleware/no-transform";
|
|
7
8
|
import { accountLifecycle, lifecycleJobs } from "../services";
|
|
8
9
|
|
|
9
10
|
const DeletedAccountSchema = z.object({
|
|
@@ -63,7 +64,9 @@ const TriggerJobResponseSchema = z.object({
|
|
|
63
64
|
jobId: z.string(),
|
|
64
65
|
});
|
|
65
66
|
|
|
67
|
+
// Deleted-account and reminder reports list other accounts' personal data next to requester-controlled identity text.
|
|
66
68
|
const app = new Hono<AuthContext>()
|
|
69
|
+
.use("*", noTransform())
|
|
67
70
|
.use(auth.requireRole("admin"))
|
|
68
71
|
.get(
|
|
69
72
|
"/deleted-accounts",
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
MailStatusSchema,
|
|
15
15
|
} from "../contracts/outgoing-mail";
|
|
16
16
|
import { type AuthContext, auth, jsonResponse, requiresAdmin } from "../server";
|
|
17
|
+
import { noTransform } from "../server/middleware/no-transform";
|
|
17
18
|
import { audit } from "../services/audit";
|
|
18
19
|
import { outgoingMailLog } from "../services/outgoing-mail/admin";
|
|
19
20
|
import { MAIL_RETENTION_KEYS, readMailRetention } from "../services/outgoing-mail/retention";
|
|
@@ -96,6 +97,7 @@ const contentResponse = z.union([
|
|
|
96
97
|
]);
|
|
97
98
|
export const createAdminOutgoingMailRoutes = (authenticate: MiddlewareHandler<AuthContext> = auth.requireRole("admin")) =>
|
|
98
99
|
new Hono<AuthContext>()
|
|
100
|
+
.use("*", noTransform())
|
|
99
101
|
.use("*", authenticate)
|
|
100
102
|
.use("*", async (c, next) => {
|
|
101
103
|
c.header("Cache-Control", "no-store");
|
|
@@ -131,8 +133,10 @@ export const createAdminOutgoingMailRoutes = (authenticate: MiddlewareHandler<Au
|
|
|
131
133
|
const next = c.req.valid("json");
|
|
132
134
|
const old = await readMailRetention();
|
|
133
135
|
await sql.begin(async (tx) => {
|
|
134
|
-
|
|
135
|
-
await settings.set(MAIL_RETENTION_KEYS[
|
|
136
|
+
// complete value from explicit admin input; nothing is merged over a read (#1303)
|
|
137
|
+
await settings.set(MAIL_RETENTION_KEYS[0], next.contentDays, tx, { replaceUnreadable: true });
|
|
138
|
+
// complete value from explicit admin input; nothing is merged over a read (#1303)
|
|
139
|
+
await settings.set(MAIL_RETENTION_KEYS[1], next.recordDays, tx, { replaceUnreadable: true });
|
|
136
140
|
await audit.record(
|
|
137
141
|
{
|
|
138
142
|
...context(c),
|
package/src/api/app-approval.ts
CHANGED
|
@@ -5,6 +5,8 @@ import { describeRoute } from "hono-openapi";
|
|
|
5
5
|
import { z } from "zod";
|
|
6
6
|
import {
|
|
7
7
|
APP_APPROVAL_LIMITS,
|
|
8
|
+
AppDeviceChangeLookupSchema,
|
|
9
|
+
AppDeviceChangeReferenceSchema,
|
|
8
10
|
AppDeviceMutationSchema,
|
|
9
11
|
AppDeviceRequestSchema,
|
|
10
12
|
AppLoginReferenceSchema,
|
|
@@ -15,7 +17,17 @@ import {
|
|
|
15
17
|
AppPairingStartSchema,
|
|
16
18
|
} from "../contracts/app-approval";
|
|
17
19
|
import { type AuthContext, auth, rateLimit, v } from "../server";
|
|
18
|
-
import {
|
|
20
|
+
import { noTransform } from "../server/middleware/no-transform";
|
|
21
|
+
import { addressPrefix, clientIp, describeRequest } from "../server/request-context";
|
|
22
|
+
import {
|
|
23
|
+
type AppApprovalActor,
|
|
24
|
+
AppApprovalError,
|
|
25
|
+
type AppTerminalLogins,
|
|
26
|
+
appApproval,
|
|
27
|
+
type createAppApprovalService,
|
|
28
|
+
} from "../services/app-approval";
|
|
29
|
+
import { type NewBrowserSignInNotifier, readBrowserCookie } from "../services/known-browsers";
|
|
30
|
+
import { SessionAppDeviceInactiveError, SessionEpochChangedError } from "../services/session";
|
|
19
31
|
import { appNameIn } from "../ssr/runtime";
|
|
20
32
|
import { createDeviceActions } from "./app-approval-actions";
|
|
21
33
|
|
|
@@ -24,7 +36,12 @@ const DevicePageSchema = z.object({ after: z.string().uuid().optional() }).stric
|
|
|
24
36
|
type Service = ReturnType<typeof createAppApprovalService>;
|
|
25
37
|
|
|
26
38
|
/** This is a separate credential surface, never a user API-key or OAuth scope. */
|
|
27
|
-
export const createAppApprovalRoutes = (
|
|
39
|
+
export const createAppApprovalRoutes = (
|
|
40
|
+
options: { notify: NewBrowserSignInNotifier },
|
|
41
|
+
service: Service = appApproval,
|
|
42
|
+
actions = createDeviceActions,
|
|
43
|
+
terminal?: AppTerminalLogins,
|
|
44
|
+
) => {
|
|
28
45
|
const actor = async (c: Parameters<typeof auth.session.getToken>[0]): Promise<AppApprovalActor> => {
|
|
29
46
|
// The web session (or an explicit bearer session), never the mobile app's session: a phone
|
|
30
47
|
// that pairs sign-in devices would hand out authority that outlives its removal.
|
|
@@ -36,6 +53,8 @@ export const createAppApprovalRoutes = (service: Service = appApproval, actions
|
|
|
36
53
|
return { userId: session.user.id, sid: session.data.sid, admin: session.user.roles.includes("admin") };
|
|
37
54
|
};
|
|
38
55
|
return new Hono<AuthContext>()
|
|
56
|
+
.use("/device", noTransform())
|
|
57
|
+
.use("/device/*", noTransform())
|
|
39
58
|
.onError((error, c) => {
|
|
40
59
|
if (error instanceof AppApprovalError) return c.json({ code: error.code, message: error.code }, error.status);
|
|
41
60
|
if (error instanceof HTTPException && error.status < 500) return c.json({ code: "INVALID_REQUEST" }, error.status);
|
|
@@ -89,7 +108,19 @@ export const createAppApprovalRoutes = (service: Service = appApproval, actions
|
|
|
89
108
|
)
|
|
90
109
|
.post("/manage/devices/update", v("json", AppDeviceMutationSchema), async (c) => {
|
|
91
110
|
const input = c.req.valid("json");
|
|
92
|
-
await service.mutateDevice(
|
|
111
|
+
const result = await service.mutateDevice(
|
|
112
|
+
await actor(c),
|
|
113
|
+
input.deviceId,
|
|
114
|
+
input.operation === "rename" ? input.name : undefined,
|
|
115
|
+
input.operation === "revoke" ? input.endSessions : undefined,
|
|
116
|
+
);
|
|
117
|
+
return result ? c.json(result) : c.body(null, 204);
|
|
118
|
+
})
|
|
119
|
+
.post("/manage/device-changes/status", v("json", AppDeviceChangeLookupSchema), async (c) => {
|
|
120
|
+
return c.json(await service.findRemoval(await actor(c), c.req.valid("json")));
|
|
121
|
+
})
|
|
122
|
+
.post("/manage/device-changes/cancel", v("json", AppDeviceChangeReferenceSchema), async (c) => {
|
|
123
|
+
await service.cancelChange(await actor(c), c.req.valid("json").changeId);
|
|
93
124
|
return c.body(null, 204);
|
|
94
125
|
})
|
|
95
126
|
.post(
|
|
@@ -111,6 +142,8 @@ export const createAppApprovalRoutes = (service: Service = appApproval, actions
|
|
|
111
142
|
await service.deviceCommand(
|
|
112
143
|
c.req.valid("json"),
|
|
113
144
|
actions((appId, locale) => appNameIn(c, appId, locale, appId)),
|
|
145
|
+
terminal,
|
|
146
|
+
{ address: clientIp(c) },
|
|
114
147
|
),
|
|
115
148
|
),
|
|
116
149
|
)
|
|
@@ -120,7 +153,10 @@ export const createAppApprovalRoutes = (service: Service = appApproval, actions
|
|
|
120
153
|
v("json", AppLoginStartSchema),
|
|
121
154
|
async (c) => {
|
|
122
155
|
const input = c.req.valid("json");
|
|
123
|
-
|
|
156
|
+
const description = describeRequest(c);
|
|
157
|
+
const cookie = readBrowserCookie(c);
|
|
158
|
+
const prefix = addressPrefix(clientIp(c));
|
|
159
|
+
return c.json(await service.startLogin(input.identifier, input.category, { ...description, cookie, prefix }), 202);
|
|
124
160
|
},
|
|
125
161
|
)
|
|
126
162
|
.post("/login/status", v("json", AppLoginReferenceSchema), async (c) => {
|
|
@@ -136,8 +172,20 @@ export const createAppApprovalRoutes = (service: Service = appApproval, actions
|
|
|
136
172
|
v("json", AppLoginReferenceSchema),
|
|
137
173
|
async (c) => {
|
|
138
174
|
const input = c.req.valid("json");
|
|
139
|
-
const userId = await service.consumeLogin(input.requestId, input.browserSecret);
|
|
140
|
-
|
|
175
|
+
const { userId, authEpoch, deviceId, deviceName } = await service.consumeLogin(input.requestId, input.browserSecret);
|
|
176
|
+
try {
|
|
177
|
+
await auth.session.create(c, userId, {
|
|
178
|
+
method: "cloud_login",
|
|
179
|
+
device: deviceName,
|
|
180
|
+
notify: options.notify,
|
|
181
|
+
authEpoch,
|
|
182
|
+
appDeviceId: deviceId,
|
|
183
|
+
});
|
|
184
|
+
} catch (error) {
|
|
185
|
+
if (error instanceof SessionEpochChangedError || error instanceof SessionAppDeviceInactiveError)
|
|
186
|
+
throw new AppApprovalError("CONFLICT", 409);
|
|
187
|
+
throw error;
|
|
188
|
+
}
|
|
141
189
|
return c.body(null, 204);
|
|
142
190
|
},
|
|
143
191
|
);
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import type { Context } from "hono";
|
|
2
2
|
import { HTTPException } from "hono/http-exception";
|
|
3
|
+
import { listCapabilities } from "../_internal/registry";
|
|
3
4
|
import { resolveAiChatRequestContext } from "../ai/request-context";
|
|
4
5
|
import type { ApprovalListQuery, ApprovalPage } from "../contracts/approvals";
|
|
6
|
+
import { CapabilityActionCallReviewSchema, capabilityActionPolicy } from "../contracts/capabilities";
|
|
5
7
|
import type { AuthContext } from "../server/middleware/auth";
|
|
6
8
|
import { listPending, presentPendingApproval } from "../services/approvals/list";
|
|
9
|
+
import { logger } from "../services/logging";
|
|
7
10
|
import { getLocalizedRuntimeContext } from "../ssr/runtime";
|
|
8
11
|
|
|
9
12
|
export const loadPendingApprovals = async <T extends AuthContext>(c: Context<T>, query: ApprovalListQuery): Promise<ApprovalPage> => {
|
|
@@ -11,5 +14,24 @@ export const loadPendingApprovals = async <T extends AuthContext>(c: Context<T>,
|
|
|
11
14
|
if (ctx instanceof Response) throw new HTTPException(403, { res: ctx });
|
|
12
15
|
const page = await listPending({ approverUserId: ctx.ownerUserId, appId: query.app, link: query.link, cursor: query.cursor });
|
|
13
16
|
const apps = getLocalizedRuntimeContext(c).apps;
|
|
14
|
-
|
|
17
|
+
const needsPolicy = page.items.some((row) => {
|
|
18
|
+
if (row.storedReview && "sentence" in row.storedReview) return false;
|
|
19
|
+
const review = CapabilityActionCallReviewSchema.safeParse(row.details);
|
|
20
|
+
return review.success && !review.data.ask;
|
|
21
|
+
});
|
|
22
|
+
const capabilities = needsPolicy
|
|
23
|
+
? await listCapabilities().catch((error: unknown) => {
|
|
24
|
+
logger("approvals").warn("Approval capability registry unavailable", {
|
|
25
|
+
error: error instanceof Error ? error.message : String(error),
|
|
26
|
+
});
|
|
27
|
+
return [];
|
|
28
|
+
})
|
|
29
|
+
: [];
|
|
30
|
+
return {
|
|
31
|
+
...page,
|
|
32
|
+
items: page.items.map((row) => {
|
|
33
|
+
const manifest = capabilities.find((entry) => entry.appId === row.appId)?.manifest;
|
|
34
|
+
return presentPendingApproval(row, apps, manifest ? capabilityActionPolicy(manifest, row.action) : undefined);
|
|
35
|
+
}),
|
|
36
|
+
};
|
|
15
37
|
};
|
package/src/api/approvals.ts
CHANGED
|
@@ -18,6 +18,7 @@ import { ErrorResponseSchema } from "../contracts/shared";
|
|
|
18
18
|
import { rateLimit } from "../server";
|
|
19
19
|
import { respond } from "../server/api";
|
|
20
20
|
import { type AuthContext, auth } from "../server/middleware/auth";
|
|
21
|
+
import { noTransform } from "../server/middleware/no-transform";
|
|
21
22
|
import { jsonResponse, requiresAuth } from "../server/middleware/openapi";
|
|
22
23
|
import { v } from "../server/middleware/validator";
|
|
23
24
|
import { ok } from "../server/services";
|
|
@@ -26,6 +27,7 @@ import { decodeApprovalCursor } from "../services/approvals/cursor";
|
|
|
26
27
|
import { loadPendingApprovals } from "./approvals-loader";
|
|
27
28
|
|
|
28
29
|
export const approvalRoutes = new Hono<AuthContext>()
|
|
30
|
+
.use("*", noTransform())
|
|
29
31
|
.use(rateLimit())
|
|
30
32
|
.use(auth.requireRole("authenticated"))
|
|
31
33
|
.get(
|
|
@@ -111,7 +113,7 @@ export const approvalRoutes = new Hono<AuthContext>()
|
|
|
111
113
|
client: c.req.header(APPROVAL_CLIENT_HEADER),
|
|
112
114
|
}),
|
|
113
115
|
},
|
|
114
|
-
toolApprovalContext: ctx.
|
|
116
|
+
toolApprovalContext: ctx.toolApprovalChangeContext,
|
|
115
117
|
signedInSession: ctx.signedInSession,
|
|
116
118
|
});
|
|
117
119
|
if (!result.ok) return toAiActionFailureResponse(c, result);
|
package/src/api/auth.ts
CHANGED
|
@@ -76,7 +76,10 @@ export const createAuthRoutes = (notificationSender: AuthNotificationSender) =>
|
|
|
76
76
|
}
|
|
77
77
|
|
|
78
78
|
// Issue through the configured migration gate (legacy opaque or JWT family).
|
|
79
|
-
const sessionToken = await auth.session.create(c, loginResult.userId
|
|
79
|
+
const sessionToken = await auth.session.create(c, loginResult.userId, {
|
|
80
|
+
method: "password",
|
|
81
|
+
notify: notificationSender.sendNewBrowserSignIn,
|
|
82
|
+
});
|
|
80
83
|
|
|
81
84
|
log.info("Login successful", { uid: loginResult.user.uid });
|
|
82
85
|
return c.json({
|
|
@@ -119,7 +122,10 @@ export const createAuthRoutes = (notificationSender: AuthNotificationSender) =>
|
|
|
119
122
|
return c.json({ message: result.error.message, code: result.error.code }, status as 400 | 401 | 403 | 404 | 409 | 500);
|
|
120
123
|
}
|
|
121
124
|
|
|
122
|
-
const sessionToken = await auth.session.create(c, result.data.user.id
|
|
125
|
+
const sessionToken = await auth.session.create(c, result.data.user.id, {
|
|
126
|
+
method: "passkey",
|
|
127
|
+
notify: notificationSender.sendNewBrowserSignIn,
|
|
128
|
+
});
|
|
123
129
|
log.info("Passkey login successful", { uid: result.data.user.uid });
|
|
124
130
|
return c.json({ session_token: sessionToken, user: result.data.user });
|
|
125
131
|
},
|
|
@@ -177,7 +183,10 @@ export const createAuthRoutes = (notificationSender: AuthNotificationSender) =>
|
|
|
177
183
|
return jsonError(c, changeResult.message, changeResult.status === 401 ? 401 : 400);
|
|
178
184
|
}
|
|
179
185
|
|
|
180
|
-
const sessionToken = await auth.session.create(c, changeResult.userId
|
|
186
|
+
const sessionToken = await auth.session.create(c, changeResult.userId, {
|
|
187
|
+
method: "expired_password",
|
|
188
|
+
notify: notificationSender.sendNewBrowserSignIn,
|
|
189
|
+
});
|
|
181
190
|
|
|
182
191
|
log.info("Password changed via expired flow", { uid: username });
|
|
183
192
|
return c.json({ session_token: sessionToken, user: changeResult.user });
|
|
@@ -228,7 +237,11 @@ export const createAuthRoutes = (notificationSender: AuthNotificationSender) =>
|
|
|
228
237
|
}
|
|
229
238
|
|
|
230
239
|
// Create session (no IPA session for email-only users)
|
|
231
|
-
const sessionToken = await auth.session.create(c, verifyResult.userId
|
|
240
|
+
const sessionToken = await auth.session.create(c, verifyResult.userId, {
|
|
241
|
+
method: "email_code",
|
|
242
|
+
// An account created by this sign-in has nobody else to warn.
|
|
243
|
+
notify: verifyResult.createdGuest ? null : notificationSender.sendNewBrowserSignIn,
|
|
244
|
+
});
|
|
232
245
|
|
|
233
246
|
if (verifyResult.createdGuest) {
|
|
234
247
|
log.info("Guest user created", {
|
|
@@ -288,7 +301,10 @@ export const createAuthRoutes = (notificationSender: AuthNotificationSender) =>
|
|
|
288
301
|
return jsonError(c, result.message, status);
|
|
289
302
|
}
|
|
290
303
|
|
|
291
|
-
const sessionToken = await auth.session.create(c, result.userId
|
|
304
|
+
const sessionToken = await auth.session.create(c, result.userId, {
|
|
305
|
+
method: "password_reset",
|
|
306
|
+
notify: notificationSender.sendNewBrowserSignIn,
|
|
307
|
+
});
|
|
292
308
|
log.info("Password reset completed", { uid: result.user.uid });
|
|
293
309
|
return c.json({ user: result.user, session_token: sessionToken });
|
|
294
310
|
},
|
|
@@ -359,7 +375,10 @@ export const createAuthRoutes = (notificationSender: AuthNotificationSender) =>
|
|
|
359
375
|
const user = await accounts.users.get({ uid: "admin" });
|
|
360
376
|
if (!user) return jsonError(c, "Failed to resolve admin user.", 500);
|
|
361
377
|
|
|
362
|
-
const sessionToken = await auth.session.create(c, user.id
|
|
378
|
+
const sessionToken = await auth.session.create(c, user.id, {
|
|
379
|
+
method: "admin_token",
|
|
380
|
+
notify: notificationSender.sendNewBrowserSignIn,
|
|
381
|
+
});
|
|
363
382
|
log.info("Admin login successful");
|
|
364
383
|
return c.json({ session_token: sessionToken, user });
|
|
365
384
|
},
|
package/src/api/capabilities.ts
CHANGED
|
@@ -3,6 +3,11 @@ import { describeRoute } from "hono-openapi";
|
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { readBoundedJson } from "../_internal/bounded-json";
|
|
5
5
|
import { resolveCapabilityManifestPresentation } from "../_internal/capabilities";
|
|
6
|
+
import {
|
|
7
|
+
capabilityActionReceiptIssues,
|
|
8
|
+
capabilityActionReviewIssues,
|
|
9
|
+
capabilityQueryChecksActionOutcome,
|
|
10
|
+
} from "../_internal/capability-reviews";
|
|
6
11
|
import { getCapability, listApps } from "../_internal/registry";
|
|
7
12
|
import { resolveCommandLink } from "../capabilities/command-link";
|
|
8
13
|
import {
|
|
@@ -25,12 +30,13 @@ import {
|
|
|
25
30
|
CAPABILITY_MAX_RESULT_BYTES,
|
|
26
31
|
CAPABILITY_ORIGIN_HEADER,
|
|
27
32
|
CAPABILITY_PROTOCOL_VERSION,
|
|
28
|
-
|
|
33
|
+
CapabilityActionReviewDataSchema,
|
|
29
34
|
type CapabilityCatalog,
|
|
30
35
|
CapabilityCatalogSchema,
|
|
31
36
|
CapabilityErrorSchema,
|
|
32
37
|
CapabilityIdempotencyKeySchema,
|
|
33
38
|
type CapabilityOrigin,
|
|
39
|
+
capabilityActionPolicy,
|
|
34
40
|
capabilityResultJsonSchema,
|
|
35
41
|
} from "../contracts/capabilities";
|
|
36
42
|
import { CapabilityStreamSchema } from "../contracts/capability-streams";
|
|
@@ -48,6 +54,7 @@ import {
|
|
|
48
54
|
requiresAuth,
|
|
49
55
|
v,
|
|
50
56
|
} from "../server";
|
|
57
|
+
import { noTransform } from "../server/middleware/no-transform";
|
|
51
58
|
import { logger, settings } from "../services";
|
|
52
59
|
import { invocationAuthorityFromRequest } from "../services/identity/invocation-authority";
|
|
53
60
|
import { capabilityInvocationOperation } from "../services/identity/invocation-operations";
|
|
@@ -60,7 +67,7 @@ import { capabilityMessages } from "../shared/capability-messages";
|
|
|
60
67
|
import { dispatchCapabilityStream, sealCapabilityStream } from "./capability-streams";
|
|
61
68
|
|
|
62
69
|
const log = logger("capabilities");
|
|
63
|
-
const QUERY_TIMEOUT_MS = 15_000;
|
|
70
|
+
export const QUERY_TIMEOUT_MS = 15_000;
|
|
64
71
|
const ACTION_TIMEOUT_MS = 30_000;
|
|
65
72
|
const DEFAULT_CATALOG_LIMIT = 10;
|
|
66
73
|
const MAX_CATALOG_LIMIT = 25;
|
|
@@ -238,6 +245,8 @@ export type CapabilityDispatchParams = {
|
|
|
238
245
|
authority?: RequestAuthority;
|
|
239
246
|
mandate?: MandatedCapabilityAuthority;
|
|
240
247
|
callingAppId?: string;
|
|
248
|
+
/** Internal observation only: called immediately before attempting the app request. */
|
|
249
|
+
onUpstreamAttempt?: () => void;
|
|
241
250
|
dependencies?: CapabilityDispatchDependencies;
|
|
242
251
|
};
|
|
243
252
|
|
|
@@ -381,6 +390,7 @@ const runCapabilityDispatch = async (
|
|
|
381
390
|
actionApproval: mandate.actionApproval ?? "none",
|
|
382
391
|
input: params.input,
|
|
383
392
|
capabilityApproval: "approval" in operation ? (operation.approval ?? "always") : "always",
|
|
393
|
+
capabilityPolicy: capabilityActionPolicy(entry.manifest, params.capabilityId),
|
|
384
394
|
requestId,
|
|
385
395
|
},
|
|
386
396
|
async (mandateAuthority) => {
|
|
@@ -547,6 +557,7 @@ const runCapabilityDispatch = async (
|
|
|
547
557
|
|
|
548
558
|
let response: Response;
|
|
549
559
|
try {
|
|
560
|
+
params.onUpstreamAttempt?.();
|
|
550
561
|
response = await fetchUpstream(
|
|
551
562
|
`${entry.endpoint}/${params.kind}/${encodeURIComponent(params.capabilityId)}${params.review ? "/review" : ""}`,
|
|
552
563
|
{
|
|
@@ -608,18 +619,39 @@ const runCapabilityDispatch = async (
|
|
|
608
619
|
return settle(invalid, "uncertain");
|
|
609
620
|
}
|
|
610
621
|
|
|
622
|
+
const reviewContext = {
|
|
623
|
+
policy: capabilityActionPolicy(entry.manifest, params.capabilityId),
|
|
624
|
+
approval: "approval" in operation ? operation.approval : undefined,
|
|
625
|
+
queries: entry.manifest.queries,
|
|
626
|
+
validateQueryInput: (query: (typeof entry.manifest.queries)[number], input: Record<string, unknown>) =>
|
|
627
|
+
schemaValidator(`${query.schemaHash}:input`, query.inputSchema)?.safeParse(input).success === true,
|
|
628
|
+
...(entry.presentation ? { locales: [entry.presentation.baseLocale, ...Object.keys(entry.presentation.translations)] } : {}),
|
|
629
|
+
};
|
|
611
630
|
const resultValidator = params.review
|
|
612
|
-
?
|
|
631
|
+
? CapabilityActionReviewDataSchema
|
|
613
632
|
: schemaValidator(`${operation.schemaHash}:result`, capabilityResultJsonSchema(operation.dataSchema));
|
|
614
633
|
const parsedResult = resultValidator?.safeParse(upstreamBody.data);
|
|
615
|
-
const
|
|
616
|
-
|
|
617
|
-
const
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
(
|
|
621
|
-
|
|
622
|
-
|
|
634
|
+
const field = (value: unknown, key: string): unknown =>
|
|
635
|
+
value !== null && typeof value === "object" ? Object.getOwnPropertyDescriptor(value, key)?.value : undefined;
|
|
636
|
+
const receipt = field(upstreamBody.data, "receipt");
|
|
637
|
+
const links = field(upstreamBody.data, "links");
|
|
638
|
+
const reviewIssues = params.review
|
|
639
|
+
? capabilityActionReviewIssues(upstreamBody.data, reviewContext)
|
|
640
|
+
: receipt === undefined
|
|
641
|
+
? []
|
|
642
|
+
: params.kind === "queries"
|
|
643
|
+
? ["Queries cannot return a receipt"]
|
|
644
|
+
: capabilityActionReceiptIssues(receipt, links);
|
|
645
|
+
const nestedReceipt = field(field(upstreamBody.data, "data"), "receipt");
|
|
646
|
+
if (
|
|
647
|
+
!params.review &&
|
|
648
|
+
params.kind === "queries" &&
|
|
649
|
+
nestedReceipt !== undefined &&
|
|
650
|
+
!("idempotency" in operation) &&
|
|
651
|
+
capabilityQueryChecksActionOutcome(operation)
|
|
652
|
+
)
|
|
653
|
+
reviewIssues.push(...capabilityActionReceiptIssues(nestedReceipt, links));
|
|
654
|
+
if (!resultValidator || !parsedResult?.success || reviewIssues.length > 0) {
|
|
623
655
|
if (actionWithoutRetrySafety) return settle(outcomeUnknown(), "uncertain");
|
|
624
656
|
const invalid = errorResponse("INVALID_APP_RESPONSE", messages.outsideResultSchema({ appId: params.appId }), 502);
|
|
625
657
|
return settle(invalid, "uncertain");
|
|
@@ -741,6 +773,7 @@ export const createCapabilityRoutes = (dependencies: CapabilityRouteDependencies
|
|
|
741
773
|
(c.req.param("kind") === "actions" ? requireWriteScope : requireReadScope)(c, next);
|
|
742
774
|
|
|
743
775
|
return new Hono<AuthContext>()
|
|
776
|
+
.use("/capabilities/v1/*", noTransform())
|
|
744
777
|
.use("/capabilities/v1/*", dependencies.authenticate ?? auth.requireRole("authenticated"))
|
|
745
778
|
.use("/capabilities/v1/*", rejectReservedWorkloadCredential)
|
|
746
779
|
.post(
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { readBoundedJson } from "../_internal/bounded-json";
|
|
3
|
+
import { capabilityActionReceiptIssues } from "../_internal/capability-reviews";
|
|
3
4
|
import { getCapability } from "../_internal/registry";
|
|
4
5
|
import { recordCapabilityExecution } from "../capabilities/executions";
|
|
5
6
|
import { exactStream } from "../capabilities/stream-body";
|
|
@@ -245,6 +246,17 @@ export async function dispatchCapabilityStream(
|
|
|
245
246
|
await record(502, "STREAM_FAILED");
|
|
246
247
|
return denied(502, "STREAM_FAILED");
|
|
247
248
|
}
|
|
249
|
+
// The same receipt rules as an ordinary Action result, so a stream completion cannot skip them.
|
|
250
|
+
if (
|
|
251
|
+
nested &&
|
|
252
|
+
typeof nested === "object" &&
|
|
253
|
+
"receipt" in nested &&
|
|
254
|
+
nested.receipt !== undefined &&
|
|
255
|
+
(grant.kind !== "actions" || capabilityActionReceiptIssues(nested.receipt, "links" in nested ? nested.links : undefined).length > 0)
|
|
256
|
+
) {
|
|
257
|
+
await record(502, "STREAM_FAILED");
|
|
258
|
+
return denied(502, "STREAM_FAILED");
|
|
259
|
+
}
|
|
248
260
|
}
|
|
249
261
|
if (!response.ok) {
|
|
250
262
|
const error = CapabilityErrorSchema.safeParse(receipt.data);
|
package/src/api/help.ts
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
|
-
import { keyboardPlatform } from "@k2b/ui";
|
|
2
1
|
import { Hono, type MiddlewareHandler } from "hono";
|
|
3
|
-
import { helpAgentMarkdown } from "../_internal/help-catalog";
|
|
4
2
|
import { type AuthContext, auth, getLocale } from "../server";
|
|
3
|
+
import { buildHelpDocumentPayload } from "../server/help-payload";
|
|
5
4
|
import { createHelpReader, type HelpReaderFactory } from "../services/help";
|
|
6
|
-
import
|
|
7
|
-
import { renderHelpMarkdown } from "../shared/markdown";
|
|
5
|
+
import type { HelpSearchPayload } from "../shared/help";
|
|
6
|
+
import type { renderHelpMarkdown } from "../shared/markdown";
|
|
8
7
|
import { helpApiMessages } from "./help-messages";
|
|
9
8
|
|
|
10
9
|
export type HelpRouteDependencies = {
|
|
@@ -36,30 +35,7 @@ export const createHelpRoutes = (dependencies: HelpRouteDependencies = {}) => {
|
|
|
36
35
|
const { t } = helpApiMessages.resolve([locale]);
|
|
37
36
|
const document = await reader(locale, c.get("actor")).read({ appId: c.req.param("appId"), documentId: c.req.param("documentId") });
|
|
38
37
|
if (!document) return c.json({ code: "HELP_NOT_FOUND", message: t.notFound }, 404);
|
|
39
|
-
const
|
|
40
|
-
document.links[`${reference.appId ?? document.appId}/${reference.documentId}`];
|
|
41
|
-
const resolve = (reference: string) => {
|
|
42
|
-
const ref = parseHelpReference(reference);
|
|
43
|
-
return ref ? links(ref) : undefined;
|
|
44
|
-
};
|
|
45
|
-
const payload: HelpDocumentPayload = {
|
|
46
|
-
locale: document.locale,
|
|
47
|
-
id: document.documentId,
|
|
48
|
-
title: document.title,
|
|
49
|
-
markdown: helpAgentMarkdown(document),
|
|
50
|
-
kind: document.kind,
|
|
51
|
-
needs: document.needs,
|
|
52
|
-
next: document.next ? resolve(document.next) : undefined,
|
|
53
|
-
related: document.related.flatMap((reference) => {
|
|
54
|
-
const target = resolve(reference);
|
|
55
|
-
return target ? [target] : [];
|
|
56
|
-
}),
|
|
57
|
-
html: (dependencies.renderMarkdown ?? renderHelpMarkdown)(document.markdown, {
|
|
58
|
-
locale: document.locale,
|
|
59
|
-
platform: keyboardPlatform(c.req.header("User-Agent") ?? ""),
|
|
60
|
-
links,
|
|
61
|
-
}),
|
|
62
|
-
};
|
|
38
|
+
const payload = buildHelpDocumentPayload(document, c.req.header("User-Agent") ?? "", dependencies.renderMarkdown);
|
|
63
39
|
return c.json(payload);
|
|
64
40
|
});
|
|
65
41
|
};
|
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 adminGeoIpRoutes from "./admin-geoip";
|
|
29
30
|
import adminIdentityRoutes from "./admin-identity";
|
|
30
31
|
import adminLifecycleRoutes from "./admin-lifecycle";
|
|
31
32
|
import adminLinuxIdentityRoutes from "./admin-linux-identities";
|
|
@@ -56,6 +57,7 @@ export { createPwaPhoneRoutes } from "./pwa-phone";
|
|
|
56
57
|
* the typed shape for the client.
|
|
57
58
|
*/
|
|
58
59
|
export type CoreApiOptions = {
|
|
60
|
+
terminalLogins?: import("../services/app-approval").AppTerminalLogins;
|
|
59
61
|
notifications: import("../services/auth-flows/notification-sender").AuthNotificationSender;
|
|
60
62
|
};
|
|
61
63
|
|
|
@@ -68,7 +70,10 @@ const buildCoreApi = (options: CoreApiOptions) => {
|
|
|
68
70
|
const widgetRoutes = createWidgetRoutes();
|
|
69
71
|
return new Hono()
|
|
70
72
|
.use(prettyJSON())
|
|
71
|
-
.route(
|
|
73
|
+
.route(
|
|
74
|
+
"/auth/app-approval/v1",
|
|
75
|
+
createAppApprovalRoutes({ notify: options.notifications.sendNewBrowserSignIn }, undefined, undefined, options.terminalLogins),
|
|
76
|
+
)
|
|
72
77
|
.route("/auth/pwa/v1", createPwaRoutes())
|
|
73
78
|
.route("/auth", createAuthRoutes(options.notifications))
|
|
74
79
|
.route("/me", meRoutes)
|
|
@@ -85,6 +90,7 @@ const buildCoreApi = (options: CoreApiOptions) => {
|
|
|
85
90
|
.route("/admin/core/settings", adminCoreSettingsRoutes)
|
|
86
91
|
.route("/admin/core/rail", adminRailRoutes)
|
|
87
92
|
.route("/admin/core/linux-identities", adminLinuxIdentityRoutes)
|
|
93
|
+
.route("/admin/core/geoip", adminGeoIpRoutes)
|
|
88
94
|
.route("/admin/identity", adminIdentityRoutes)
|
|
89
95
|
.route("/admin/sync", createSyncOpsProxyRoutes())
|
|
90
96
|
.route("/admin/lifecycle", adminLifecycleRoutes)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { i18n } from "@k2b/stdlib";
|
|
2
|
+
|
|
3
|
+
export const lastingAccessApiMessages = i18n.define({
|
|
4
|
+
baseLocale: "en",
|
|
5
|
+
messages: {
|
|
6
|
+
en: {
|
|
7
|
+
browserSignInRequired: "Create API keys and passkeys in the browser.",
|
|
8
|
+
reauthenticate: "Sign in again to confirm it is you.",
|
|
9
|
+
forbidden: "This account cannot create new access.",
|
|
10
|
+
},
|
|
11
|
+
de: {
|
|
12
|
+
browserSignInRequired: "API-Schlüssel und Passkeys legst du im Browser an.",
|
|
13
|
+
reauthenticate: "Melde dich erneut an, um zu bestätigen, dass du es bist.",
|
|
14
|
+
forbidden: "Dieses Konto kann keinen neuen Zugang anlegen.",
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
});
|
package/src/api/mcp.ts
CHANGED
|
@@ -42,6 +42,7 @@ import {
|
|
|
42
42
|
} from "../contracts/capabilities";
|
|
43
43
|
import type { AppRegistryEntry, CapabilityRegistryEntry } from "../contracts/registry";
|
|
44
44
|
import { type AuthContext, auth, type RequestAuthority, rateLimit, rejectReservedWorkloadCredential, resolveLocale } from "../server";
|
|
45
|
+
import { noTransform } from "../server/middleware/no-transform";
|
|
45
46
|
import { createHelpReader, type HelpArticle, type HelpMetadata, type HelpReader, type HelpReaderFactory } from "../services/help";
|
|
46
47
|
import { normalizeInvocationRequestId } from "../services/identity/invocation-token";
|
|
47
48
|
import { logger } from "../services/logging";
|
|
@@ -750,6 +751,8 @@ export const createMcpProtectedResourceRoutes = (dependencies: Pick<McpRouteDepe
|
|
|
750
751
|
/** One authenticated, stateless MCP endpoint projected from live capabilities. */
|
|
751
752
|
export const createMcpRoutes = (dependencies: McpRouteDependencies = {}) =>
|
|
752
753
|
new Hono<AuthContext>()
|
|
754
|
+
.use("/mcp/v1", noTransform())
|
|
755
|
+
.use("/mcp/v1/*", noTransform())
|
|
753
756
|
.use("/mcp/v1", validateMcpOrigin(dependencies.getAppUrl ?? defaultAppUrl))
|
|
754
757
|
.use("/mcp/v1", dependencies.authenticate ?? mcpAuthentication(dependencies.getAppUrl ?? defaultAppUrl))
|
|
755
758
|
.use("/mcp/v1", rejectReservedWorkloadCredential)
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import { bodyLimit } from "hono/body-limit";
|
|
3
|
+
import { describeRoute } from "hono-openapi";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { CapabilityAppIdSchema } from "../contracts/capabilities";
|
|
6
|
+
import { panelProviderNeedsConsent } from "../contracts/panel-context-plain";
|
|
7
|
+
import { type AuthContext, jsonResponse, requiresAuth, v } from "../server";
|
|
8
|
+
import { PanelConsentCancelled, panelContextConsent } from "../services/panel-context-consent";
|
|
9
|
+
import { getRuntimeContext } from "../ssr/runtime";
|
|
10
|
+
|
|
11
|
+
const AnswerSchema = z.object({ allowed: z.boolean(), revision: z.number().int().min(0).max(2_147_483_647) }).strict();
|
|
12
|
+
const AnswersSchema = z.object({ answers: z.record(CapabilityAppIdSchema, AnswerSchema) }).strict();
|
|
13
|
+
const ProviderSchema = z.object({ provider: CapabilityAppIdSchema }).strict();
|
|
14
|
+
const SavedSchema = ProviderSchema.extend(AnswerSchema.shape).strict();
|
|
15
|
+
|
|
16
|
+
/** Mounted below /me's session and user guards. The live request registry owns provider membership. */
|
|
17
|
+
export const createMePanelConsentRoutes = (service = panelContextConsent) => {
|
|
18
|
+
const providers = (c: Parameters<typeof getRuntimeContext>[0]) =>
|
|
19
|
+
getRuntimeContext(c)
|
|
20
|
+
.apps.filter((app) => app.panel && panelProviderNeedsConsent(app.id))
|
|
21
|
+
.map((app) => app.id);
|
|
22
|
+
return new Hono<AuthContext>()
|
|
23
|
+
.use("*", async (c, next) => {
|
|
24
|
+
c.header("Cache-Control", "no-store");
|
|
25
|
+
if (!c.get("user")) return c.json({ message: "Authentication required" }, 401);
|
|
26
|
+
await next();
|
|
27
|
+
})
|
|
28
|
+
.get(
|
|
29
|
+
"/panel-consent",
|
|
30
|
+
describeRoute({
|
|
31
|
+
tags: ["Me"],
|
|
32
|
+
summary: "Read personal panel context answers",
|
|
33
|
+
...requiresAuth,
|
|
34
|
+
responses: { 200: jsonResponse(AnswersSchema, "Answers for registered panel providers") },
|
|
35
|
+
}),
|
|
36
|
+
async (c) => c.json({ answers: await service.list(c.get("user").id, providers(c)) }),
|
|
37
|
+
)
|
|
38
|
+
.put(
|
|
39
|
+
"/panel-consent/:provider",
|
|
40
|
+
describeRoute({
|
|
41
|
+
tags: ["Me"],
|
|
42
|
+
summary: "Save a personal panel context answer",
|
|
43
|
+
...requiresAuth,
|
|
44
|
+
responses: {
|
|
45
|
+
200: jsonResponse(SavedSchema, "Saved answer"),
|
|
46
|
+
404: { description: "Panel provider not found" },
|
|
47
|
+
409: { description: "Answer changed since it was read" },
|
|
48
|
+
},
|
|
49
|
+
}),
|
|
50
|
+
// The answer and revision fit within 256 bytes, including ordinary JSON whitespace.
|
|
51
|
+
bodyLimit({ maxSize: 256 }),
|
|
52
|
+
v("param", ProviderSchema),
|
|
53
|
+
v("json", AnswerSchema),
|
|
54
|
+
async (c) => {
|
|
55
|
+
const { provider } = c.req.valid("param");
|
|
56
|
+
if (!providers(c).includes(provider)) return c.json({ code: "PANEL_PROVIDER_NOT_FOUND", message: "Panel provider not found" }, 404);
|
|
57
|
+
const { allowed, revision } = c.req.valid("json");
|
|
58
|
+
let saved: Awaited<ReturnType<typeof service.set>>;
|
|
59
|
+
try {
|
|
60
|
+
saved = await service.set(c.get("user").id, provider, allowed, revision, c.req.raw.signal);
|
|
61
|
+
} catch (error) {
|
|
62
|
+
// The browser stopped waiting; the save was rolled back and nobody reads this answer.
|
|
63
|
+
if (error instanceof PanelConsentCancelled) return c.json({ code: "CANCELLED", message: "Request cancelled" }, 503);
|
|
64
|
+
throw error;
|
|
65
|
+
}
|
|
66
|
+
return saved
|
|
67
|
+
? c.json({ provider, ...saved })
|
|
68
|
+
: c.json({ code: "CONFLICT", message: "Panel consent changed. Read the current answer before saving again." }, 409);
|
|
69
|
+
},
|
|
70
|
+
);
|
|
71
|
+
};
|