@k2b/cloud 0.42.0 → 0.44.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 +13 -5
- package/scripts/app-favicon.ts +7 -6
- package/scripts/build.ts +3 -0
- package/scripts/installable-assets.ts +61 -0
- package/scripts/preload.ts +3 -0
- package/src/_internal/app-access-path.ts +12 -0
- package/src/_internal/app-access-request.ts +13 -0
- package/src/_internal/app-access-resources.ts +141 -0
- package/src/_internal/app-event-catalog.ts +40 -0
- package/src/_internal/app-event-schemas.ts +127 -0
- package/src/_internal/application-package.ts +40 -0
- package/src/_internal/built-in-app-ids.json +27 -0
- package/src/_internal/capabilities.ts +185 -12
- package/src/_internal/capability-origin.ts +4 -0
- package/src/_internal/capability-streams.ts +10 -5
- package/src/_internal/define-app.ts +159 -45
- package/src/_internal/installable-assets.ts +90 -0
- package/src/_internal/installable-head.ts +22 -0
- package/src/_internal/object-store-area.ts +9 -0
- package/src/_internal/object-store-config.ts +101 -0
- package/src/_internal/object-store-key.ts +14 -0
- package/src/_internal/page-responses.ts +34 -3
- package/src/_internal/process-identity.ts +7 -2
- package/src/_internal/registry-validation.ts +15 -1
- package/src/_internal/registry.ts +44 -1
- package/src/_internal/runtime-context.ts +9 -1
- package/src/_internal/runtime-watcher.ts +8 -0
- package/src/_internal/status-preserving-ssr.ts +12 -1
- package/src/access/AppAccessEditor.browser-fixture.ts +429 -0
- package/src/access/AppAccessEditor.browser-frame.tsx +68 -0
- package/src/access/AppAccessEditor.browser-harness.tsx +65 -0
- package/src/access/AppAccessEditor.tsx +380 -0
- package/src/access/PermissionEditor.browser-harness.tsx +22 -2
- package/src/access/PermissionEditor.tsx +386 -77
- package/src/access/PrincipalAvatar.tsx +63 -0
- package/src/access/PrincipalPicker.tsx +29 -8
- package/src/access/ResourceApiKeys.tsx +3 -4
- package/src/access/app-access-adapter.ts +51 -0
- package/src/access/app-access-messages.ts +172 -0
- package/src/access/messages.ts +14 -0
- package/src/access/ui.ts +4 -1
- package/src/account/Avatar.tsx +21 -3
- package/src/account/ProfilePictureDialog.tsx +699 -0
- package/src/account/profile-picture-messages.ts +269 -0
- package/src/account/ui.ts +8 -1
- package/src/ai/admin.ts +3 -0
- package/src/ai/app-frame.ts +12 -0
- package/src/ai/application-tasks.ts +101 -0
- package/src/ai/assistant-models.ts +126 -64
- package/src/ai/capabilities.ts +19 -4
- package/src/ai/capability-execution.ts +5 -2
- package/src/ai/chart-block.ts +63 -30
- package/src/ai/chat/chart-block.tsx +12 -3
- package/src/ai/chat/composer-adapter.ts +8 -7
- package/src/ai/chat/message-actions.tsx +1 -1
- package/src/ai/chat/message-utils.ts +2 -2
- package/src/ai/chat/presentation.tsx +22 -1
- package/src/ai/chat-quotas.ts +5 -4
- package/src/ai/chat-tasks.ts +2 -2
- package/src/ai/client/controller.ts +11 -10
- package/src/ai/client/projection.ts +3 -3
- package/src/ai/code-capability-routes.ts +1 -0
- package/src/ai/compaction.ts +3 -3
- package/src/ai/credentials.ts +13 -114
- package/src/ai/default-tool-contracts.ts +4 -3
- package/src/ai/dictation-runtime.ts +4 -4
- package/src/ai/enrich.ts +2 -2
- package/src/ai/executor.ts +38 -27
- package/src/ai/http.ts +17 -29
- package/src/ai/index.ts +11 -5
- package/src/ai/inference-calls.ts +6 -4
- package/src/ai/maintenance.ts +1 -1
- package/src/ai/memory-learning-runs.ts +5 -5
- package/src/ai/memory-learning.ts +3 -3
- package/src/ai/message-queue.ts +1 -2
- package/src/ai/migrate.ts +75 -42
- package/src/ai/model-access-migrate.ts +3 -17
- package/src/ai/model-access.ts +44 -85
- package/src/ai/model-card.tsx +134 -0
- package/src/ai/model-catalog.ts +359 -0
- package/src/ai/model-chooser-messages.ts +88 -0
- package/src/ai/model-chooser.tsx +577 -0
- package/src/ai/model-delete.ts +234 -0
- package/src/ai/model-fixture.ts +39 -0
- package/src/ai/model-icon.tsx +36 -0
- package/src/ai/model-overview.ts +53 -0
- package/src/ai/model-usage.ts +45 -0
- package/src/ai/openai-api-messages.ts +0 -4
- package/src/ai/openai-api-slots.ts +10 -2
- package/src/ai/openai-api.ts +83 -16
- package/src/ai/personal-agent.ts +1 -1
- package/src/ai/prefs.ts +12 -10
- package/src/ai/projects-routes.ts +2 -1
- package/src/ai/projects.ts +13 -12
- package/src/ai/protocol.ts +2 -2
- package/src/ai/provider-connections.ts +161 -0
- package/src/ai/provider-errors.ts +138 -0
- package/src/ai/provider-fetch.ts +4 -0
- package/src/ai/provider-retry.ts +1 -1
- package/src/ai/provider.ts +2 -1
- package/src/ai/quota-limits.ts +36 -0
- package/src/ai/quota-provider.ts +11 -2
- package/src/ai/quota-report.ts +7 -4
- package/src/ai/quotas-migrate.ts +1 -0
- package/src/ai/quotas.ts +19 -8
- package/src/ai/recent-resources.ts +6 -0
- package/src/ai/request-headers.ts +14 -87
- package/src/ai/resource-refs.ts +6 -2
- package/src/ai/routes.ts +17 -18
- package/src/ai/runtime.ts +50 -22
- package/src/ai/settings.ts +93 -580
- package/src/ai/store.ts +45 -23
- package/src/ai/stream.ts +1 -1
- package/src/ai/structured-runs.ts +2 -2
- package/src/ai/structured.ts +29 -46
- package/src/ai/task-contracts.ts +25 -5
- package/src/ai/task-execution.ts +15 -6
- package/src/ai/task-model.ts +9 -0
- package/src/ai/transcription.ts +37 -21
- package/src/ai/turn-failure.ts +24 -2
- package/src/ai/types.ts +50 -62
- package/src/ai/ui.tsx +13 -0
- package/src/ai/usage.ts +8 -8
- package/src/ai/validate.ts +8 -7
- package/src/ai/view-block.ts +2 -2
- package/src/api/accounts-entities.ts +40 -16
- package/src/api/admin-ai-quotas.ts +34 -56
- package/src/api/admin-ai-usage.ts +1 -1
- package/src/api/admin-ai.ts +173 -0
- package/src/api/admin-core-settings.ts +68 -244
- package/src/api/admin-directory-shares.ts +55 -0
- package/src/api/admin-identity.ts +81 -2
- package/src/api/app-access.ts +85 -0
- package/src/api/app-approval.ts +3 -1
- package/src/api/app-event-delivery.ts +51 -0
- package/src/api/app-events.ts +123 -0
- package/src/api/app-resource-access.ts +48 -0
- package/src/api/auth/account-mismatch.ts +24 -0
- package/src/api/auth.ts +26 -3
- package/src/api/capabilities.ts +127 -15
- package/src/api/capability-streams.ts +2 -5
- package/src/api/identity-child-invocation.ts +145 -0
- package/src/api/index.ts +11 -2
- package/src/api/mcp.ts +180 -5
- package/src/api/me.ts +38 -17
- package/src/api/pwa-phone.ts +87 -48
- package/src/api/pwa.ts +34 -8
- package/src/api/resource-links.ts +83 -6
- package/src/api/resource-resolve.ts +50 -0
- package/src/api/search/schemas.ts +3 -1
- package/src/api/search.ts +9 -7
- package/src/api/widgets.ts +1 -0
- package/src/app-pwa/authority.ts +75 -0
- package/src/app-pwa/contracts.ts +177 -0
- package/src/app-pwa/core-query-error.ts +20 -0
- package/src/app-pwa/index.ts +23 -0
- package/src/app-pwa/protocol.ts +27 -0
- package/src/app-pwa/schema.ts +59 -0
- package/src/app-pwa/service.ts +861 -0
- package/src/app-pwa/transaction.ts +338 -0
- package/src/app-pwa/transport-input.ts +57 -0
- package/src/app-pwa/transport.ts +235 -0
- package/src/approvals/messages.ts +2 -2
- package/src/browser/app-access-recovery.ts +44 -0
- package/src/browser/app-session.ts +14 -3
- package/src/browser/cloud-id.ts +86 -0
- package/src/browser/live-websocket.ts +5 -0
- package/src/browser/pwa-drafts.ts +57 -0
- package/src/browser/resource-clipboard.ts +4 -0
- package/src/browser/resource-links.ts +52 -0
- package/src/browser/resource-picker.tsx +3 -2
- package/src/capabilities/calendar-sources.ts +302 -0
- package/src/capabilities/executions.ts +10 -3
- package/src/capabilities/migrate.ts +4 -0
- package/src/capabilities/nested.ts +43 -0
- package/src/capabilities/server-invoke.ts +202 -0
- package/src/capabilities/server.ts +26 -187
- package/src/capabilities/testing.ts +5 -2
- package/src/cli/access.ts +6 -5
- package/src/cli/account.ts +57 -0
- package/src/cli/admin/agents.ts +66 -0
- package/src/cli/admin/ai-models.ts +199 -0
- package/src/cli/admin/ai-quotas.ts +6 -102
- package/src/cli/admin/ai-usage.ts +4 -4
- package/src/cli/admin/app-credentials.ts +5 -3
- package/src/cli/admin/index.ts +7 -3
- package/src/cli/app-access.ts +60 -0
- package/src/cli/apps.ts +89 -26
- package/src/cli/avatar.ts +64 -0
- package/src/cli/capabilities.ts +22 -1
- package/src/cli/events.ts +320 -0
- package/src/cli/index.ts +1 -1
- package/src/config/env.ts +40 -0
- package/src/contracts/app-access.ts +52 -5
- package/src/contracts/app-approval.ts +8 -1
- package/src/contracts/app-details.ts +89 -0
- package/src/contracts/app-events.ts +51 -0
- package/src/contracts/app.ts +24 -5
- package/src/contracts/calendar-source.ts +274 -0
- package/src/contracts/calendar-view.ts +35 -0
- package/src/contracts/capabilities.ts +164 -3
- package/src/contracts/geo-admin.ts +2 -0
- package/src/contracts/geo-route.ts +0 -11
- package/src/contracts/geo.ts +14 -2
- package/src/contracts/index.ts +6 -0
- package/src/contracts/installable.ts +40 -0
- package/src/contracts/notification-types.ts +36 -9
- package/src/contracts/profile.ts +32 -0
- package/src/contracts/pwa-builds.ts +14 -0
- package/src/contracts/pwa-location.ts +38 -0
- package/src/contracts/pwa.ts +24 -3
- package/src/contracts/registry.ts +8 -0
- package/src/contracts/resource-clipboard.ts +35 -0
- package/src/contracts/resource-pages.ts +140 -0
- package/src/contracts/resource-resolution.ts +13 -0
- package/src/contracts/shared.ts +4 -0
- package/src/contracts/user-notifications.ts +2 -0
- package/src/events/app-event-delivery.ts +100 -0
- package/src/events/app-events.testing.ts +65 -0
- package/src/events/app-events.ts +134 -0
- package/src/events/core-user-created.ts +24 -0
- package/src/events/index.ts +9 -0
- package/src/events/testing.ts +2 -0
- package/src/index.ts +2 -1
- package/src/server/api/respond.ts +8 -3
- package/src/server/index.ts +1 -0
- package/src/server/middleware/app-access.ts +7 -6
- package/src/server/middleware/auth.ts +168 -66
- package/src/server/middleware/index.ts +1 -0
- package/src/server/middleware/invocation.ts +4 -2
- package/src/server/middleware/workload.ts +3 -3
- package/src/server/oauth-scope.ts +10 -0
- package/src/server/public-storage.ts +106 -0
- package/src/server/services/access.ts +8 -0
- package/src/services/accounts/app.ts +35 -3
- package/src/services/accounts/avatar.ts +342 -35
- package/src/services/accounts/base-group.ts +4 -5
- package/src/services/accounts/directory-shares.ts +157 -0
- package/src/services/accounts/entities.ts +50 -6
- package/src/services/accounts/groups.ts +3 -1
- package/src/services/accounts/identities.ts +64 -12
- package/src/services/accounts/local-groups.ts +1 -1
- package/src/services/accounts/users.ts +23 -15
- package/src/services/app-access-management.ts +519 -0
- package/src/services/app-access.ts +25 -7
- package/src/services/app-approval.ts +15 -4
- package/src/services/app-details.ts +224 -0
- package/src/services/app-discovery.ts +2 -1
- package/src/services/app-event-shapes.ts +139 -0
- package/src/services/app-events/accept.ts +116 -0
- package/src/services/app-events/admin.ts +250 -0
- package/src/services/app-events/declarations.ts +187 -0
- package/src/services/app-events/deliver.ts +218 -0
- package/src/services/app-events.ts +92 -0
- package/src/services/config-conflict.ts +11 -0
- package/src/services/core-user-events.ts +1 -0
- package/src/services/freeipa-config.ts +9 -0
- package/src/services/geo/admin.ts +4 -1
- package/src/services/geo/ledger.ts +47 -14
- package/src/services/geo/route-chain.ts +40 -18
- package/src/services/geo/route-estimate.ts +19 -0
- package/src/services/geo/route-plan.ts +0 -2
- package/src/services/geo/route-service.ts +57 -39
- package/src/services/geo/service.ts +51 -25
- package/src/services/identity/core-origin.ts +12 -0
- package/src/services/identity/index.ts +2 -1
- package/src/services/identity/invocation-actor.ts +50 -2
- package/src/services/identity/invocation-authority.ts +38 -1
- package/src/services/identity/invocation-token.ts +255 -50
- package/src/services/identity/issue-invocation.ts +9 -1
- package/src/services/identity/models-only-auth.ts +13 -4
- package/src/services/identity/nested-invocation.ts +128 -0
- package/src/services/image-metadata.ts +65 -1
- package/src/services/index.ts +24 -1
- package/src/services/ipa/sync.ts +6 -2
- package/src/services/ipa/users.ts +4 -1
- package/src/services/mandates/policy.ts +19 -12
- package/src/services/notifications/browser.ts +4 -0
- package/src/services/notifications/catalog.ts +5 -2
- package/src/services/notifications/platform.ts +7 -2
- package/src/services/notifications/user.ts +18 -7
- package/src/services/oauth-tokens.ts +28 -2
- package/src/services/object-store.ts +217 -0
- package/src/services/providers/local/users.ts +5 -0
- package/src/services/pwa-devices.ts +147 -37
- package/src/services/service-account-credentials.ts +5 -2
- package/src/services/session/index.ts +14 -0
- package/src/services/settings/core-settings.ts +91 -47
- package/src/services/settings/index.ts +8 -0
- package/src/services/settings/versions.ts +26 -0
- package/src/shared/ai-models.ts +274 -0
- package/src/shared/ai-quotas.ts +2 -2
- package/src/shared/ai-usage.ts +2 -2
- package/src/shared/index.ts +2 -0
- package/src/shared-text/documents.ts +25 -8
- package/src/shared-text/log.ts +1 -1
- package/src/shared-text/text-diff.ts +75 -0
- package/src/ssr/AdminArea.tsx +131 -0
- package/src/ssr/AdminLayout.tsx +11 -3
- package/src/ssr/AdminSidebar.tsx +7 -7
- package/src/ssr/AppAccessDenied.tsx +114 -0
- package/src/ssr/AppBadge.tsx +14 -0
- package/src/ssr/AppLaunchpadPanel.tsx +1 -1
- package/src/ssr/AppPwaRuntime.island.tsx +82 -0
- package/src/ssr/CloudShell.island.tsx +25 -4
- package/src/ssr/Layout.tsx +15 -7
- package/src/ssr/PwaLayout.tsx +73 -8
- package/src/ssr/PwaPartCount.island.tsx +23 -0
- package/src/ssr/PwaRuntime.island.tsx +85 -6
- package/src/ssr/SettingsSaveOutcome.tsx +58 -0
- package/src/ssr/SettingsSaveStatus.island.tsx +16 -0
- package/src/ssr/ShellRuntime.island.tsx +5 -0
- package/src/ssr/__snapshots__/Layout.unchanged.render.test.tsx.snap +3 -3
- package/src/ssr/__snapshots__/LayoutHelp.ssr.render.test.tsx.snap +1 -1
- package/src/ssr/admin-active-link.ts +16 -0
- package/src/ssr/admin-navigation.ts +26 -20
- package/src/ssr/app-badges.ts +31 -7
- package/src/ssr/app-pwa-document.tsx +16 -0
- package/src/ssr/app-pwa-messages.ts +17 -0
- package/src/ssr/index.ts +8 -0
- package/src/ssr/layout-context.ts +1 -1
- package/src/ssr/page-trail-runtime.ts +3 -1
- package/src/ssr/platform-messages.ts +9 -1
- package/src/ssr/pwa-messages.ts +10 -2
- package/src/ssr/rail-navigation.ts +5 -1
- package/src/ssr/settings-draft.ts +153 -0
- package/src/ssr/shell-data.tsx +2 -0
- package/src/styles/global.css +1 -0
- package/src/styles/profile-picture.css +270 -0
- package/src/styles/utilities-navigation.css +6 -0
- package/src/workflows/ai/policy.ts +49 -0
- package/src/workflows/ai/runtime.ts +131 -21
- package/src/workflows/ai/store.ts +283 -35
- package/src/workflows/ai/types.ts +8 -2
- package/src/workflows/ai-actions.ts +148 -120
- package/src/workflows/authoring.ts +31 -0
- package/src/workflows/contracts.ts +1 -0
- package/src/workflows/definition.ts +2 -0
- package/src/workflows/editor.ts +15 -0
- package/src/workflows/index.ts +2 -1
- package/src/workflows/language/compiler.ts +34 -3
- package/src/workflows/runtime/executor.ts +132 -33
- package/src/workflows/runtime/index.ts +2 -0
- package/src/workflows/runtime/ports.ts +5 -1
- package/src/workflows/store/actions.ts +1 -1
- package/src/workflows/store/definitions.ts +5 -0
- package/src/workflows/store/runs.ts +5 -3
- package/src/workflows/store/worker.ts +7 -2
- package/src/ai/model-pricing.ts +0 -43
- package/src/ai/model-request-settings.ts +0 -98
- package/src/ai/settings-messages.ts +0 -129
- package/src/shared/ai-model-request-settings.ts +0 -21
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { defineCapabilities } from "../contracts/capabilities";
|
|
3
|
+
import { appEvents, bindAppEvents } from "./app-events";
|
|
4
|
+
|
|
5
|
+
export const coreUserEventTypes = {
|
|
6
|
+
user: { title: "User", description: "A Cloud user identity.", icon: "ti ti-user" },
|
|
7
|
+
} as const;
|
|
8
|
+
export const coreUserEvents = {
|
|
9
|
+
"user.created": {
|
|
10
|
+
version: 1,
|
|
11
|
+
title: "User created",
|
|
12
|
+
description: "A user identity was first created in Cloud.",
|
|
13
|
+
subject: "user",
|
|
14
|
+
schema: z.object({ userId: z.uuid(), provider: z.enum(["local", "ipa"]) }).strict(),
|
|
15
|
+
},
|
|
16
|
+
} as const;
|
|
17
|
+
const declarations = defineCapabilities({ protocolVersion: 2, types: coreUserEventTypes, events: coreUserEvents });
|
|
18
|
+
const publisher = appEvents(declarations);
|
|
19
|
+
|
|
20
|
+
/** Creation services explicitly bind Core's shared event declarations before publishing. */
|
|
21
|
+
export const coreUserEventPublisher = () => {
|
|
22
|
+
bindAppEvents("core", declarations);
|
|
23
|
+
return publisher;
|
|
24
|
+
};
|
package/src/events/index.ts
CHANGED
|
@@ -1,2 +1,11 @@
|
|
|
1
|
+
export type {
|
|
2
|
+
AppEventDefinition,
|
|
3
|
+
AppEventDeliveryContext,
|
|
4
|
+
AppEventEnvelope,
|
|
5
|
+
AppEventSubscriptionDefinition,
|
|
6
|
+
} from "../contracts/app-events";
|
|
7
|
+
export { APP_EVENT_MAX_BYTES, AppEventEnvelopeSchema, appEventSubscriptionId } from "../contracts/app-events";
|
|
8
|
+
|
|
9
|
+
export { appEvents } from "./app-events";
|
|
1
10
|
export type { LiveChannel, LiveViewer } from "./live";
|
|
2
11
|
export { defineLive } from "./live";
|
package/src/events/testing.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { CursorMismatchError, RetentionGapError, type TopicEvent } from "@k2b/sync";
|
|
2
2
|
import type { LiveEnvelope, LiveTopic } from "./live-engine";
|
|
3
3
|
|
|
4
|
+
export { appEventFixture, createAppEventTestHarness, deliverAppEvent } from "./app-events.testing";
|
|
5
|
+
|
|
4
6
|
/** Publishes an application's real outbox without starting its application. */
|
|
5
7
|
export { startLiveOutbox } from "./live";
|
|
6
8
|
export {
|
package/src/index.ts
CHANGED
|
@@ -36,9 +36,10 @@ export type { RuntimeCompatibilityIssue } from "./_internal/runtime-compatibilit
|
|
|
36
36
|
export { assessRuntimeCompatibility } from "./_internal/runtime-compatibility";
|
|
37
37
|
export { buildRuntimeFromRegistry } from "./_internal/runtime-context";
|
|
38
38
|
export { type AppSkillDefinition, skill } from "./ai/app-skills";
|
|
39
|
-
export type { AppAccessDeclaration, AppAccessRule, FixedAppAccessRule } from "./contracts/app-access";
|
|
39
|
+
export type { AppAccessDeclaration, AppAccessRule, FixedAppAccessRule, ResourceAppAccessAdapter } from "./contracts/app-access";
|
|
40
40
|
export { appAccess } from "./contracts/app-access";
|
|
41
41
|
export { defineCapabilities } from "./contracts/capabilities";
|
|
42
|
+
export type { AppInstallable } from "./contracts/installable";
|
|
42
43
|
export type {
|
|
43
44
|
AnyBoundNotificationDefinition,
|
|
44
45
|
BoundNotificationDefinition,
|
|
@@ -43,6 +43,11 @@ const toErrorResponse = (result: AnyResult): [ApiErrorBody, number] => {
|
|
|
43
43
|
return [{ message: "Internal server error", code: "INTERNAL" }, 500];
|
|
44
44
|
};
|
|
45
45
|
|
|
46
|
+
export async function respond<T>(
|
|
47
|
+
c: Context,
|
|
48
|
+
resultOrFn: ResultOrFn<AnyResult<T>>,
|
|
49
|
+
successStatus: 202,
|
|
50
|
+
): Promise<JsonTypedResponse<T, 202> | ApiErrorResponse>;
|
|
46
51
|
export async function respond<E extends ServiceError>(
|
|
47
52
|
c: Context,
|
|
48
53
|
resultOrFn: ResultOrFn<Result<never, E>>,
|
|
@@ -71,8 +76,8 @@ export async function respond<T>(
|
|
|
71
76
|
export async function respond<T>(
|
|
72
77
|
c: Context,
|
|
73
78
|
resultOrFn: ResultOrFn<AnyResult<T>>,
|
|
74
|
-
successStatus: SuccessStatus = 200,
|
|
75
|
-
): Promise<JsonTypedResponse<T, SuccessStatus> | ApiErrorResponse> {
|
|
79
|
+
successStatus: SuccessStatus | 202 = 200,
|
|
80
|
+
): Promise<JsonTypedResponse<T, SuccessStatus | 202> | ApiErrorResponse> {
|
|
76
81
|
const result = typeof resultOrFn === "function" ? await resultOrFn() : await resultOrFn;
|
|
77
82
|
|
|
78
83
|
if (!result.ok) {
|
|
@@ -80,7 +85,7 @@ export async function respond<T>(
|
|
|
80
85
|
return c.json(body, status as ApiErrorStatus) as ApiErrorResponse;
|
|
81
86
|
}
|
|
82
87
|
|
|
83
|
-
return c.json(result.data, successStatus
|
|
88
|
+
return c.json(result.data, successStatus) as JsonTypedResponse<T, SuccessStatus | 202>;
|
|
84
89
|
}
|
|
85
90
|
|
|
86
91
|
export const respondMessage = (c: Context, resultPromise: Promise<Result<void>>, message: string) =>
|
package/src/server/index.ts
CHANGED
|
@@ -8,7 +8,7 @@ import type { AppMeta } from "../../contracts/app";
|
|
|
8
8
|
import { appAccessAuthority, canUseApp } from "../../services/app-access";
|
|
9
9
|
import { recordAppAccessDenied } from "../../services/app-access-metrics";
|
|
10
10
|
import { createLoginRedirectUrl } from "../../shared/redirect";
|
|
11
|
-
import { type AuthContext, auth, type RequestAuthority } from "./auth";
|
|
11
|
+
import { type AuthContext, auth, loadAuthenticatedActor, type RequestAuthority } from "./auth";
|
|
12
12
|
|
|
13
13
|
type AccessApp = Pick<AppMeta, "id" | "access"> & { name?: string };
|
|
14
14
|
export const APP_ACCESS_RECHECK_MS = 50_000;
|
|
@@ -22,7 +22,7 @@ export const stopAppAccessConnections = (): void => {
|
|
|
22
22
|
export const appAccessDeniedBody = (app: AccessApp) => ({
|
|
23
23
|
code: "app_access_denied" as const,
|
|
24
24
|
app: app.id,
|
|
25
|
-
message: `You cannot use ${app.name ?? app.id}. ${app.access?.kind === "grants" ? "Ask someone who manages access to this app." : "Ask your Cloud administrator about access."}`,
|
|
25
|
+
message: `You cannot use ${app.name ?? app.id}. ${app.access?.kind === "grants" ? "Ask someone who manages access to this app." : app.access?.kind === "resources" ? "Ask someone who manages resource permissions in this app." : "Ask your Cloud administrator about access."}`,
|
|
26
26
|
});
|
|
27
27
|
|
|
28
28
|
export class AppAccessDeniedError extends Error {
|
|
@@ -280,10 +280,10 @@ export const requireAppAccess = (
|
|
|
280
280
|
const isPublic = appPublicRouteMatcher(app);
|
|
281
281
|
return createMiddleware<AuthContext>(async (c, next) => {
|
|
282
282
|
if (!app.access || isPublic(c.req.path)) return next();
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
283
|
+
// The framework checks application entry before the app router is matched.
|
|
284
|
+
// Route-level OAuth policy belongs to the app authentication middleware.
|
|
285
|
+
await loadAuthenticatedActor(c);
|
|
286
|
+
const resolved = appAccessAuthority(c);
|
|
287
287
|
const browser =
|
|
288
288
|
c.req.method === "GET" && c.req.header("accept")?.includes("text/html") && c.req.header("upgrade")?.toLowerCase() !== "websocket";
|
|
289
289
|
if (!resolved) {
|
|
@@ -304,6 +304,7 @@ export const requireAppAccess = (
|
|
|
304
304
|
const body = appAccessDeniedBody(app);
|
|
305
305
|
const response = browser ? await renderDenied(c, body.message) : c.json(body, 403);
|
|
306
306
|
response.headers.set("Cache-Control", "private, no-store");
|
|
307
|
+
response.headers.set("X-Cloud-App-Access-Denied", app.id);
|
|
307
308
|
return response;
|
|
308
309
|
}
|
|
309
310
|
await next();
|
|
@@ -1,5 +1,8 @@
|
|
|
1
|
-
import type { Context } from "hono";
|
|
1
|
+
import type { Context, Env, MiddlewareHandler } from "hono";
|
|
2
2
|
import { createMiddleware } from "hono/factory";
|
|
3
|
+
import { matchedRoutes } from "hono/route";
|
|
4
|
+
import { findTargetHandler } from "hono/utils/handler";
|
|
5
|
+
import { bindAppAccessRequest } from "../../_internal/app-access-request";
|
|
3
6
|
import { measureServerPhase } from "../../_internal/server-timing";
|
|
4
7
|
import {
|
|
5
8
|
AI_INFERENCE_SCOPE,
|
|
@@ -29,6 +32,7 @@ export type InvocationProvenance = {
|
|
|
29
32
|
callingAppId: string;
|
|
30
33
|
credentialKind: "session" | "oauth" | "api_key" | "mandate";
|
|
31
34
|
invocationId: string;
|
|
35
|
+
parentInvocationId?: string;
|
|
32
36
|
requestId?: string | null;
|
|
33
37
|
mandateId?: string;
|
|
34
38
|
mandateRevision?: number;
|
|
@@ -55,6 +59,8 @@ export type ServiceAccountRequestActor =
|
|
|
55
59
|
delegatedUser: User;
|
|
56
60
|
scopes: string[];
|
|
57
61
|
credentialId?: string | null;
|
|
62
|
+
/** Public OAuth client identifier; safe to retain, never the bearer token. */
|
|
63
|
+
oauthClientId?: string | null;
|
|
58
64
|
credentialExpiresAt?: string | null;
|
|
59
65
|
delegation?: InvocationProvenance;
|
|
60
66
|
}
|
|
@@ -64,6 +70,8 @@ export type ServiceAccountRequestActor =
|
|
|
64
70
|
delegatedUser: null;
|
|
65
71
|
scopes: string[];
|
|
66
72
|
credentialId?: string | null;
|
|
73
|
+
/** Public OAuth client identifier; safe to retain, never the bearer token. */
|
|
74
|
+
oauthClientId?: string | null;
|
|
67
75
|
credentialExpiresAt?: string | null;
|
|
68
76
|
delegation?: InvocationProvenance;
|
|
69
77
|
};
|
|
@@ -72,7 +80,14 @@ export type RequestActor = UserRequestActor | ServiceAccountRequestActor;
|
|
|
72
80
|
|
|
73
81
|
export type RequestCredentialKind = "session" | "oauth" | "api_key" | "invocation";
|
|
74
82
|
|
|
83
|
+
/** Core keeps only revocation coordinates, never the original bearer credential. */
|
|
84
|
+
export type RequestCredentialProof =
|
|
85
|
+
| { kind: "session"; userId: string; sid: string; authEpoch: number; expiresAt: string }
|
|
86
|
+
| { kind: "oauth"; payload: import("jose").JWTPayload }
|
|
87
|
+
| { kind: "api_key"; credentialId: string };
|
|
88
|
+
|
|
75
89
|
export type RequestAuthority = {
|
|
90
|
+
credentialProof?: RequestCredentialProof;
|
|
76
91
|
actor: RequestActor;
|
|
77
92
|
accessSubject: AccessSubject;
|
|
78
93
|
credentialKind: RequestCredentialKind;
|
|
@@ -92,6 +107,7 @@ export type AuthContext = {
|
|
|
92
107
|
sessionKind?: SessionKind;
|
|
93
108
|
/** Constraints carried by OAuth/API credentials; absent sessions are unrestricted by transport scope. */
|
|
94
109
|
credentialScopes?: string[];
|
|
110
|
+
credentialProof?: RequestCredentialProof;
|
|
95
111
|
/** OAuth scopes for bearer-token requests. Absent for sessions and API credentials. */
|
|
96
112
|
oauthScopes?: string[];
|
|
97
113
|
};
|
|
@@ -159,6 +175,13 @@ const loadAuthenticatedActorUncached = async (
|
|
|
159
175
|
c.set("sessionToken", token);
|
|
160
176
|
c.set("credentialKind", "session");
|
|
161
177
|
c.set("sessionKind", authenticatedSession.data.kind);
|
|
178
|
+
c.set("credentialProof", {
|
|
179
|
+
kind: "session",
|
|
180
|
+
userId: user.id,
|
|
181
|
+
sid: authenticatedSession.data.sid,
|
|
182
|
+
authEpoch: authenticatedSession.data.authEpoch,
|
|
183
|
+
expiresAt: authenticatedSession.data.expiresAt,
|
|
184
|
+
});
|
|
162
185
|
return { token, user, actor };
|
|
163
186
|
}
|
|
164
187
|
|
|
@@ -185,6 +208,7 @@ const loadAuthenticatedActorUncached = async (
|
|
|
185
208
|
};
|
|
186
209
|
c.set("actor", actor);
|
|
187
210
|
c.set("credentialKind", "api_key");
|
|
211
|
+
c.set("credentialProof", { kind: "api_key", credentialId: authResult.credential.id });
|
|
188
212
|
c.set("credentialScopes", authResult.credential.scopes);
|
|
189
213
|
if (authResult.delegatedUser) {
|
|
190
214
|
c.set("accessSubject", { type: "user", userId: authResult.delegatedUser.id });
|
|
@@ -200,6 +224,14 @@ const loadAuthenticatedActorUncached = async (
|
|
|
200
224
|
const authResult = await oauthTokens.verifyAccessToken(bearer, expectedAudience);
|
|
201
225
|
if (!authResult) return { token: null, user: null, actor: null };
|
|
202
226
|
|
|
227
|
+
c.set("credentialProof", {
|
|
228
|
+
kind: "oauth",
|
|
229
|
+
payload: Object.fromEntries(
|
|
230
|
+
["sub", "id", "uid", "client_id", "auth_epoch", "service_account_id", "scope", "exp"].flatMap((key) =>
|
|
231
|
+
authResult.payload[key] === undefined ? [] : [[key, authResult.payload[key]]],
|
|
232
|
+
),
|
|
233
|
+
),
|
|
234
|
+
});
|
|
203
235
|
if (authResult.kind === "user") {
|
|
204
236
|
if (isAccountExpired(authResult.user.accountExpires)) return { token: null, user: null, actor: null };
|
|
205
237
|
const actor: RequestActor = { kind: "user", user: authResult.user };
|
|
@@ -221,6 +253,7 @@ const loadAuthenticatedActorUncached = async (
|
|
|
221
253
|
delegatedUser: authResult.delegatedUser,
|
|
222
254
|
scopes: authResult.scopes,
|
|
223
255
|
credentialId: null,
|
|
256
|
+
oauthClientId: authResult.oauthClientId,
|
|
224
257
|
credentialExpiresAt: typeof authResult.payload.exp === "number" ? new Date(authResult.payload.exp * 1_000).toISOString() : null,
|
|
225
258
|
};
|
|
226
259
|
c.set("actor", actor);
|
|
@@ -239,7 +272,7 @@ const loadAuthenticatedActorUncached = async (
|
|
|
239
272
|
return { token: null, user: null, actor: null };
|
|
240
273
|
};
|
|
241
274
|
|
|
242
|
-
const loadAuthenticatedActor = (
|
|
275
|
+
export const loadAuthenticatedActor = (
|
|
243
276
|
c: Context<AuthContext>,
|
|
244
277
|
options: Pick<RoleOptions, "oauthAudience"> = {},
|
|
245
278
|
): Promise<AuthenticatedActorResult> => {
|
|
@@ -289,8 +322,24 @@ export const isRequestCredentialCurrent = async (
|
|
|
289
322
|
return false;
|
|
290
323
|
};
|
|
291
324
|
|
|
325
|
+
const readOnlyMiddlewares = new WeakSet<object>();
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Declare middleware that only authenticates, authorizes, validates or reads.
|
|
329
|
+
* A read-only route cannot exempt another unmarked ALL handler: Hono uses
|
|
330
|
+
* ALL for both `.use()` middleware and terminal `.all()` routes.
|
|
331
|
+
* This declaration never exempts an endpoint from the default write requirement.
|
|
332
|
+
*/
|
|
333
|
+
const readOnlyMiddleware = <E extends Env = AuthContext>(handler: MiddlewareHandler<E>): MiddlewareHandler<E> => {
|
|
334
|
+
readOnlyMiddlewares.add(handler);
|
|
335
|
+
return handler;
|
|
336
|
+
};
|
|
337
|
+
|
|
292
338
|
/**
|
|
293
339
|
* Universal auth middleware. Handles authentication AND authorization.
|
|
340
|
+
* OAuth requests require `write` or `admin` scope for methods other than GET,
|
|
341
|
+
* HEAD and OPTIONS, unless the route explicitly declares `auth.readOnly`.
|
|
342
|
+
* Browser sessions and personal API keys keep their existing authority.
|
|
294
343
|
*
|
|
295
344
|
* @param args - Roles to check (OR logic) + optional RoleOptions at the end. Special roles:
|
|
296
345
|
* - "*": No check, always passes (like optionalAuth)
|
|
@@ -320,45 +369,49 @@ const requireRole = (...args: (RoleOrSpecial | RoleOptions)[]) => {
|
|
|
320
369
|
const options: RoleOptions = hasOptions ? (args.pop() as RoleOptions) : {};
|
|
321
370
|
const roles = args as RoleOrSpecial[];
|
|
322
371
|
|
|
323
|
-
return
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
await loadAuthenticatedActor(c, options);
|
|
327
|
-
return next();
|
|
328
|
-
}
|
|
372
|
+
return readOnlyMiddleware(
|
|
373
|
+
createMiddleware<AuthContext>(async (c, next) => {
|
|
374
|
+
const continueRequest = () => requireOAuthMutationScope(c, next);
|
|
329
375
|
|
|
330
|
-
|
|
376
|
+
// "*" skips role checks but still resolves any credential and its scopes.
|
|
377
|
+
if (roles.includes("*")) {
|
|
378
|
+
await loadAuthenticatedActor(c, options);
|
|
379
|
+
return continueRequest();
|
|
380
|
+
}
|
|
331
381
|
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
382
|
+
const { user, actor } = await loadAuthenticatedActor(c, options);
|
|
383
|
+
|
|
384
|
+
// "anonymous" = must NOT be logged in
|
|
385
|
+
if (roles.includes("anonymous")) {
|
|
386
|
+
if (actor) {
|
|
387
|
+
return handleReject(c, options, "forbidden");
|
|
388
|
+
}
|
|
389
|
+
return continueRequest();
|
|
336
390
|
}
|
|
337
|
-
return next();
|
|
338
|
-
}
|
|
339
391
|
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
392
|
+
// All other roles require authentication
|
|
393
|
+
if (!actor) {
|
|
394
|
+
return handleReject(c, options, "unauthenticated");
|
|
395
|
+
}
|
|
344
396
|
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
397
|
+
// "authenticated" = any logged-in user
|
|
398
|
+
if (roles.includes("authenticated")) {
|
|
399
|
+
return continueRequest();
|
|
400
|
+
}
|
|
349
401
|
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
402
|
+
if (!user) {
|
|
403
|
+
return handleReject(c, options, "forbidden");
|
|
404
|
+
}
|
|
353
405
|
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
406
|
+
// Check if user has at least one required role
|
|
407
|
+
const hasRequiredRole = roles.some((role) => user.roles.includes(role as Role));
|
|
408
|
+
if (!hasRequiredRole) {
|
|
409
|
+
return handleReject(c, options, "forbidden");
|
|
410
|
+
}
|
|
359
411
|
|
|
360
|
-
|
|
361
|
-
|
|
412
|
+
return continueRequest();
|
|
413
|
+
}),
|
|
414
|
+
);
|
|
362
415
|
};
|
|
363
416
|
|
|
364
417
|
/**
|
|
@@ -376,35 +429,80 @@ const requireRole = (...args: (RoleOrSpecial | RoleOptions)[]) => {
|
|
|
376
429
|
* turns into further down.
|
|
377
430
|
*/
|
|
378
431
|
const requireUser = (options: RoleOptions = {}) =>
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
432
|
+
readOnlyMiddleware(
|
|
433
|
+
createMiddleware<AuthContext>(async (c, next) => {
|
|
434
|
+
const { user } = await loadAuthenticatedActor(c);
|
|
435
|
+
if (user) return requireOAuthMutationScope(c, next);
|
|
436
|
+
// A caller that reaches here is authenticated but has no user, so the
|
|
437
|
+
// generic "Insufficient permissions" would send them looking for a missing
|
|
438
|
+
// role. `onReject` still wins, which is what keeps SSR redirects working.
|
|
439
|
+
if (options.onReject) return handleReject(c, options, "forbidden");
|
|
440
|
+
return c.json({ message: "Self-service endpoints require a user-backed actor", code: "FORBIDDEN" }, 403);
|
|
441
|
+
}),
|
|
442
|
+
);
|
|
388
443
|
|
|
389
444
|
/** Require one OAuth scope when the caller uses OAuth. Sessions and API credentials are unaffected. */
|
|
390
445
|
const requireOAuthScope = (...requiredScopes: string[]) =>
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
446
|
+
readOnlyMiddleware(
|
|
447
|
+
createMiddleware<AuthContext>(async (c, next) => {
|
|
448
|
+
const scopes = c.get("oauthScopes");
|
|
449
|
+
if (!scopes || requiredScopes.some((scope) => scopes.includes(scope))) return next();
|
|
450
|
+
return c.json({ code: "FORBIDDEN", message: `OAuth scope ${requiredScopes.join(" or ")} is required` }, 403);
|
|
451
|
+
}),
|
|
452
|
+
);
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* Explicitly declare a read-only POST route. Register on the route itself,
|
|
456
|
+
* as its first handler, never with `.use()`: authentication can see the declaration before it runs.
|
|
457
|
+
* This does not replace resource authorization or make a handler read-only.
|
|
458
|
+
*/
|
|
459
|
+
const readOnly = requireOAuthScope("read", "admin");
|
|
460
|
+
|
|
461
|
+
/** Internal protocol dispatchers check scope against the resolved operation. */
|
|
462
|
+
export const oauthScopeByOperation = createMiddleware<AuthContext>(async (_c, next) => next());
|
|
463
|
+
|
|
464
|
+
export const requireOAuthMutationScope = createMiddleware<AuthContext>(async (c, next) => {
|
|
465
|
+
if (["GET", "HEAD", "OPTIONS"].includes(c.req.method)) return next();
|
|
466
|
+
// Only an explicit method registration can relax the default. A declaration
|
|
467
|
+
// accidentally mounted with `.use()` or on a wildcard cannot exempt a subtree.
|
|
468
|
+
const matched = matchedRoutes(c);
|
|
469
|
+
const lastMethod = matched.findLastIndex((route) => route.method !== "ALL");
|
|
470
|
+
const unguardedAllHandler = matched
|
|
471
|
+
.slice(c.req.routeIndex + 1, lastMethod)
|
|
472
|
+
.some((route) => route.method === "ALL" && !readOnlyMiddlewares.has(findTargetHandler(route.handler)));
|
|
473
|
+
const routes = matched.filter((route) => route.method !== "ALL");
|
|
474
|
+
const first = routes[0];
|
|
475
|
+
const handler = first && findTargetHandler(first.handler);
|
|
476
|
+
const declared =
|
|
477
|
+
!unguardedAllHandler &&
|
|
478
|
+
first?.method === c.req.method &&
|
|
479
|
+
!first.path.includes("*") &&
|
|
480
|
+
(handler === readOnly || handler === oauthScopeByOperation) &&
|
|
481
|
+
routes.every((route) => route.method === first.method && route.path === first.path);
|
|
482
|
+
if (declared) return handler === readOnly ? requireOAuthScope("read", "admin")(c, next) : next();
|
|
483
|
+
return requireOAuthScope("write", "admin")(c, next);
|
|
484
|
+
});
|
|
396
485
|
|
|
397
486
|
const getAuthority = <E extends AuthContext>(c: Context<E>): RequestAuthority => {
|
|
398
487
|
const actor = c.get("actor");
|
|
399
488
|
const accessSubject = c.get("accessSubject");
|
|
400
489
|
const credentialKind = c.get("credentialKind");
|
|
401
490
|
if (!actor || !accessSubject || !credentialKind) throw new Error("Request authority has not been resolved");
|
|
402
|
-
return
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
491
|
+
return bindAppAccessRequest(
|
|
492
|
+
{
|
|
493
|
+
actor,
|
|
494
|
+
accessSubject,
|
|
495
|
+
credentialKind,
|
|
496
|
+
scopes: [...(c.get("credentialScopes") ?? [])],
|
|
497
|
+
...(c.get("credentialProof") ? { credentialProof: c.get("credentialProof") } : {}),
|
|
498
|
+
},
|
|
499
|
+
c.req.raw,
|
|
500
|
+
credentialKind === "session"
|
|
501
|
+
? c.get("sessionToken")
|
|
502
|
+
: credentialKind !== "invocation"
|
|
503
|
+
? (session.getBearerToken(c) ?? undefined)
|
|
504
|
+
: undefined,
|
|
505
|
+
);
|
|
408
506
|
};
|
|
409
507
|
|
|
410
508
|
/**
|
|
@@ -444,23 +542,25 @@ const redirectToLogin: RoleOptions = {
|
|
|
444
542
|
};
|
|
445
543
|
|
|
446
544
|
const requireAccount = (options: AccountOptions) =>
|
|
447
|
-
|
|
448
|
-
|
|
545
|
+
readOnlyMiddleware(
|
|
546
|
+
createMiddleware<AuthContext>(async (c, next) => {
|
|
547
|
+
const { user } = await loadAuthenticatedActor(c);
|
|
449
548
|
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
549
|
+
if (!user) {
|
|
550
|
+
return handleReject(c, options, "unauthenticated");
|
|
551
|
+
}
|
|
453
552
|
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
553
|
+
if (options.provider && user.provider !== options.provider) {
|
|
554
|
+
return handleReject(c, options, "forbidden");
|
|
555
|
+
}
|
|
457
556
|
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
557
|
+
if (options.profile && user.profile !== options.profile) {
|
|
558
|
+
return handleReject(c, options, "forbidden");
|
|
559
|
+
}
|
|
461
560
|
|
|
462
|
-
|
|
463
|
-
|
|
561
|
+
return requireOAuthMutationScope(c, next);
|
|
562
|
+
}),
|
|
563
|
+
);
|
|
464
564
|
|
|
465
565
|
// ==========================
|
|
466
566
|
// Export
|
|
@@ -474,6 +574,8 @@ export const auth = {
|
|
|
474
574
|
browserSignIn,
|
|
475
575
|
rejectAppSession,
|
|
476
576
|
requireOAuthScope,
|
|
577
|
+
readOnly,
|
|
578
|
+
readOnlyMiddleware,
|
|
477
579
|
getAuthority,
|
|
478
580
|
requireAccount,
|
|
479
581
|
redirect,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Context, MiddlewareHandler } from "hono";
|
|
2
2
|
import { createMiddleware } from "hono/factory";
|
|
3
|
+
import { bindAppAccessRequest } from "../../_internal/app-access-request";
|
|
3
4
|
import { getApp } from "../../_internal/registry";
|
|
4
5
|
import type { AppMeta } from "../../contracts/app";
|
|
5
6
|
import { canUseApp } from "../../services/app-access";
|
|
@@ -7,7 +8,7 @@ import { recordAppAccessDenied } from "../../services/app-access-metrics";
|
|
|
7
8
|
import { type ResolvedInvocationAuthority, resolveInvocationAuthority } from "../../services/identity/invocation-actor";
|
|
8
9
|
import { type CloudInvocationClaims, isInvocationJwtCandidate, verifyInvocationToken } from "../../services/identity/invocation-token";
|
|
9
10
|
import { appAccessDeniedBody } from "./app-access";
|
|
10
|
-
import type
|
|
11
|
+
import { type AuthContext, requireOAuthMutationScope } from "./auth";
|
|
11
12
|
|
|
12
13
|
export type InvocationExpectation = {
|
|
13
14
|
targetAppId: string;
|
|
@@ -53,6 +54,7 @@ export const requireInvocation = (
|
|
|
53
54
|
if (!claims) return reject(c);
|
|
54
55
|
const authority = await (dependencies.resolve ?? resolveInvocationAuthority)(claims);
|
|
55
56
|
if (!authority) return reject(c);
|
|
57
|
+
bindAppAccessRequest(authority, c.req.raw);
|
|
56
58
|
const app = await (dependencies.getApp ?? getApp)(expectation.targetAppId).catch(() => null);
|
|
57
59
|
if (!app || !(await canUseApp(authority, app))) {
|
|
58
60
|
if (app) recordAppAccessDenied(app.id);
|
|
@@ -62,5 +64,5 @@ export const requireInvocation = (
|
|
|
62
64
|
return c.json({ code: "SCHEMA_MISMATCH", message: "The invocation schema no longer matches the target" }, 409);
|
|
63
65
|
}
|
|
64
66
|
installAuthority(c, claims, authority);
|
|
65
|
-
return next
|
|
67
|
+
return requireOAuthMutationScope(c, next);
|
|
66
68
|
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Context, MiddlewareHandler } from "hono";
|
|
2
2
|
import { createMiddleware } from "hono/factory";
|
|
3
3
|
import { hasReservedWorkloadScope } from "../../services/identity/workload-auth";
|
|
4
|
-
import type
|
|
4
|
+
import { type AuthContext, auth } from "./auth";
|
|
5
5
|
|
|
6
6
|
/** Reserved app workload keys are valid only at their dedicated Core authority routes. */
|
|
7
7
|
export const isReservedWorkloadCredential = (c: Context<AuthContext>): boolean => {
|
|
@@ -17,6 +17,6 @@ export const isReservedWorkloadCredential = (c: Context<AuthContext>): boolean =
|
|
|
17
17
|
const reservedWorkloadResponse = (c: Context<AuthContext>): Response =>
|
|
18
18
|
c.json({ code: "FORBIDDEN", message: "App workload credentials require a dedicated Core authority route" }, 403);
|
|
19
19
|
|
|
20
|
-
export const rejectReservedWorkloadCredential: MiddlewareHandler<AuthContext> =
|
|
21
|
-
isReservedWorkloadCredential(c) ? reservedWorkloadResponse(c) : await next(),
|
|
20
|
+
export const rejectReservedWorkloadCredential: MiddlewareHandler<AuthContext> = auth.readOnlyMiddleware(
|
|
21
|
+
createMiddleware<AuthContext>(async (c, next) => (isReservedWorkloadCredential(c) ? reservedWorkloadResponse(c) : await next())),
|
|
22
22
|
);
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { RequestAuthority } from "./middleware/auth";
|
|
2
|
+
|
|
3
|
+
/** Credential ceilings survive inter-app invocation; sessions and API keys retain their own contracts. */
|
|
4
|
+
export const hasOAuthScope = (authority: RequestAuthority | undefined, ...required: string[]): boolean => {
|
|
5
|
+
if (!authority) return true;
|
|
6
|
+
const oauth =
|
|
7
|
+
authority.credentialKind === "oauth" ||
|
|
8
|
+
(authority.credentialKind === "invocation" && authority.actor.delegation?.credentialKind === "oauth");
|
|
9
|
+
return !oauth || required.some((scope) => authority.scopes.includes(scope));
|
|
10
|
+
};
|