@k2b/cloud 0.39.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.
Files changed (194) hide show
  1. package/README.md +8 -0
  2. package/package.json +8 -4
  3. package/src/_internal/app-favicon.ts +4 -2
  4. package/src/_internal/capabilities.ts +72 -48
  5. package/src/_internal/capability-reviews.ts +81 -0
  6. package/src/_internal/capability-schemas.ts +29 -0
  7. package/src/_internal/capability-sentences.ts +216 -1
  8. package/src/_internal/capability-streams.ts +7 -0
  9. package/src/_internal/define-app.ts +36 -15
  10. package/src/_internal/font-preloads.ts +2 -1
  11. package/src/_internal/runtime-watcher.ts +6 -0
  12. package/src/_internal/static-assets.ts +52 -3
  13. package/src/_internal/status-preserving-ssr.ts +2 -0
  14. package/src/access/PermissionEditor.tsx +2 -2
  15. package/src/account/EntitySearch.tsx +1 -1
  16. package/src/ai/action-review.ts +101 -0
  17. package/src/ai/background-review.ts +72 -0
  18. package/src/ai/capabilities.ts +29 -11
  19. package/src/ai/capability-execution.ts +76 -43
  20. package/src/ai/capability-result.ts +95 -0
  21. package/src/ai/chat/action-card.ts +398 -0
  22. package/src/ai/chat/action-messages.ts +129 -0
  23. package/src/ai/chat/action-question.browser-harness.tsx +245 -0
  24. package/src/ai/chat/action-question.fixtures.ts +733 -0
  25. package/src/ai/chat/action-question.tsx +669 -0
  26. package/src/ai/chat/blocks.tsx +19 -25
  27. package/src/ai/chat/message-actions.tsx +6 -0
  28. package/src/ai/chat/tool-action.tsx +481 -0
  29. package/src/ai/chat/turn-view.tsx +10 -160
  30. package/src/ai/chat/view-block.tsx +15 -5
  31. package/src/ai/chat-task-contracts.ts +4 -1
  32. package/src/ai/chat-task-reasons-migrate.ts +7 -0
  33. package/src/ai/chat-task-reasons.ts +1 -1
  34. package/src/ai/chat-tasks.ts +55 -25
  35. package/src/ai/client/controller.ts +59 -0
  36. package/src/ai/default-tools.ts +9 -1
  37. package/src/ai/executor.ts +190 -39
  38. package/src/ai/index.ts +2 -0
  39. package/src/ai/migrate.ts +11 -2
  40. package/src/ai/projects-routes.ts +3 -0
  41. package/src/ai/protocol.ts +14 -2
  42. package/src/ai/recent-resources.ts +240 -0
  43. package/src/ai/recent-sent-refs.ts +38 -0
  44. package/src/ai/routes.ts +145 -1
  45. package/src/ai/runtime.ts +1 -0
  46. package/src/ai/skills-routes.ts +3 -0
  47. package/src/ai/skills.ts +10 -1
  48. package/src/ai/store.ts +227 -24
  49. package/src/ai/tool-audit.ts +8 -0
  50. package/src/ai/tool-wording.ts +24 -2
  51. package/src/ai/types.ts +28 -3
  52. package/src/api/accounts-entities.ts +2 -1
  53. package/src/api/admin-ai-projects.ts +2 -0
  54. package/src/api/admin-ai-skills.ts +2 -0
  55. package/src/api/admin-ai-usage.ts +2 -0
  56. package/src/api/admin-geoip.ts +80 -0
  57. package/src/api/admin-identity.ts +3 -0
  58. package/src/api/admin-lifecycle.ts +3 -0
  59. package/src/api/admin-outgoing-mail.ts +2 -0
  60. package/src/api/app-approval.ts +46 -5
  61. package/src/api/approvals-loader.ts +23 -1
  62. package/src/api/approvals.ts +2 -0
  63. package/src/api/auth.ts +25 -6
  64. package/src/api/capabilities.ts +44 -11
  65. package/src/api/capability-streams.ts +12 -0
  66. package/src/api/help.ts +4 -28
  67. package/src/api/index.ts +6 -1
  68. package/src/api/lasting-access-messages.ts +17 -0
  69. package/src/api/mcp.ts +3 -0
  70. package/src/api/me-panel-consent.ts +71 -0
  71. package/src/api/me.ts +80 -33
  72. package/src/api/search.ts +3 -1
  73. package/src/api/sync-ops.ts +2 -0
  74. package/src/api/widgets.ts +3 -0
  75. package/src/approvals/Approvals.tsx +180 -238
  76. package/src/approvals/messages.ts +12 -24
  77. package/src/browser/CloudResourceSearch.browser-harness.tsx +10 -2
  78. package/src/browser/app-approval.ts +54 -5
  79. package/src/browser/commands.ts +22 -6
  80. package/src/browser/composer-catalog.ts +45 -3
  81. package/src/browser/composer-insert.ts +10 -0
  82. package/src/browser/composer-open-messages.ts +17 -0
  83. package/src/browser/composer-open.ts +110 -0
  84. package/src/browser/discoverable-command.ts +6 -0
  85. package/src/browser/hints.ts +6 -0
  86. package/src/browser/live-connection.ts +26 -2
  87. package/src/browser/navigation-apps.ts +7 -0
  88. package/src/browser/panel-context-bridge.ts +30 -0
  89. package/src/browser/panel-context-receiver.ts +53 -0
  90. package/src/browser/panel-menu.tsx +56 -0
  91. package/src/browser/panel-messages.ts +11 -0
  92. package/src/browser/panel-shortcuts.ts +31 -0
  93. package/src/browser/panel-slot.ts +412 -10
  94. package/src/browser/panel.ts +37 -0
  95. package/src/browser/search-commands.ts +6 -3
  96. package/src/capabilities/client.ts +8 -2
  97. package/src/capabilities/reviews.ts +205 -0
  98. package/src/capabilities/server.ts +10 -3
  99. package/src/capabilities/testing.ts +47 -0
  100. package/src/capabilities/types.ts +2 -2
  101. package/src/cli/account.ts +13 -3
  102. package/src/cli/admin/geoip.ts +115 -0
  103. package/src/cli/admin/index.ts +3 -0
  104. package/src/contracts/app-approval.ts +120 -13
  105. package/src/contracts/approvals.ts +2 -2
  106. package/src/contracts/capabilities.ts +410 -18
  107. package/src/contracts/geoip.ts +42 -0
  108. package/src/contracts/index.ts +3 -0
  109. package/src/contracts/panel-context-plain.ts +99 -0
  110. package/src/contracts/panel-context.ts +93 -0
  111. package/src/contracts/seen-hints.ts +35 -0
  112. package/src/server/client-ip.ts +5 -0
  113. package/src/server/help-payload.ts +38 -0
  114. package/src/server/middleware/auth.ts +11 -1
  115. package/src/server/middleware/middleware.ts +9 -0
  116. package/src/server/middleware/no-transform.ts +19 -0
  117. package/src/server/middleware/rate-limit.ts +4 -6
  118. package/src/server/request-context/address.ts +45 -0
  119. package/src/server/request-context/geoip.ts +380 -0
  120. package/src/server/request-context/index.ts +33 -0
  121. package/src/server/request-context/user-agent.ts +95 -0
  122. package/src/services/app-approval.ts +382 -59
  123. package/src/services/approvals/device.ts +7 -5
  124. package/src/services/approvals/index.ts +6 -5
  125. package/src/services/approvals/list.ts +26 -5
  126. package/src/services/auth-flows/notification-sender.ts +3 -0
  127. package/src/services/geoip/index.ts +164 -0
  128. package/src/services/geoip/store.ts +30 -0
  129. package/src/services/geoip/upload.ts +150 -0
  130. package/src/services/identity/routes.ts +5 -1
  131. package/src/services/index.ts +11 -0
  132. package/src/services/known-browsers.ts +312 -0
  133. package/src/services/login-address-store.ts +51 -0
  134. package/src/services/login-context.ts +118 -0
  135. package/src/services/mandates/index.ts +3 -0
  136. package/src/services/mandates/policy.ts +15 -3
  137. package/src/services/notifications/email-frame.ts +1 -1
  138. package/src/services/oauth-tokens.ts +16 -2
  139. package/src/services/panel-context-consent.ts +58 -0
  140. package/src/services/service-account-credentials.ts +21 -0
  141. package/src/services/session/index.ts +97 -16
  142. package/src/services/session/recent.ts +13 -0
  143. package/src/services/webauthn.ts +12 -0
  144. package/src/shared/email-html.ts +2 -0
  145. package/src/shared/help.ts +3 -0
  146. package/src/shared-text/cursor.ts +22 -0
  147. package/src/shared-text/documents.ts +142 -0
  148. package/src/shared-text/events.ts +43 -0
  149. package/src/shared-text/index.ts +12 -0
  150. package/src/shared-text/log.ts +48 -0
  151. package/src/shared-text/text-diff.ts +23 -0
  152. package/src/shared-text/types.ts +41 -0
  153. package/src/ssr/AdminLayout.tsx +2 -0
  154. package/src/ssr/AppLaunchpadPanel.tsx +2 -2
  155. package/src/ssr/CloudShell.island.tsx +10 -2
  156. package/src/ssr/Layout.tsx +25 -2
  157. package/src/ssr/LayoutHelp.browser-harness.tsx +2 -0
  158. package/src/ssr/LayoutHelp.tsx +435 -274
  159. package/src/ssr/LayoutHelpPanel.browser-harness.tsx +136 -0
  160. package/src/ssr/LayoutHelpTrigger.tsx +27 -2
  161. package/src/ssr/MobileNavigation.tsx +2 -2
  162. package/src/ssr/PanelLayout.tsx +9 -1
  163. package/src/ssr/PanelRuntime.island.tsx +7 -0
  164. package/src/ssr/PanelSlot.tsx +53 -3
  165. package/src/ssr/ShellRuntime.island.tsx +11 -2
  166. package/src/ssr/app-color.ts +14 -9
  167. package/src/ssr/app-navigation.ts +16 -3
  168. package/src/ssr/compact-shell.browser-harness.ts +30 -22
  169. package/src/ssr/dialog-app-colors.browser-harness.tsx +43 -0
  170. package/src/ssr/help-messages.ts +4 -0
  171. package/src/ssr/help.ts +20 -0
  172. package/src/ssr/hints.ts +25 -0
  173. package/src/ssr/index.ts +2 -0
  174. package/src/ssr/islands/index.ts +4 -3
  175. package/src/ssr/layout-context.ts +2 -0
  176. package/src/ssr/layout-help-article.server.ts +50 -0
  177. package/src/ssr/layout-help-start.ts +33 -7
  178. package/src/ssr/panel-consent.ts +29 -0
  179. package/src/ssr/panel-context.ts +10 -0
  180. package/src/ssr/panel-slot.browser-harness.tsx +53 -4
  181. package/src/ssr/platform-messages.ts +18 -0
  182. package/src/ssr/shell-data.tsx +2 -1
  183. package/src/styles/effects.css +198 -9
  184. package/src/styles/file-chooser.css +2 -1
  185. package/src/styles/global.css +49 -12
  186. package/src/styles/panel.css +47 -4
  187. package/src/styles/resource-search.css +6 -2
  188. package/src/styles/tokens.css +17 -11
  189. package/src/styles/utilities-buttons.css +3 -2
  190. package/src/styles/utilities-data.css +2 -1
  191. package/src/styles/utilities-feedback.css +2 -2
  192. package/src/styles/utilities-layout.css +3 -13
  193. package/src/styles/utilities-markdown-editor.css +6 -6
  194. package/src/styles/utilities-navigation.css +17 -22
@@ -7,6 +7,7 @@ import { listApps } from "../_internal/registry";
7
7
  import { CapabilityAppIdSchema } from "../contracts/capabilities";
8
8
  import { AI_INFERENCE_SCOPE } from "../contracts/shared";
9
9
  import { type AuthContext, auth, err, respond, v } from "../server";
10
+ import { noTransform } from "../server/middleware/no-transform";
10
11
  import { audit } from "../services/audit";
11
12
  import { getIdentitySigningKeyStatus, identityMetrics, revokeIdentitySigningKey, rewrapIdentitySigningKeys } from "../services/identity";
12
13
  import { WORKLOAD_SCOPES } from "../services/identity/workload-auth";
@@ -110,6 +111,8 @@ export const createAdminIdentityRoutes = (
110
111
  service: IdentityAdminDependencies = dependencies,
111
112
  ) =>
112
113
  new Hono<AuthContext>()
114
+ // Mandate lists hold every user's grant inputs, which users write at will and which can carry private text.
115
+ .use("*", noTransform())
113
116
  .use(authenticate)
114
117
  .use("*", async (c, next) => {
115
118
  c.header("Cache-Control", "no-store");
@@ -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");
@@ -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,6 +17,8 @@ import {
15
17
  AppPairingStartSchema,
16
18
  } from "../contracts/app-approval";
17
19
  import { type AuthContext, auth, rateLimit, v } from "../server";
20
+ import { noTransform } from "../server/middleware/no-transform";
21
+ import { addressPrefix, clientIp, describeRequest } from "../server/request-context";
18
22
  import {
19
23
  type AppApprovalActor,
20
24
  AppApprovalError,
@@ -22,6 +26,8 @@ import {
22
26
  appApproval,
23
27
  type createAppApprovalService,
24
28
  } from "../services/app-approval";
29
+ import { type NewBrowserSignInNotifier, readBrowserCookie } from "../services/known-browsers";
30
+ import { SessionAppDeviceInactiveError, SessionEpochChangedError } from "../services/session";
25
31
  import { appNameIn } from "../ssr/runtime";
26
32
  import { createDeviceActions } from "./app-approval-actions";
27
33
 
@@ -30,7 +36,12 @@ const DevicePageSchema = z.object({ after: z.string().uuid().optional() }).stric
30
36
  type Service = ReturnType<typeof createAppApprovalService>;
31
37
 
32
38
  /** This is a separate credential surface, never a user API-key or OAuth scope. */
33
- export const createAppApprovalRoutes = (service: Service = appApproval, actions = createDeviceActions, terminal?: AppTerminalLogins) => {
39
+ export const createAppApprovalRoutes = (
40
+ options: { notify: NewBrowserSignInNotifier },
41
+ service: Service = appApproval,
42
+ actions = createDeviceActions,
43
+ terminal?: AppTerminalLogins,
44
+ ) => {
34
45
  const actor = async (c: Parameters<typeof auth.session.getToken>[0]): Promise<AppApprovalActor> => {
35
46
  // The web session (or an explicit bearer session), never the mobile app's session: a phone
36
47
  // that pairs sign-in devices would hand out authority that outlives its removal.
@@ -42,6 +53,8 @@ export const createAppApprovalRoutes = (service: Service = appApproval, actions
42
53
  return { userId: session.user.id, sid: session.data.sid, admin: session.user.roles.includes("admin") };
43
54
  };
44
55
  return new Hono<AuthContext>()
56
+ .use("/device", noTransform())
57
+ .use("/device/*", noTransform())
45
58
  .onError((error, c) => {
46
59
  if (error instanceof AppApprovalError) return c.json({ code: error.code, message: error.code }, error.status);
47
60
  if (error instanceof HTTPException && error.status < 500) return c.json({ code: "INVALID_REQUEST" }, error.status);
@@ -95,7 +108,19 @@ export const createAppApprovalRoutes = (service: Service = appApproval, actions
95
108
  )
96
109
  .post("/manage/devices/update", v("json", AppDeviceMutationSchema), async (c) => {
97
110
  const input = c.req.valid("json");
98
- await service.mutateDevice(await actor(c), input.deviceId, input.operation === "rename" ? input.name : undefined);
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);
99
124
  return c.body(null, 204);
100
125
  })
101
126
  .post(
@@ -118,6 +143,7 @@ export const createAppApprovalRoutes = (service: Service = appApproval, actions
118
143
  c.req.valid("json"),
119
144
  actions((appId, locale) => appNameIn(c, appId, locale, appId)),
120
145
  terminal,
146
+ { address: clientIp(c) },
121
147
  ),
122
148
  ),
123
149
  )
@@ -127,7 +153,10 @@ export const createAppApprovalRoutes = (service: Service = appApproval, actions
127
153
  v("json", AppLoginStartSchema),
128
154
  async (c) => {
129
155
  const input = c.req.valid("json");
130
- return c.json(await service.startLogin(input.identifier, input.category), 202);
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);
131
160
  },
132
161
  )
133
162
  .post("/login/status", v("json", AppLoginReferenceSchema), async (c) => {
@@ -143,8 +172,20 @@ export const createAppApprovalRoutes = (service: Service = appApproval, actions
143
172
  v("json", AppLoginReferenceSchema),
144
173
  async (c) => {
145
174
  const input = c.req.valid("json");
146
- const userId = await service.consumeLogin(input.requestId, input.browserSecret);
147
- await auth.session.create(c, userId);
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
+ }
148
189
  return c.body(null, 204);
149
190
  },
150
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
- return { ...page, items: page.items.map((row) => presentPendingApproval(row, apps)) };
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
  };
@@ -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(
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
  },
@@ -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
- CapabilityActionReviewSchema,
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
- ? CapabilityActionReviewSchema
631
+ ? CapabilityActionReviewDataSchema
613
632
  : schemaValidator(`${operation.schemaHash}:result`, capabilityResultJsonSchema(operation.dataSchema));
614
633
  const parsedResult = resultValidator?.safeParse(upstreamBody.data);
615
- const parsedReview = params.review ? CapabilityActionReviewSchema.safeParse(upstreamBody.data) : null;
616
- // A rememberable Action may leave out its scope for arguments that should ask every time; any other Action has none.
617
- const reviewApprovalScopeIsValid =
618
- !params.review ||
619
- !parsedReview?.success ||
620
- ("approval" in operation && operation.approval === "rememberable") ||
621
- parsedReview.data.approvalScope === undefined;
622
- if (!resultValidator || !parsedResult?.success || !reviewApprovalScopeIsValid) {
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 { type HelpDocumentPayload, type HelpSearchPayload, parseHelpReference } from "../shared/help";
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 links = (reference: { appId?: string; documentId: string }) =>
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";
@@ -69,7 +70,10 @@ const buildCoreApi = (options: CoreApiOptions) => {
69
70
  const widgetRoutes = createWidgetRoutes();
70
71
  return new Hono()
71
72
  .use(prettyJSON())
72
- .route("/auth/app-approval/v1", createAppApprovalRoutes(undefined, undefined, options.terminalLogins))
73
+ .route(
74
+ "/auth/app-approval/v1",
75
+ createAppApprovalRoutes({ notify: options.notifications.sendNewBrowserSignIn }, undefined, undefined, options.terminalLogins),
76
+ )
73
77
  .route("/auth/pwa/v1", createPwaRoutes())
74
78
  .route("/auth", createAuthRoutes(options.notifications))
75
79
  .route("/me", meRoutes)
@@ -86,6 +90,7 @@ const buildCoreApi = (options: CoreApiOptions) => {
86
90
  .route("/admin/core/settings", adminCoreSettingsRoutes)
87
91
  .route("/admin/core/rail", adminRailRoutes)
88
92
  .route("/admin/core/linux-identities", adminLinuxIdentityRoutes)
93
+ .route("/admin/core/geoip", adminGeoIpRoutes)
89
94
  .route("/admin/identity", adminIdentityRoutes)
90
95
  .route("/admin/sync", createSyncOpsProxyRoutes())
91
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
+ };