@k2b/cloud 0.40.0 → 0.42.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +9 -5
- package/src/_internal/bounded-json.ts +14 -2
- package/src/_internal/capabilities.ts +87 -8
- package/src/_internal/capability-reviews.ts +6 -0
- package/src/_internal/capability-sentences.ts +2 -1
- package/src/_internal/capability-timeout.ts +71 -0
- package/src/_internal/define-app.ts +158 -92
- package/src/_internal/heartbeat.ts +203 -14
- package/src/_internal/http-drain.ts +56 -0
- package/src/_internal/registry-validation.ts +3 -0
- package/src/_internal/registry.ts +28 -1
- package/src/_internal/runtime-context.ts +1 -0
- package/src/_internal/setup-lock.ts +103 -0
- package/src/_internal/status-preserving-ssr.ts +2 -0
- package/src/access/PermissionEditor.tsx +16 -17
- package/src/access/PrincipalPicker.tsx +45 -10
- package/src/access/messages.ts +8 -0
- package/src/access/principal-label.ts +18 -0
- package/src/access/ui.ts +1 -0
- package/src/account/EntitySearch.tsx +5 -4
- package/src/ai/action-review.ts +2 -1
- package/src/ai/app-skill-store.ts +1 -1
- package/src/ai/capabilities.ts +21 -3
- package/src/ai/capability-execution.ts +14 -12
- package/src/ai/chat/action-card.ts +76 -4
- package/src/ai/chat/action-messages.ts +14 -0
- package/src/ai/chat/action-question.browser-harness.tsx +12 -0
- package/src/ai/chat/action-question.tsx +214 -23
- package/src/ai/chat/action-text.ts +288 -0
- package/src/ai/chat/tool-action-words.ts +31 -0
- package/src/ai/chat/tool-action.tsx +1 -26
- package/src/ai/chat-tasks.ts +2 -1
- package/src/ai/client/controller.ts +150 -32
- package/src/ai/client/transport.ts +14 -2
- package/src/ai/code-capability-routes.ts +27 -23
- package/src/ai/code-runtime-tools.ts +14 -16
- package/src/ai/code-source-tools.ts +16 -18
- package/src/ai/executor.ts +64 -21
- package/src/ai/fetch-file-tool.ts +67 -76
- package/src/ai/firecrawl-tools.ts +15 -2
- package/src/ai/http.ts +1 -0
- package/src/ai/index.ts +1 -0
- package/src/ai/migrate.ts +28 -0
- package/src/ai/open-questions.ts +168 -0
- package/src/ai/projects.ts +1 -2
- package/src/ai/protocol.ts +2 -2
- package/src/ai/question-text.ts +8 -0
- package/src/ai/recent-resources.ts +12 -5
- package/src/ai/review-content.ts +77 -0
- package/src/ai/routes.ts +119 -35
- package/src/ai/runtime.ts +1 -0
- package/src/ai/skill-tool.ts +17 -6
- package/src/ai/skills-routes.ts +72 -27
- package/src/ai/skills.ts +139 -60
- package/src/ai/store.ts +184 -31
- package/src/ai/testing.ts +36 -0
- package/src/ai/tool-sentences.ts +152 -34
- package/src/ai/tool-wording.ts +12 -6
- package/src/ai/tools.ts +8 -3
- package/src/ai/types.ts +25 -4
- package/src/ai/usage.ts +2 -2
- package/src/api/admin-core-settings.ts +15 -2
- package/src/api/admin-geo.ts +54 -0
- package/src/api/admin-linux-identities.ts +6 -3
- package/src/api/admin-outgoing-mail.ts +29 -15
- package/src/api/app-approval-actions.ts +31 -7
- package/src/api/approvals-loader.ts +3 -3
- package/src/api/approvals.ts +51 -0
- package/src/api/apps.ts +4 -2
- package/src/api/auth/schemas.ts +3 -0
- package/src/api/auth.ts +54 -18
- package/src/api/capabilities.ts +258 -181
- package/src/api/capability-streams.ts +21 -13
- package/src/api/cli-plugins.ts +4 -1
- package/src/api/geo-glyphs.ts +11 -0
- package/src/api/geo.ts +128 -0
- package/src/api/help.ts +12 -4
- package/src/api/index.ts +5 -0
- package/src/api/mcp.ts +73 -41
- package/src/api/me-panel-consent.ts +8 -5
- package/src/api/me.ts +79 -3
- package/src/api/search.ts +21 -14
- package/src/api/sync-ops.ts +19 -13
- package/src/api/widgets.ts +42 -18
- package/src/approvals/Approvals.tsx +30 -66
- package/src/approvals/approval-card.ts +68 -0
- package/src/browser/CloudResourceSearch.browser-harness.tsx +53 -1
- package/src/browser/CloudResourceSearch.tsx +3 -13
- package/src/browser/PanelLeaveNotice.tsx +18 -0
- package/src/browser/navigation-search.ts +78 -25
- package/src/browser/panel-leave.ts +69 -0
- package/src/browser/panel-messages.ts +12 -0
- package/src/browser/panel-slot.ts +173 -24
- package/src/browser/panel.ts +3 -0
- package/src/browser/resource-search-messages.ts +2 -0
- package/src/capabilities/client.ts +28 -1
- package/src/capabilities/executions.ts +22 -3
- package/src/capabilities/geo-client.ts +57 -0
- package/src/capabilities/response.ts +2 -1
- package/src/capabilities/review-storage.ts +24 -0
- package/src/capabilities/reviews.ts +17 -3
- package/src/capabilities/server.ts +36 -16
- package/src/cli/access.ts +15 -23
- package/src/cli/admin/linux.ts +13 -2
- package/src/cli/admin/outgoing-mail.ts +26 -4
- package/src/cli/capabilities.ts +19 -0
- package/src/config/env.ts +0 -6
- package/src/contracts/app-access.ts +48 -0
- package/src/contracts/app-approval.ts +3 -3
- package/src/contracts/app.ts +4 -1
- package/src/contracts/approvals.ts +2 -2
- package/src/contracts/avatar.ts +186 -0
- package/src/contracts/capabilities.ts +149 -55
- package/src/contracts/geo-admin.ts +127 -0
- package/src/contracts/geo-map.ts +19 -0
- package/src/contracts/geo-route.ts +168 -0
- package/src/contracts/geo.ts +248 -0
- package/src/contracts/index.ts +4 -0
- package/src/contracts/outgoing-mail.ts +17 -4
- package/src/contracts/registry.ts +2 -0
- package/src/contracts/shared.ts +3 -0
- package/src/events/app-access.ts +68 -0
- package/src/events/live-delivery.ts +128 -0
- package/src/events/live.ts +14 -130
- package/src/geo/CloudMap.tsx +95 -0
- package/src/geo/map-messages.ts +17 -0
- package/src/geo/ui.ts +1 -0
- package/src/index.ts +2 -0
- package/src/server/client-ip.ts +36 -0
- package/src/server/index.ts +3 -4
- package/src/server/middleware/app-access.ts +312 -0
- package/src/server/middleware/auth.ts +5 -3
- package/src/server/middleware/invocation.ts +11 -0
- package/src/server/middleware/rate-limit.ts +4 -4
- package/src/server/services/access.ts +13 -9
- package/src/server/services/index.ts +0 -2
- package/src/server/services/services.ts +0 -2
- package/src/services/accounts/app.ts +23 -3
- package/src/services/accounts/posix.ts +21 -7
- package/src/services/accounts/users.ts +12 -1
- package/src/services/allowed-network-fetch.ts +16 -10
- package/src/services/app-access-metrics.ts +24 -0
- package/src/services/app-access.ts +123 -0
- package/src/services/app-discovery.ts +27 -0
- package/src/services/approvals/content.ts +40 -0
- package/src/services/approvals/device.ts +49 -33
- package/src/services/approvals/index.ts +15 -6
- package/src/services/approvals/list.ts +6 -11
- package/src/services/approvals/question.ts +72 -0
- package/src/services/audit/index.ts +1 -0
- package/src/services/auth-flows/ipa.ts +39 -2
- package/src/services/auth-flows/password-reset.ts +32 -6
- package/src/services/downstream-budget.ts +87 -0
- package/src/services/geo/admin.ts +262 -0
- package/src/services/geo/bucket.ts +67 -0
- package/src/services/geo/cache.ts +57 -0
- package/src/services/geo/costs.ts +5 -0
- package/src/services/geo/errors.ts +41 -0
- package/src/services/geo/fixtures/README.md +17 -0
- package/src/services/geo/fixtures/along.json +355 -0
- package/src/services/geo/fixtures/autocomplete.json +218 -0
- package/src/services/geo/fixtures/geocode.json +129 -0
- package/src/services/geo/fixtures/places.json +355 -0
- package/src/services/geo/fixtures/reverse.json +61 -0
- package/src/services/geo/fixtures/routing.json +57 -0
- package/src/services/geo/geoapify-route.ts +93 -0
- package/src/services/geo/geoapify.ts +281 -0
- package/src/services/geo/ledger.ts +122 -0
- package/src/services/geo/map.ts +160 -0
- package/src/services/geo/migrate.ts +25 -0
- package/src/services/geo/polyline.ts +184 -0
- package/src/services/geo/route-chain.ts +145 -0
- package/src/services/geo/route-costs.ts +9 -0
- package/src/services/geo/route-geometry.ts +39 -0
- package/src/services/geo/route-plan.ts +172 -0
- package/src/services/geo/route-service.ts +433 -0
- package/src/services/geo/service.ts +300 -0
- package/src/services/geo/settings.ts +49 -0
- package/src/services/help/index.ts +16 -7
- package/src/services/help/types.ts +6 -1
- package/src/services/identity/invocation-actor.ts +20 -2
- package/src/services/identity/issue-invocation.ts +67 -0
- package/src/services/identity/key-ring.ts +5 -3
- package/src/services/identity/runtime-config.ts +23 -0
- package/src/services/index.ts +11 -0
- package/src/services/mandates/index.ts +40 -3
- package/src/services/network-security.ts +6 -3
- package/src/services/network-transport.ts +99 -0
- package/src/services/notifications/catalog.ts +3 -2
- package/src/services/notifications/runtime.ts +1 -0
- package/src/services/notifications/web-push-transport.ts +45 -38
- package/src/services/outgoing-mail/dispatcher.ts +6 -1
- package/src/services/outgoing-mail/index.ts +55 -1
- package/src/services/outgoing-mail/messages.ts +82 -3
- package/src/services/outgoing-mail/resolve-host.ts +2 -2
- package/src/services/outgoing-mail/retention.ts +13 -1
- package/src/services/outgoing-mail/store.ts +36 -6
- package/src/services/outgoing-mail/test-send.ts +34 -4
- package/src/services/outgoing-mail/transport.ts +1 -1
- package/src/services/pdf/gotenberg.ts +11 -38
- package/src/services/pdf/markdown.ts +160 -2
- package/src/services/pdf/template-preview.ts +1 -1
- package/src/services/public-http.ts +28 -40
- package/src/services/session/index.ts +203 -29
- package/src/services/settings/app.ts +4 -1
- package/src/services/settings/core-settings.ts +88 -0
- package/src/services/settings/defaults.ts +8 -2
- package/src/services/settings/errors.ts +11 -0
- package/src/services/settings/geo-budget.ts +40 -0
- package/src/services/settings/index.ts +4 -3
- package/src/services/settings/store.ts +25 -17
- package/src/services/weather/geo.ts +29 -111
- package/src/services/weather/index.ts +4 -3
- package/src/services/weather/locations.ts +8 -5
- package/src/services/weather/migrate.ts +1 -0
- package/src/services/weather/settings.ts +0 -16
- package/src/shared/__snapshots__/avatar-art.test.ts.snap +5 -0
- package/src/shared/avatar-art.ts +400 -0
- package/src/shared/index.ts +2 -0
- package/src/shared/network-address.ts +22 -3
- package/src/shared/template-rendering.ts +36 -4
- package/src/shared-text/documents.ts +1 -1
- package/src/shared-text/index.ts +2 -0
- package/src/shared-text/log.ts +51 -3
- package/src/shared-text/snapshots.ts +340 -0
- package/src/ssr/AdminLayout.tsx +4 -2
- package/src/ssr/CloudShell.island.tsx +6 -0
- package/src/ssr/HelpPanelArticle.island.tsx +308 -0
- package/src/ssr/HelpPanelBack.island.tsx +32 -0
- package/src/ssr/HelpPanelMenu.island.tsx +133 -0
- package/src/ssr/HelpPanelSearch.island.tsx +123 -0
- package/src/ssr/HelpPanelShortcuts.island.tsx +22 -0
- package/src/ssr/HelpPanelView.island.tsx +27 -0
- package/src/ssr/Layout.tsx +65 -15
- package/src/ssr/LayoutHelp.browser-harness.tsx +1 -1
- package/src/ssr/LayoutHelp.tsx +185 -542
- package/src/ssr/LayoutHelpPanel.browser-harness.tsx +6 -7
- package/src/ssr/LayoutHelpPanel.server.browser-harness.tsx +131 -0
- package/src/ssr/LayoutHelpPanel.server.tsx +115 -0
- package/src/ssr/PanelRuntime.island.tsx +11 -7
- package/src/ssr/PwaLayout.tsx +2 -2
- package/src/ssr/RailApps.tsx +6 -0
- package/src/ssr/ShellRuntime.island.tsx +23 -7
- package/src/ssr/__snapshots__/Layout.unchanged.render.test.tsx.snap +151 -0
- package/src/ssr/__snapshots__/LayoutHelp.ssr.render.test.tsx.snap +29 -0
- package/src/ssr/admin-navigation.ts +1 -0
- package/src/ssr/app-badges.ts +6 -1
- package/src/ssr/compact-shell.browser-harness.ts +44 -5
- package/src/ssr/help.ts +7 -5
- package/src/ssr/index.ts +2 -1
- package/src/ssr/islands/index.ts +3 -4
- package/src/ssr/layout-context.ts +2 -0
- package/src/ssr/layout-help-panel-controls.tsx +43 -0
- package/src/ssr/layout-help-panel-document.ts +67 -0
- package/src/ssr/layout-help-panel-lifecycle.ts +32 -0
- package/src/ssr/layout-help-panel-menu.tsx +113 -0
- package/src/ssr/layout-help-panel-modules.ts +7 -0
- package/src/ssr/layout-help-panel-reader.tsx +159 -0
- package/src/ssr/layout-help-panel-search.tsx +69 -0
- package/src/ssr/layout-help-panel-shortcuts.ts +7 -0
- package/src/ssr/layout-help-panel-state.ts +141 -0
- package/src/ssr/layout-help-panel-types.ts +19 -0
- package/src/ssr/layout-help-views.tsx +484 -0
- package/src/ssr/mobile-menu-history.ts +4 -0
- package/src/ssr/page-swap-fixture/Column.island.tsx +15 -0
- package/src/ssr/page-swap-fixture/ProviderProbe.island.tsx +31 -0
- package/src/ssr/page-swap.browser-harness.tsx +177 -0
- package/src/ssr/page-swap.ts +28 -0
- package/src/ssr/page-trail-runtime.ts +32 -13
- package/src/ssr/panel-consent.ts +2 -2
- package/src/ssr/panel-idle.ts +9 -0
- package/src/ssr/panel-slot.browser-harness.tsx +3 -0
- package/src/ssr/platform-messages.ts +8 -0
- package/src/ssr/runtime.ts +17 -0
- package/src/ssr/shell-data.tsx +10 -2
- package/src/styles/approvals.css +6 -0
- package/src/styles/effects.css +15 -0
- package/src/styles/global.css +70 -6
- package/src/server/services/geo.ts +0 -215
|
@@ -2,8 +2,13 @@ import { z } from "zod";
|
|
|
2
2
|
import {
|
|
3
3
|
CAPABILITY_FRAMEWORK_ERROR_CODES,
|
|
4
4
|
CAPABILITY_MAX_CATALOG_BYTES,
|
|
5
|
+
CAPABILITY_MAX_REVIEW_CONTENT_BYTES,
|
|
6
|
+
CAPABILITY_MAX_REVIEW_RESPONSE_BYTES,
|
|
5
7
|
CapabilityActionReviewDataSchema,
|
|
6
8
|
CapabilityCatalogSchema,
|
|
9
|
+
type CapabilityReviewContentRef,
|
|
10
|
+
CapabilityReviewContentRefSchema,
|
|
11
|
+
CapabilityReviewContentSchema,
|
|
7
12
|
capabilityResultSchema,
|
|
8
13
|
} from "../contracts/capabilities";
|
|
9
14
|
import { capabilityMessages } from "../shared/capability-messages";
|
|
@@ -142,7 +147,29 @@ export const reviewCapabilityAction = async <TInput = unknown>(
|
|
|
142
147
|
body: JSON.stringify({ input: invocation.input }),
|
|
143
148
|
signal: invocation.signal,
|
|
144
149
|
});
|
|
145
|
-
return readCapabilityResponse(
|
|
150
|
+
return await readCapabilityResponse(
|
|
151
|
+
response,
|
|
152
|
+
CapabilityActionReviewDataSchema,
|
|
153
|
+
CAPABILITY_MAX_REVIEW_RESPONSE_BYTES,
|
|
154
|
+
invocation.signal,
|
|
155
|
+
);
|
|
156
|
+
} catch (cause) {
|
|
157
|
+
return unavailable(cause, undefined, clientLocale(options.headers));
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
/** Load exactly one stored snapshot using the caller's normal Cloud authentication. A locator grants no access. */
|
|
162
|
+
export const readCapabilityReviewContent = async (
|
|
163
|
+
reference: CapabilityReviewContentRef,
|
|
164
|
+
options: CapabilityHttpOptions & { signal?: AbortSignal } = {},
|
|
165
|
+
) => {
|
|
166
|
+
try {
|
|
167
|
+
const ref = CapabilityReviewContentRefSchema.parse(reference);
|
|
168
|
+
const response = await (options.fetch ?? globalThis.fetch)(
|
|
169
|
+
requestUrl(options.baseUrl ?? "/api", `${ref.href.slice(4)}?digest=${encodeURIComponent(ref.digest)}`),
|
|
170
|
+
{ headers: options.headers, credentials: "same-origin", cache: "no-store", signal: options.signal },
|
|
171
|
+
);
|
|
172
|
+
return await readCapabilityResponse(response, CapabilityReviewContentSchema, CAPABILITY_MAX_REVIEW_CONTENT_BYTES, options.signal);
|
|
146
173
|
} catch (cause) {
|
|
147
174
|
return unavailable(cause, undefined, clientLocale(options.headers));
|
|
148
175
|
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { sql } from "bun";
|
|
2
|
+
import { createCapabilityDeadline } from "../_internal/capability-timeout";
|
|
2
3
|
import type { CapabilityOrigin } from "../contracts/capabilities";
|
|
4
|
+
import { REQUEST_IDLE_TIMEOUT_SECONDS } from "../server/request-timeout";
|
|
3
5
|
import { pruneApprovalRequests } from "../services/approvals";
|
|
4
6
|
import { logger } from "../services/logging";
|
|
5
7
|
import { CAPABILITY_CLAIM_RETENTION_HOURS, pruneCapabilityIdempotencyClaims } from "./claims";
|
|
@@ -101,7 +103,17 @@ export type CapabilityExecution = {
|
|
|
101
103
|
/** Insert one execution record. Called only by the capability dispatcher and the surfaces that deny before it. */
|
|
102
104
|
export const recordCapabilityExecution = async (input: CapabilityExecutionInput): Promise<string> => {
|
|
103
105
|
const durationMs = Math.max(0, Math.round(input.completedAt.getTime() - input.startedAt.getTime()));
|
|
104
|
-
|
|
106
|
+
// An observational write has its own transport-headroom budget, even when
|
|
107
|
+
// the invocation already expired. Do not hold its caller on a dead database.
|
|
108
|
+
const deadline = createCapabilityDeadline(REQUEST_IDLE_TIMEOUT_SECONDS * 1000, new AbortController().signal);
|
|
109
|
+
try {
|
|
110
|
+
return await deadline.wait(() =>
|
|
111
|
+
sql.begin(async (db) => {
|
|
112
|
+
// A connection obtained after the deadline must never start the insert.
|
|
113
|
+
deadline.check();
|
|
114
|
+
await db`SELECT set_config('statement_timeout', ${`${deadline.remainingMs()}ms`}, true)`;
|
|
115
|
+
deadline.check();
|
|
116
|
+
const [row] = await db<{ id: string }[]>`
|
|
105
117
|
INSERT INTO capabilities.executions (
|
|
106
118
|
request_id, origin, app_id, capability, kind, destructive,
|
|
107
119
|
actor_kind, actor_id, user_id, access_subject_type, access_subject_id,
|
|
@@ -117,8 +129,15 @@ export const recordCapabilityExecution = async (input: CapabilityExecutionInput)
|
|
|
117
129
|
${input.idempotencyKey ?? null}, ${input.replayed ?? false}, ${input.startedAt}, ${input.completedAt}, ${durationMs}
|
|
118
130
|
)
|
|
119
131
|
RETURNING id::text AS id
|
|
120
|
-
|
|
121
|
-
|
|
132
|
+
`;
|
|
133
|
+
// Roll back a late result rather than commit an abandoned transaction.
|
|
134
|
+
deadline.check();
|
|
135
|
+
return row!.id;
|
|
136
|
+
}),
|
|
137
|
+
);
|
|
138
|
+
} finally {
|
|
139
|
+
deadline.clear();
|
|
140
|
+
}
|
|
122
141
|
};
|
|
123
142
|
|
|
124
143
|
export type CapabilityExecutionFilter = {
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { z } from "zod";
|
|
2
|
+
import { geoContract } from "../contracts/geo";
|
|
3
|
+
import { geoMapContract } from "../contracts/geo-map";
|
|
4
|
+
import { geoRouteContract } from "../contracts/geo-route";
|
|
5
|
+
import { type CapabilityCaller, invokeCapabilityWithDataSchema } from "./server";
|
|
6
|
+
|
|
7
|
+
/** Calls Core using the caller's interactive identity, invocation transport, or durable mandate. */
|
|
8
|
+
export const geoClient = {
|
|
9
|
+
mapStyle: (input: z.input<typeof geoMapContract.style.input>, caller: CapabilityCaller) =>
|
|
10
|
+
invokeCapabilityWithDataSchema(
|
|
11
|
+
{ kind: "query", appId: "core", capabilityId: geoMapContract.style.id, input },
|
|
12
|
+
geoMapContract.style.data,
|
|
13
|
+
caller,
|
|
14
|
+
),
|
|
15
|
+
calculateRoute: (input: z.input<typeof geoRouteContract.calculateRoute.input>, caller: CapabilityCaller) =>
|
|
16
|
+
invokeCapabilityWithDataSchema(
|
|
17
|
+
{ kind: "query", appId: "core", capabilityId: geoRouteContract.calculateRoute.id, input },
|
|
18
|
+
geoRouteContract.calculateRoute.data,
|
|
19
|
+
caller,
|
|
20
|
+
),
|
|
21
|
+
updateRoute: (input: z.input<typeof geoRouteContract.updateRoute.input>, caller: CapabilityCaller) =>
|
|
22
|
+
invokeCapabilityWithDataSchema(
|
|
23
|
+
{ kind: "query", appId: "core", capabilityId: geoRouteContract.updateRoute.id, input },
|
|
24
|
+
geoRouteContract.updateRoute.data,
|
|
25
|
+
caller,
|
|
26
|
+
),
|
|
27
|
+
autocomplete: (input: z.input<typeof geoContract.autocomplete.input>, caller: CapabilityCaller) =>
|
|
28
|
+
invokeCapabilityWithDataSchema(
|
|
29
|
+
{ kind: "query", appId: "core", capabilityId: geoContract.autocomplete.id, input },
|
|
30
|
+
geoContract.autocomplete.data,
|
|
31
|
+
caller,
|
|
32
|
+
),
|
|
33
|
+
geocode: (input: z.input<typeof geoContract.geocode.input>, caller: CapabilityCaller) =>
|
|
34
|
+
invokeCapabilityWithDataSchema(
|
|
35
|
+
{ kind: "query", appId: "core", capabilityId: geoContract.geocode.id, input },
|
|
36
|
+
geoContract.geocode.data,
|
|
37
|
+
caller,
|
|
38
|
+
),
|
|
39
|
+
reverse: (input: z.input<typeof geoContract.reverse.input>, caller: CapabilityCaller) =>
|
|
40
|
+
invokeCapabilityWithDataSchema(
|
|
41
|
+
{ kind: "query", appId: "core", capabilityId: geoContract.reverse.id, input },
|
|
42
|
+
geoContract.reverse.data,
|
|
43
|
+
caller,
|
|
44
|
+
),
|
|
45
|
+
searchPlaces: (input: z.input<typeof geoContract.searchPlaces.input>, caller: CapabilityCaller) =>
|
|
46
|
+
invokeCapabilityWithDataSchema(
|
|
47
|
+
{ kind: "query", appId: "core", capabilityId: geoContract.searchPlaces.id, input },
|
|
48
|
+
geoContract.searchPlaces.data,
|
|
49
|
+
caller,
|
|
50
|
+
),
|
|
51
|
+
availability: (caller: CapabilityCaller) =>
|
|
52
|
+
invokeCapabilityWithDataSchema(
|
|
53
|
+
{ kind: "query", appId: "core", capabilityId: geoContract.availability.id, input: {} },
|
|
54
|
+
geoContract.availability.data,
|
|
55
|
+
caller,
|
|
56
|
+
),
|
|
57
|
+
};
|
|
@@ -7,8 +7,9 @@ export const readCapabilityResponse = async <T>(
|
|
|
7
7
|
response: Response,
|
|
8
8
|
schema: z.ZodType<T>,
|
|
9
9
|
maxBytes = CAPABILITY_MAX_RESULT_BYTES,
|
|
10
|
+
signal?: AbortSignal,
|
|
10
11
|
): Promise<CapabilityResultState<T>> => {
|
|
11
|
-
const parsedBody = await readBoundedJson(response, maxBytes);
|
|
12
|
+
const parsedBody = await readBoundedJson(response, maxBytes, signal);
|
|
12
13
|
if (!parsedBody.ok && parsedBody.reason === "too_large") {
|
|
13
14
|
return {
|
|
14
15
|
ok: false,
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { CapabilityReviewContentSchema } from "../contracts/capabilities";
|
|
2
|
+
|
|
3
|
+
/** JSONB cannot store NUL or lone UTF-16 surrogates. Nest the content's JSON as
|
|
4
|
+
* text in the existing snapshot, preserving every code unit and the public digest. */
|
|
5
|
+
export const serializeReviewSnapshot = (review: unknown): string => {
|
|
6
|
+
if (review && typeof review === "object" && "content" in review && review.content !== undefined)
|
|
7
|
+
return JSON.stringify({ ...review, content: JSON.stringify(CapabilityReviewContentSchema.parse(review.content)) });
|
|
8
|
+
return JSON.stringify(review);
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export const parseStoredReviewContent = (value: unknown) => {
|
|
12
|
+
try {
|
|
13
|
+
return CapabilityReviewContentSchema.safeParse(typeof value === "string" ? JSON.parse(value) : value);
|
|
14
|
+
} catch {
|
|
15
|
+
return CapabilityReviewContentSchema.safeParse(null);
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export const restoreReviewSnapshot = (review: unknown): unknown => {
|
|
20
|
+
if (!review || typeof review !== "object" || !("content" in review)) return review;
|
|
21
|
+
const content = parseStoredReviewContent(review.content);
|
|
22
|
+
// Invalid stored content must fail validation, never become a content-free review.
|
|
23
|
+
return { ...review, content: content.success ? content.data : null };
|
|
24
|
+
};
|
|
@@ -14,10 +14,11 @@ export const capabilityPreviewIssues = (
|
|
|
14
14
|
preview: CapabilityPreview | undefined,
|
|
15
15
|
links: readonly CapabilitySemanticLink[] | undefined,
|
|
16
16
|
sends = false,
|
|
17
|
+
completeContent = false,
|
|
17
18
|
): string[] => {
|
|
18
19
|
if (!preview) return [];
|
|
19
20
|
const issues: string[] = [];
|
|
20
|
-
const open = links?.some((link) => link.rel === "open");
|
|
21
|
+
const open = completeContent || links?.some((link) => link.rel === "open");
|
|
21
22
|
if ("truncated" in preview && preview.truncated && !open) issues.push("A truncated preview requires an open link");
|
|
22
23
|
if (preview.kind === "message") {
|
|
23
24
|
if ((Object.values(preview.more ?? {}).some((count) => count > 0) || (preview.moreAttachments ?? 0) > 0) && !open)
|
|
@@ -33,9 +34,22 @@ export const capabilityReviewFormIssues = (review: CapabilityActionCallReview):
|
|
|
33
34
|
for (const form of ["confirm", "decline"] as const)
|
|
34
35
|
if (Array.from(capabilityReviewSentence(review, form, { timeZone: "UTC" })).length > 40)
|
|
35
36
|
issues.push(`${form} exceeds 40 rendered characters`);
|
|
36
|
-
issues.push(...capabilityPreviewIssues(review.preview, review.links, review.ask === "sends"));
|
|
37
|
+
issues.push(...capabilityPreviewIssues(review.preview, review.links, review.ask === "sends", Boolean(review.content)));
|
|
38
|
+
if (
|
|
39
|
+
review.content &&
|
|
40
|
+
review.preview &&
|
|
41
|
+
review.content.kind !== review.preview.kind &&
|
|
42
|
+
!(review.content.kind === "message" && review.preview.kind === "page")
|
|
43
|
+
)
|
|
44
|
+
issues.push("Full content must have the same kind as its preview");
|
|
45
|
+
if (review.content?.kind === "message" && review.ask === "sends") {
|
|
46
|
+
if (!review.content.from) issues.push("Full sends content requires from");
|
|
47
|
+
if (!review.content.to.length && !review.content.cc?.length && !review.content.bcc?.length)
|
|
48
|
+
issues.push("Full sends content requires a recipient");
|
|
49
|
+
}
|
|
37
50
|
try {
|
|
38
|
-
|
|
51
|
+
const { content: _content, ...compact } = review;
|
|
52
|
+
if (new TextEncoder().encode(JSON.stringify(compact)).byteLength > CAPABILITY_MAX_REVIEW_BYTES) issues.push("Review exceeds 32 KiB");
|
|
39
53
|
} catch {
|
|
40
54
|
issues.push("Review is not JSON serializable");
|
|
41
55
|
}
|
|
@@ -6,11 +6,13 @@ import { dispatchCapability, loadCapabilityCatalogPage } from "../api/capabiliti
|
|
|
6
6
|
import { env } from "../config/env";
|
|
7
7
|
import {
|
|
8
8
|
CAPABILITY_FRAMEWORK_ERROR_CODES,
|
|
9
|
+
CAPABILITY_MAX_REVIEW_RESPONSE_BYTES,
|
|
9
10
|
CapabilityActionReviewDataSchema,
|
|
10
11
|
type CapabilityOrigin,
|
|
11
12
|
capabilityResultSchema,
|
|
12
13
|
} from "../contracts/capabilities";
|
|
13
14
|
import type { RequestAuthority } from "../server";
|
|
15
|
+
import { canUseApp } from "../services/app-access";
|
|
14
16
|
import { get } from "../services/settings";
|
|
15
17
|
import { resolveAppPresentation } from "../shared/app-presentation";
|
|
16
18
|
import { publicCloudOrigin } from "../shared/app-url";
|
|
@@ -184,7 +186,7 @@ export const invokeCapabilityWithDataSchema = <TDataSchema extends z.ZodType, TI
|
|
|
184
186
|
): Promise<CapabilityClientResult<z.output<TDataSchema>>> => invokeCapabilityWithResultSchema(invocation, dataSchema, caller);
|
|
185
187
|
|
|
186
188
|
export const listCapabilityCatalog = async (
|
|
187
|
-
options: { cursor?: string; limit?: number; locale?: string } = {},
|
|
189
|
+
options: { cursor?: string; limit?: number; locale?: string; authority?: RequestAuthority | null } = {},
|
|
188
190
|
): Promise<CapabilityCatalogClientResult> => {
|
|
189
191
|
const limit = options.limit ?? 25;
|
|
190
192
|
const messages = capabilityMessages(options.locale);
|
|
@@ -197,30 +199,35 @@ export const listCapabilityCatalog = async (
|
|
|
197
199
|
try {
|
|
198
200
|
return {
|
|
199
201
|
ok: true,
|
|
200
|
-
data: await loadCapabilityCatalogPage({ cursor: options.cursor, limit }, {}, options.locale),
|
|
202
|
+
data: await loadCapabilityCatalogPage({ cursor: options.cursor, limit }, {}, options.locale, options.authority ?? null),
|
|
201
203
|
};
|
|
202
204
|
} catch {
|
|
203
205
|
return { ok: false, error: { code: "APP_UNAVAILABLE", message: messages.cloudUnavailable, status: 503 } };
|
|
204
206
|
}
|
|
205
207
|
};
|
|
206
208
|
|
|
207
|
-
export const getCapabilityCatalogApp = async (
|
|
209
|
+
export const getCapabilityCatalogApp = async (
|
|
210
|
+
appId: string,
|
|
211
|
+
locale?: string,
|
|
212
|
+
authority: RequestAuthority | null = null,
|
|
213
|
+
): Promise<CapabilityCatalogAppClientResult> => {
|
|
208
214
|
try {
|
|
209
215
|
const [capability, registeredApp] = await Promise.all([getCapability(appId), getApp(appId)]);
|
|
210
216
|
const presentedApp = registeredApp && locale ? resolveAppPresentation(registeredApp, locale) : registeredApp;
|
|
211
217
|
return {
|
|
212
218
|
ok: true,
|
|
213
|
-
data:
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
219
|
+
data:
|
|
220
|
+
capability && registeredApp && (await canUseApp(authority, registeredApp))
|
|
221
|
+
? {
|
|
222
|
+
appId: capability.appId,
|
|
223
|
+
appName: presentedApp?.name ?? capability.appName,
|
|
224
|
+
appIcon: capability.appIcon,
|
|
225
|
+
appDescription: presentedApp?.description ?? capability.appDescription,
|
|
226
|
+
manifest: locale
|
|
227
|
+
? resolveCapabilityManifestPresentation(capability.manifest, capability.presentation, locale)
|
|
228
|
+
: capability.manifest,
|
|
229
|
+
}
|
|
230
|
+
: null,
|
|
224
231
|
};
|
|
225
232
|
} catch {
|
|
226
233
|
return { ok: false, error: { code: "APP_UNAVAILABLE", message: capabilityMessages(locale).cloudUnavailable, status: 503 } };
|
|
@@ -237,7 +244,10 @@ export const getCapabilityActionWording = async (
|
|
|
237
244
|
appId: string,
|
|
238
245
|
localId: string,
|
|
239
246
|
locale?: string,
|
|
247
|
+
authority: RequestAuthority | null = null,
|
|
240
248
|
): Promise<CapabilityActionWording | null> => {
|
|
249
|
+
const registeredApp = await getApp(appId);
|
|
250
|
+
if (!registeredApp || !(await canUseApp(authority, registeredApp))) return null;
|
|
241
251
|
const capability = await getCapability(appId);
|
|
242
252
|
const action = capability?.manifest.actions.find((candidate) => candidate.localId === localId);
|
|
243
253
|
return capability && action ? resolveCapabilityActionWording(action, capability.presentation, locale ?? DEFAULT_LOCALE) : null;
|
|
@@ -263,7 +273,12 @@ export const reviewCapabilityAction = async <TInput = unknown>(
|
|
|
263
273
|
locale: caller.locale ?? undefined,
|
|
264
274
|
origin: caller.origin ?? "app",
|
|
265
275
|
});
|
|
266
|
-
return readCapabilityResponse(
|
|
276
|
+
return await readCapabilityResponse(
|
|
277
|
+
outcome,
|
|
278
|
+
CapabilityActionReviewDataSchema,
|
|
279
|
+
CAPABILITY_MAX_REVIEW_RESPONSE_BYTES,
|
|
280
|
+
combineCapabilitySignals(caller.signal, invocation.signal),
|
|
281
|
+
);
|
|
267
282
|
}
|
|
268
283
|
const response = await (caller.transport ?? fetch)(
|
|
269
284
|
new Request(
|
|
@@ -276,7 +291,12 @@ export const reviewCapabilityAction = async <TInput = unknown>(
|
|
|
276
291
|
{ body: JSON.stringify({ input: invocation.input }) },
|
|
277
292
|
),
|
|
278
293
|
);
|
|
279
|
-
return readCapabilityResponse(
|
|
294
|
+
return await readCapabilityResponse(
|
|
295
|
+
response,
|
|
296
|
+
CapabilityActionReviewDataSchema,
|
|
297
|
+
CAPABILITY_MAX_REVIEW_RESPONSE_BYTES,
|
|
298
|
+
combineCapabilitySignals(caller.signal, invocation.signal),
|
|
299
|
+
);
|
|
280
300
|
} catch (cause) {
|
|
281
301
|
return unavailable(cause, undefined, caller.locale ?? undefined);
|
|
282
302
|
}
|
package/src/cli/access.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { principalLabel } from "../access/principal-label";
|
|
1
2
|
import type { AccessEntry, EntityListItem, PermissionLevel, Principal } from "../contracts";
|
|
2
3
|
import { arg, command, confirmFlag, flag, paginationFlags } from "./commands";
|
|
3
4
|
import type { CloudCliContext, CloudCliTableColumn } from "./index";
|
|
@@ -120,21 +121,7 @@ const principalKey = (principal: Principal): string => {
|
|
|
120
121
|
}
|
|
121
122
|
};
|
|
122
123
|
|
|
123
|
-
const entryDisplayName = (entry: AccessEntry): string =>
|
|
124
|
-
if (entry.displayName) return entry.displayName;
|
|
125
|
-
switch (entry.principal.type) {
|
|
126
|
-
case "user":
|
|
127
|
-
return entry.principal.userId;
|
|
128
|
-
case "group":
|
|
129
|
-
return entry.principal.groupId;
|
|
130
|
-
case "service_account":
|
|
131
|
-
return entry.principal.serviceAccountId;
|
|
132
|
-
case "authenticated":
|
|
133
|
-
return "All users (incl. guests)";
|
|
134
|
-
case "public":
|
|
135
|
-
return "Public";
|
|
136
|
-
}
|
|
137
|
-
};
|
|
124
|
+
const entryDisplayName = (entry: AccessEntry, locale?: string): string => principalLabel(entry, locale);
|
|
138
125
|
|
|
139
126
|
const formatEntityCandidate = (item: EntityListItem): string => {
|
|
140
127
|
if (item.kind === "user") return `${item.user.displayName} (${item.user.uid}${item.user.mail ? `, ${item.user.mail}` : ""})`;
|
|
@@ -257,17 +244,20 @@ const entryTypeLabel = (entry: AccessEntry): string => {
|
|
|
257
244
|
};
|
|
258
245
|
|
|
259
246
|
// Resource-bound service accounts back resource API keys, which are managed with those keys.
|
|
260
|
-
const accessRows = (
|
|
247
|
+
const accessRows = (
|
|
248
|
+
entries: AccessEntry[],
|
|
249
|
+
options: { includeServiceAccounts?: boolean; scopes?: readonly string[]; locale?: string } = {},
|
|
250
|
+
) =>
|
|
261
251
|
entries
|
|
262
252
|
.filter((entry) => options.includeServiceAccounts || entry.serviceAccountKind !== "resource_bound")
|
|
263
253
|
.sort((a, b) => {
|
|
264
254
|
const rank = permissionRank(b.permission) - permissionRank(a.permission);
|
|
265
255
|
if (rank !== 0) return rank;
|
|
266
|
-
return entryDisplayName(a).localeCompare(entryDisplayName(b));
|
|
256
|
+
return entryDisplayName(a, options.locale).localeCompare(entryDisplayName(b, options.locale));
|
|
267
257
|
})
|
|
268
258
|
.map((entry) => ({
|
|
269
259
|
accessId: entry.id,
|
|
270
|
-
principal: entryDisplayName(entry),
|
|
260
|
+
principal: entryDisplayName(entry, options.locale),
|
|
271
261
|
type: entryTypeLabel(entry),
|
|
272
262
|
permission: entry.permission,
|
|
273
263
|
...(options.scopes ? { scope: entry.scope ?? options.scopes[0] } : {}),
|
|
@@ -287,7 +277,7 @@ export const printAccessEntries = (
|
|
|
287
277
|
options: { includeServiceAccounts?: boolean; jsonValue?: unknown; scopes?: readonly string[] } = {},
|
|
288
278
|
) => {
|
|
289
279
|
if (printStructured(ctx, options.jsonValue ?? entries)) return;
|
|
290
|
-
const rows = accessRows(entries, options);
|
|
280
|
+
const rows = accessRows(entries, { ...options, locale: ctx.options.locale });
|
|
291
281
|
if (rows.length === 0) {
|
|
292
282
|
ctx.print("No direct grants.");
|
|
293
283
|
return;
|
|
@@ -426,7 +416,7 @@ export const createAccessCommands = <TResource extends AccessResource>(adapter:
|
|
|
426
416
|
const principal = await resolveAccessPrincipal(ctx, flags as PrincipalFlags, adapter);
|
|
427
417
|
const entry = await adapter.grant(ctx, resource, principal, permission, scope);
|
|
428
418
|
if (!printStructured(ctx, { resource, entry }))
|
|
429
|
-
ctx.print(`Granted ${permission}${scopeSuffix(scope)} on ${resource.label} to ${entryDisplayName(entry)}.`);
|
|
419
|
+
ctx.print(`Granted ${permission}${scopeSuffix(scope)} on ${resource.label} to ${entryDisplayName(entry, ctx.options.locale)}.`);
|
|
430
420
|
},
|
|
431
421
|
});
|
|
432
422
|
|
|
@@ -462,13 +452,15 @@ export const createAccessCommands = <TResource extends AccessResource>(adapter:
|
|
|
462
452
|
if (existing) {
|
|
463
453
|
await adapter.update(ctx, resource, existing.id, permission, scope);
|
|
464
454
|
if (!printStructured(ctx, { resource, accessId: existing.id, permission, scope, action: "updated" })) {
|
|
465
|
-
ctx.print(
|
|
455
|
+
ctx.print(
|
|
456
|
+
`Updated ${entryDisplayName(existing, ctx.options.locale)} to ${permission}${scopeSuffix(scope)} on ${resource.label}.`,
|
|
457
|
+
);
|
|
466
458
|
}
|
|
467
459
|
return;
|
|
468
460
|
}
|
|
469
461
|
const entry = await adapter.grant(ctx, resource, principal, permission, scope);
|
|
470
462
|
if (!printStructured(ctx, { resource, entry, action: "created" })) {
|
|
471
|
-
ctx.print(`Granted ${permission}${scopeSuffix(scope)} on ${resource.label} to ${entryDisplayName(entry)}.`);
|
|
463
|
+
ctx.print(`Granted ${permission}${scopeSuffix(scope)} on ${resource.label} to ${entryDisplayName(entry, ctx.options.locale)}.`);
|
|
472
464
|
}
|
|
473
465
|
},
|
|
474
466
|
});
|
|
@@ -496,7 +488,7 @@ export const createAccessCommands = <TResource extends AccessResource>(adapter:
|
|
|
496
488
|
const entry = entries.find((candidate) => principalKey(candidate.principal) === principalKey(principal));
|
|
497
489
|
if (!entry) throw new Error("No direct grant for that principal.");
|
|
498
490
|
accessId = entry.id;
|
|
499
|
-
label = entryDisplayName(entry);
|
|
491
|
+
label = entryDisplayName(entry, ctx.options.locale);
|
|
500
492
|
} else if (principalFlagCount(flags as PrincipalFlags) > 0) {
|
|
501
493
|
throw new Error("Pass either --access-id or one principal flag, not both.");
|
|
502
494
|
}
|
package/src/cli/admin/linux.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
1
2
|
import { type LinuxIdentityConfiguration, LinuxIdentityConfigurationSchema } from "../../contracts/posix";
|
|
2
3
|
import { cliText, command, confirmFlag, flag, printStructured, readCliInput } from "../index";
|
|
3
4
|
import { apiGet, apiJson, queryString } from "./shared";
|
|
@@ -31,6 +32,10 @@ export const linuxCommands = [
|
|
|
31
32
|
flags: {
|
|
32
33
|
config: flag.input({ required: true, description: "Configuration JSON: --config, --config-file, or --stdin" }),
|
|
33
34
|
rangeReserved: flag.boolean({ name: "range-reserved", description: "Confirm the range is reserved across all connected systems" }),
|
|
35
|
+
replaceUnreadable: flag.boolean({
|
|
36
|
+
name: "replace-unreadable",
|
|
37
|
+
description: "Explicitly replace an unreadable stored Linux configuration",
|
|
38
|
+
}),
|
|
34
39
|
yes: confirmFlag("Confirm replacing Linux identity configuration"),
|
|
35
40
|
},
|
|
36
41
|
async run({ ctx, flags }) {
|
|
@@ -40,7 +45,9 @@ export const linuxCommands = [
|
|
|
40
45
|
);
|
|
41
46
|
const input = await readCliInput(flags.config, { label: "Linux configuration", required: true });
|
|
42
47
|
if (input === undefined) throw new Error("Missing Linux configuration.");
|
|
43
|
-
const config = LinuxIdentityConfigurationSchema.
|
|
48
|
+
const { unreadable: _unreadable, ...config } = LinuxIdentityConfigurationSchema.safeExtend({
|
|
49
|
+
unreadable: z.boolean().optional(),
|
|
50
|
+
}).parse(JSON.parse(input));
|
|
44
51
|
if (config.enabled && !flags.rangeReserved)
|
|
45
52
|
throw new Error(
|
|
46
53
|
cliText(ctx, {
|
|
@@ -48,7 +55,11 @@ export const linuxCommands = [
|
|
|
48
55
|
de: "Zum Aktivieren ist --range-reserved erforderlich.",
|
|
49
56
|
}),
|
|
50
57
|
);
|
|
51
|
-
const result = await apiJson<unknown>(ctx, "PUT", `${path}/configuration`, {
|
|
58
|
+
const result = await apiJson<unknown>(ctx, "PUT", `${path}/configuration`, {
|
|
59
|
+
config,
|
|
60
|
+
rangeReserved: flags.rangeReserved,
|
|
61
|
+
...(flags.replaceUnreadable ? { replaceUnreadable: true } : {}),
|
|
62
|
+
});
|
|
52
63
|
if (!printStructured(ctx, result)) ctx.print(JSON.stringify(result, null, 2));
|
|
53
64
|
},
|
|
54
65
|
}),
|
|
@@ -35,13 +35,25 @@ export const outgoingMailCommands = [
|
|
|
35
35
|
flags: {
|
|
36
36
|
contentDays: flag.string({ required: true, description: "Content retention in days" }),
|
|
37
37
|
recordDays: flag.string({ required: true, description: "Record retention in days (at least content retention)" }),
|
|
38
|
+
replaceContent: flag.boolean({ name: "replace-unreadable-content", description: "Explicitly replace unreadable content retention" }),
|
|
39
|
+
replaceRecords: flag.boolean({ name: "replace-unreadable-records", description: "Explicitly replace unreadable record retention" }),
|
|
38
40
|
yes: confirmFlag("Confirm changing outgoing mail retention"),
|
|
39
41
|
},
|
|
40
42
|
async run({ ctx, flags }) {
|
|
41
43
|
confirmed(flags.yes);
|
|
42
44
|
const retention = MailRetentionSchema.parse({ contentDays: Number(flags.contentDays), recordDays: Number(flags.recordDays) });
|
|
43
45
|
if (retention.recordDays < retention.contentDays) throw new Error("Record retention must be at least content retention.");
|
|
44
|
-
const result = await apiJson<MailRetention>(ctx, "PUT", `${root}/retention`,
|
|
46
|
+
const result = await apiJson<MailRetention>(ctx, "PUT", `${root}/retention`, {
|
|
47
|
+
...retention,
|
|
48
|
+
...(flags.replaceContent || flags.replaceRecords
|
|
49
|
+
? {
|
|
50
|
+
replaceUnreadable: [
|
|
51
|
+
...(flags.replaceContent ? ["outgoing_mail.content_retention_days"] : []),
|
|
52
|
+
...(flags.replaceRecords ? ["outgoing_mail.record_retention_days"] : []),
|
|
53
|
+
],
|
|
54
|
+
}
|
|
55
|
+
: {}),
|
|
56
|
+
});
|
|
45
57
|
if (!printStructured(ctx, result)) print(ctx, result);
|
|
46
58
|
},
|
|
47
59
|
}),
|
|
@@ -183,7 +195,12 @@ export const outgoingMailCommands = [
|
|
|
183
195
|
printJsonOrTable(
|
|
184
196
|
ctx,
|
|
185
197
|
result,
|
|
186
|
-
result.items.map((app) => ({
|
|
198
|
+
result.items.map((app) => ({
|
|
199
|
+
...app,
|
|
200
|
+
reads: app.readDeclared,
|
|
201
|
+
profiles: app.profiles.join(","),
|
|
202
|
+
logApps: app.logApps.map((id) => `${id}:${app.logContentApps?.includes(id) ? "content" : "metadata"}`).join(","),
|
|
203
|
+
})),
|
|
187
204
|
[
|
|
188
205
|
{ key: "appId" },
|
|
189
206
|
{ key: "name" },
|
|
@@ -226,15 +243,20 @@ export const outgoingMailCommands = [
|
|
|
226
243
|
summary: "Choose which other applications' mail logs an app may read",
|
|
227
244
|
args: { app: arg.required({ valueLabel: "app" }) },
|
|
228
245
|
flags: {
|
|
229
|
-
apps: flag.string({ description: "Comma-separated source application IDs" }),
|
|
246
|
+
apps: flag.string({ description: "Comma-separated source application IDs (replaces all grants)" }),
|
|
247
|
+
contentApps: flag.string({ description: "Subset of --apps allowed to read text and attachment names" }),
|
|
230
248
|
ownOnly: flag.boolean({ description: "Remove all other application log grants" }),
|
|
231
249
|
yes: confirmFlag("Confirm application mail log access change"),
|
|
232
250
|
},
|
|
233
251
|
async run({ ctx, args, flags }) {
|
|
234
252
|
confirmed(flags.yes);
|
|
235
253
|
if ((flags.apps !== undefined) === !!flags.ownOnly) throw new Error("Choose exactly one of --apps or --own-only.");
|
|
254
|
+
if (flags.contentApps !== undefined && flags.apps === undefined) throw new Error("--content-apps requires --apps.");
|
|
236
255
|
const reader = MailLogAppIdSchema.safeParse(args.app);
|
|
237
|
-
const value = MailLogAccessSchema.safeParse({
|
|
256
|
+
const value = MailLogAccessSchema.safeParse({
|
|
257
|
+
apps: flags.apps?.split(",").map((app) => app.trim()) ?? [],
|
|
258
|
+
...(flags.contentApps !== undefined ? { contentApps: flags.contentApps.split(",").map((app) => app.trim()) } : {}),
|
|
259
|
+
});
|
|
238
260
|
if (!reader.success || args.app === "core" || !value.success || value.data.apps.includes(args.app))
|
|
239
261
|
throw new Error("Choose a valid reader and distinct source apps other than Core or the reader itself.");
|
|
240
262
|
print(ctx, await apiJson(ctx, "PUT", `${root}/apps/${encodeURIComponent(args.app)}/log-access`, value.data));
|
package/src/cli/capabilities.ts
CHANGED
|
@@ -4,6 +4,8 @@ import {
|
|
|
4
4
|
CAPABILITY_FRAMEWORK_ERROR_CODES,
|
|
5
5
|
CAPABILITY_MAX_CATALOG_BYTES,
|
|
6
6
|
CAPABILITY_MAX_RESULT_BYTES,
|
|
7
|
+
CAPABILITY_MAX_REVIEW_RESPONSE_BYTES,
|
|
8
|
+
CapabilityActionReviewDataSchema,
|
|
7
9
|
CapabilityCatalogSchema,
|
|
8
10
|
CapabilityErrorSchema,
|
|
9
11
|
type CapabilityManifest,
|
|
@@ -295,6 +297,23 @@ export default defineCliCommands({
|
|
|
295
297
|
await invoke({ ctx, kind: "queries", appId, capabilityId: reader.localId, inputValue: { id: ref.id }, manifest: app.manifest });
|
|
296
298
|
},
|
|
297
299
|
}),
|
|
300
|
+
command("review", {
|
|
301
|
+
summary: "Read the complete proposed Action without executing it",
|
|
302
|
+
args: { appId: arg.required({ description: "Owning app id" }), actionId: arg.required({ description: "App-local action id" }) },
|
|
303
|
+
flags: { input: flag.input({ required: true, description: "Strict JSON input; supports --input-file or --stdin" }) },
|
|
304
|
+
run: async ({ ctx, args, flags }) => {
|
|
305
|
+
const response = await ctx.fetch(
|
|
306
|
+
`/api/capabilities/v1/actions/${encodeURIComponent(args.appId)}/${encodeURIComponent(args.actionId)}/review`,
|
|
307
|
+
{
|
|
308
|
+
method: "POST",
|
|
309
|
+
headers: { "content-type": "application/json" },
|
|
310
|
+
body: JSON.stringify({ input: await parseInput(flags.input) }),
|
|
311
|
+
},
|
|
312
|
+
);
|
|
313
|
+
const review = CapabilityActionReviewDataSchema.parse(await readCapabilityJson(response, CAPABILITY_MAX_REVIEW_RESPONSE_BYTES));
|
|
314
|
+
printGenericResult(ctx, review);
|
|
315
|
+
},
|
|
316
|
+
}),
|
|
298
317
|
command("action", {
|
|
299
318
|
summary: "Invoke an app mutation",
|
|
300
319
|
args: {
|
package/src/config/env.ts
CHANGED
|
@@ -98,12 +98,6 @@ const registry = defineEnv({
|
|
|
98
98
|
scope: "runtime",
|
|
99
99
|
doc: "Comma-separated host names, IP addresses and CIDR ranges on internal networks allowed for outgoing mail SMTP and bounce IMAP connections; unset allows only public targets, an explicit value replaces this default, and invalid entries including `/0` stop the owning process at startup.",
|
|
100
100
|
},
|
|
101
|
-
GEO_ALLOWED_TARGETS: {
|
|
102
|
-
schema: envList,
|
|
103
|
-
default: ["geo"],
|
|
104
|
-
scope: "runtime",
|
|
105
|
-
doc: "Comma-separated host names, IP addresses and CIDR ranges on internal networks allowed for Geo requests; unset allows public targets and `geo`, an explicit value replaces this default, and invalid entries including `/0` stop the owning process at startup.",
|
|
106
|
-
},
|
|
107
101
|
|
|
108
102
|
// Identity and OAuth
|
|
109
103
|
CLOUD_IDENTITY_KEY_ENCRYPTION_KEY: {
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { RequestAuthority } from "../server/middleware/auth";
|
|
3
|
+
import { type Role, RoleSchema } from "./shared";
|
|
4
|
+
|
|
5
|
+
const publicRoutes = z
|
|
6
|
+
.array(
|
|
7
|
+
z
|
|
8
|
+
.string()
|
|
9
|
+
.startsWith("/")
|
|
10
|
+
.refine((path) => !path.startsWith("//") && !/[?#]/.test(path)),
|
|
11
|
+
)
|
|
12
|
+
.readonly()
|
|
13
|
+
.optional();
|
|
14
|
+
export const FixedAppAccessRuleSchema = z.discriminatedUnion("kind", [
|
|
15
|
+
z.object({ kind: z.literal("signedIn") }).strict(),
|
|
16
|
+
z.object({ kind: z.literal("noGuests") }).strict(),
|
|
17
|
+
z.object({ kind: z.literal("roles"), roles: z.array(RoleSchema).min(1).readonly() }).strict(),
|
|
18
|
+
]);
|
|
19
|
+
export type FixedAppAccessRule = z.infer<typeof FixedAppAccessRuleSchema>;
|
|
20
|
+
export const AppAccessRuleSchema = z.union([
|
|
21
|
+
z.object({ kind: z.literal("signedIn"), publicRoutes }).strict(),
|
|
22
|
+
z.object({ kind: z.literal("noGuests"), publicRoutes }).strict(),
|
|
23
|
+
z.object({ kind: z.literal("roles"), roles: z.array(RoleSchema).min(1).readonly(), publicRoutes }).strict(),
|
|
24
|
+
z.object({ kind: z.literal("grants"), until: FixedAppAccessRuleSchema.optional(), publicRoutes }).strict(),
|
|
25
|
+
]);
|
|
26
|
+
/** Serializable entry rule evaluated by the platform; never application callback code. */
|
|
27
|
+
export type AppAccessRule = z.infer<typeof AppAccessRuleSchema>;
|
|
28
|
+
/** The optional management restriction runs in the owning application, in addition to a management grant. */
|
|
29
|
+
export type AppAccessDeclaration = AppAccessRule & {
|
|
30
|
+
canManage?: (authority: RequestAuthority) => boolean | Promise<boolean>;
|
|
31
|
+
};
|
|
32
|
+
type PublicOptions = { publicRoutes?: readonly string[] };
|
|
33
|
+
export const appAccess = {
|
|
34
|
+
signedIn: (options: PublicOptions = {}): AppAccessRule & { kind: "signedIn" } => ({ kind: "signedIn", ...options }),
|
|
35
|
+
noGuests: (options: PublicOptions = {}): AppAccessRule & { kind: "noGuests" } => ({ kind: "noGuests", ...options }),
|
|
36
|
+
roles: (roles: readonly Role[], options: PublicOptions = {}): AppAccessRule & { kind: "roles" } => ({ kind: "roles", roles, ...options }),
|
|
37
|
+
grants: (
|
|
38
|
+
options: PublicOptions & { until?: FixedAppAccessRule; canManage?: AppAccessDeclaration["canManage"] } = {},
|
|
39
|
+
): AppAccessDeclaration => ({ kind: "grants", ...options }),
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export const compileAppAccess = (declaration: AppAccessDeclaration | undefined): AppAccessRule | undefined => {
|
|
43
|
+
if (!declaration) return undefined;
|
|
44
|
+
const { canManage, ...rule } = declaration;
|
|
45
|
+
if (canManage && (rule.kind !== "grants" || typeof canManage !== "function"))
|
|
46
|
+
throw new Error("Only configurable app access accepts canManage");
|
|
47
|
+
return AppAccessRuleSchema.parse(rule);
|
|
48
|
+
};
|
|
@@ -360,14 +360,14 @@ const actionFields = {
|
|
|
360
360
|
createdAt: DateTime,
|
|
361
361
|
expiresAt: DateTime,
|
|
362
362
|
};
|
|
363
|
-
/** One inbox row. `sentence` is the start of the
|
|
363
|
+
/** One inbox row. `sentence` is the start of the plain question; read the Action for everything the person approves. */
|
|
364
364
|
export const AppActionSummarySchema = z.object({ ...actionFields, sentence: PlainText(APP_ACTION_LIMITS.summaryChars).min(1) }).strict();
|
|
365
365
|
export const AppActionViewSchema = z
|
|
366
366
|
.object({
|
|
367
367
|
...actionFields,
|
|
368
|
-
/**
|
|
368
|
+
/** The plain Assistant question, in the originating turn's language. */
|
|
369
369
|
sentence: PlainText(APP_ACTION_LIMITS.sentenceChars).min(1),
|
|
370
|
-
/** The
|
|
370
|
+
/** The question's reason line, or `null` for older snapshots. */
|
|
371
371
|
message: PlainText(APP_ACTION_LIMITS.messageChars).nullable(),
|
|
372
372
|
details: z
|
|
373
373
|
.array(
|