@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
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { isDeepStrictEqual } from "node:util";
|
|
1
3
|
import { createRemoteJWKSet, decodeProtectedHeader, errors, type JWTVerifyGetKey, jwtVerify, SignJWT } from "jose";
|
|
2
4
|
import { z } from "zod";
|
|
3
5
|
import {
|
|
@@ -9,6 +11,7 @@ import {
|
|
|
9
11
|
IDENTITY_MAX_COMPACT_TOKEN_BYTES,
|
|
10
12
|
INVOCATION_CLOCK_TOLERANCE_SECONDS,
|
|
11
13
|
} from "./constants";
|
|
14
|
+
import { authorityFromParentInvocation } from "./invocation-authority";
|
|
12
15
|
import { type PreparedIdentitySigner, prepareIdentitySigner } from "./key-ring";
|
|
13
16
|
import { identityMetrics } from "./metrics";
|
|
14
17
|
import { getIdentityRuntimeConfig } from "./runtime-config";
|
|
@@ -44,7 +47,7 @@ export const normalizeInvocationRequestId = (value: unknown): string | undefined
|
|
|
44
47
|
return parsed.success ? parsed.data : undefined;
|
|
45
48
|
};
|
|
46
49
|
|
|
47
|
-
const
|
|
50
|
+
const InvocationPayloadBaseSchema = z
|
|
48
51
|
.object({
|
|
49
52
|
iss: z.string().url(),
|
|
50
53
|
aud: z.string().regex(/^app:[a-z][a-z0-9-]{0,79}$/),
|
|
@@ -59,6 +62,8 @@ const InvocationPayloadSchema = z
|
|
|
59
62
|
/** Only for an app session of the mobile app, which never acts as an administrator. */
|
|
60
63
|
session_kind: z.literal("app").optional(),
|
|
61
64
|
credential_id: BoundedIdentifierSchema.optional(),
|
|
65
|
+
oauth_client_id: z.string().min(1).max(200).optional(),
|
|
66
|
+
credential_expires_at: z.iso.datetime().optional(),
|
|
62
67
|
scopes: z
|
|
63
68
|
.array(ScopeSchema)
|
|
64
69
|
.max(100)
|
|
@@ -71,50 +76,72 @@ const InvocationPayloadSchema = z
|
|
|
71
76
|
mandate_revision: z.number().int().positive().optional(),
|
|
72
77
|
workload_type: BoundedIdentifierSchema.optional(),
|
|
73
78
|
workload_id: BoundedIdentifierSchema.optional(),
|
|
79
|
+
parent_invocation_id: z.uuid().optional(),
|
|
74
80
|
jti: z.string().uuid(),
|
|
75
81
|
iat: z.number().int().nonnegative(),
|
|
76
82
|
nbf: z.number().int().nonnegative(),
|
|
77
83
|
exp: z.number().int().positive(),
|
|
78
84
|
})
|
|
79
|
-
.strict()
|
|
80
|
-
.superRefine((claims, context) => {
|
|
81
|
-
if (claims.exp - claims.iat !== CLOUD_INVOCATION_TOKEN_TTL_SECONDS || claims.nbf !== claims.iat) {
|
|
82
|
-
context.addIssue({ code: "custom", message: "Invocation lifetime is invalid" });
|
|
83
|
-
}
|
|
85
|
+
.strict();
|
|
84
86
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
context.addIssue({ code: "custom", message: "A session invocation must use a user principal" });
|
|
101
|
-
}
|
|
102
|
-
if (claims.session_kind && claims.credential_kind !== "session") {
|
|
103
|
-
context.addIssue({ code: "custom", message: "Only a session invocation has a session kind" });
|
|
87
|
+
const validateInvocationClaims = (claims: z.infer<typeof InvocationPayloadBaseSchema>, context: z.RefinementCtx, subjectProbe = false) => {
|
|
88
|
+
if (
|
|
89
|
+
(claims.parent_invocation_id
|
|
90
|
+
? claims.exp <= claims.iat ||
|
|
91
|
+
claims.exp - claims.iat > CLOUD_INVOCATION_TOKEN_TTL_SECONDS ||
|
|
92
|
+
!claims.op.startsWith("capability.query:")
|
|
93
|
+
: claims.exp - claims.iat !== CLOUD_INVOCATION_TOKEN_TTL_SECONDS) ||
|
|
94
|
+
claims.nbf !== claims.iat
|
|
95
|
+
) {
|
|
96
|
+
context.addIssue({ code: "custom", message: "Invocation lifetime is invalid" });
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (claims.principal_type === "user") {
|
|
100
|
+
if (claims.access_subject_type !== "user" || claims.access_subject_id !== claims.sub || claims.delegated_user_id !== undefined) {
|
|
101
|
+
context.addIssue({ code: "custom", message: "User invocation authority is inconsistent" });
|
|
104
102
|
}
|
|
105
|
-
if (claims.credential_kind === "
|
|
106
|
-
context.addIssue({ code: "custom", message: "
|
|
103
|
+
if (claims.credential_kind === "api_key") {
|
|
104
|
+
context.addIssue({ code: "custom", message: "An API key must act through a service account" });
|
|
107
105
|
}
|
|
108
|
-
|
|
109
|
-
|
|
106
|
+
} else if (claims.delegated_user_id) {
|
|
107
|
+
if (claims.access_subject_type !== "user" || claims.access_subject_id !== claims.delegated_user_id) {
|
|
108
|
+
context.addIssue({ code: "custom", message: "Delegated invocation authority is inconsistent" });
|
|
110
109
|
}
|
|
110
|
+
} else if (claims.access_subject_type !== "service_account" || claims.access_subject_id !== claims.sub) {
|
|
111
|
+
context.addIssue({ code: "custom", message: "Service-account invocation authority is inconsistent" });
|
|
112
|
+
}
|
|
113
|
+
if (claims.credential_kind === "session" && claims.principal_type !== "user") {
|
|
114
|
+
context.addIssue({ code: "custom", message: "A session invocation must use a user principal" });
|
|
115
|
+
}
|
|
116
|
+
if (claims.session_kind && claims.credential_kind !== "session") {
|
|
117
|
+
context.addIssue({ code: "custom", message: "Only a session invocation has a session kind" });
|
|
118
|
+
}
|
|
119
|
+
if (claims.credential_kind === "session" && claims.scopes.length > 0) {
|
|
120
|
+
context.addIssue({ code: "custom", message: "A session invocation cannot carry credential scopes" });
|
|
121
|
+
}
|
|
122
|
+
if (claims.credential_kind === "api_key" && !claims.credential_id) {
|
|
123
|
+
context.addIssue({ code: "custom", message: "An API-key invocation requires credential provenance" });
|
|
124
|
+
}
|
|
111
125
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
126
|
+
const mandateFields = [claims.mandate_id, claims.mandate_revision, claims.workload_type, claims.workload_id];
|
|
127
|
+
const mandateFieldCount = mandateFields.filter((value) => value !== undefined).length;
|
|
128
|
+
if (
|
|
129
|
+
claims.credential_kind === "mandate"
|
|
130
|
+
? mandateFieldCount !== mandateFields.length && !(subjectProbe && mandateFieldCount === 0)
|
|
131
|
+
: mandateFieldCount !== 0
|
|
132
|
+
) {
|
|
133
|
+
context.addIssue({ code: "custom", message: "Mandate provenance is incomplete or unexpected" });
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const InvocationPayloadSchema = InvocationPayloadBaseSchema.superRefine((claims, context) => validateInvocationClaims(claims, context));
|
|
138
|
+
const AppAccessProbeSchema = InvocationPayloadBaseSchema.extend({
|
|
139
|
+
token_use: z.literal("app_access_probe"),
|
|
140
|
+
act: z.object({ sub: z.literal("app:core") }).strict(),
|
|
141
|
+
op: z.literal("app-access.check"),
|
|
142
|
+
schema_hash: z.null(),
|
|
143
|
+
parent_invocation_id: z.never().optional(),
|
|
144
|
+
}).superRefine((claims, context) => validateInvocationClaims({ ...claims, token_use: "invocation" }, context, true));
|
|
118
145
|
|
|
119
146
|
export type CloudInvocationClaims = z.infer<typeof InvocationPayloadSchema>;
|
|
120
147
|
export type InvocationAuthority = Pick<
|
|
@@ -127,6 +154,8 @@ export type InvocationAuthority = Pick<
|
|
|
127
154
|
| "credential_kind"
|
|
128
155
|
| "session_kind"
|
|
129
156
|
| "credential_id"
|
|
157
|
+
| "oauth_client_id"
|
|
158
|
+
| "credential_expires_at"
|
|
130
159
|
| "scopes"
|
|
131
160
|
| "mandate_id"
|
|
132
161
|
| "mandate_revision"
|
|
@@ -181,11 +210,23 @@ export const signInvocationToken = async (params: {
|
|
|
181
210
|
schemaHash: string | null;
|
|
182
211
|
authority: InvocationAuthority;
|
|
183
212
|
requestId?: string;
|
|
213
|
+
parent?: CloudInvocationClaims;
|
|
214
|
+
expiresAt?: number;
|
|
184
215
|
issuedAt?: Date;
|
|
185
216
|
jti?: string;
|
|
186
217
|
signer?: PreparedIdentitySigner;
|
|
187
218
|
issuer?: string;
|
|
188
219
|
}): Promise<{ token: string; kid: string; claims: CloudInvocationClaims }> => {
|
|
220
|
+
if (
|
|
221
|
+
params.parent &&
|
|
222
|
+
(params.parent.parent_invocation_id ||
|
|
223
|
+
params.parent.aud !== `app:${params.callingAppId}` ||
|
|
224
|
+
params.parent.aud === `app:${params.targetAppId}` ||
|
|
225
|
+
!params.operation.startsWith("capability.query:") ||
|
|
226
|
+
!isDeepStrictEqual(authorityFromParentInvocation(params.parent), params.authority))
|
|
227
|
+
) {
|
|
228
|
+
throw new Error("Child invocation cannot widen or change its parent authority");
|
|
229
|
+
}
|
|
189
230
|
const issuer = params.issuer ?? (await getIdentityRuntimeConfig()).issuer;
|
|
190
231
|
const signer = params.signer ?? (await prepareIdentitySigner("invocation"));
|
|
191
232
|
const issuedAt = params.issuedAt ?? new Date();
|
|
@@ -204,7 +245,8 @@ export const signInvocationToken = async (params: {
|
|
|
204
245
|
jti: params.jti ?? crypto.randomUUID(),
|
|
205
246
|
iat: issuedAtSeconds,
|
|
206
247
|
nbf: issuedAtSeconds,
|
|
207
|
-
|
|
248
|
+
...(params.parent ? { parent_invocation_id: params.parent.jti } : {}),
|
|
249
|
+
exp: Math.min(issuedAtSeconds + CLOUD_INVOCATION_TOKEN_TTL_SECONDS, params.parent?.exp ?? Infinity, params.expiresAt ?? Infinity),
|
|
208
250
|
});
|
|
209
251
|
|
|
210
252
|
try {
|
|
@@ -222,6 +264,26 @@ export const signInvocationToken = async (params: {
|
|
|
222
264
|
}
|
|
223
265
|
};
|
|
224
266
|
|
|
267
|
+
async function verifyWithKeyRefresh(
|
|
268
|
+
verify: (key: JWTVerifyGetKey) => ReturnType<typeof jwtVerify>,
|
|
269
|
+
key: JWTVerifyGetKey,
|
|
270
|
+
remote: { url: URL; state: RemoteSetState } | null,
|
|
271
|
+
) {
|
|
272
|
+
try {
|
|
273
|
+
return await verify(key);
|
|
274
|
+
} catch (error) {
|
|
275
|
+
if (!(error instanceof errors.JWKSNoMatchingKey) || !remote) throw error;
|
|
276
|
+
const now = Date.now();
|
|
277
|
+
if (now - remote.state.lastForcedRefreshAt < 30_000) throw error;
|
|
278
|
+
remote.state.lastForcedRefreshAt = now;
|
|
279
|
+
identityMetrics.increment("unknown_kid_refresh");
|
|
280
|
+
const refreshed = createRemoteSet(remote.url);
|
|
281
|
+
const verified = await verify(refreshed);
|
|
282
|
+
remote.state.key = refreshed;
|
|
283
|
+
return verified;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
225
287
|
/** With deferSchemaBinding, the caller must compare schema_hash after resolving live authority. */
|
|
226
288
|
export const verifyInvocationToken = async (
|
|
227
289
|
token: string,
|
|
@@ -239,7 +301,7 @@ export const verifyInvocationToken = async (
|
|
|
239
301
|
const jwksUrl = options.jwksUrl ?? runtime?.invocationJwksUrl;
|
|
240
302
|
const state = options.key || !jwksUrl ? null : remoteSet(jwksUrl);
|
|
241
303
|
if (state && jwksUrl) remote = { url: jwksUrl, state };
|
|
242
|
-
|
|
304
|
+
const key = options.key ?? state?.key;
|
|
243
305
|
if (!key) throw new Error("Cloud invocation verifier has no public key source");
|
|
244
306
|
assertProtectedHeader(decodeProtectedHeader(token));
|
|
245
307
|
const verify = (candidate: JWTVerifyGetKey) =>
|
|
@@ -251,19 +313,7 @@ export const verifyInvocationToken = async (
|
|
|
251
313
|
clockTolerance: INVOCATION_CLOCK_TOLERANCE_SECONDS,
|
|
252
314
|
currentDate: options.now,
|
|
253
315
|
});
|
|
254
|
-
|
|
255
|
-
try {
|
|
256
|
-
verified = await verify(key);
|
|
257
|
-
} catch (error) {
|
|
258
|
-
if (!(error instanceof errors.JWKSNoMatchingKey) || !remote) throw error;
|
|
259
|
-
const now = Date.now();
|
|
260
|
-
if (now - remote.state.lastForcedRefreshAt < 30_000) throw error;
|
|
261
|
-
remote.state.lastForcedRefreshAt = now;
|
|
262
|
-
identityMetrics.increment("unknown_kid_refresh");
|
|
263
|
-
key = createRemoteSet(remote.url);
|
|
264
|
-
verified = await verify(key);
|
|
265
|
-
remote.state.key = key;
|
|
266
|
-
}
|
|
316
|
+
const verified = await verifyWithKeyRefresh(verify, key, remote);
|
|
267
317
|
const parsed = InvocationPayloadSchema.safeParse(verified.payload);
|
|
268
318
|
if (!parsed.success || parsed.data.op !== operation || (!options.deferSchemaBinding && parsed.data.schema_hash !== schemaHash)) {
|
|
269
319
|
throw new Error("Cloud invocation JWT claims do not match the target operation");
|
|
@@ -280,3 +330,158 @@ export const verifyInvocationToken = async (
|
|
|
280
330
|
export const clearInvocationVerifierCachesForTest = (): void => {
|
|
281
331
|
remoteSets.clear();
|
|
282
332
|
};
|
|
333
|
+
|
|
334
|
+
const AppEventInvocationClaimsSchema = z
|
|
335
|
+
.object({
|
|
336
|
+
iss: z.string().url(),
|
|
337
|
+
aud: z.string(),
|
|
338
|
+
token_use: z.literal("app_event_delivery"),
|
|
339
|
+
act: z.literal("app:core"),
|
|
340
|
+
op: z.literal("app-event.deliver"),
|
|
341
|
+
subscription_id: z.string().min(1).max(400),
|
|
342
|
+
event_id: z.uuid(),
|
|
343
|
+
payload_hash: z.string().regex(/^[a-f0-9]{64}$/),
|
|
344
|
+
iat: z.number().int(),
|
|
345
|
+
nbf: z.number().int(),
|
|
346
|
+
exp: z.number().int(),
|
|
347
|
+
jti: z.uuid(),
|
|
348
|
+
})
|
|
349
|
+
.strict();
|
|
350
|
+
|
|
351
|
+
export const appEventPayloadHash = (payload: unknown): string => createHash("sha256").update(JSON.stringify(payload)).digest("hex");
|
|
352
|
+
|
|
353
|
+
/** Core-to-app invocation, bound to one subscription, event and exact request body. */
|
|
354
|
+
export const signAppEventInvocation = async (input: {
|
|
355
|
+
appId: string;
|
|
356
|
+
subscriptionId: string;
|
|
357
|
+
eventId: string;
|
|
358
|
+
payload: unknown;
|
|
359
|
+
issuer?: string;
|
|
360
|
+
signer?: PreparedIdentitySigner;
|
|
361
|
+
now?: Date;
|
|
362
|
+
jti?: string;
|
|
363
|
+
}) => {
|
|
364
|
+
const issuer = input.issuer ?? (await getIdentityRuntimeConfig()).issuer;
|
|
365
|
+
const signer = input.signer ?? (await prepareIdentitySigner("invocation"));
|
|
366
|
+
const now = Math.floor((input.now?.getTime() ?? Date.now()) / 1_000);
|
|
367
|
+
if (signer.signUntil.getTime() < (now + CLOUD_INVOCATION_TOKEN_TTL_SECONDS) * 1000)
|
|
368
|
+
throw new Error("Invocation signing key expires before the delivery token");
|
|
369
|
+
const claims = AppEventInvocationClaimsSchema.parse({
|
|
370
|
+
iss: issuer,
|
|
371
|
+
aud: `app:${AppIdSchema.parse(input.appId)}`,
|
|
372
|
+
token_use: "app_event_delivery",
|
|
373
|
+
act: "app:core",
|
|
374
|
+
op: "app-event.deliver",
|
|
375
|
+
subscription_id: input.subscriptionId,
|
|
376
|
+
event_id: input.eventId,
|
|
377
|
+
payload_hash: appEventPayloadHash(input.payload),
|
|
378
|
+
iat: now,
|
|
379
|
+
nbf: now,
|
|
380
|
+
exp: now + CLOUD_INVOCATION_TOKEN_TTL_SECONDS,
|
|
381
|
+
jti: input.jti ?? crypto.randomUUID(),
|
|
382
|
+
});
|
|
383
|
+
return new SignJWT(claims)
|
|
384
|
+
.setProtectedHeader({ alg: CLOUD_IDENTITY_ALGORITHM, typ: CLOUD_INVOCATION_TOKEN_TYPE, kid: signer.kid })
|
|
385
|
+
.sign(signer.key);
|
|
386
|
+
};
|
|
387
|
+
|
|
388
|
+
export const verifyAppEventInvocation = async (
|
|
389
|
+
token: string,
|
|
390
|
+
input: { appId: string; subscriptionId: string; eventId: string; payload: unknown },
|
|
391
|
+
options: { issuer?: string; key?: JWTVerifyGetKey; jwksUrl?: URL; now?: Date } = {},
|
|
392
|
+
): Promise<boolean> => {
|
|
393
|
+
if (new TextEncoder().encode(token).byteLength > IDENTITY_MAX_COMPACT_TOKEN_BYTES) return false;
|
|
394
|
+
try {
|
|
395
|
+
assertProtectedHeader(decodeProtectedHeader(token));
|
|
396
|
+
const runtime = options.issuer && (options.key || options.jwksUrl) ? null : await getIdentityRuntimeConfig();
|
|
397
|
+
const issuer = options.issuer ?? runtime!.issuer;
|
|
398
|
+
const url = options.jwksUrl ?? runtime?.invocationJwksUrl;
|
|
399
|
+
const state = options.key || !url ? null : remoteSet(url);
|
|
400
|
+
const key = options.key ?? state?.key;
|
|
401
|
+
if (!key) return false;
|
|
402
|
+
const verify = (key: JWTVerifyGetKey) =>
|
|
403
|
+
jwtVerify(token, key, {
|
|
404
|
+
algorithms: [CLOUD_IDENTITY_ALGORITHM],
|
|
405
|
+
audience: `app:${AppIdSchema.parse(input.appId)}`,
|
|
406
|
+
issuer,
|
|
407
|
+
typ: CLOUD_INVOCATION_TOKEN_TYPE,
|
|
408
|
+
clockTolerance: INVOCATION_CLOCK_TOLERANCE_SECONDS,
|
|
409
|
+
currentDate: options.now,
|
|
410
|
+
});
|
|
411
|
+
const result = await verifyWithKeyRefresh(verify, key, state && url ? { url, state } : null);
|
|
412
|
+
const claims = AppEventInvocationClaimsSchema.parse(result.payload);
|
|
413
|
+
return (
|
|
414
|
+
claims.exp - claims.iat === CLOUD_INVOCATION_TOKEN_TTL_SECONDS &&
|
|
415
|
+
claims.nbf === claims.iat &&
|
|
416
|
+
claims.subscription_id === input.subscriptionId &&
|
|
417
|
+
claims.event_id === input.eventId &&
|
|
418
|
+
claims.payload_hash === appEventPayloadHash(input.payload)
|
|
419
|
+
);
|
|
420
|
+
} catch {
|
|
421
|
+
return false;
|
|
422
|
+
}
|
|
423
|
+
};
|
|
424
|
+
|
|
425
|
+
/** Core-only, target-bound existence proof. It cannot authorize an application operation. */
|
|
426
|
+
export const signAppAccessProbe = async (input: {
|
|
427
|
+
appId: string;
|
|
428
|
+
authority: InvocationAuthority;
|
|
429
|
+
issuer?: string;
|
|
430
|
+
signer?: PreparedIdentitySigner;
|
|
431
|
+
now?: Date;
|
|
432
|
+
}) => {
|
|
433
|
+
const issuer = input.issuer ?? (await getIdentityRuntimeConfig()).issuer;
|
|
434
|
+
const signer = input.signer ?? (await prepareIdentitySigner("invocation"));
|
|
435
|
+
const now = Math.floor((input.now?.getTime() ?? Date.now()) / 1_000);
|
|
436
|
+
if (signer.signUntil.getTime() < (now + CLOUD_INVOCATION_TOKEN_TTL_SECONDS) * 1000)
|
|
437
|
+
throw new Error("Invocation signing key expires before the app-access probe");
|
|
438
|
+
const claims = AppAccessProbeSchema.parse({
|
|
439
|
+
...input.authority,
|
|
440
|
+
iss: issuer,
|
|
441
|
+
aud: `app:${AppIdSchema.parse(input.appId)}`,
|
|
442
|
+
token_use: "app_access_probe",
|
|
443
|
+
act: { sub: "app:core" },
|
|
444
|
+
op: "app-access.check",
|
|
445
|
+
schema_hash: null,
|
|
446
|
+
ver: CLOUD_INVOCATION_PROTOCOL_VERSION,
|
|
447
|
+
iat: now,
|
|
448
|
+
nbf: now,
|
|
449
|
+
exp: now + CLOUD_INVOCATION_TOKEN_TTL_SECONDS,
|
|
450
|
+
jti: crypto.randomUUID(),
|
|
451
|
+
});
|
|
452
|
+
const token = await new SignJWT(claims)
|
|
453
|
+
.setProtectedHeader({ alg: CLOUD_IDENTITY_ALGORITHM, typ: CLOUD_INVOCATION_TOKEN_TYPE, kid: signer.kid })
|
|
454
|
+
.sign(signer.key);
|
|
455
|
+
if (new TextEncoder().encode(token).byteLength > IDENTITY_MAX_COMPACT_TOKEN_BYTES) throw new Error("App-access probe exceeds 4 KiB");
|
|
456
|
+
return token;
|
|
457
|
+
};
|
|
458
|
+
|
|
459
|
+
export const verifyAppAccessProbe = async (
|
|
460
|
+
token: string,
|
|
461
|
+
appId: string,
|
|
462
|
+
options: { issuer?: string; key?: JWTVerifyGetKey; jwksUrl?: URL; now?: Date } = {},
|
|
463
|
+
): Promise<z.infer<typeof AppAccessProbeSchema> | null> => {
|
|
464
|
+
if (new TextEncoder().encode(token).byteLength > IDENTITY_MAX_COMPACT_TOKEN_BYTES) return null;
|
|
465
|
+
try {
|
|
466
|
+
assertProtectedHeader(decodeProtectedHeader(token));
|
|
467
|
+
const runtime = options.issuer && (options.key || options.jwksUrl) ? null : await getIdentityRuntimeConfig();
|
|
468
|
+
const issuer = options.issuer ?? runtime!.issuer;
|
|
469
|
+
const url = options.jwksUrl ?? runtime?.invocationJwksUrl;
|
|
470
|
+
const state = options.key || !url ? null : remoteSet(url);
|
|
471
|
+
const key = options.key ?? state?.key;
|
|
472
|
+
if (!key) return null;
|
|
473
|
+
const verify = (key: JWTVerifyGetKey) =>
|
|
474
|
+
jwtVerify(token, key, {
|
|
475
|
+
algorithms: [CLOUD_IDENTITY_ALGORITHM],
|
|
476
|
+
audience: `app:${AppIdSchema.parse(appId)}`,
|
|
477
|
+
issuer,
|
|
478
|
+
typ: CLOUD_INVOCATION_TOKEN_TYPE,
|
|
479
|
+
clockTolerance: INVOCATION_CLOCK_TOLERANCE_SECONDS,
|
|
480
|
+
currentDate: options.now,
|
|
481
|
+
});
|
|
482
|
+
const result = await verifyWithKeyRefresh(verify, key, state && url ? { url, state } : null);
|
|
483
|
+
return AppAccessProbeSchema.parse(result.payload);
|
|
484
|
+
} catch {
|
|
485
|
+
return null;
|
|
486
|
+
}
|
|
487
|
+
};
|
|
@@ -7,6 +7,7 @@ import { canUseApp } from "../app-access";
|
|
|
7
7
|
import { recordAppAccessDenied } from "../app-access-metrics";
|
|
8
8
|
import { CLOUD_INVOCATION_TOKEN_TTL_SECONDS } from "./constants";
|
|
9
9
|
import { resolveInvocationAuthority } from "./invocation-actor";
|
|
10
|
+
import { matchesInvocationSubject } from "./invocation-authority";
|
|
10
11
|
import { type InvocationAuthority, signInvocationToken } from "./invocation-token";
|
|
11
12
|
import { readIdentityGroupsAdmin } from "./runtime-config";
|
|
12
13
|
|
|
@@ -32,6 +33,7 @@ export const resolveIssuanceAuthority = async (
|
|
|
32
33
|
|
|
33
34
|
type RegisteredAppAccessOptions = {
|
|
34
35
|
db?: SQL;
|
|
36
|
+
signal?: AbortSignal;
|
|
35
37
|
getApp?: (id: string) => Promise<Pick<AppMeta, "id" | "access"> | null>;
|
|
36
38
|
};
|
|
37
39
|
|
|
@@ -40,8 +42,10 @@ export const requireRegisteredAppAccess = async (
|
|
|
40
42
|
appId: string,
|
|
41
43
|
options: RegisteredAppAccessOptions = {},
|
|
42
44
|
): Promise<void> => {
|
|
45
|
+
options.signal?.throwIfAborted();
|
|
43
46
|
const app = await (options.getApp ?? getApp)(appId).catch(() => null);
|
|
44
|
-
if (!app || !authority || !(await canUseApp(authority, app, { db: options.db }))) {
|
|
47
|
+
if (!app || !authority || !(await canUseApp(authority, app, { db: options.db, signal: options.signal }))) {
|
|
48
|
+
options.signal?.throwIfAborted();
|
|
45
49
|
if (app) recordAppAccessDenied(app.id);
|
|
46
50
|
throw new AppAccessDeniedError(app ?? { id: appId });
|
|
47
51
|
}
|
|
@@ -56,12 +60,16 @@ export const issueInvocationToken = async (
|
|
|
56
60
|
sign?: typeof signInvocationToken;
|
|
57
61
|
} = {},
|
|
58
62
|
): ReturnType<typeof signInvocationToken> => {
|
|
63
|
+
dependencies.signal?.throwIfAborted();
|
|
59
64
|
const authority = await (dependencies.resolve ?? resolveIssuanceAuthority)(
|
|
60
65
|
params.authority,
|
|
61
66
|
params.callingAppId,
|
|
62
67
|
dependencies.db,
|
|
63
68
|
dependencies.groupsAdmin,
|
|
64
69
|
);
|
|
70
|
+
if (params.parent && (!authority || !matchesInvocationSubject(params.parent, authority)))
|
|
71
|
+
throw new AppAccessDeniedError({ id: params.targetAppId });
|
|
65
72
|
await requireRegisteredAppAccess(authority, params.targetAppId, dependencies);
|
|
73
|
+
dependencies.signal?.throwIfAborted();
|
|
66
74
|
return (dependencies.sign ?? signInvocationToken)(params);
|
|
67
75
|
};
|
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
import { AI_INFERENCE_SCOPE
|
|
1
|
+
import { AI_INFERENCE_SCOPE } from "../../contracts/shared";
|
|
2
|
+
import type { AccessSubject } from "../../server/services/access";
|
|
2
3
|
import { isAccountExpired } from "../account-model";
|
|
3
4
|
import { serviceAccountCredentials } from "../service-account-credentials";
|
|
5
|
+
import { isStandaloneServiceAccountKind } from "../service-accounts";
|
|
4
6
|
|
|
5
7
|
export const authenticateModelsOnlyCredential = async (
|
|
6
8
|
token: string | null | undefined,
|
|
7
|
-
): Promise<{
|
|
9
|
+
): Promise<{ subject: AccessSubject; credentialId: string } | null> => {
|
|
8
10
|
if (!token || !serviceAccountCredentials.isApiToken(token)) return null;
|
|
9
11
|
const authenticated = await serviceAccountCredentials.authenticateApiToken(token);
|
|
10
12
|
if (!authenticated) return null;
|
|
@@ -14,12 +16,19 @@ export const authenticateModelsOnlyCredential = async (
|
|
|
14
16
|
credential.status !== "active" ||
|
|
15
17
|
credential.serviceAccountId !== serviceAccount.id ||
|
|
16
18
|
serviceAccount.status !== "active" ||
|
|
19
|
+
credential.scopes.length !== 1 ||
|
|
20
|
+
credential.scopes[0] !== AI_INFERENCE_SCOPE
|
|
21
|
+
)
|
|
22
|
+
return null;
|
|
23
|
+
if (isStandaloneServiceAccountKind(serviceAccount.kind)) {
|
|
24
|
+
return { subject: { type: "service_account", serviceAccountId: serviceAccount.id }, credentialId: credential.id };
|
|
25
|
+
}
|
|
26
|
+
if (
|
|
17
27
|
serviceAccount.kind !== "user_delegated" ||
|
|
18
28
|
!delegatedUser ||
|
|
19
29
|
serviceAccount.delegatedUserId !== delegatedUser.id ||
|
|
20
|
-
!credential.scopes.includes(AI_INFERENCE_SCOPE) ||
|
|
21
30
|
isAccountExpired(delegatedUser.accountExpires)
|
|
22
31
|
)
|
|
23
32
|
return null;
|
|
24
|
-
return { user: delegatedUser, credentialId: credential.id };
|
|
33
|
+
return { subject: { type: "user", userId: delegatedUser.id }, credentialId: credential.id };
|
|
25
34
|
};
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { RequestCredentialProof } from "../../server/middleware/auth";
|
|
3
|
+
import { resolveOAuthTokenActor } from "../oauth-tokens";
|
|
4
|
+
import { requestCacheRedis } from "../request-cache-redis";
|
|
5
|
+
import { getOverview } from "../service-account-credentials";
|
|
6
|
+
import { loadJwtSessionIdentity } from "../session/user";
|
|
7
|
+
import { CLOUD_INVOCATION_TOKEN_TTL_SECONDS, IDENTITY_MAX_COMPACT_TOKEN_BYTES } from "./constants";
|
|
8
|
+
import type { CloudInvocationClaims } from "./invocation-token";
|
|
9
|
+
import { getIdentityRuntimeConfig } from "./runtime-config";
|
|
10
|
+
|
|
11
|
+
// The first consumer performs one resource-reader check. Budget exactly that
|
|
12
|
+
// dependency: one Query, one hop, no recursive fan-out or automatic retries.
|
|
13
|
+
// Optional lease setup may spend at most 1% of the parent's 30-second token
|
|
14
|
+
// lifetime (300 ms), taken from the original operation's remaining budget.
|
|
15
|
+
export const NESTED_INVOCATION_SETUP_BUDGET_MS = (CLOUD_INVOCATION_TOKEN_TTL_SECONDS * 1000) / 100;
|
|
16
|
+
|
|
17
|
+
const ProofSchema = z.discriminatedUnion("kind", [
|
|
18
|
+
z
|
|
19
|
+
.object({
|
|
20
|
+
kind: z.literal("session"),
|
|
21
|
+
userId: z.uuid(),
|
|
22
|
+
sid: z.uuid(),
|
|
23
|
+
authEpoch: z.number().int().nonnegative(),
|
|
24
|
+
expiresAt: z.iso.datetime(),
|
|
25
|
+
})
|
|
26
|
+
.strict(),
|
|
27
|
+
z.object({ kind: z.literal("oauth"), payload: z.record(z.string(), z.unknown()) }).strict(),
|
|
28
|
+
z.object({ kind: z.literal("api_key"), credentialId: z.uuid() }).strict(),
|
|
29
|
+
]);
|
|
30
|
+
const LeaseSchema = z
|
|
31
|
+
.object({
|
|
32
|
+
appId: z.string(),
|
|
33
|
+
deadline: z.number().int().positive(),
|
|
34
|
+
origin: z.enum(["http", "app", "assistant", "mcp"]),
|
|
35
|
+
proof: ProofSchema.optional(),
|
|
36
|
+
assistantTurn: z.object({ conversationId: z.uuid(), turnId: z.uuid() }).strict().optional(),
|
|
37
|
+
})
|
|
38
|
+
.strict()
|
|
39
|
+
.refine((lease) => (lease.origin === "assistant") === !!lease.assistantTurn, { message: "Turn authority requires the Assistant origin" });
|
|
40
|
+
export type NestedInvocationLease = z.infer<typeof LeaseSchema>;
|
|
41
|
+
|
|
42
|
+
const key = (id: string) => `identity:nested:v1:${id}`;
|
|
43
|
+
const TAKE = `local value = redis.call('GET', KEYS[1])
|
|
44
|
+
if not value or redis.call('EXISTS', KEYS[2]) == 1 then return nil end
|
|
45
|
+
redis.call('SET', KEYS[2], '1', 'PX', ARGV[1])
|
|
46
|
+
return value`;
|
|
47
|
+
|
|
48
|
+
export const nestedInvocationLeases = {
|
|
49
|
+
async create(claims: CloudInvocationClaims, lease: NestedInvocationLease, signal?: AbortSignal): Promise<void> {
|
|
50
|
+
const value = LeaseSchema.parse(lease);
|
|
51
|
+
const ttl = Math.min(claims.exp * 1000, value.deadline) - Date.now();
|
|
52
|
+
if (ttl <= 0) throw new Error("Invocation expired");
|
|
53
|
+
const json = JSON.stringify(value);
|
|
54
|
+
if (Buffer.byteLength(json) > IDENTITY_MAX_COMPACT_TOKEN_BYTES)
|
|
55
|
+
throw new Error("Invocation lease exceeds the identity metadata budget");
|
|
56
|
+
const client = await requestCacheRedis();
|
|
57
|
+
signal?.throwIfAborted();
|
|
58
|
+
await client.send("SET", [key(claims.jti), json, "PX", String(ttl), "NX"]);
|
|
59
|
+
// A command already sent cannot be cancelled. Remove its late result when
|
|
60
|
+
// the bounded setup has released the original dispatch in the meantime.
|
|
61
|
+
if (signal?.aborted) {
|
|
62
|
+
await client.send("DEL", [key(claims.jti)]);
|
|
63
|
+
signal.throwIfAborted();
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
async read(id: string): Promise<NestedInvocationLease | null> {
|
|
67
|
+
const value = await (await requestCacheRedis()).send("GET", [key(id)]);
|
|
68
|
+
if (typeof value !== "string" || Buffer.byteLength(value) > IDENTITY_MAX_COMPACT_TOKEN_BYTES) return null;
|
|
69
|
+
const parsed = LeaseSchema.safeParse(JSON.parse(value));
|
|
70
|
+
return parsed.success && parsed.data.deadline > Date.now() ? parsed.data : null;
|
|
71
|
+
},
|
|
72
|
+
async take(id: string): Promise<NestedInvocationLease | null> {
|
|
73
|
+
const value = await (await requestCacheRedis()).send("EVAL", [
|
|
74
|
+
TAKE,
|
|
75
|
+
"2",
|
|
76
|
+
key(id),
|
|
77
|
+
`${key(id)}:used`,
|
|
78
|
+
String(CLOUD_INVOCATION_TOKEN_TTL_SECONDS * 1000),
|
|
79
|
+
]);
|
|
80
|
+
if (typeof value !== "string" || Buffer.byteLength(value) > IDENTITY_MAX_COMPACT_TOKEN_BYTES) return null;
|
|
81
|
+
const parsed = LeaseSchema.safeParse(JSON.parse(value));
|
|
82
|
+
return parsed.success && parsed.data.deadline > Date.now() ? parsed.data : null;
|
|
83
|
+
},
|
|
84
|
+
async revoke(id: string): Promise<void> {
|
|
85
|
+
await (await requestCacheRedis()).send("DEL", [key(id)]);
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
/** Recheck the source's revocation coordinates; no reusable credential is stored. */
|
|
90
|
+
export const isNestedSourceCurrent = async (
|
|
91
|
+
proof: RequestCredentialProof,
|
|
92
|
+
parent: Pick<CloudInvocationClaims, "sub" | "credential_kind" | "credential_id" | "principal_type" | "scopes">,
|
|
93
|
+
): Promise<boolean> => {
|
|
94
|
+
if (proof.kind !== parent.credential_kind) return false;
|
|
95
|
+
if (proof.kind === "session") {
|
|
96
|
+
return (
|
|
97
|
+
proof.userId === parent.sub &&
|
|
98
|
+
Date.parse(proof.expiresAt) > Date.now() &&
|
|
99
|
+
!!(await loadJwtSessionIdentity({ userId: proof.userId, sid: proof.sid, authEpoch: proof.authEpoch }))
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
if (proof.kind === "api_key") {
|
|
103
|
+
if (proof.credentialId !== parent.credential_id) return false;
|
|
104
|
+
const credential = await getOverview({ id: proof.credentialId });
|
|
105
|
+
return (
|
|
106
|
+
!!credential &&
|
|
107
|
+
credential.status === "active" &&
|
|
108
|
+
credential.serviceAccountId === parent.sub &&
|
|
109
|
+
(!credential.expiresAt || Date.parse(credential.expiresAt) > Date.now()) &&
|
|
110
|
+
parent.scopes.length === credential.scopes.length &&
|
|
111
|
+
parent.scopes.every((scope) => credential.scopes.includes(scope))
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
if (typeof proof.payload.exp !== "number" || proof.payload.exp * 1000 <= Date.now()) return false;
|
|
115
|
+
const current = await resolveOAuthTokenActor(proof.payload, (await getIdentityRuntimeConfig()).groupsAdmin);
|
|
116
|
+
return (
|
|
117
|
+
!!current &&
|
|
118
|
+
current.kind === parent.principal_type &&
|
|
119
|
+
(current.kind === "user" ? current.user.id : current.serviceAccount.id) === parent.sub &&
|
|
120
|
+
parent.scopes.every((scope) => current.scopes.includes(scope))
|
|
121
|
+
);
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
export const nestedSourceDeadline = (proof: RequestCredentialProof | undefined): number => {
|
|
125
|
+
if (proof?.kind === "session") return Date.parse(proof.expiresAt);
|
|
126
|
+
if (proof?.kind === "oauth") return typeof proof.payload.exp === "number" ? proof.payload.exp * 1000 : 0;
|
|
127
|
+
return Infinity;
|
|
128
|
+
};
|