@k2b/cloud 0.44.0 → 0.46.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +9 -2
- package/src/_internal/app-queries.ts +73 -0
- package/src/_internal/capabilities.ts +137 -9
- package/src/_internal/define-app.ts +2 -0
- package/src/_internal/registry-validation.ts +17 -4
- package/src/_internal/registry.ts +32 -4
- package/src/_internal/resource-principals.ts +36 -0
- package/src/access/GroupCoverage.tsx +20 -5
- package/src/access/PermissionEditor.tsx +11 -2
- package/src/access/PrincipalAvatar.tsx +30 -21
- package/src/access/messages.ts +4 -4
- package/src/account/EntitySearch.tsx +12 -0
- package/src/ai/capabilities.ts +4 -2
- package/src/ai/chat/map-messages.ts +270 -0
- package/src/ai/chat/map-notebook.browser-harness.tsx +28 -0
- package/src/ai/chat/map-notebook.tsx +301 -0
- package/src/ai/chat/map-widget.browser-harness.tsx +71 -0
- package/src/ai/chat/map-widget.css +107 -0
- package/src/ai/chat/map-widget.tsx +1279 -0
- package/src/ai/chat/message-utils.ts +1 -0
- package/src/ai/chat/tool-names.ts +2 -0
- package/src/ai/chat/turn-layout.ts +2 -1
- package/src/ai/chat/turn-view.tsx +17 -1
- package/src/ai/default-tools.ts +3 -0
- package/src/ai/dictation-runtime.ts +6 -2
- package/src/ai/executor.ts +22 -4
- package/src/ai/index.ts +1 -0
- package/src/ai/map-contracts.ts +209 -0
- package/src/ai/map-execution.ts +14 -0
- package/src/ai/map-notebook-save.ts +276 -0
- package/src/ai/map-notebook.ts +32 -0
- package/src/ai/map-opening-hours.ts +40 -0
- package/src/ai/map-routes.ts +84 -0
- package/src/ai/map-service.ts +288 -0
- package/src/ai/map-store.ts +137 -0
- package/src/ai/map-tool.ts +73 -0
- package/src/ai/migrate.ts +3 -0
- package/src/ai/project-tool.ts +4 -4
- package/src/ai/projects-authorization.ts +53 -0
- package/src/ai/projects-routes.ts +56 -46
- package/src/ai/projects.ts +100 -79
- package/src/ai/protocol.ts +3 -1
- package/src/ai/recent-resources.ts +3 -3
- package/src/ai/routes.ts +32 -30
- package/src/ai/skill-tool.ts +4 -4
- package/src/ai/skills-routes.ts +27 -26
- package/src/ai/skills.ts +120 -81
- package/src/ai/store.ts +19 -6
- package/src/ai/tools.ts +14 -0
- package/src/ai/types.ts +3 -0
- package/src/api/accounts-entities.ts +6 -3
- package/src/api/admin-ai-skills.ts +4 -1
- package/src/api/auth.ts +9 -2
- package/src/api/capabilities.ts +90 -12
- package/src/api/mcp.ts +4 -2
- package/src/browser/notifications.ts +25 -8
- package/src/browser/session.ts +39 -0
- package/src/browser/shared-text-history-validation.ts +151 -0
- package/src/browser/shared-text-history.ts +464 -0
- package/src/browser/shared-text-offline-authorized.ts +29 -0
- package/src/browser/shared-text-offline.ts +351 -0
- package/src/browser/shared-text-provider.ts +714 -0
- package/src/browser/shared-text-table-contract.ts +150 -0
- package/src/browser/shared-text.ts +19 -0
- package/src/capabilities/discussion-providers.ts +99 -0
- package/src/capabilities/executions.ts +2 -2
- package/src/capabilities/migrate.ts +10 -1
- package/src/capabilities/resource-readers.ts +58 -0
- package/src/capabilities/server-invoke.ts +23 -13
- package/src/capabilities/server.ts +17 -6
- package/src/capabilities/testing.ts +13 -0
- package/src/contracts/capabilities.ts +137 -4
- package/src/contracts/discussions.ts +91 -0
- package/src/contracts/geo-document.ts +60 -0
- package/src/contracts/geo-snapshot.ts +86 -0
- package/src/contracts/geo.ts +14 -0
- package/src/contracts/index.ts +2 -0
- package/src/contracts/outgoing-mail.ts +1 -1
- package/src/contracts/resource-types.ts +21 -0
- package/src/contracts/shared.ts +2 -1
- package/src/geo/AddressAutocomplete.tsx +213 -0
- package/src/geo/messages.ts +29 -0
- package/src/geo/navigation.ts +25 -0
- package/src/geo/ui.ts +2 -0
- package/src/server/api/respond.ts +10 -2
- package/src/server/index.ts +1 -0
- package/src/server/services/access.ts +23 -10
- package/src/server/services/index.ts +1 -0
- package/src/services/accounts/app.ts +56 -17
- package/src/services/accounts/entities.ts +36 -16
- package/src/services/accounts/group-sql.ts +23 -0
- package/src/services/accounts/groups.ts +26 -17
- package/src/services/accounts/identities.ts +8 -1
- package/src/services/accounts/identity-reconciliation.ts +3 -1
- package/src/services/accounts/local-groups.ts +141 -27
- package/src/services/accounts/machine-group-management.ts +94 -0
- package/src/services/accounts/users.ts +38 -8
- package/src/services/geo/cache.ts +2 -1
- package/src/services/geo/geoapify.ts +3 -0
- package/src/services/identity/invocation-token.ts +110 -0
- package/src/services/mandates/index.ts +2 -2
- package/src/services/mandates/policy.ts +1 -0
- package/src/services/pdf/geo-snapshot.ts +107 -0
- package/src/services/pdf/index.ts +2 -0
- package/src/services/session/index.ts +12 -3
- package/src/shared/email-html.ts +1 -0
- package/src/shared/markdown/extensions/katex.ts +8 -1
- package/src/shared/markdown/extensions/tables.ts +14 -1
- package/src/shared/markdown/index.ts +22 -11
- package/src/shared-text/anchors.ts +29 -0
- package/src/shared-text/awareness.ts +87 -0
- package/src/shared-text/changes.ts +27 -0
- package/src/shared-text/documents.ts +428 -77
- package/src/shared-text/events.ts +12 -2
- package/src/shared-text/index.ts +16 -1
- package/src/shared-text/log.ts +24 -5
- package/src/shared-text/snapshots.ts +69 -23
- package/src/shared-text/structural-history.ts +377 -0
- package/src/shared-text/table-actions.ts +432 -0
- package/src/shared-text/types.ts +24 -5
- package/src/ssr/AdminArea.tsx +2 -2
- package/src/ssr/AdminLayout.tsx +1 -1
- package/src/ssr/AdminSidebar.tsx +1 -1
- package/src/ssr/AppLaunchpadPanel.tsx +1 -1
- package/src/ssr/CloudShell.island.tsx +19 -3
- package/src/ssr/Layout.tsx +1 -1
- package/src/ssr/MobileProfileActions.tsx +2 -2
- package/src/ssr/ProfilePreferences.tsx +2 -1
- package/src/ssr/__snapshots__/Layout.unchanged.render.test.tsx.snap +3 -3
- package/src/ssr/admin-navigation.ts +1 -11
- package/src/ssr/app-launchpad.tsx +6 -1
- package/src/ssr/platform-messages.ts +2 -0
- package/src/ssr/profile-actions.ts +3 -6
- package/src/ssr/shell-data.tsx +1 -1
- package/src/styles/global.css +2 -0
- package/src/workflows/language/expressions.ts +3 -2
- package/src/workflows/runtime/executor.ts +2 -1
package/src/ai/types.ts
CHANGED
|
@@ -313,6 +313,7 @@ export type AiStoredMessage = {
|
|
|
313
313
|
toolPresentations?: Record<string, AiToolPresentation>;
|
|
314
314
|
toolReviews?: Record<string, AiStoredActionReview>;
|
|
315
315
|
toolChecks?: Record<string, AiActionCheck>;
|
|
316
|
+
toolInterimResults?: Record<string, unknown>;
|
|
316
317
|
/** How the person decided an approval; `expired` when the turn ended while it still waited. */
|
|
317
318
|
toolOutcomes?: Record<string, "rejected" | "approved" | "expired">;
|
|
318
319
|
/** Requests a website approval let through, by tool call and then by `AiWebsiteReceipt.index`. */
|
|
@@ -1159,6 +1160,8 @@ export type AiToolRuntime<TInput extends z.ZodType = z.ZodType, TOutput extends
|
|
|
1159
1160
|
timeZone?: string;
|
|
1160
1161
|
/** Localized short status; never include payloads or secrets. */
|
|
1161
1162
|
reportProgress?: (message: string) => Promise<void>;
|
|
1163
|
+
/** A validated result snapshot while a server tool is still running. */
|
|
1164
|
+
reportResult?: (result: z.infer<TOutput>) => Promise<void>;
|
|
1162
1165
|
/** Asks like `requestApproval`, and lets the person remember the approval of `target`. */
|
|
1163
1166
|
requestApprovalFor?: (message: string, target: AiApprovalTarget, review?: CapabilityActionCallReview) => Promise<boolean>;
|
|
1164
1167
|
/** Relays a decision for an effect another platform component consumes and executes. */
|
|
@@ -36,6 +36,7 @@ const QuerySchema = z
|
|
|
36
36
|
profile: UserProfileSchema.optional(),
|
|
37
37
|
user_ids: z.string().optional(),
|
|
38
38
|
group_ids: z.string().optional(),
|
|
39
|
+
service_account_ids: z.string().optional(),
|
|
39
40
|
service_account_kinds: z.string().optional(),
|
|
40
41
|
service_account_status: ServiceAccountStatusSchema.optional(),
|
|
41
42
|
exclude_user_ids: z.string().optional(),
|
|
@@ -47,7 +48,6 @@ const QuerySchema = z
|
|
|
47
48
|
parent_group_id: z.uuid().optional(),
|
|
48
49
|
managed_by_user_id: z.uuid().optional(),
|
|
49
50
|
recursive: z.enum(["true", "false"]).optional(),
|
|
50
|
-
include_personal: z.enum(["true", "false"]).optional(),
|
|
51
51
|
})
|
|
52
52
|
.refine(
|
|
53
53
|
(value) => {
|
|
@@ -112,7 +112,7 @@ export const createAccountsEntitiesRoutes = (dependencies: AccountsEntitiesRoute
|
|
|
112
112
|
tags: ["Accounts"],
|
|
113
113
|
summary: "List visible directory entries",
|
|
114
114
|
description:
|
|
115
|
-
"List visible users and groups with SQL-backed filtering and pagination. Guest accounts see only themselves and their effective groups; relation filters require a full user account. Standalone and agent accounts with read scope see only explicitly shared directory entries and cannot query relationships. Personal Linux groups
|
|
115
|
+
"List visible users and groups with SQL-backed filtering and pagination. Guest accounts see only themselves and their effective groups; relation filters require a full user account. Standalone and agent accounts with read scope see only explicitly shared directory entries and cannot query relationships. Personal Linux groups are excluded, including exact group IDs and relation filters. Inspect the owning user's Linux identity for diagnostic attributes.",
|
|
116
116
|
...requiresAuth,
|
|
117
117
|
responses: {
|
|
118
118
|
200: jsonResponse(EntitiesListResponseSchema, "Paginated mixed entity list"),
|
|
@@ -153,6 +153,9 @@ export const createAccountsEntitiesRoutes = (dependencies: AccountsEntitiesRoute
|
|
|
153
153
|
const groupIds = parseUuidList(query.group_ids, "group_ids");
|
|
154
154
|
if (!groupIds.ok) return respond(c, fail(err.badInput(groupIds.message)));
|
|
155
155
|
|
|
156
|
+
const serviceAccountIds = parseUuidList(query.service_account_ids, "service_account_ids");
|
|
157
|
+
if (!serviceAccountIds.ok) return respond(c, fail(err.badInput(serviceAccountIds.message)));
|
|
158
|
+
|
|
156
159
|
const filters = {
|
|
157
160
|
search: query.search,
|
|
158
161
|
kinds: kinds.value,
|
|
@@ -160,6 +163,7 @@ export const createAccountsEntitiesRoutes = (dependencies: AccountsEntitiesRoute
|
|
|
160
163
|
profile: query.profile,
|
|
161
164
|
userIds: userIds.value,
|
|
162
165
|
groupIds: groupIds.value,
|
|
166
|
+
serviceAccountIds: serviceAccountIds.value,
|
|
163
167
|
serviceAccountKinds: serviceAccountKinds.value,
|
|
164
168
|
serviceAccountStatus: query.service_account_status,
|
|
165
169
|
excludeUserIds: excludeUserIds.value,
|
|
@@ -171,7 +175,6 @@ export const createAccountsEntitiesRoutes = (dependencies: AccountsEntitiesRoute
|
|
|
171
175
|
parentGroupId: query.parent_group_id,
|
|
172
176
|
managedByUserId: query.managed_by_user_id,
|
|
173
177
|
recursive: query.recursive === "true",
|
|
174
|
-
includePersonal: query.include_personal === "true",
|
|
175
178
|
};
|
|
176
179
|
if (!user) {
|
|
177
180
|
const result = await (dependencies.searchShares ?? directoryShares.search)({
|
|
@@ -94,7 +94,10 @@ export const createAdminAiSkillsRoutes = (authenticate: MiddlewareHandler<AuthCo
|
|
|
94
94
|
.get("/:skillId/projects", async (c) => {
|
|
95
95
|
const skill = await aiSkills.admin.getByShortId(c.req.param("skillId")!);
|
|
96
96
|
if (!skill) return notFound(c, "Skill");
|
|
97
|
-
const projects = await aiSkills.admin.linkedProjects(skill.id,
|
|
97
|
+
const projects = await aiSkills.admin.linkedProjects(skill.id, {
|
|
98
|
+
actor: c.get("actor"),
|
|
99
|
+
accessSubject: c.get("accessSubject"),
|
|
100
|
+
});
|
|
98
101
|
return respond(c, ok({ projects: projects.map((project) => ({ id: project.shortId, name: project.name })) }));
|
|
99
102
|
})
|
|
100
103
|
.get("/:skillId/access", async (c) => {
|
package/src/api/auth.ts
CHANGED
|
@@ -162,13 +162,20 @@ export const createAuthRoutes = (notificationSender: AuthNotificationSender) =>
|
|
|
162
162
|
tags: ["Auth"],
|
|
163
163
|
summary: "Logout",
|
|
164
164
|
description:
|
|
165
|
-
"Idempotent: clears the session cookie and revokes the current credential if present. No authentication required
|
|
165
|
+
"Idempotent: clears the session cookie and revokes the current credential if present. No authentication required. An optional X-Cloud-Logout-User header binds revocation to that user UUID (or none) without writing the shared browser cookie; a changed account returns 409 without revocation.",
|
|
166
|
+
parameters: [{ in: "header", name: "X-Cloud-Logout-User", required: false, schema: { type: "string" } }],
|
|
166
167
|
responses: {
|
|
167
168
|
200: jsonResponse(MessageResponseSchema, "Session invalidated"),
|
|
169
|
+
400: jsonResponse(ErrorResponseSchema, "Invalid expected account"),
|
|
170
|
+
409: jsonResponse(ErrorResponseSchema, "Browser account changed"),
|
|
168
171
|
},
|
|
169
172
|
}),
|
|
170
173
|
async (c) => {
|
|
171
|
-
|
|
174
|
+
const expected = c.req.header("x-cloud-logout-user");
|
|
175
|
+
if (expected !== undefined && expected !== "none" && !z.string().uuid().safeParse(expected).success)
|
|
176
|
+
return c.json({ message: "Invalid expected account" }, 400);
|
|
177
|
+
if (!(await auth.session.delete(c, expected === "none" ? null : expected)))
|
|
178
|
+
return c.json({ message: "Browser account changed; reload before signing out" }, 409);
|
|
172
179
|
log.info("Logout");
|
|
173
180
|
return c.json({ message: "Logged out" });
|
|
174
181
|
},
|
package/src/api/capabilities.ts
CHANGED
|
@@ -3,14 +3,14 @@ import { describeRoute } from "hono-openapi";
|
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { type CatalogSubscription, projectAppEventCatalog, readCatalogSubscriptions } from "../_internal/app-event-catalog";
|
|
5
5
|
import { readBoundedJson } from "../_internal/bounded-json";
|
|
6
|
-
import { resolveCapabilityManifestPresentation } from "../_internal/capabilities";
|
|
6
|
+
import { isPersonVisibleCapabilityQuery, resolveCapabilityManifestPresentation } from "../_internal/capabilities";
|
|
7
7
|
import {
|
|
8
8
|
capabilityActionReceiptIssues,
|
|
9
9
|
capabilityActionReviewIssues,
|
|
10
10
|
capabilityQueryChecksActionOutcome,
|
|
11
11
|
} from "../_internal/capability-reviews";
|
|
12
12
|
import { capabilityTimeoutMs, createCapabilityDeadline, withCapabilityRequestTimeout } from "../_internal/capability-timeout";
|
|
13
|
-
import { getCapability, listApps } from "../_internal/registry";
|
|
13
|
+
import { getApp, getCapability, listApps } from "../_internal/registry";
|
|
14
14
|
import { resolveCommandLink } from "../capabilities/command-link";
|
|
15
15
|
import {
|
|
16
16
|
type CapabilityClaimScope,
|
|
@@ -59,12 +59,13 @@ import { AppAccessDeniedError, appAccessDeniedBody } from "../server/middleware/
|
|
|
59
59
|
import { oauthScopeByOperation } from "../server/middleware/auth";
|
|
60
60
|
import { noTransform } from "../server/middleware/no-transform";
|
|
61
61
|
import { hasOAuthScope } from "../server/oauth-scope";
|
|
62
|
+
import { ratelimit } from "../server/ratelimit";
|
|
62
63
|
import { appAccessAuthority, canUseApp } from "../services/app-access";
|
|
63
64
|
import { discoverableApps } from "../services/app-discovery";
|
|
64
65
|
import { authorityFromParentInvocation, invocationAuthorityFromRequest } from "../services/identity/invocation-authority";
|
|
65
66
|
import { capabilityInvocationOperation } from "../services/identity/invocation-operations";
|
|
66
67
|
import type { CloudInvocationClaims, InvocationAuthority } from "../services/identity/invocation-token";
|
|
67
|
-
import { normalizeInvocationRequestId } from "../services/identity/invocation-token";
|
|
68
|
+
import { normalizeInvocationRequestId, signAppQueryInvocation } from "../services/identity/invocation-token";
|
|
68
69
|
import { issueInvocationToken as signInvocationToken } from "../services/identity/issue-invocation";
|
|
69
70
|
import { withActiveIdentitySigner } from "../services/identity/key-ring";
|
|
70
71
|
import {
|
|
@@ -73,6 +74,7 @@ import {
|
|
|
73
74
|
nestedInvocationLeases,
|
|
74
75
|
nestedSourceDeadline,
|
|
75
76
|
} from "../services/identity/nested-invocation";
|
|
77
|
+
import type { AuthenticatedWorkload } from "../services/identity/workload-auth";
|
|
76
78
|
import { logger } from "../services/logging";
|
|
77
79
|
import { withMandateIssueAuthority } from "../services/mandates";
|
|
78
80
|
import * as settings from "../services/settings";
|
|
@@ -80,6 +82,9 @@ import { resolveAppPresentation } from "../shared/app-presentation";
|
|
|
80
82
|
import { capabilityMessages } from "../shared/capability-messages";
|
|
81
83
|
import { dispatchCapabilityStream, sealCapabilityStream } from "./capability-streams";
|
|
82
84
|
|
|
85
|
+
// Approved reader-check budget, shared across targets for each calling application.
|
|
86
|
+
export const APP_QUERY_REQUESTS_PER_MINUTE = 1_501;
|
|
87
|
+
const appQueryLimit = ratelimit({ id: "app-queries", limit: APP_QUERY_REQUESTS_PER_MINUTE, windowSecs: 60 });
|
|
83
88
|
const log = logger("capabilities");
|
|
84
89
|
const DEFAULT_CATALOG_LIMIT = 10;
|
|
85
90
|
const MAX_CATALOG_LIMIT = 25;
|
|
@@ -114,6 +119,7 @@ const CapabilityCatalogQuerySchema = z
|
|
|
114
119
|
export type CapabilityRouteDependencies = {
|
|
115
120
|
listApps?: () => Promise<AppRegistryEntry[]>;
|
|
116
121
|
eventSubscriptions?: () => Promise<CatalogSubscription[]>;
|
|
122
|
+
getApp?: (appId: string) => Promise<AppRegistryEntry | null>;
|
|
117
123
|
getCapability?: (appId: string) => Promise<CapabilityRegistryEntry | null>;
|
|
118
124
|
fetch?: (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
|
|
119
125
|
authenticate?: MiddlewareHandler<AuthContext>;
|
|
@@ -122,12 +128,15 @@ export type CapabilityRouteDependencies = {
|
|
|
122
128
|
withMandateIssueAuthority?: typeof withMandateIssueAuthority;
|
|
123
129
|
queryTimeoutMs?: number;
|
|
124
130
|
actionTimeoutMs?: number;
|
|
131
|
+
checkAppQueryRate?: (appId: string) => Promise<{ limited: boolean }>;
|
|
125
132
|
reviewTimeoutMs?: number;
|
|
126
133
|
appUrl?: () => Promise<string>;
|
|
127
134
|
};
|
|
128
135
|
|
|
129
136
|
export type CapabilityDispatchDependencies = Pick<
|
|
130
137
|
CapabilityRouteDependencies,
|
|
138
|
+
| "checkAppQueryRate"
|
|
139
|
+
| "getApp"
|
|
131
140
|
| "getCapability"
|
|
132
141
|
| "fetch"
|
|
133
142
|
| "queryTimeoutMs"
|
|
@@ -223,7 +232,7 @@ export const loadCapabilityCatalogPage = async (
|
|
|
223
232
|
appName: presentedEntry.name,
|
|
224
233
|
appIcon: capability.appIcon,
|
|
225
234
|
appDescription: presentedEntry.description,
|
|
226
|
-
manifest,
|
|
235
|
+
manifest: { ...manifest, queries: manifest.queries.filter(isPersonVisibleCapabilityQuery) },
|
|
227
236
|
...(manifest.events ? { events: projectAppEventCatalog(manifest, manifest.events.length ? await loadSubscribers() : []) } : {}),
|
|
228
237
|
};
|
|
229
238
|
const projectedBytes = new TextEncoder().encode(JSON.stringify(projected)).byteLength;
|
|
@@ -276,6 +285,8 @@ export type CapabilityDispatchParams = {
|
|
|
276
285
|
assistantTurn?: { conversationId: string; turnId: string };
|
|
277
286
|
/** Core-only absolute child deadline; cannot be extended by the application. */
|
|
278
287
|
deadline?: number;
|
|
288
|
+
/** Core-only authenticated workload. Never populated from request JSON. */
|
|
289
|
+
workload?: AuthenticatedWorkload;
|
|
279
290
|
mandate?: MandatedCapabilityAuthority;
|
|
280
291
|
callingAppId?: string;
|
|
281
292
|
/** Internal observation only: called immediately before attempting the app request. */
|
|
@@ -342,6 +353,43 @@ const runCapabilityDispatch = async (
|
|
|
342
353
|
);
|
|
343
354
|
return error;
|
|
344
355
|
}
|
|
356
|
+
if ("appOnly" in operation && operation.appOnly !== undefined && !params.workload)
|
|
357
|
+
return errorResponse("FORBIDDEN", "App authority required for this query", 403);
|
|
358
|
+
if (params.workload) {
|
|
359
|
+
if (
|
|
360
|
+
params.authority ||
|
|
361
|
+
params.mandate ||
|
|
362
|
+
params.parentInvocation ||
|
|
363
|
+
params.review ||
|
|
364
|
+
params.kind !== "queries" ||
|
|
365
|
+
!("appOnly" in operation) ||
|
|
366
|
+
operation.appOnly !== true ||
|
|
367
|
+
operation.stream
|
|
368
|
+
)
|
|
369
|
+
return errorResponse("FORBIDDEN", "App caller is not allowed for this query", 403);
|
|
370
|
+
try {
|
|
371
|
+
const caller = await deadline.wait(() => (params.dependencies?.getApp ?? getApp)(params.workload!.appId));
|
|
372
|
+
if (!caller) return errorResponse("APP_UNAVAILABLE", "Calling app registry entry unavailable", 503);
|
|
373
|
+
if (!caller.platformPermissions?.includes("resources:readers"))
|
|
374
|
+
return errorResponse("APP_QUERY_NOT_ADMITTED", "App caller does not declare resources:readers", 403);
|
|
375
|
+
} catch {
|
|
376
|
+
deadline.check();
|
|
377
|
+
return errorResponse("APP_UNAVAILABLE", "App registry unavailable", 503);
|
|
378
|
+
}
|
|
379
|
+
try {
|
|
380
|
+
if (
|
|
381
|
+
(
|
|
382
|
+
await deadline.wait(() =>
|
|
383
|
+
(params.dependencies?.checkAppQueryRate ?? ((appId) => appQueryLimit.check(appId)))(params.workload!.appId),
|
|
384
|
+
)
|
|
385
|
+
).limited
|
|
386
|
+
)
|
|
387
|
+
return errorResponse("RATE_LIMITED", "App query rate limit exceeded; retry after one minute", 429);
|
|
388
|
+
} catch {
|
|
389
|
+
deadline.check();
|
|
390
|
+
return errorResponse("APP_UNAVAILABLE", "App query rate limiter unavailable", 503);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
345
393
|
if (params.review && (params.kind !== "actions" || !("review" in operation) || operation.review !== true)) {
|
|
346
394
|
const error = errorResponse(
|
|
347
395
|
"CAPABILITY_NOT_FOUND",
|
|
@@ -488,6 +536,33 @@ const runCapabilityDispatch = async (
|
|
|
488
536
|
});
|
|
489
537
|
})()
|
|
490
538
|
: await (async () => {
|
|
539
|
+
if (params.workload) {
|
|
540
|
+
const workload = params.workload;
|
|
541
|
+
return deadline.wait(() =>
|
|
542
|
+
(params.dependencies?.withActiveSigner ?? withActiveIdentitySigner)(
|
|
543
|
+
"invocation",
|
|
544
|
+
async (signer) => {
|
|
545
|
+
deadline.check();
|
|
546
|
+
const signed = await signAppQueryInvocation({
|
|
547
|
+
workload,
|
|
548
|
+
targetAppId: params.appId,
|
|
549
|
+
capabilityId: params.capabilityId,
|
|
550
|
+
schemaHash: operation.schemaHash,
|
|
551
|
+
input: input.data,
|
|
552
|
+
requestId,
|
|
553
|
+
signer,
|
|
554
|
+
});
|
|
555
|
+
observed.invocationId = signed.claims.jti;
|
|
556
|
+
return new Headers({
|
|
557
|
+
"content-type": "application/json",
|
|
558
|
+
accept: "application/json",
|
|
559
|
+
authorization: `Bearer ${signed.token}`,
|
|
560
|
+
});
|
|
561
|
+
},
|
|
562
|
+
{ signal, timeoutMs: deadline.remainingMs() },
|
|
563
|
+
),
|
|
564
|
+
);
|
|
565
|
+
}
|
|
491
566
|
if (!params.authority) throw new Error("Resolved request authority is required for Cloud invocation issuance");
|
|
492
567
|
const authority = params.authority;
|
|
493
568
|
const signed = await deadline.wait(() =>
|
|
@@ -665,12 +740,15 @@ const runCapabilityDispatch = async (
|
|
|
665
740
|
try {
|
|
666
741
|
params.onUpstreamAttempt?.();
|
|
667
742
|
const fetchResponse = () =>
|
|
668
|
-
fetchUpstream(
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
743
|
+
fetchUpstream(
|
|
744
|
+
`${entry.endpoint}/${params.workload ? "app-queries" : params.kind}/${encodeURIComponent(params.capabilityId)}${params.review ? "/review" : ""}`,
|
|
745
|
+
{
|
|
746
|
+
method: "POST",
|
|
747
|
+
headers,
|
|
748
|
+
body: requestBody,
|
|
749
|
+
signal,
|
|
750
|
+
},
|
|
751
|
+
);
|
|
674
752
|
// A read may be released at its deadline; Action effects retain their existing uncertainty handling.
|
|
675
753
|
response = params.kind === "queries" || params.review ? await deadline.wait(fetchResponse) : await fetchResponse();
|
|
676
754
|
} catch (error) {
|
|
@@ -895,12 +973,12 @@ export const dispatchCapability = async (params: CapabilityDispatchParams): Prom
|
|
|
895
973
|
origin: params.origin,
|
|
896
974
|
invocationId: observed.invocationId,
|
|
897
975
|
parentInvocationId: params.parentInvocation?.jti,
|
|
898
|
-
viaAppId: params.parentInvocation ? params.callingAppId : undefined,
|
|
976
|
+
viaAppId: params.workload?.appId ?? (params.parentInvocation ? params.callingAppId : undefined),
|
|
899
977
|
appId: params.appId,
|
|
900
978
|
capability: `${params.appId}.${params.capabilityId}`,
|
|
901
979
|
kind: params.kind === "queries" ? "query" : "action",
|
|
902
980
|
destructive: observed.destructive,
|
|
903
|
-
actorKind: principal?.type ?? null,
|
|
981
|
+
actorKind: params.workload ? "app" : (principal?.type ?? null),
|
|
904
982
|
actorId: principal?.id ?? null,
|
|
905
983
|
userId: actor?.kind === "user" ? actor.user.id : (actor?.delegatedUser?.id ?? (principal?.type === "user" ? principal.id : null)),
|
|
906
984
|
accessSubject: subject && subject.id !== principal?.id ? subject : null,
|
package/src/api/mcp.ts
CHANGED
|
@@ -16,7 +16,7 @@ import { Hono, type MiddlewareHandler } from "hono";
|
|
|
16
16
|
import { z } from "zod";
|
|
17
17
|
import { type CatalogSubscription, projectAppEventCatalog, readEventCatalogSubscriptions } from "../_internal/app-event-catalog";
|
|
18
18
|
import { readBoundedJson } from "../_internal/bounded-json";
|
|
19
|
-
import { resolveCapabilityManifestPresentation } from "../_internal/capabilities";
|
|
19
|
+
import { isPersonVisibleCapabilityQuery, resolveCapabilityManifestPresentation } from "../_internal/capabilities";
|
|
20
20
|
import { capabilitySentenceText } from "../_internal/capability-sentences";
|
|
21
21
|
import { withCapabilityRequestTimeout } from "../_internal/capability-timeout";
|
|
22
22
|
import {
|
|
@@ -311,7 +311,9 @@ const orderedCapabilityEntries = function* (app: CapabilityRegistryEntry): Gener
|
|
|
311
311
|
for (const operation of [...app.manifest.actions].sort((left, right) => left.localId.localeCompare(right.localId))) {
|
|
312
312
|
yield { app, kind: "actions", operation };
|
|
313
313
|
}
|
|
314
|
-
for (const operation of
|
|
314
|
+
for (const operation of app.manifest.queries
|
|
315
|
+
.filter(isPersonVisibleCapabilityQuery)
|
|
316
|
+
.sort((left, right) => left.localId.localeCompare(right.localId))) {
|
|
315
317
|
yield { app, kind: "queries", operation };
|
|
316
318
|
}
|
|
317
319
|
};
|
|
@@ -13,6 +13,8 @@ export type BrowserNotificationState = {
|
|
|
13
13
|
permission: NotificationPermission;
|
|
14
14
|
enabled: boolean;
|
|
15
15
|
reason?: string;
|
|
16
|
+
/** On iPhone and iPad outside the Home Screen app: notifications start working once Cloud is added to the Home Screen. */
|
|
17
|
+
homeScreenRequired?: true;
|
|
16
18
|
};
|
|
17
19
|
|
|
18
20
|
const isIos = (): boolean =>
|
|
@@ -21,7 +23,15 @@ const isIos = (): boolean =>
|
|
|
21
23
|
const isStandalone = (): boolean =>
|
|
22
24
|
window.matchMedia("(display-mode: standalone)").matches || (navigator as Navigator & { standalone?: boolean }).standalone === true;
|
|
23
25
|
|
|
24
|
-
const support = (): { supported: boolean; reason?: string } => {
|
|
26
|
+
const support = (): { supported: boolean; reason?: string; homeScreenRequired?: true } => {
|
|
27
|
+
// Safari on iPhone and iPad offers push only to the Home Screen app, so it lacks the APIs checked below.
|
|
28
|
+
if (typeof window !== "undefined" && isIos() && !isStandalone()) {
|
|
29
|
+
return {
|
|
30
|
+
supported: false,
|
|
31
|
+
reason: "On iPhone and iPad, add Cloud to your Home Screen before enabling browser notifications.",
|
|
32
|
+
homeScreenRequired: true,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
25
35
|
if (
|
|
26
36
|
typeof window === "undefined" ||
|
|
27
37
|
!window.isSecureContext ||
|
|
@@ -31,12 +41,6 @@ const support = (): { supported: boolean; reason?: string } => {
|
|
|
31
41
|
) {
|
|
32
42
|
return { supported: false, reason: "Browser notifications are not supported in this browser." };
|
|
33
43
|
}
|
|
34
|
-
if (isIos() && !isStandalone()) {
|
|
35
|
-
return {
|
|
36
|
-
supported: false,
|
|
37
|
-
reason: "On iPhone and iPad, add Cloud to your Home Screen before enabling browser notifications.",
|
|
38
|
-
};
|
|
39
|
-
}
|
|
40
44
|
return { supported: true };
|
|
41
45
|
};
|
|
42
46
|
|
|
@@ -101,7 +105,13 @@ const pushSubscriptionError = (cause: unknown): Error => {
|
|
|
101
105
|
const currentState = async (registration?: ServiceWorkerRegistration): Promise<BrowserNotificationState> => {
|
|
102
106
|
const capability = support();
|
|
103
107
|
if (!capability.supported) {
|
|
104
|
-
return {
|
|
108
|
+
return {
|
|
109
|
+
supported: false,
|
|
110
|
+
permission: "denied",
|
|
111
|
+
enabled: false,
|
|
112
|
+
reason: capability.reason,
|
|
113
|
+
...(capability.homeScreenRequired ? { homeScreenRequired: true } : {}),
|
|
114
|
+
};
|
|
105
115
|
}
|
|
106
116
|
return withNotificationTimeout(
|
|
107
117
|
(async () => {
|
|
@@ -131,6 +141,13 @@ export const browserNotificationClient = {
|
|
|
131
141
|
return (await response.json()).state;
|
|
132
142
|
},
|
|
133
143
|
|
|
144
|
+
/** End do not disturb now, for every app. Quiet hours keep their schedule. Returns the quiet state afterwards. */
|
|
145
|
+
endDoNotDisturb: async (): Promise<NotificationQuietState> => {
|
|
146
|
+
const response = await apiClient.me.notifications.quiet.$patch({ json: { doNotDisturbUntil: null } });
|
|
147
|
+
if (!response.ok) throw await responseError(response, "Failed to end do not disturb.");
|
|
148
|
+
return (await response.json()).state;
|
|
149
|
+
},
|
|
150
|
+
|
|
134
151
|
/** Close this application's group on this device without opting into notifications. */
|
|
135
152
|
closeGroup: async (appId: string, group: string): Promise<void> => {
|
|
136
153
|
const tag = notificationGroupTag(appId, group);
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { apiClient } from "../clients/core";
|
|
2
|
+
|
|
3
|
+
/** Fence and remove application-owned entries in the authenticated user's browser namespace. */
|
|
4
|
+
const clearUserStorage = (userId: string): void => {
|
|
5
|
+
const prefix = `cloud:user:${userId}:`;
|
|
6
|
+
localStorage.setItem(`cloud:user-epoch:${userId}`, crypto.randomUUID());
|
|
7
|
+
if (typeof window !== "undefined") window.dispatchEvent(new CustomEvent("cloud:user-storage-invalidated", { detail: { userId } }));
|
|
8
|
+
const keys = Array.from({ length: localStorage.length }, (_, index) => localStorage.key(index));
|
|
9
|
+
for (const key of keys) if (key?.startsWith(prefix)) localStorage.removeItem(key);
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
/** Revoke this browser's credential even if local storage cleanup fails. The caller keeps a visible retry until both succeed. */
|
|
13
|
+
export const signOutBrowserSession = async (userId: string | undefined, errorMessage: string): Promise<void> => {
|
|
14
|
+
await import("./notifications").then(({ browserNotificationClient }) => browserNotificationClient.disable()).catch(() => undefined);
|
|
15
|
+
let cleanupFailed = false;
|
|
16
|
+
try {
|
|
17
|
+
if (userId) {
|
|
18
|
+
clearUserStorage(userId);
|
|
19
|
+
const { logoutSharedTextOffline } = await import("./shared-text-offline");
|
|
20
|
+
await logoutSharedTextOffline(userId);
|
|
21
|
+
}
|
|
22
|
+
} catch {
|
|
23
|
+
cleanupFailed = true;
|
|
24
|
+
}
|
|
25
|
+
const response = await apiClient.auth.logout.$post({}, { headers: { "x-cloud-logout-user": userId ?? "none" } });
|
|
26
|
+
// Close the interval between the first local fence and credential revocation:
|
|
27
|
+
// a tab that authenticated during it must not retain or recreate device data.
|
|
28
|
+
if (response.ok && userId) {
|
|
29
|
+
try {
|
|
30
|
+
clearUserStorage(userId);
|
|
31
|
+
const { logoutSharedTextOffline } = await import("./shared-text-offline");
|
|
32
|
+
await logoutSharedTextOffline(userId);
|
|
33
|
+
cleanupFailed = false;
|
|
34
|
+
} catch {
|
|
35
|
+
cleanupFailed = true;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
if (!response.ok || cleanupFailed) throw new Error(errorMessage);
|
|
39
|
+
};
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import * as T from "@k2b/ui/text-editor/model";
|
|
2
|
+
import * as Y from "yjs";
|
|
3
|
+
|
|
4
|
+
type NativeFrame = Y.UndoManager["undoStack"][number];
|
|
5
|
+
type Span = { from: number; to: number };
|
|
6
|
+
type Piece = Span & { original: number | null };
|
|
7
|
+
|
|
8
|
+
export class SharedTextNativeHistoryError extends Error {
|
|
9
|
+
readonly code = "shared_text_history_target";
|
|
10
|
+
constructor(readonly reason: "dependency" | "unavailable") {
|
|
11
|
+
super("The input history target is unavailable");
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const intersect = (set: NativeFrame["insertions"], item: Y.Item): Span[] =>
|
|
16
|
+
(set.clients.get(item.id.client) ?? []).flatMap((run) => {
|
|
17
|
+
const from = Math.max(run.clock, item.id.clock) - item.id.clock;
|
|
18
|
+
const to = Math.min(run.clock + run.len, item.id.clock + item.length) - item.id.clock;
|
|
19
|
+
return from < to ? [{ from, to }] : [];
|
|
20
|
+
});
|
|
21
|
+
const subtract = (wanted: Span[], excluded: Span[]): Span[] =>
|
|
22
|
+
wanted.flatMap((span) => {
|
|
23
|
+
const result: Span[] = [];
|
|
24
|
+
let from = span.from;
|
|
25
|
+
for (const cut of excluded) {
|
|
26
|
+
if (cut.to <= from || cut.from >= span.to) continue;
|
|
27
|
+
if (cut.from > from) result.push({ from, to: cut.from });
|
|
28
|
+
from = Math.max(from, cut.to);
|
|
29
|
+
}
|
|
30
|
+
if (from < span.to) result.push({ from, to: span.to });
|
|
31
|
+
return result;
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
type Region = Span & { key: string | undefined };
|
|
35
|
+
const regions = (source: string, original: (position: number) => number | null): Region[] =>
|
|
36
|
+
T.findTables(source).flatMap((table) =>
|
|
37
|
+
table.lines.flatMap((line, row) =>
|
|
38
|
+
line.cells.map((_cell, column) => {
|
|
39
|
+
const left = line.leadingPipe ? line.pipes[column]! + 1 : column === 0 ? line.from : line.pipes[column - 1]! + 1;
|
|
40
|
+
const right = (line.leadingPipe ? line.pipes[column + 1] : line.pipes[column]) ?? line.to;
|
|
41
|
+
const separator = table.lines[1]!.cells[column];
|
|
42
|
+
const hyphen = separator ? original(separator.from + separator.text.indexOf("-")) : null;
|
|
43
|
+
const header = original(table.lines[0]!.to);
|
|
44
|
+
const rowIdentity = row < 2 ? row : original(line.from - 1);
|
|
45
|
+
// Extra physical cells have no separator hyphen. Their surviving left
|
|
46
|
+
// delimiter distinguishes them even when columns are inserted/removed.
|
|
47
|
+
// An unknown identity is never evidence that two targets are the same.
|
|
48
|
+
const columnIdentity = separator ? hyphen : original(left - 1);
|
|
49
|
+
return {
|
|
50
|
+
from: left,
|
|
51
|
+
to: right,
|
|
52
|
+
key:
|
|
53
|
+
header === null || rowIdentity === null || columnIdentity === null
|
|
54
|
+
? undefined
|
|
55
|
+
: `${header}:${rowIdentity}:${separator ? "column" : "extra"}:${columnIdentity}`,
|
|
56
|
+
};
|
|
57
|
+
}),
|
|
58
|
+
),
|
|
59
|
+
);
|
|
60
|
+
const locate = (regions: Region[], position: number, end: number) => {
|
|
61
|
+
let low = 0,
|
|
62
|
+
high = regions.length;
|
|
63
|
+
while (low < high) {
|
|
64
|
+
const middle = (low + high) >>> 1;
|
|
65
|
+
if (regions[middle]!.to <= position) low = middle + 1;
|
|
66
|
+
else high = middle;
|
|
67
|
+
}
|
|
68
|
+
const region = regions[low];
|
|
69
|
+
return region && region.from <= position ? { key: region.key, to: region.to } : { key: null, to: region?.from ?? end };
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
/** Project root plain-text native effects without a second manager or live mutation.
|
|
73
|
+
* Identity sets and linked Item/content fields are exported Yjs contracts. Native
|
|
74
|
+
* restoration occupies the original deleted item's gap; redone items already
|
|
75
|
+
* have their effect and are not copied again. Unknown content refuses safely.
|
|
76
|
+
*/
|
|
77
|
+
export const validateNativeHistory = (
|
|
78
|
+
text: Y.Text,
|
|
79
|
+
frame: NativeFrame,
|
|
80
|
+
liveInsertions: NativeFrame["insertions"],
|
|
81
|
+
baseline: Uint8Array | null,
|
|
82
|
+
) => {
|
|
83
|
+
const known = baseline ? Y.decodeStateVector(baseline) : new Map<number, number>();
|
|
84
|
+
const chunks: string[] = [];
|
|
85
|
+
const pieces: Piece[] = [];
|
|
86
|
+
const protectedSpans: { before: number; after: number; length: number }[] = [];
|
|
87
|
+
let before = 0,
|
|
88
|
+
after = 0;
|
|
89
|
+
const append = (value: string, original: number | null) => {
|
|
90
|
+
if (!value.length) return;
|
|
91
|
+
chunks.push(value);
|
|
92
|
+
pieces.push({ from: after, to: after + value.length, original });
|
|
93
|
+
after += value.length;
|
|
94
|
+
};
|
|
95
|
+
for (const item of Y.getTypeChildren(text)) {
|
|
96
|
+
// Adopted frames lack correspondence captured by this controller. Native
|
|
97
|
+
// Undo follows pre-existing redone chains for insertions; projecting only
|
|
98
|
+
// the original IDs would omit their live effects. Leave such frames intact.
|
|
99
|
+
if (baseline === null && item.redone !== null && intersect(frame.insertions, item).length)
|
|
100
|
+
throw new SharedTextNativeHistoryError("unavailable");
|
|
101
|
+
if (item.deleted) {
|
|
102
|
+
const restore = subtract(intersect(frame.deletions, item), intersect(frame.insertions, item));
|
|
103
|
+
if (!restore.length || item.redone !== null) continue;
|
|
104
|
+
const content = item.content;
|
|
105
|
+
if (!(content instanceof Y.ContentString)) throw new SharedTextNativeHistoryError("unavailable");
|
|
106
|
+
for (const span of restore) append(content.str.slice(span.from, span.to), null);
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
if (!item.countable) continue;
|
|
110
|
+
const content = item.content;
|
|
111
|
+
if (!(content instanceof Y.ContentString)) throw new SharedTextNativeHistoryError("unavailable");
|
|
112
|
+
const keep = subtract([{ from: 0, to: item.length }], intersect(liveInsertions, item));
|
|
113
|
+
for (const span of keep) {
|
|
114
|
+
const newer = Math.max(span.from, (known.get(item.id.client) ?? 0) - item.id.clock);
|
|
115
|
+
if (newer < span.to) protectedSpans.push({ before: before + newer, after: after + newer - span.from, length: span.to - newer });
|
|
116
|
+
append(content.str.slice(span.from, span.to), before + span.from);
|
|
117
|
+
}
|
|
118
|
+
before += item.length;
|
|
119
|
+
}
|
|
120
|
+
const source = text.toString();
|
|
121
|
+
const candidate = chunks.join("");
|
|
122
|
+
if (before !== source.length) throw new SharedTextNativeHistoryError("unavailable");
|
|
123
|
+
const original = (position: number) => {
|
|
124
|
+
let low = 0,
|
|
125
|
+
high = pieces.length;
|
|
126
|
+
while (low < high) {
|
|
127
|
+
const middle = (low + high) >>> 1;
|
|
128
|
+
if (pieces[middle]!.to <= position) low = middle + 1;
|
|
129
|
+
else high = middle;
|
|
130
|
+
}
|
|
131
|
+
const piece = pieces[low];
|
|
132
|
+
return piece?.original != null ? piece.original + position - piece.from : null;
|
|
133
|
+
};
|
|
134
|
+
const beforeRegions = regions(source, (position) => position);
|
|
135
|
+
const afterRegions = regions(candidate, original);
|
|
136
|
+
for (const span of protectedSpans) {
|
|
137
|
+
let offset = 0;
|
|
138
|
+
while (offset < span.length) {
|
|
139
|
+
const oldAt = span.before + offset,
|
|
140
|
+
newAt = span.after + offset;
|
|
141
|
+
const oldTarget = locate(beforeRegions, oldAt, source.length);
|
|
142
|
+
const newTarget = locate(afterRegions, newAt, candidate.length);
|
|
143
|
+
if (oldTarget.key === undefined || newTarget.key === undefined || oldTarget.key !== newTarget.key)
|
|
144
|
+
throw new SharedTextNativeHistoryError("dependency");
|
|
145
|
+
const length = Math.min(span.length - offset, oldTarget.to - oldAt, newTarget.to - newAt);
|
|
146
|
+
if (length <= 0) throw new SharedTextNativeHistoryError("unavailable");
|
|
147
|
+
offset += length;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return candidate;
|
|
151
|
+
};
|