@k2b/cloud 0.39.0 → 0.41.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 +11 -4
- package/src/_internal/app-favicon.ts +4 -2
- 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 +217 -1
- package/src/_internal/capability-streams.ts +7 -0
- package/src/_internal/define-app.ts +49 -18
- package/src/_internal/font-preloads.ts +2 -1
- package/src/_internal/heartbeat.ts +203 -14
- package/src/_internal/registry.ts +28 -1
- package/src/_internal/runtime-watcher.ts +6 -0
- package/src/_internal/setup-lock.ts +103 -0
- package/src/_internal/static-assets.ts +52 -3
- package/src/_internal/status-preserving-ssr.ts +2 -0
- package/src/access/PermissionEditor.tsx +2 -2
- package/src/account/EntitySearch.tsx +1 -1
- package/src/ai/action-review.ts +101 -0
- package/src/ai/background-review.ts +72 -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 +464 -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 +680 -0
- package/src/ai/chat/action-text.ts +288 -0
- package/src/ai/chat/blocks.tsx +19 -25
- package/src/ai/chat/message-actions.tsx +6 -0
- package/src/ai/chat/tool-action-words.ts +31 -0
- package/src/ai/chat/tool-action.tsx +456 -0
- package/src/ai/chat/turn-view.tsx +10 -160
- package/src/ai/chat/view-block.tsx +15 -5
- package/src/ai/chat-task-contracts.ts +4 -1
- package/src/ai/chat-task-reasons-migrate.ts +7 -0
- package/src/ai/chat-task-reasons.ts +1 -1
- package/src/ai/chat-tasks.ts +55 -25
- package/src/ai/client/controller.ts +59 -0
- package/src/ai/default-tools.ts +9 -1
- package/src/ai/executor.ts +198 -39
- package/src/ai/firecrawl-tools.ts +15 -2
- package/src/ai/index.ts +3 -0
- package/src/ai/migrate.ts +26 -2
- package/src/ai/open-questions.ts +172 -0
- package/src/ai/projects-routes.ts +3 -0
- package/src/ai/protocol.ts +14 -2
- package/src/ai/question-text.ts +8 -0
- package/src/ai/recent-resources.ts +240 -0
- package/src/ai/recent-sent-refs.ts +38 -0
- package/src/ai/routes.ts +195 -5
- package/src/ai/runtime.ts +1 -0
- package/src/ai/skills-routes.ts +3 -0
- package/src/ai/skills.ts +10 -1
- package/src/ai/store.ts +245 -24
- package/src/ai/testing.ts +36 -0
- package/src/ai/tool-audit.ts +8 -0
- package/src/ai/tool-sentences.ts +152 -34
- package/src/ai/tool-wording.ts +36 -8
- package/src/ai/tools.ts +8 -3
- package/src/ai/types.ts +43 -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-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 +2 -0
- package/src/api/app-approval-actions.ts +31 -7
- package/src/api/app-approval.ts +46 -5
- package/src/api/approvals-loader.ts +23 -1
- package/src/api/approvals.ts +2 -0
- package/src/api/auth/schemas.ts +3 -0
- package/src/api/auth.ts +67 -12
- 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 +6 -1
- package/src/api/lasting-access-messages.ts +17 -0
- package/src/api/mcp.ts +30 -3
- package/src/api/me-panel-consent.ts +71 -0
- package/src/api/me.ts +159 -36
- 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 +166 -262
- package/src/approvals/approval-card.ts +68 -0
- package/src/approvals/messages.ts +12 -24
- package/src/browser/CloudResourceSearch.browser-harness.tsx +10 -2
- package/src/browser/app-approval.ts +54 -5
- package/src/browser/commands.ts +22 -6
- package/src/browser/composer-catalog.ts +45 -3
- package/src/browser/composer-insert.ts +10 -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 +11 -0
- package/src/browser/panel-shortcuts.ts +31 -0
- package/src/browser/panel-slot.ts +412 -10
- package/src/browser/panel.ts +37 -0
- 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 +123 -16
- 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/seen-hints.ts +35 -0
- package/src/contracts/shared.ts +3 -0
- package/src/server/client-ip.ts +5 -0
- package/src/server/help-payload.ts +38 -0
- package/src/server/middleware/auth.ts +13 -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/services/accounts/app.ts +23 -3
- package/src/services/accounts/users.ts +12 -1
- package/src/services/app-approval.ts +382 -59
- package/src/services/approvals/device.ts +49 -31
- package/src/services/approvals/index.ts +7 -5
- package/src/services/approvals/list.ts +27 -6
- package/src/services/approvals/question.ts +68 -0
- package/src/services/audit/index.ts +1 -0
- package/src/services/auth-flows/ipa.ts +39 -2
- package/src/services/auth-flows/notification-sender.ts +3 -0
- package/src/services/auth-flows/password-reset.ts +32 -6
- 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 +13 -0
- 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/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/markdown.ts +160 -2
- package/src/services/service-account-credentials.ts +21 -0
- package/src/services/session/index.ts +296 -41
- package/src/services/session/recent.ts +13 -0
- package/src/services/webauthn.ts +12 -0
- package/src/shared/email-html.ts +2 -0
- package/src/shared/help.ts +3 -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 +10 -2
- 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 +25 -2
- package/src/ssr/LayoutHelp.browser-harness.tsx +2 -0
- package/src/ssr/LayoutHelp.tsx +222 -419
- package/src/ssr/LayoutHelpPanel.browser-harness.tsx +135 -0
- package/src/ssr/LayoutHelpPanel.server.browser-harness.tsx +131 -0
- package/src/ssr/LayoutHelpPanel.server.tsx +115 -0
- package/src/ssr/LayoutHelpTrigger.tsx +27 -2
- package/src/ssr/MobileNavigation.tsx +2 -2
- package/src/ssr/PanelLayout.tsx +9 -1
- package/src/ssr/PanelRuntime.island.tsx +7 -0
- package/src/ssr/PanelSlot.tsx +53 -3
- package/src/ssr/ShellRuntime.island.tsx +11 -2
- package/src/ssr/__snapshots__/LayoutHelp.ssr.render.test.tsx.snap +29 -0
- package/src/ssr/app-color.ts +14 -9
- package/src/ssr/app-navigation.ts +16 -3
- package/src/ssr/compact-shell.browser-harness.ts +30 -22
- package/src/ssr/dialog-app-colors.browser-harness.tsx +43 -0
- package/src/ssr/help-messages.ts +4 -0
- package/src/ssr/help.ts +21 -0
- package/src/ssr/hints.ts +25 -0
- package/src/ssr/index.ts +3 -0
- package/src/ssr/layout-context.ts +2 -0
- package/src/ssr/layout-help-article.server.ts +50 -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-start.ts +33 -7
- package/src/ssr/layout-help-views.tsx +484 -0
- package/src/ssr/panel-consent.ts +29 -0
- package/src/ssr/panel-context.ts +10 -0
- package/src/ssr/panel-idle.ts +9 -0
- package/src/ssr/panel-slot.browser-harness.tsx +53 -4
- package/src/ssr/platform-messages.ts +18 -0
- package/src/ssr/shell-data.tsx +2 -1
- package/src/styles/approvals.css +6 -0
- package/src/styles/effects.css +213 -9
- package/src/styles/file-chooser.css +2 -1
- package/src/styles/global.css +49 -12
- package/src/styles/panel.css +47 -4
- package/src/styles/resource-search.css +6 -2
- package/src/styles/tokens.css +17 -11
- 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 +17 -22
package/src/ai/tool-wording.ts
CHANGED
|
@@ -2,17 +2,24 @@ import {
|
|
|
2
2
|
type CapabilityActionWording,
|
|
3
3
|
capabilityActionOutcome,
|
|
4
4
|
capabilityActionSubject,
|
|
5
|
-
|
|
5
|
+
capabilityReviewSentence,
|
|
6
|
+
capabilitySentenceText,
|
|
6
7
|
} from "../_internal/capability-sentences";
|
|
7
|
-
import {
|
|
8
|
+
import { CapabilityActionReceiptSchema } from "../contracts/capabilities";
|
|
9
|
+
import { type AiStoredActionReview, isLegacyActionReview, storedActionReview } from "./action-review";
|
|
10
|
+
import { isRecord } from "./chat/message-utils";
|
|
8
11
|
import type { AiTurnBlock } from "./protocol";
|
|
9
|
-
import {
|
|
12
|
+
import { cloudToolReview } from "./tool-sentences";
|
|
10
13
|
|
|
11
14
|
type ToolBlock = Extract<AiTurnBlock, { kind: "tool" }>;
|
|
12
15
|
|
|
16
|
+
/** The review Cloud stored for the call, else the one its open approval carries. */
|
|
17
|
+
const reviewOf = (block: ToolBlock): AiStoredActionReview | null =>
|
|
18
|
+
block.review ?? (block.approval?.review ? storedActionReview(block.approval.review) : null);
|
|
19
|
+
|
|
13
20
|
/**
|
|
14
21
|
* How the chat words one call that acts on something: an app Action through its saved presentation, a
|
|
15
|
-
* Cloud tool through
|
|
22
|
+
* Cloud tool through its per-call review. `null` for calls that only read.
|
|
16
23
|
*/
|
|
17
24
|
export const toolWording = (block: ToolBlock, locale: string): CapabilityActionWording | null => {
|
|
18
25
|
const presentation = block.presentation;
|
|
@@ -24,21 +31,42 @@ export const toolWording = (block: ToolBlock, locale: string): CapabilityActionW
|
|
|
24
31
|
...(presentation.fields ? { fields: presentation.fields } : {}),
|
|
25
32
|
};
|
|
26
33
|
}
|
|
27
|
-
const
|
|
28
|
-
return
|
|
34
|
+
const review = cloudToolReview(block.name, block.args, locale);
|
|
35
|
+
return review
|
|
36
|
+
? { title: capabilitySentenceText(review.sentence.action, review.slots, { locale: review.locale, timeZone: "UTC" }) }
|
|
37
|
+
: null;
|
|
29
38
|
};
|
|
30
39
|
|
|
31
|
-
/**
|
|
40
|
+
/**
|
|
41
|
+
* What the call does or would do, as one sentence; `null` for calls that only read. A stored review's action sentence
|
|
42
|
+
* wins, so it reads the same after a reload and after the app changed. An old review's message is the question of the
|
|
43
|
+
* card, not the call's short subject: steps and receipts keep the app's catalog sentence.
|
|
44
|
+
*/
|
|
32
45
|
export const toolSubject = (block: ToolBlock, locale: string, timeZone: string): string | null => {
|
|
46
|
+
const review = reviewOf(block);
|
|
47
|
+
if (review && !isLegacyActionReview(review))
|
|
48
|
+
return capabilitySentenceText(review.sentence.action, review.slots, { locale: review.locale, timeZone });
|
|
49
|
+
const cloudReview = cloudToolReview(block.name, block.args, locale);
|
|
50
|
+
if (cloudReview) return capabilitySentenceText(cloudReview.sentence.action, cloudReview.slots, { locale: cloudReview.locale, timeZone });
|
|
33
51
|
const wording = toolWording(block, locale);
|
|
34
52
|
return wording ? capabilityActionSubject(wording, block.args, { locale, timeZone }) : null;
|
|
35
53
|
};
|
|
36
54
|
|
|
37
55
|
/** The sentence for one outcome, or `null` when the tool has none that renders for this call. */
|
|
38
56
|
export const toolOutcome = (block: ToolBlock, key: "done" | "rejected" | "notRun", locale: string, timeZone: string): string | null => {
|
|
57
|
+
const result = isRecord(block.result) ? block.result : undefined;
|
|
58
|
+
const review = reviewOf(block);
|
|
59
|
+
if (review && !isLegacyActionReview(review)) {
|
|
60
|
+
const receipt = key === "done" ? CapabilityActionReceiptSchema.safeParse(result?.receipt) : null;
|
|
61
|
+
if (receipt?.success) return capabilitySentenceText(receipt.data.text, receipt.data.slots ?? {}, { locale: review.locale, timeZone });
|
|
62
|
+
return capabilityReviewSentence(review, key, { timeZone });
|
|
63
|
+
}
|
|
64
|
+
if (review && key === "done" && review.legacy.summary) return review.legacy.summary;
|
|
65
|
+
const cloudReview = cloudToolReview(block.name, block.args, locale);
|
|
66
|
+
// Only a form the tool names; otherwise the card keeps Cloud's own outcome words for the call.
|
|
67
|
+
if (cloudReview) return key === "done" || cloudReview.sentence[key] ? capabilityReviewSentence(cloudReview, key, { timeZone }) : null;
|
|
39
68
|
const wording = toolWording(block, locale);
|
|
40
69
|
if (!wording) return null;
|
|
41
|
-
const result = isRecord(block.result) ? block.result : undefined;
|
|
42
70
|
// App Actions return their data inside the result envelope; Cloud tools return the data itself.
|
|
43
71
|
const data = block.presentation?.kind === "capability" ? result?.data : block.result;
|
|
44
72
|
return capabilityActionOutcome(wording, key, { input: block.args, data }, { locale, timeZone });
|
package/src/ai/tools.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Tool, ToolContext } from "@k2b/nessi";
|
|
2
2
|
import { defineTool as defineNessiTool } from "@k2b/nessi";
|
|
3
3
|
import type { z } from "zod";
|
|
4
|
+
import type { CapabilityActionCallReview } from "../contracts/capabilities";
|
|
4
5
|
import type { RequestActor } from "../server";
|
|
5
6
|
import { aiToolNeedsApproval } from "./approvals";
|
|
6
7
|
import type {
|
|
@@ -64,7 +65,7 @@ export const defineAiTool = <TInput extends z.ZodType, TOutput extends z.ZodType
|
|
|
64
65
|
/** Localized short status; never include payloads or secrets. */
|
|
65
66
|
reportProgress?: (message: string) => Promise<void>;
|
|
66
67
|
/** Asks like `requestApproval`, and lets the person remember the approval of `target`. */
|
|
67
|
-
requestApprovalFor?: (message: string, target: AiApprovalTarget) => Promise<boolean>;
|
|
68
|
+
requestApprovalFor?: (message: string, target: AiApprovalTarget, review?: CapabilityActionCallReview) => Promise<boolean>;
|
|
68
69
|
/** Relays a decision for an effect another platform component consumes and executes. */
|
|
69
70
|
requestRelayedApproval?: (message: string, options: { approvalId?: string; target?: AiApprovalTarget }) => Promise<boolean>;
|
|
70
71
|
/** Shows requests a website approval let through as they go out, so the chat keeps them whatever the outcome. */
|
|
@@ -122,6 +123,8 @@ export type AiToolPreparationContext = {
|
|
|
122
123
|
linkApproval?: (approvalCallId: string, approvalId: string) => void;
|
|
123
124
|
/** Hears what a nested approval asks for before the approval is requested. */
|
|
124
125
|
describeApproval?: (approvalCallId: string, target: AiApprovalTarget) => void;
|
|
126
|
+
/** Registers the deterministic review before requesting or consuming a nested approval. */
|
|
127
|
+
describeReview?: (approvalCallId: string, review: CapabilityActionCallReview) => void | Promise<void>;
|
|
125
128
|
reportWebsiteReceipts?: (callId: string, receipts: AiWebsiteReceipt[]) => Promise<void>;
|
|
126
129
|
};
|
|
127
130
|
|
|
@@ -168,10 +171,11 @@ export const prepareAiTools = (input: AiToolPreparationContext & { tools?: AiRun
|
|
|
168
171
|
}
|
|
169
172
|
// Nessi numbers the approvals of one execution in request order; count them to name each one's call ID.
|
|
170
173
|
let approvals = 0;
|
|
171
|
-
const requestApproval = async (message: string, target?: AiApprovalTarget): Promise<boolean> => {
|
|
174
|
+
const requestApproval = async (message: string, target?: AiApprovalTarget, review?: CapabilityActionCallReview): Promise<boolean> => {
|
|
172
175
|
while (true) {
|
|
173
176
|
const approvalCallId = `${callId}-approval-${approvals++}`;
|
|
174
177
|
if (target) input.describeApproval?.(approvalCallId, target);
|
|
178
|
+
if (review) await input.describeReview?.(approvalCallId, review);
|
|
175
179
|
if (!(await ctx.requestApproval(message))) return false;
|
|
176
180
|
const used = await use(approvalCallId);
|
|
177
181
|
if (used === "approved") return true;
|
|
@@ -182,7 +186,8 @@ export const prepareAiTools = (input: AiToolPreparationContext & { tools?: AiRun
|
|
|
182
186
|
return {
|
|
183
187
|
...ctx,
|
|
184
188
|
requestApproval,
|
|
185
|
-
requestApprovalFor: (message: string, target: AiApprovalTarget) =>
|
|
189
|
+
requestApprovalFor: (message: string, target: AiApprovalTarget, review?: CapabilityActionCallReview) =>
|
|
190
|
+
requestApproval(message, target, review),
|
|
186
191
|
requestRelayedApproval: async (message: string, options: { approvalId?: string; target?: AiApprovalTarget }) => {
|
|
187
192
|
const approvalCallId = `${callId}-approval-${approvals++}`;
|
|
188
193
|
if (options.approvalId) input.linkApproval?.(approvalCallId, options.approvalId);
|
package/src/ai/types.ts
CHANGED
|
@@ -13,11 +13,37 @@ import type {
|
|
|
13
13
|
import type { Usage } from "@k2b/nessi/ai";
|
|
14
14
|
import type { z } from "zod";
|
|
15
15
|
import type { CapabilitySentenceField } from "../_internal/capability-sentences";
|
|
16
|
-
import type {
|
|
16
|
+
import type {
|
|
17
|
+
CapabilityActionCallReview,
|
|
18
|
+
CapabilityActionReviewData,
|
|
19
|
+
CapabilityActionSentences,
|
|
20
|
+
CloudResourceRef,
|
|
21
|
+
} from "../contracts/capabilities";
|
|
17
22
|
import type { RequestActor } from "../server";
|
|
23
|
+
import type { AiActionCheck, AiStoredActionReview } from "./action-review";
|
|
18
24
|
import type { AiLocalClient, AiLocalContext, AiLocalToolName } from "./local-tool-contracts";
|
|
19
25
|
import type { AiTurnBlock } from "./protocol";
|
|
20
26
|
|
|
27
|
+
/** Current reader-confirmed metadata for a recently sent resource. */
|
|
28
|
+
export type AiRecentResource = {
|
|
29
|
+
ref: CloudResourceRef;
|
|
30
|
+
title: string;
|
|
31
|
+
icon?: string;
|
|
32
|
+
href?: string;
|
|
33
|
+
kind: string;
|
|
34
|
+
preview?: string;
|
|
35
|
+
appName: string;
|
|
36
|
+
count: number;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export type AiRecentSentResourceRef = {
|
|
40
|
+
ref: CloudResourceRef;
|
|
41
|
+
count: number;
|
|
42
|
+
lastSentAt: string;
|
|
43
|
+
/** Internal conversation UUID, only for authorized Studio reads. */
|
|
44
|
+
conversationId: string;
|
|
45
|
+
};
|
|
46
|
+
|
|
21
47
|
export const AI_MODEL_CAPABILITIES = ["streaming", "tools", "vision", "transcription"] as const;
|
|
22
48
|
export type AiModelCapability = (typeof AI_MODEL_CAPABILITIES)[number];
|
|
23
49
|
|
|
@@ -308,6 +334,8 @@ export type AiStoredMessage = {
|
|
|
308
334
|
trigger: "scheduled" | "manual";
|
|
309
335
|
};
|
|
310
336
|
toolPresentations?: Record<string, AiToolPresentation>;
|
|
337
|
+
toolReviews?: Record<string, AiStoredActionReview>;
|
|
338
|
+
toolChecks?: Record<string, AiActionCheck>;
|
|
311
339
|
/** How the person decided an approval; `expired` when the turn ended while it still waited. */
|
|
312
340
|
toolOutcomes?: Record<string, "rejected" | "approved" | "expired">;
|
|
313
341
|
/** Requests a website approval let through, by tool call and then by `AiWebsiteReceipt.index`. */
|
|
@@ -428,7 +456,14 @@ export type AiPendingTurnAction =
|
|
|
428
456
|
name: string;
|
|
429
457
|
args: unknown;
|
|
430
458
|
message?: string;
|
|
431
|
-
review
|
|
459
|
+
/** Stored review, including the declared ask reason for new-shape reviews. */
|
|
460
|
+
review?: CapabilityActionReviewData;
|
|
461
|
+
/** Owning application, with its registry name in the turn's request locale. */
|
|
462
|
+
app?: { id: string; name: string };
|
|
463
|
+
/** The chat's current title, when known. */
|
|
464
|
+
chatTitle?: string | null;
|
|
465
|
+
/** Expiry of the platform approval request, as an ISO timestamp. */
|
|
466
|
+
expiresAt?: string;
|
|
432
467
|
allowAlways: boolean;
|
|
433
468
|
/** Whether the approval can be remembered for this chat. */
|
|
434
469
|
allowChat: boolean;
|
|
@@ -464,7 +499,7 @@ export type AiPendingTurnActionRecord = {
|
|
|
464
499
|
name: string;
|
|
465
500
|
args: unknown;
|
|
466
501
|
message?: string;
|
|
467
|
-
review?:
|
|
502
|
+
review?: CapabilityActionReviewData;
|
|
468
503
|
approvalScope: string;
|
|
469
504
|
allowAlways: boolean;
|
|
470
505
|
/** Whether the approval can be remembered for this chat; defaults to `allowAlways`. */
|
|
@@ -621,6 +656,7 @@ export type AiBackgroundRunContext = {
|
|
|
621
656
|
trigger?: "scheduled" | "manual";
|
|
622
657
|
timezone?: string;
|
|
623
658
|
stopDeliveredAt?: string;
|
|
659
|
+
needsApproval?: { action: string };
|
|
624
660
|
taskId: string;
|
|
625
661
|
occurrenceId: string;
|
|
626
662
|
context: Array<{ seq: number; kind: "message" | "summary"; message: Message }>;
|
|
@@ -765,6 +801,7 @@ export type AiConversationService = {
|
|
|
765
801
|
before?: string;
|
|
766
802
|
limit?: number;
|
|
767
803
|
}): Promise<{ resources: AiConversationResourceRef[]; nextCursor?: string }>;
|
|
804
|
+
listRecentSentResourceRefs(input: { ownerUserId: string }): Promise<AiRecentSentResourceRef[]>;
|
|
768
805
|
listUserConversationResources(input: {
|
|
769
806
|
ownerUserId: string;
|
|
770
807
|
search?: string;
|
|
@@ -1007,6 +1044,8 @@ export type AiConversationService = {
|
|
|
1007
1044
|
getPendingTurnAction(input: { conversationId: string; turnId: string; callId: string }): Promise<AiPendingTurnActionRecord | null>;
|
|
1008
1045
|
findPendingTurnActionByApproval(input: { approvalId: string; ownerUserId: string }): Promise<AiPendingTurnActionRecord | null>;
|
|
1009
1046
|
listPendingActionRecords(input: { conversationId: string; turnId: string }): Promise<AiPendingTurnActionRecord[]>;
|
|
1047
|
+
/** The reviews Cloud stored with a turn's tool calls, keyed by call ID; the newest message wins. */
|
|
1048
|
+
listTurnToolReviews(input: { conversationId: string; turnId: string }): Promise<Map<string, AiStoredActionReview>>;
|
|
1010
1049
|
listResolvedPendingActions(input: { conversationId: string; turnId: string }): Promise<AiPendingTurnActionRecord[]>;
|
|
1011
1050
|
resolvePendingTurnAction(input: {
|
|
1012
1051
|
conversationId: string;
|
|
@@ -1126,7 +1165,7 @@ export type AiToolRuntime<TInput extends z.ZodType = z.ZodType, TOutput extends
|
|
|
1126
1165
|
/** Localized short status; never include payloads or secrets. */
|
|
1127
1166
|
reportProgress?: (message: string) => Promise<void>;
|
|
1128
1167
|
/** Asks like `requestApproval`, and lets the person remember the approval of `target`. */
|
|
1129
|
-
requestApprovalFor?: (message: string, target: AiApprovalTarget) => Promise<boolean>;
|
|
1168
|
+
requestApprovalFor?: (message: string, target: AiApprovalTarget, review?: CapabilityActionCallReview) => Promise<boolean>;
|
|
1130
1169
|
/** Relays a decision for an effect another platform component consumes and executes. */
|
|
1131
1170
|
requestRelayedApproval?: (message: string, options: { approvalId?: string; target?: AiApprovalTarget }) => Promise<boolean>;
|
|
1132
1171
|
/** Shows requests a website approval let through as they go out, so the chat keeps them whatever the outcome. */
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
UserProviderSchema,
|
|
15
15
|
} from "../contracts";
|
|
16
16
|
import { type AuthContext, auth, expectUserBackedActor, jsonResponse, requiresAuth, respond, v } from "../server";
|
|
17
|
+
import { noTransform } from "../server/middleware/no-transform";
|
|
17
18
|
import { accountsAppService as accountsService } from "../services";
|
|
18
19
|
|
|
19
20
|
const EntitiesListResponseSchema = z.object({
|
|
@@ -91,7 +92,7 @@ type AccountsEntitiesRouteDependencies = {
|
|
|
91
92
|
};
|
|
92
93
|
|
|
93
94
|
export const createAccountsEntitiesRoutes = (dependencies: AccountsEntitiesRouteDependencies = {}) =>
|
|
94
|
-
new Hono<AuthContext>().get(
|
|
95
|
+
new Hono<AuthContext>().use("/entities/*", noTransform()).get(
|
|
95
96
|
"/entities",
|
|
96
97
|
dependencies.authenticate ?? auth.requireRole("authenticated"),
|
|
97
98
|
dependencies.requireUser ?? auth.requireUser(),
|
|
@@ -3,6 +3,7 @@ import { z } from "zod";
|
|
|
3
3
|
import { AiProjectLastAdminError, aiProjects } from "../ai/projects";
|
|
4
4
|
import { AuthenticatedPrincipalSchema } from "../contracts/shared";
|
|
5
5
|
import { type AuthContext, auth, err, fail, ok, respond, v } from "../server";
|
|
6
|
+
import { noTransform } from "../server/middleware/no-transform";
|
|
6
7
|
|
|
7
8
|
const ListProjectsSchema = z
|
|
8
9
|
.object({
|
|
@@ -20,6 +21,7 @@ const lastAdminConflict = (c: Parameters<typeof respond>[0], error: AiProjectLas
|
|
|
20
21
|
|
|
21
22
|
export const createAdminAiProjectsRoutes = (authenticate: MiddlewareHandler<AuthContext> = auth.requireRole("admin")) =>
|
|
22
23
|
new Hono<AuthContext>()
|
|
24
|
+
.use("*", noTransform())
|
|
23
25
|
.use("*", authenticate)
|
|
24
26
|
.get("/", v("query", ListProjectsSchema), async (c) => {
|
|
25
27
|
const query = c.req.valid("query");
|
|
@@ -4,6 +4,7 @@ import { aiAppSkills } from "../ai/app-skill-store";
|
|
|
4
4
|
import { AiSkillInputError, AiSkillLastAdminError, AiSkillRevisionConflictError, aiSkills } from "../ai/skills";
|
|
5
5
|
import { AuthenticatedPrincipalSchema } from "../contracts/shared";
|
|
6
6
|
import { type AuthContext, auth, err, fail, ok, respond, v } from "../server";
|
|
7
|
+
import { noTransform } from "../server/middleware/no-transform";
|
|
7
8
|
|
|
8
9
|
const ListSkillsSchema = z
|
|
9
10
|
.object({
|
|
@@ -32,6 +33,7 @@ const installAppSkill = async (c: Parameters<typeof respond>[0], install: () =>
|
|
|
32
33
|
|
|
33
34
|
export const createAdminAiSkillsRoutes = (authenticate: MiddlewareHandler<AuthContext> = auth.requireRole("admin")) =>
|
|
34
35
|
new Hono<AuthContext>()
|
|
36
|
+
.use("*", noTransform())
|
|
35
37
|
.use("*", authenticate)
|
|
36
38
|
.get("/", v("query", ListSkillsSchema), async (c) => {
|
|
37
39
|
const query = c.req.valid("query");
|
|
@@ -3,6 +3,7 @@ import { describeRoute } from "hono-openapi";
|
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { aiUsage } from "../ai/usage";
|
|
5
5
|
import { type AuthContext, auth, err, fail, ok, respond, v } from "../server";
|
|
6
|
+
import { noTransform } from "../server/middleware/no-transform";
|
|
6
7
|
import { AiUsageQuerySchema } from "../shared/ai-usage";
|
|
7
8
|
|
|
8
9
|
export const createAdminAiUsageRoutes = (
|
|
@@ -10,6 +11,7 @@ export const createAdminAiUsageRoutes = (
|
|
|
10
11
|
service: typeof aiUsage = aiUsage,
|
|
11
12
|
) =>
|
|
12
13
|
new Hono<AuthContext>()
|
|
14
|
+
.use("*", noTransform())
|
|
13
15
|
.use("*", authenticate)
|
|
14
16
|
.get(
|
|
15
17
|
"/report",
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { Hono, type MiddlewareHandler } from "hono";
|
|
2
|
+
import { describeRoute } from "hono-openapi";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { GEOIP_MAX_DATABASE_BYTES, GEOIP_MAX_REQUEST_BYTES } from "../contracts/geoip";
|
|
5
|
+
import { type AuthContext, auth, v } from "../server";
|
|
6
|
+
import { REQUEST_IDLE_TIMEOUT_MAX_SECONDS, REQUEST_IDLE_TIMEOUT_SECONDS, setRequestTimeout } from "../server/request-timeout";
|
|
7
|
+
import { GeoIpError, geoip } from "../services/geoip";
|
|
8
|
+
|
|
9
|
+
export const createAdminGeoIpRoutes = (
|
|
10
|
+
authenticate: MiddlewareHandler<AuthContext> = auth.requireRole("admin"),
|
|
11
|
+
service: typeof geoip = geoip,
|
|
12
|
+
) =>
|
|
13
|
+
new Hono<AuthContext>()
|
|
14
|
+
.use(authenticate)
|
|
15
|
+
.onError((error, c) => {
|
|
16
|
+
if (error instanceof GeoIpError)
|
|
17
|
+
return c.json(
|
|
18
|
+
{
|
|
19
|
+
code: error.code,
|
|
20
|
+
...(error.code === "geoip_too_large" ? { maxDatabaseBytes: GEOIP_MAX_DATABASE_BYTES } : {}),
|
|
21
|
+
...(error.code === "geoip_request_too_large" ? { maxRequestBytes: GEOIP_MAX_REQUEST_BYTES } : {}),
|
|
22
|
+
},
|
|
23
|
+
error.status,
|
|
24
|
+
);
|
|
25
|
+
if (c.req.raw.signal.aborted) return new Response(null, { status: 499 });
|
|
26
|
+
throw error;
|
|
27
|
+
})
|
|
28
|
+
.get(
|
|
29
|
+
"/",
|
|
30
|
+
describeRoute({ tags: ["Sign-in locations"], summary: "Show active sign-in location databases and upload limits" }),
|
|
31
|
+
auth.requireOAuthScope("read", "admin"),
|
|
32
|
+
async (c) => c.json(await service.status(c.get("user"))),
|
|
33
|
+
)
|
|
34
|
+
.put(
|
|
35
|
+
"/",
|
|
36
|
+
describeRoute({
|
|
37
|
+
tags: ["Sign-in locations"],
|
|
38
|
+
summary: "Replace a City or ASN database with a raw or gzip MMDB upload",
|
|
39
|
+
requestBody: {
|
|
40
|
+
required: true,
|
|
41
|
+
content: {
|
|
42
|
+
"application/octet-stream": { schema: { type: "string", format: "binary" } },
|
|
43
|
+
"application/gzip": { schema: { type: "string", format: "binary" } },
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
}),
|
|
47
|
+
auth.requireOAuthScope("write", "admin"),
|
|
48
|
+
async (c) => {
|
|
49
|
+
setRequestTimeout(c, REQUEST_IDLE_TIMEOUT_MAX_SECONDS);
|
|
50
|
+
try {
|
|
51
|
+
const length = c.req.header("Content-Length");
|
|
52
|
+
return c.json(
|
|
53
|
+
await service.upload(
|
|
54
|
+
c.get("user"),
|
|
55
|
+
c.req.raw.body ??
|
|
56
|
+
new ReadableStream({
|
|
57
|
+
start(controller) {
|
|
58
|
+
controller.close();
|
|
59
|
+
},
|
|
60
|
+
}),
|
|
61
|
+
{
|
|
62
|
+
contentLength: length === undefined ? undefined : Number(length),
|
|
63
|
+
signal: c.req.raw.signal,
|
|
64
|
+
},
|
|
65
|
+
),
|
|
66
|
+
);
|
|
67
|
+
} finally {
|
|
68
|
+
setRequestTimeout(c, REQUEST_IDLE_TIMEOUT_SECONDS);
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
)
|
|
72
|
+
.delete(
|
|
73
|
+
"/:kind",
|
|
74
|
+
describeRoute({ tags: ["Sign-in locations"], summary: "Reset one sign-in database to the bundled snapshot" }),
|
|
75
|
+
auth.requireOAuthScope("write", "admin"),
|
|
76
|
+
v("param", z.object({ kind: z.enum(["city", "asn"]) })),
|
|
77
|
+
async (c) => c.json(await service.reset(c.get("user"), c.req.valid("param").kind)),
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
export default createAdminGeoIpRoutes();
|
|
@@ -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");
|
|
@@ -1,20 +1,44 @@
|
|
|
1
1
|
import { submitAiTurnAction } from "../ai/runtime";
|
|
2
2
|
import { aiConversations } from "../ai/store";
|
|
3
|
-
import { APP_ACTION_LIMITS } from "../contracts/app-approval";
|
|
3
|
+
import { APP_ACTION_LIMITS, type AppActionSummary } from "../contracts/app-approval";
|
|
4
|
+
import { APPROVALS_PAGE_SIZE } from "../contracts/approvals";
|
|
4
5
|
import type { AppDeviceActions } from "../services/app-approval";
|
|
5
|
-
import { listPending } from "../services/approvals";
|
|
6
|
-
import { appActionSummary, appActionView } from "../services/approvals/device";
|
|
6
|
+
import { listPending, loadApprovalActionPolicies, pendingApprovalCard } from "../services/approvals";
|
|
7
|
+
import { appActionOffered, appActionSummary, appActionView } from "../services/approvals/device";
|
|
8
|
+
|
|
9
|
+
/** Pages of open requests one inbox poll reads at most while it skips browser-only questions. */
|
|
10
|
+
const INBOX_PAGES = 5;
|
|
7
11
|
|
|
8
12
|
export const createDeviceActions = (appName: (appId: string, locale: string) => string): AppDeviceActions => {
|
|
9
13
|
const view: AppDeviceActions["view"] = async ({ userId, issuer, actionId }) => {
|
|
10
14
|
const { items } = await listPending({ approverUserId: userId, id: actionId, limit: 1 });
|
|
11
|
-
|
|
15
|
+
const row = items[0];
|
|
16
|
+
if (!row) return null;
|
|
17
|
+
const policy = (await loadApprovalActionPolicies([row]))(row);
|
|
18
|
+
const mapped = await appActionView(row, { issuer, appName, policy });
|
|
19
|
+
return appActionOffered(mapped, pendingApprovalCard(row, appName, policy), policy) ? mapped : null;
|
|
12
20
|
};
|
|
13
21
|
return {
|
|
14
22
|
inbox: async ({ userId, issuer }) => {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
23
|
+
// Browser-only questions are skipped, so read on past them. One poll reads at most this many pages of open
|
|
24
|
+
// requests, newest first, which keeps each 5-second poll bounded.
|
|
25
|
+
const actions: AppActionSummary[] = [];
|
|
26
|
+
let count = 0;
|
|
27
|
+
let cursor: string | undefined;
|
|
28
|
+
for (let page = 0; page < INBOX_PAGES && actions.length < APP_ACTION_LIMITS.inboxActions; page += 1) {
|
|
29
|
+
const result = await listPending({ approverUserId: userId, limit: APPROVALS_PAGE_SIZE, ...(cursor ? { cursor } : {}) });
|
|
30
|
+
count = result.count;
|
|
31
|
+
const policyOf = await loadApprovalActionPolicies(result.items);
|
|
32
|
+
for (const row of result.items) {
|
|
33
|
+
const policy = policyOf(row);
|
|
34
|
+
const mapped = await appActionView(row, { issuer, appName, policy });
|
|
35
|
+
if (appActionOffered(mapped, pendingApprovalCard(row, appName, policy), policy)) actions.push(appActionSummary(mapped));
|
|
36
|
+
if (actions.length === APP_ACTION_LIMITS.inboxActions) break;
|
|
37
|
+
}
|
|
38
|
+
if (!result.nextCursor) break;
|
|
39
|
+
cursor = result.nextCursor;
|
|
40
|
+
}
|
|
41
|
+
return { actions, moreActions: Math.max(0, count - actions.length) };
|
|
18
42
|
},
|
|
19
43
|
view,
|
|
20
44
|
decide: async ({ userId, deviceId, issuer, actionId, digest, approved }) => {
|
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,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 = (
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|