@k2b/cloud 0.43.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 +7 -2
- 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 +158 -12
- package/src/_internal/capability-origin.ts +4 -0
- package/src/_internal/capability-streams.ts +7 -5
- package/src/_internal/define-app.ts +57 -6
- 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/process-identity.ts +7 -2
- package/src/_internal/registry-validation.ts +11 -1
- package/src/_internal/registry.ts +8 -1
- package/src/_internal/runtime-context.ts +8 -1
- package/src/access/AppAccessEditor.tsx +2 -0
- package/src/access/PermissionEditor.tsx +9 -3
- package/src/access/PrincipalPicker.tsx +14 -4
- package/src/access/ResourceApiKeys.tsx +3 -4
- package/src/access/app-access-messages.ts +6 -0
- package/src/account/ProfilePictureDialog.tsx +699 -0
- package/src/account/profile-picture-messages.ts +269 -0
- package/src/account/ui.ts +7 -0
- 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/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/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 +31 -22
- package/src/ai/http.ts +17 -29
- package/src/ai/index.ts +11 -5
- package/src/ai/inference-calls.ts +5 -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.ts +26 -9
- 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 +55 -6
- package/src/ai/provider-fetch.ts +4 -0
- package/src/ai/provider.ts +1 -1
- package/src/ai/quota-limits.ts +36 -0
- package/src/ai/quota-provider.ts +4 -2
- package/src/ai/quota-report.ts +7 -4
- 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 +21 -44
- 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 +27 -22
- 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 +16 -0
- 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/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 +28 -4
- package/src/api/index.ts +7 -1
- package/src/api/mcp.ts +175 -2
- package/src/api/me.ts +6 -4
- package/src/api/pwa-phone.ts +75 -26
- 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/app-pwa/contracts.ts +4 -0
- package/src/app-pwa/core-query-error.ts +20 -0
- package/src/app-pwa/index.ts +1 -0
- package/src/app-pwa/service.ts +91 -0
- package/src/app-pwa/transport.ts +22 -0
- package/src/approvals/messages.ts +2 -2
- package/src/browser/cloud-id.ts +86 -0
- package/src/browser/resource-clipboard.ts +4 -0
- package/src/browser/resource-links.ts +52 -0
- package/src/capabilities/server-invoke.ts +2 -11
- package/src/capabilities/testing.ts +5 -2
- package/src/cli/access.ts +6 -5
- 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 +5 -2
- package/src/cli/apps.ts +89 -26
- package/src/cli/capabilities.ts +22 -1
- package/src/cli/events.ts +320 -0
- package/src/config/env.ts +40 -0
- package/src/contracts/app-access.ts +26 -2
- package/src/contracts/app-details.ts +89 -0
- package/src/contracts/app-events.ts +51 -0
- package/src/contracts/app.ts +21 -5
- package/src/contracts/capabilities.ts +143 -3
- package/src/contracts/index.ts +2 -0
- package/src/contracts/notification-types.ts +36 -9
- package/src/contracts/pwa.ts +17 -1
- package/src/contracts/registry.ts +6 -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 +1 -1
- package/src/server/api/respond.ts +8 -3
- package/src/server/index.ts +1 -0
- package/src/server/middleware/app-access.ts +1 -1
- package/src/server/middleware/auth.ts +21 -7
- package/src/server/middleware/index.ts +1 -0
- package/src/server/middleware/invocation.ts +2 -0
- package/src/server/public-storage.ts +106 -0
- package/src/server/services/access.ts +8 -0
- package/src/services/accounts/app.ts +6 -0
- package/src/services/accounts/entities.ts +14 -3
- package/src/services/accounts/groups.ts +3 -1
- package/src/services/accounts/identities.ts +62 -10
- package/src/services/accounts/users.ts +7 -6
- package/src/services/app-access.ts +25 -7
- 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/ledger.ts +28 -10
- package/src/services/geo/service.ts +43 -24
- package/src/services/identity/core-origin.ts +12 -0
- package/src/services/identity/invocation-actor.ts +20 -2
- package/src/services/identity/invocation-authority.ts +4 -0
- package/src/services/identity/invocation-token.ts +238 -56
- package/src/services/identity/issue-invocation.ts +6 -1
- package/src/services/image-metadata.ts +65 -1
- package/src/services/index.ts +13 -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/session/index.ts +14 -0
- package/src/services/settings/core-settings.ts +71 -43
- 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 +9 -3
- package/src/ssr/AdminSidebar.tsx +7 -7
- package/src/ssr/AppAccessDenied.tsx +2 -0
- package/src/ssr/AppBadge.tsx +14 -0
- package/src/ssr/AppLaunchpadPanel.tsx +1 -1
- package/src/ssr/CloudShell.island.tsx +25 -4
- package/src/ssr/Layout.tsx +8 -6
- package/src/ssr/PwaLayout.tsx +16 -1
- package/src/ssr/PwaPartCount.island.tsx +23 -0
- package/src/ssr/SettingsSaveOutcome.tsx +58 -0
- package/src/ssr/SettingsSaveStatus.island.tsx +16 -0
- package/src/ssr/__snapshots__/Layout.unchanged.render.test.tsx.snap +3 -3
- package/src/ssr/admin-active-link.ts +16 -0
- package/src/ssr/admin-navigation.ts +25 -20
- package/src/ssr/app-badges.ts +31 -7
- package/src/ssr/index.ts +8 -0
- package/src/ssr/page-trail-runtime.ts +3 -1
- package/src/ssr/platform-messages.ts +9 -1
- package/src/ssr/pwa-messages.ts +2 -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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@k2b/cloud",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.44.0",
|
|
4
4
|
"description": "Application platform library for independently deployed Hono and SolidJS services behind a dynamic gateway.",
|
|
5
5
|
"license": "AGPL-3.0-or-later",
|
|
6
6
|
"repository": {
|
|
@@ -39,7 +39,9 @@
|
|
|
39
39
|
"./browser/pwa-drafts": "./src/browser/pwa-drafts.ts",
|
|
40
40
|
"./browser/reload": "./src/browser/reload.ts",
|
|
41
41
|
"./browser/notifications": "./src/browser/notifications.ts",
|
|
42
|
+
"./browser/resource-links": "./src/browser/resource-links.ts",
|
|
42
43
|
"./browser/resource-clipboard": "./src/browser/resource-clipboard.ts",
|
|
44
|
+
"./browser/cloud-id": "./src/browser/cloud-id.ts",
|
|
43
45
|
"./browser/commands": "./src/browser/commands.ts",
|
|
44
46
|
"./browser/panel": "./src/browser/panel.ts",
|
|
45
47
|
"./browser/search": "./src/browser/search.ts",
|
|
@@ -57,6 +59,7 @@
|
|
|
57
59
|
"./cli/account": "./src/cli/account.ts",
|
|
58
60
|
"./cli/apps": "./src/cli/apps.ts",
|
|
59
61
|
"./cli/capabilities": "./src/cli/capabilities.ts",
|
|
62
|
+
"./cli/events": "./src/cli/events.ts",
|
|
60
63
|
"./cli/admin": "./src/cli/admin/index.ts",
|
|
61
64
|
"./shared": "./src/shared/index.ts",
|
|
62
65
|
"./shared-text": "./src/shared-text/index.ts",
|
|
@@ -68,6 +71,7 @@
|
|
|
68
71
|
"./services/*": "./src/services/*",
|
|
69
72
|
"./ai": "./src/ai/index.ts",
|
|
70
73
|
"./ai/admin": "./src/ai/admin.ts",
|
|
74
|
+
"./ai/app-frame": "./src/ai/app-frame.ts",
|
|
71
75
|
"./ai/browser": "./src/ai/browser.ts",
|
|
72
76
|
"./ai/question-text": "./src/ai/question-text.ts",
|
|
73
77
|
"./ai/live": "./src/ai/live.ts",
|
|
@@ -119,7 +123,7 @@
|
|
|
119
123
|
"@tailwindcss/typography": "0.5.20",
|
|
120
124
|
"@k2b/nessi": "0.18.0",
|
|
121
125
|
"@k2b/ssr": "0.20.1",
|
|
122
|
-
"@k2b/ui": "0.
|
|
126
|
+
"@k2b/ui": "0.34.0",
|
|
123
127
|
"@k2b/stdlib": "0.30.0",
|
|
124
128
|
"@k2b/sync": "7.0.1",
|
|
125
129
|
"@nats-io/jetstream": "3.4.0",
|
|
@@ -127,6 +131,7 @@
|
|
|
127
131
|
"@modelcontextprotocol/sdk": "1.32.1",
|
|
128
132
|
"bun-plugin-tailwind": "0.1.2",
|
|
129
133
|
"hono-openapi": "1.3.3",
|
|
134
|
+
"diff": "9.0.0",
|
|
130
135
|
"entities": "8.0.0",
|
|
131
136
|
"htmlparser2": "12.0.0",
|
|
132
137
|
"jose": "6.2.12",
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { RequestAuthority } from "../server/middleware/auth";
|
|
2
|
+
|
|
3
|
+
// Request lifetime only. Never put reusable credentials on serializable authority.
|
|
4
|
+
const requests = new WeakMap<RequestAuthority, Request>();
|
|
5
|
+
const credentials = new WeakMap<RequestAuthority, string>();
|
|
6
|
+
export const bindAppAccessRequest = (authority: RequestAuthority, request: Request, selectedCredential?: string): RequestAuthority => {
|
|
7
|
+
requests.set(authority, request);
|
|
8
|
+
if (selectedCredential) credentials.set(authority, selectedCredential);
|
|
9
|
+
return authority;
|
|
10
|
+
};
|
|
11
|
+
export const appAccessRequest = (authority: RequestAuthority): Request | undefined => requests.get(authority);
|
|
12
|
+
|
|
13
|
+
export const appAccessCredential = (authority: RequestAuthority): string | undefined => credentials.get(authority);
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { isDeepStrictEqual } from "node:util";
|
|
2
|
+
import { type SQL, sql } from "bun";
|
|
3
|
+
import { waitWithin } from "../api/fanout";
|
|
4
|
+
import type { AppMeta } from "../contracts/app";
|
|
5
|
+
import { type AppAccessDeclaration, type AppAccessRule, AppAccessRuleSchema, type ResourceAppAccessAdapter } from "../contracts/app-access";
|
|
6
|
+
import type { RequestAuthority } from "../server/middleware/auth";
|
|
7
|
+
import { coreOrigin } from "../services/identity/core-origin";
|
|
8
|
+
import { invocationAuthorityFromRequest } from "../services/identity/invocation-authority";
|
|
9
|
+
import { type InvocationAuthority, signAppAccessProbe } from "../services/identity/invocation-token";
|
|
10
|
+
import { appAccessCredential, appAccessRequest } from "./app-access-request";
|
|
11
|
+
import { getProcessApplicationId } from "./process-identity";
|
|
12
|
+
import { getApp } from "./registry";
|
|
13
|
+
|
|
14
|
+
export const RESOURCE_ACCESS_PATH = "/api/_internal/app-access/v1";
|
|
15
|
+
// Same budget as the app-entry live revalidation deadline. No retries.
|
|
16
|
+
export const RESOURCE_ACCESS_TIMEOUT_MS = 10_000;
|
|
17
|
+
const localAdapters = new Map<string, Readonly<Record<string, ResourceAppAccessAdapter>>>();
|
|
18
|
+
const requestDecisions = new WeakMap<Request, Map<string, Promise<boolean>>>();
|
|
19
|
+
type ResourceRule = Extract<AppAccessRule, { kind: "resources" }>;
|
|
20
|
+
|
|
21
|
+
/** Called only for the running application, never populated from registry data. */
|
|
22
|
+
export const bindResourceAppAccess = (appId: string, declaration?: AppAccessDeclaration): (() => void) => {
|
|
23
|
+
if (declaration?.kind !== "resources" || Array.isArray(declaration.adapters)) return () => {};
|
|
24
|
+
const adapters = declaration.adapters;
|
|
25
|
+
localAdapters.set(appId, adapters);
|
|
26
|
+
return () => {
|
|
27
|
+
if (localAdapters.get(appId) === adapters) localAdapters.delete(appId);
|
|
28
|
+
};
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
// This probe authorizes only a boolean existence check, never an app operation.
|
|
32
|
+
// Retain the original credential ceilings and mandate provenance when Core probes.
|
|
33
|
+
export const probeAuthority = (authority: RequestAuthority): InvocationAuthority => {
|
|
34
|
+
if (authority.credentialKind !== "invocation") return invocationAuthorityFromRequest(authority);
|
|
35
|
+
const provenance = authority.actor.delegation;
|
|
36
|
+
if (!provenance) throw new Error("Missing invocation provenance");
|
|
37
|
+
const actor = authority.actor;
|
|
38
|
+
const subject = authority.accessSubject;
|
|
39
|
+
return {
|
|
40
|
+
sub: actor.kind === "user" ? actor.user.id : actor.serviceAccount.id,
|
|
41
|
+
principal_type: actor.kind,
|
|
42
|
+
access_subject_type: subject.type,
|
|
43
|
+
access_subject_id: subject.type === "user" ? subject.userId : subject.serviceAccountId,
|
|
44
|
+
...(actor.kind === "service_account" && actor.delegatedUser ? { delegated_user_id: actor.delegatedUser.id } : {}),
|
|
45
|
+
credential_kind: provenance.credentialKind,
|
|
46
|
+
...(actor.kind === "user" && actor.sessionKind === "app" ? { session_kind: "app" as const } : {}),
|
|
47
|
+
...(actor.kind === "service_account" && actor.credentialId ? { credential_id: actor.credentialId } : {}),
|
|
48
|
+
scopes: authority.scopes,
|
|
49
|
+
...(provenance.mandateId
|
|
50
|
+
? {
|
|
51
|
+
mandate_id: provenance.mandateId,
|
|
52
|
+
mandate_revision: provenance.mandateRevision,
|
|
53
|
+
workload_type: provenance.workloadType,
|
|
54
|
+
workload_id: provenance.workloadId,
|
|
55
|
+
}
|
|
56
|
+
: {}),
|
|
57
|
+
};
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
export const hasLocalResourceAppAccess = (appId: string): boolean => localAdapters.has(appId);
|
|
61
|
+
|
|
62
|
+
export const resourceAppAccess = async (
|
|
63
|
+
authority: RequestAuthority,
|
|
64
|
+
app: Pick<AppMeta, "id" | "access">,
|
|
65
|
+
options: { db?: SQL; signal?: AbortSignal } = {},
|
|
66
|
+
): Promise<boolean> => {
|
|
67
|
+
const parsed = AppAccessRuleSchema.safeParse(app.access);
|
|
68
|
+
if (!parsed.success || parsed.data.kind !== "resources") return false;
|
|
69
|
+
const rule = parsed.data;
|
|
70
|
+
const request = appAccessRequest(authority);
|
|
71
|
+
// A transaction can see uncommitted rights. Never reuse a request's earlier decision inside it.
|
|
72
|
+
const cache = request && !options.db ? (requestDecisions.get(request) ?? new Map<string, Promise<boolean>>()) : undefined;
|
|
73
|
+
if (request && cache) requestDecisions.set(request, cache);
|
|
74
|
+
const key = JSON.stringify([app.id, rule, authority]);
|
|
75
|
+
const previous = cache?.get(key);
|
|
76
|
+
if (previous) return options.signal ? waitWithin(previous, options.signal) : previous;
|
|
77
|
+
const deadline = options.signal ?? AbortSignal.timeout(RESOURCE_ACCESS_TIMEOUT_MS);
|
|
78
|
+
const signal = request ? AbortSignal.any([deadline, request.signal]) : deadline;
|
|
79
|
+
signal.throwIfAborted();
|
|
80
|
+
const pending = waitWithin(resolveResourceAccess(authority, app.id, rule, options.db ?? sql, signal), signal);
|
|
81
|
+
cache?.set(key, pending);
|
|
82
|
+
return pending;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const resolveResourceAccess = async (
|
|
86
|
+
authority: RequestAuthority,
|
|
87
|
+
appId: string,
|
|
88
|
+
rule: ResourceRule,
|
|
89
|
+
db: SQL,
|
|
90
|
+
signal: AbortSignal,
|
|
91
|
+
): Promise<boolean> => {
|
|
92
|
+
const adapters = localAdapters.get(appId);
|
|
93
|
+
if (adapters) {
|
|
94
|
+
const bound =
|
|
95
|
+
authority.actor.kind === "service_account" && authority.actor.serviceAccount.kind === "resource_bound"
|
|
96
|
+
? authority.actor.serviceAccount
|
|
97
|
+
: null;
|
|
98
|
+
for (const name of rule.adapters) {
|
|
99
|
+
if (bound && bound.resourceType !== name) continue;
|
|
100
|
+
signal.throwIfAborted();
|
|
101
|
+
const adapter = adapters[name];
|
|
102
|
+
if (!adapter) throw new Error("Resource adapter declaration changed");
|
|
103
|
+
if ((await adapter.hasAnyRight(authority.accessSubject, { authority, db, signal })) === true) return true;
|
|
104
|
+
}
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
if (getProcessApplicationId() === "core") {
|
|
108
|
+
const target = await getApp(appId);
|
|
109
|
+
if (!target || target.access?.kind !== "resources" || !isDeepStrictEqual(target.access, rule)) return false;
|
|
110
|
+
signal.throwIfAborted();
|
|
111
|
+
const token = await signAppAccessProbe({
|
|
112
|
+
appId,
|
|
113
|
+
authority: probeAuthority(authority),
|
|
114
|
+
});
|
|
115
|
+
signal.throwIfAborted();
|
|
116
|
+
const response = await fetch(new URL(`${RESOURCE_ACCESS_PATH}/check`, target.baseUrl), {
|
|
117
|
+
method: "POST",
|
|
118
|
+
headers: { authorization: `Bearer ${token}` },
|
|
119
|
+
signal,
|
|
120
|
+
redirect: "error",
|
|
121
|
+
});
|
|
122
|
+
await response.body?.cancel();
|
|
123
|
+
if (response.status !== 204 && response.status !== 403) throw new Error("Resource app-access probe unavailable");
|
|
124
|
+
return response.status === 204;
|
|
125
|
+
}
|
|
126
|
+
// Invocations and probe tokens cannot be exchanged for further probes.
|
|
127
|
+
if (authority.credentialKind === "invocation") return false;
|
|
128
|
+
const credential = appAccessCredential(authority);
|
|
129
|
+
if (!credential) return false;
|
|
130
|
+
// Forward exactly the credential admitted on the source route, never a second cookie.
|
|
131
|
+
// Reusable credentials go only to Core; the owner receives only a dedicated probe.
|
|
132
|
+
const response = await fetch(new URL(`${RESOURCE_ACCESS_PATH}/apps/${encodeURIComponent(appId)}`, await coreOrigin(true)), {
|
|
133
|
+
method: "POST",
|
|
134
|
+
headers: { authorization: `Bearer ${credential}` },
|
|
135
|
+
signal,
|
|
136
|
+
redirect: "error",
|
|
137
|
+
});
|
|
138
|
+
await response.body?.cancel();
|
|
139
|
+
if (response.status !== 204 && response.status !== 403) throw new Error("Core app-access probe unavailable");
|
|
140
|
+
return response.status === 204;
|
|
141
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { sql } from "bun";
|
|
2
|
+
import type { CapabilityEventCatalogEntry, CapabilityManifest } from "../contracts/capabilities";
|
|
3
|
+
|
|
4
|
+
export type CatalogSubscription = CapabilityEventCatalogEntry["subscribers"][number] & { eventType: string; version: number };
|
|
5
|
+
|
|
6
|
+
export const readCatalogSubscriptions = async (): Promise<CatalogSubscription[]> => {
|
|
7
|
+
const rows = await sql<(Omit<CatalogSubscription, "firstSeenAt"> & { firstSeenAt: Date })[]>`
|
|
8
|
+
SELECT app_id AS "appId", id AS "subscriptionId", event_type AS "eventType", version, state, reason,
|
|
9
|
+
first_seen_at AS "firstSeenAt" FROM events.app_subscriptions ORDER BY id
|
|
10
|
+
`;
|
|
11
|
+
return rows.map((row) => ({ ...row, firstSeenAt: row.firstSeenAt.toISOString() }));
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export const readEventCatalogSubscriptions = async (type: string, version: number): Promise<CatalogSubscription[]> => {
|
|
15
|
+
const rows = await sql<(Omit<CatalogSubscription, "firstSeenAt"> & { firstSeenAt: Date })[]>`
|
|
16
|
+
SELECT app_id AS "appId", id AS "subscriptionId", event_type AS "eventType", version, state, reason,
|
|
17
|
+
first_seen_at AS "firstSeenAt" FROM events.app_subscriptions
|
|
18
|
+
WHERE event_type = ${type} AND version = ${version} ORDER BY id
|
|
19
|
+
`;
|
|
20
|
+
return rows.map((row) => ({ ...row, firstSeenAt: row.firstSeenAt.toISOString() }));
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/** Callers filter persisted subscription states to applications their discovery surface may expose. */
|
|
24
|
+
export const projectAppEventCatalog = (
|
|
25
|
+
manifest: CapabilityManifest,
|
|
26
|
+
subscribers: readonly CatalogSubscription[],
|
|
27
|
+
): CapabilityEventCatalogEntry[] =>
|
|
28
|
+
(manifest.events ?? []).map((event) => ({
|
|
29
|
+
type: `${manifest.appId}.${event.localId}`,
|
|
30
|
+
version: event.version,
|
|
31
|
+
title: event.title,
|
|
32
|
+
description: event.description,
|
|
33
|
+
subject: `${manifest.appId}.${event.subject}`,
|
|
34
|
+
place: event.place ? `${manifest.appId}.${event.place}` : null,
|
|
35
|
+
automation: event.automation !== false,
|
|
36
|
+
dataSchema: event.dataSchema,
|
|
37
|
+
subscribers: subscribers
|
|
38
|
+
.filter((subscription) => subscription.eventType === `${manifest.appId}.${event.localId}` && subscription.version === event.version)
|
|
39
|
+
.map(({ eventType: _eventType, version: _version, ...subscription }) => subscription),
|
|
40
|
+
}));
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { APP_EVENT_MAX_BYTES } from "../contracts/app-events";
|
|
3
|
+
import { capabilityHash, projectSchema } from "./capability-schemas";
|
|
4
|
+
|
|
5
|
+
/** Closed objects, finite strings and bounded arrays; no recursive refs or open records. */
|
|
6
|
+
export const assertBoundedAppEventSchema = (schema: Record<string, unknown>): void => {
|
|
7
|
+
const visit = (node: unknown): void => {
|
|
8
|
+
if (!node || typeof node !== "object" || Array.isArray(node)) throw new Error("Event data needs a bounded JSON schema");
|
|
9
|
+
const value = node as Record<string, unknown>;
|
|
10
|
+
if (value.$ref || value.$dynamicRef) throw new Error("Event data cannot contain recursive or referenced schemas");
|
|
11
|
+
if (value.const !== undefined || Array.isArray(value.enum)) return;
|
|
12
|
+
if (Array.isArray(value.anyOf) || Array.isArray(value.oneOf)) {
|
|
13
|
+
for (const branch of (value.anyOf ?? value.oneOf) as unknown[]) visit(branch);
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
if (value.type === "object") {
|
|
17
|
+
if (value.additionalProperties !== false) throw new Error("Event objects must reject unknown properties");
|
|
18
|
+
for (const property of Object.values(value.properties ?? {})) visit(property);
|
|
19
|
+
} else if (value.type === "array") {
|
|
20
|
+
if (!Number.isSafeInteger(value.maxItems) || Number(value.maxItems) < 0) throw new Error("Event arrays need maxItems");
|
|
21
|
+
visit(value.items);
|
|
22
|
+
} else if (value.type === "string") {
|
|
23
|
+
if ((!Number.isSafeInteger(value.maxLength) || Number(value.maxLength) < 0) && value.format !== "uuid") {
|
|
24
|
+
throw new Error("Event strings need maxLength (UUIDs have a fixed bound)");
|
|
25
|
+
}
|
|
26
|
+
} else if (!["number", "integer", "boolean", "null"].includes(String(value.type))) {
|
|
27
|
+
throw new Error("Event data needs a bounded JSON schema");
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
if (schema.type !== "object") throw new Error("Event data must be a closed object");
|
|
31
|
+
visit(schema);
|
|
32
|
+
z.fromJSONSchema(structuredClone(schema));
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export const compileAppEventSchema = (schema: z.ZodType, label: string, mode: "producer" | "reader" = "producer") => {
|
|
36
|
+
const dataSchema = projectSchema(schema, label, mode === "reader" ? "output" : "input");
|
|
37
|
+
const output = projectSchema(schema, label, "output");
|
|
38
|
+
if (capabilityHash(dataSchema) !== capabilityHash(output)) throw new Error(`${label} must preserve its JSON shape`);
|
|
39
|
+
assertBoundedAppEventSchema(dataSchema);
|
|
40
|
+
return { dataSchema, schemaHash: capabilityHash({ dataSchema }) };
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
type JsonValue = z.infer<ReturnType<typeof z.json>>;
|
|
44
|
+
|
|
45
|
+
const decimalNumber = (value: number): string => {
|
|
46
|
+
const text = String(value);
|
|
47
|
+
const match = /^(-?)(\d+)(?:\.(\d+))?e([+-]?\d+)$/.exec(text);
|
|
48
|
+
if (!match) return text;
|
|
49
|
+
const [, sign = "", whole = "", fraction = "", exponent = "0"] = match;
|
|
50
|
+
const digits = whole + fraction;
|
|
51
|
+
const point = whole.length + Number(exponent);
|
|
52
|
+
return (
|
|
53
|
+
sign +
|
|
54
|
+
(point <= 0
|
|
55
|
+
? `0.${"0".repeat(-point)}${digits}`
|
|
56
|
+
: point >= digits.length
|
|
57
|
+
? digits + "0".repeat(point - digits.length)
|
|
58
|
+
: `${digits.slice(0, point)}.${digits.slice(point)}`)
|
|
59
|
+
);
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
/** PostgreSQL jsonb::text uses spaces after separators and expands numeric exponents. */
|
|
63
|
+
export const appEventJsonbBytes = (value: JsonValue): number => {
|
|
64
|
+
const text = (entry: JsonValue): string => {
|
|
65
|
+
if (Array.isArray(entry)) return `[${entry.map(text).join(", ")}]`;
|
|
66
|
+
if (entry !== null && typeof entry === "object")
|
|
67
|
+
return `{${Object.entries(entry)
|
|
68
|
+
.map(([key, child]) => `${JSON.stringify(key)}: ${text(child)}`)
|
|
69
|
+
.join(", ")}}`;
|
|
70
|
+
return typeof entry === "number" ? decimalNumber(entry) : JSON.stringify(entry);
|
|
71
|
+
};
|
|
72
|
+
return new TextEncoder().encode(text(value)).byteLength;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
export const assertAppEventSize = (value: JsonValue): void => {
|
|
76
|
+
if (appEventJsonbBytes(value) > APP_EVENT_MAX_BYTES)
|
|
77
|
+
throw new Error(`App event exceeds ${APP_EVENT_MAX_BYTES} PostgreSQL jsonb-text bytes`);
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
/** Lower bound for required data; no allocation proportional to maxItems/maxLength. */
|
|
81
|
+
export const minimumAppEventDataBytes = (schema: Record<string, unknown>): number => {
|
|
82
|
+
if (schema.const !== undefined) return appEventJsonbBytes(z.json().parse(schema.const));
|
|
83
|
+
if (Array.isArray(schema.enum)) return Math.min(...schema.enum.map((value) => appEventJsonbBytes(z.json().parse(value))));
|
|
84
|
+
const alternatives = schema.anyOf ?? schema.oneOf;
|
|
85
|
+
if (Array.isArray(alternatives))
|
|
86
|
+
return Math.min(...alternatives.map((value) => minimumAppEventDataBytes(z.record(z.string(), z.unknown()).parse(value))));
|
|
87
|
+
if (schema.type === "string") return 2 + (schema.format === "uuid" ? 36 : Number(schema.minLength ?? 0));
|
|
88
|
+
if (schema.type === "null") return 4;
|
|
89
|
+
if (schema.type === "boolean") return 4;
|
|
90
|
+
if (schema.type === "number" || schema.type === "integer") return 1;
|
|
91
|
+
if (schema.type === "array") {
|
|
92
|
+
const count = Number(schema.minItems ?? 0);
|
|
93
|
+
return 2 + count * minimumAppEventDataBytes(z.record(z.string(), z.unknown()).parse(schema.items)) + Math.max(0, count - 1) * 2;
|
|
94
|
+
}
|
|
95
|
+
const properties = z.record(z.string(), z.unknown()).parse(schema.properties ?? {});
|
|
96
|
+
const keys = z.array(z.string()).parse(schema.required ?? []);
|
|
97
|
+
return (
|
|
98
|
+
2 +
|
|
99
|
+
Math.max(0, keys.length - 1) * 2 +
|
|
100
|
+
keys.reduce(
|
|
101
|
+
(bytes, key) =>
|
|
102
|
+
bytes + appEventJsonbBytes(key) + 2 + minimumAppEventDataBytes(z.record(z.string(), z.unknown()).parse(properties[key])),
|
|
103
|
+
0,
|
|
104
|
+
)
|
|
105
|
+
);
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
export const assertAppEventDeclarationSize = (
|
|
109
|
+
appId: string,
|
|
110
|
+
localId: string,
|
|
111
|
+
definition: { version: number; subject: string; place?: string },
|
|
112
|
+
dataSchema: Record<string, unknown>,
|
|
113
|
+
): void => {
|
|
114
|
+
const id = "00000000-0000-4000-8000-000000000001";
|
|
115
|
+
const shell = {
|
|
116
|
+
id,
|
|
117
|
+
type: `${appId}.${localId}`,
|
|
118
|
+
version: definition.version,
|
|
119
|
+
source: appId,
|
|
120
|
+
time: "2000-01-01T00:00:00.000Z",
|
|
121
|
+
subject: { type: `${appId}.${definition.subject}`, id },
|
|
122
|
+
...(definition.place ? { place: { type: `${appId}.${definition.place}`, id } } : {}),
|
|
123
|
+
data: null,
|
|
124
|
+
};
|
|
125
|
+
if (appEventJsonbBytes(shell) - 4 + minimumAppEventDataBytes(dataSchema) > APP_EVENT_MAX_BYTES)
|
|
126
|
+
throw new Error(`Event ${localId} required envelope cannot fit ${APP_EVENT_MAX_BYTES} bytes`);
|
|
127
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { type SQL, sql } from "bun";
|
|
2
|
+
import { ensureSchema } from "../services/postgres";
|
|
3
|
+
import builtInAppIds from "./built-in-app-ids.json";
|
|
4
|
+
|
|
5
|
+
export const resolveApplicationPackage = (appId: string, declared?: string): string => {
|
|
6
|
+
const packageName = declared ?? appId;
|
|
7
|
+
if (!/^[a-z][a-z0-9-]{0,79}$/.test(appId) || typeof packageName !== "string" || !/^[a-z][a-z0-9-]{0,79}$/.test(packageName)) {
|
|
8
|
+
throw new TypeError("Application IDs and packages must be lowercase names of 1–80 characters.");
|
|
9
|
+
}
|
|
10
|
+
if (appId === "storage" || packageName === "storage")
|
|
11
|
+
throw new Error("Application ID and package storage are reserved for public storage routing.");
|
|
12
|
+
if (packageName !== appId && builtInAppIds.includes(packageName)) {
|
|
13
|
+
throw new Error(`Package "${packageName}" is reserved for its built-in application.`);
|
|
14
|
+
}
|
|
15
|
+
return packageName;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/** Durable reservations survive app registry expiry. Serialize checks and writes across replicas. */
|
|
19
|
+
export const registerApplicationPackage = async (appId: string, declared?: string, db: SQL = sql): Promise<void> => {
|
|
20
|
+
const packageName = resolveApplicationPackage(appId, declared);
|
|
21
|
+
await db.begin(async (tx) => {
|
|
22
|
+
await tx`SELECT pg_advisory_xact_lock(hashtext('cloud.application-packages'))`;
|
|
23
|
+
await ensureSchema(tx, "platform");
|
|
24
|
+
await tx`CREATE TABLE IF NOT EXISTS platform.application_packages (
|
|
25
|
+
app_id TEXT PRIMARY KEY, package_name TEXT NOT NULL
|
|
26
|
+
)`.simple();
|
|
27
|
+
const conflicts = await tx<{ app_id: string; package_name: string }[]>`
|
|
28
|
+
SELECT app_id, package_name FROM platform.application_packages
|
|
29
|
+
WHERE (app_id <> ${appId} AND (app_id = ${packageName} OR package_name = ${appId}))
|
|
30
|
+
OR (app_id = ${appId} AND package_name <> ${packageName})
|
|
31
|
+
LIMIT 1
|
|
32
|
+
`;
|
|
33
|
+
if (conflicts.length) {
|
|
34
|
+
throw new Error(
|
|
35
|
+
`Application "${appId}" cannot claim package "${packageName}": an application ID or existing package assignment is reserved.`,
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
await tx`INSERT INTO platform.application_packages (app_id, package_name) VALUES (${appId}, ${packageName}) ON CONFLICT DO NOTHING`;
|
|
39
|
+
});
|
|
40
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
[
|
|
2
|
+
"accounts",
|
|
3
|
+
"api-docs",
|
|
4
|
+
"assistant",
|
|
5
|
+
"capabilities",
|
|
6
|
+
"chat",
|
|
7
|
+
"contacts",
|
|
8
|
+
"core",
|
|
9
|
+
"dashboard",
|
|
10
|
+
"faq",
|
|
11
|
+
"filesv2",
|
|
12
|
+
"gateway",
|
|
13
|
+
"gateway-ops",
|
|
14
|
+
"grids",
|
|
15
|
+
"ipa-hosts",
|
|
16
|
+
"mail",
|
|
17
|
+
"notebooks",
|
|
18
|
+
"oauth",
|
|
19
|
+
"proxy-auth",
|
|
20
|
+
"pulse",
|
|
21
|
+
"pwa",
|
|
22
|
+
"quotes",
|
|
23
|
+
"spaces",
|
|
24
|
+
"tools",
|
|
25
|
+
"venue",
|
|
26
|
+
"weather"
|
|
27
|
+
]
|