@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
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { type CloudResourceRef, CloudResourceRefSchema } from "./capabilities";
|
|
3
|
+
import { cloudResourceHref } from "./resource-href";
|
|
3
4
|
|
|
4
5
|
export const CLOUD_RESOURCE_CLIPBOARD_MIME_TYPE = "application/vnd.k2b.cloud-resource-ref+json";
|
|
5
6
|
export const CLOUD_RESOURCE_CLIPBOARD_WEB_FORMAT = `web ${CLOUD_RESOURCE_CLIPBOARD_MIME_TYPE}`;
|
|
@@ -31,3 +32,37 @@ export const parseCloudResourceClipboard = (value: string, cloudUrl: string): Cl
|
|
|
31
32
|
return null;
|
|
32
33
|
}
|
|
33
34
|
};
|
|
35
|
+
|
|
36
|
+
/** Longest permalink a consumer accepts, like `CloudResourceHrefSchema`. */
|
|
37
|
+
const CLOUD_RESOURCE_URL_MAX_LENGTH = 8_192;
|
|
38
|
+
const CLOUD_RESOURCE_PATH = /^\/open\/([^/]+)\/~([^/]+)$/;
|
|
39
|
+
|
|
40
|
+
/** The absolute permalink of a resource: the text form people copy, paste, and open. */
|
|
41
|
+
export const formatCloudResourceUrl = (ref: CloudResourceRef, cloudUrl: string): string =>
|
|
42
|
+
new URL(cloudResourceHref(CloudResourceRefSchema.parse(ref)), cloudUrl).href;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Reads a resource from its permalink on this Cloud, absolute or root-relative. Anything else, such as another
|
|
46
|
+
* installation's link, an app page, or a query or fragment, returns `null`.
|
|
47
|
+
*/
|
|
48
|
+
export const parseCloudResourceUrl = (value: string, cloudUrl: string): CloudResourceRef | null => {
|
|
49
|
+
const text = value.trim();
|
|
50
|
+
if (text.length === 0 || text.length > CLOUD_RESOURCE_URL_MAX_LENGTH) return null;
|
|
51
|
+
let url: URL;
|
|
52
|
+
try {
|
|
53
|
+
url = text.startsWith("/") && !text.startsWith("//") ? new URL(text, cloudUrl) : new URL(text);
|
|
54
|
+
} catch {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
if (url.origin !== cloudUrl || url.search || url.hash) return null;
|
|
58
|
+
const match = CLOUD_RESOURCE_PATH.exec(url.pathname);
|
|
59
|
+
if (!match?.[1] || !match[2]) return null;
|
|
60
|
+
let id: string;
|
|
61
|
+
try {
|
|
62
|
+
id = decodeURIComponent(match[2]);
|
|
63
|
+
} catch {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
const ref = CloudResourceRefSchema.safeParse({ type: match[1], id });
|
|
67
|
+
return ref.success ? ref.data : null;
|
|
68
|
+
};
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import type { CloudResourceRef } from "./capabilities";
|
|
2
|
+
|
|
3
|
+
/** Eight alternate pages per Type, within the manifest's 200-Type budget. */
|
|
4
|
+
export const RESOURCE_PAGES_PER_TYPE = 8;
|
|
5
|
+
/** Like other manifest route declarations, page patterns are bounded short paths. */
|
|
6
|
+
export const RESOURCE_PAGE_MAX_LENGTH = 300;
|
|
7
|
+
export const RESOURCE_URL_MAX_LENGTH = 8_192;
|
|
8
|
+
|
|
9
|
+
type Part = { literal: string } | { capture: string };
|
|
10
|
+
type Pattern = { segments: Part[]; query: [string, Part][] };
|
|
11
|
+
const capture = /^:([a-zA-Z][a-zA-Z0-9_]*)$/;
|
|
12
|
+
const decode = (value: string): string => decodeURIComponent(value);
|
|
13
|
+
const part = (value: string): Part => {
|
|
14
|
+
const match = capture.exec(value);
|
|
15
|
+
if (match?.[1]) return { capture: match[1] };
|
|
16
|
+
if (!value || value.includes(":")) throw new Error("Captures must occupy a whole segment or query value");
|
|
17
|
+
const literal = decode(value);
|
|
18
|
+
if (!literal || literal === "." || literal === ".." || /[\\/\u0000-\u001f]/.test(literal)) throw new Error("Invalid literal segment");
|
|
19
|
+
return { literal };
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const parsePattern = (path: string): Pattern => {
|
|
23
|
+
if (!path.startsWith("/") || path.startsWith("//") || path.length > RESOURCE_PAGE_MAX_LENGTH || /[\s#\\]/.test(path))
|
|
24
|
+
throw new Error(`Expected a root-relative page pattern of at most ${RESOURCE_PAGE_MAX_LENGTH} characters`);
|
|
25
|
+
const [pathname = "", search = "", extra] = path.split("?");
|
|
26
|
+
if (extra !== undefined) throw new Error("Page patterns may contain only one query separator");
|
|
27
|
+
const route = pathname.replace(/\/$/, "").slice(1);
|
|
28
|
+
const segments = route ? route.split("/").map(part) : [];
|
|
29
|
+
const keys = new Set<string>();
|
|
30
|
+
const query: [string, Part][] = search
|
|
31
|
+
? search.split("&").map((entry) => {
|
|
32
|
+
const [key = "", value, extraValue] = entry.split("=");
|
|
33
|
+
if (!key || value === undefined || extraValue !== undefined) throw new Error("Expected query key=value");
|
|
34
|
+
const decodedKey = decode(key.replace(/\+/g, " "));
|
|
35
|
+
if (!decodedKey || decodedKey.includes(":") || keys.has(decodedKey)) throw new Error("Invalid or repeated query key");
|
|
36
|
+
keys.add(decodedKey);
|
|
37
|
+
return [decodedKey, part(value.replace(/\+/g, " "))];
|
|
38
|
+
})
|
|
39
|
+
: [];
|
|
40
|
+
const parts = [...segments, ...query.map(([, value]) => value)];
|
|
41
|
+
if (parts.filter((value) => "capture" in value && value.capture === "id").length !== 1)
|
|
42
|
+
throw new Error("A page pattern must contain exactly one :id capture");
|
|
43
|
+
return { segments, query };
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/** Validates syntax and ownership; prefixes use the gateway's literal segment boundary. */
|
|
47
|
+
export const resourcePageIssue = (path: string, prefixes?: readonly string[], appId?: string): string | null => {
|
|
48
|
+
try {
|
|
49
|
+
const pattern = parsePattern(path);
|
|
50
|
+
const namespace = pattern.segments[0];
|
|
51
|
+
const owner = pattern.segments[1];
|
|
52
|
+
if (
|
|
53
|
+
appId &&
|
|
54
|
+
namespace &&
|
|
55
|
+
"literal" in namespace &&
|
|
56
|
+
namespace.literal === "app" &&
|
|
57
|
+
(!owner || !("literal" in owner) || owner.literal !== appId)
|
|
58
|
+
)
|
|
59
|
+
return "Only app <id> may declare pages below /app/<id>";
|
|
60
|
+
if (
|
|
61
|
+
prefixes &&
|
|
62
|
+
!prefixes.some((prefix) => {
|
|
63
|
+
if (prefix === "/") return true;
|
|
64
|
+
if (!prefix.startsWith("/") || /[?#%\\:]/.test(prefix)) return false;
|
|
65
|
+
const segments = prefix.replace(/\/$/, "").slice(1).split("/");
|
|
66
|
+
return segments.every((value, index) => {
|
|
67
|
+
const segment = pattern.segments[index];
|
|
68
|
+
return segment && "literal" in segment && segment.literal === value;
|
|
69
|
+
});
|
|
70
|
+
})
|
|
71
|
+
)
|
|
72
|
+
return "Page pattern must lie under an application's declared route prefix";
|
|
73
|
+
return null;
|
|
74
|
+
} catch (error) {
|
|
75
|
+
return error instanceof Error ? error.message : "Invalid page pattern";
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
export type CloudResourcePage = { type: string; path: string; prefixes: readonly string[] };
|
|
80
|
+
|
|
81
|
+
/** Required query keys outrank literal path segments. Equal best scores for different refs are ambiguous. */
|
|
82
|
+
export const matchCloudResourcePage = (path: string, pages: readonly CloudResourcePage[]): CloudResourceRef | null => {
|
|
83
|
+
if (!path.startsWith("/") || path.startsWith("//") || path.length > RESOURCE_URL_MAX_LENGTH || /[\s\\]/.test(path)) return null;
|
|
84
|
+
try {
|
|
85
|
+
const url = new URL(path, "https://cloud.invalid");
|
|
86
|
+
const route = url.pathname.replace(/\/$/, "").slice(1);
|
|
87
|
+
const segments = route ? route.split("/").map(decode) : [];
|
|
88
|
+
// URLSearchParams silently repairs malformed escapes: validate before reading it.
|
|
89
|
+
decode(url.search.replace(/\+/g, " "));
|
|
90
|
+
if (segments.some((value) => !value)) return null;
|
|
91
|
+
let best: { ref: CloudResourceRef; queries: number; literals: number } | undefined;
|
|
92
|
+
let ambiguous = false;
|
|
93
|
+
for (const page of pages) {
|
|
94
|
+
if (resourcePageIssue(page.path, page.prefixes, page.type.split(".")[0])) continue;
|
|
95
|
+
const pattern = parsePattern(page.path);
|
|
96
|
+
if (segments.length !== pattern.segments.length) continue;
|
|
97
|
+
let id: string | undefined;
|
|
98
|
+
const matches = (part: Part, value: string | undefined): boolean => {
|
|
99
|
+
if (!value) return false;
|
|
100
|
+
if ("literal" in part) return part.literal === value;
|
|
101
|
+
if (part.capture === "id") id = value;
|
|
102
|
+
return true;
|
|
103
|
+
};
|
|
104
|
+
if (!pattern.segments.every((part, index) => matches(part, segments[index]))) continue;
|
|
105
|
+
if (
|
|
106
|
+
!pattern.query.every(([key, part]) => {
|
|
107
|
+
const values = url.searchParams.getAll(key);
|
|
108
|
+
return values.length === 1 && matches(part, values[0]);
|
|
109
|
+
}) ||
|
|
110
|
+
!id
|
|
111
|
+
)
|
|
112
|
+
continue;
|
|
113
|
+
const ref = { type: page.type, id };
|
|
114
|
+
const queries = pattern.query.length;
|
|
115
|
+
const literals = pattern.segments.filter((part) => "literal" in part).length;
|
|
116
|
+
if (!best || queries > best.queries || (queries === best.queries && literals > best.literals)) {
|
|
117
|
+
best = { ref, queries, literals };
|
|
118
|
+
ambiguous = false;
|
|
119
|
+
} else if (queries === best.queries && literals === best.literals && (ref.type !== best.ref.type || ref.id !== best.ref.id)) {
|
|
120
|
+
ambiguous = true;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return best && !ambiguous ? best.ref : null;
|
|
124
|
+
} catch {
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
/** No network activity: accept one local URL token and reject origin tricks before catalog lookup. */
|
|
130
|
+
export const localCloudResourceUrl = (text: string, origin: string | null): URL | null => {
|
|
131
|
+
if (!text || text.length > RESOURCE_URL_MAX_LENGTH || /[\s\\\u0000-\u001f]/.test(text) || text.startsWith("//")) return null;
|
|
132
|
+
try {
|
|
133
|
+
const relative = text.startsWith("/");
|
|
134
|
+
const url = relative ? new URL(text, origin ?? "https://cloud.invalid") : new URL(text);
|
|
135
|
+
if (!/^https?:$/.test(url.protocol) || url.username || url.password || (!relative && (!origin || url.origin !== origin))) return null;
|
|
136
|
+
return url;
|
|
137
|
+
} catch {
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { CloudResourceHrefSchema, CloudResourceRefSchema } from "./capabilities";
|
|
3
|
+
|
|
4
|
+
export const ResolvedCloudResourceUrlSchema = z
|
|
5
|
+
.object({
|
|
6
|
+
ref: CloudResourceRefSchema,
|
|
7
|
+
title: z.string().min(1).max(500),
|
|
8
|
+
icon: z.string().min(1).max(120).optional(),
|
|
9
|
+
href: CloudResourceHrefSchema,
|
|
10
|
+
})
|
|
11
|
+
.strict();
|
|
12
|
+
|
|
13
|
+
export type ResolvedCloudResourceUrl = z.infer<typeof ResolvedCloudResourceUrlSchema>;
|
package/src/contracts/shared.ts
CHANGED
|
@@ -392,6 +392,8 @@ export type SettingOption = {
|
|
|
392
392
|
export type SettingValueSource = "custom" | "env" | "default";
|
|
393
393
|
|
|
394
394
|
export type SettingEntry = {
|
|
395
|
+
updatedAt?: string | null;
|
|
396
|
+
updatedBy?: { id: string; name: string } | null;
|
|
395
397
|
key: string;
|
|
396
398
|
label: string;
|
|
397
399
|
kind: SettingKind;
|
|
@@ -443,6 +445,8 @@ export type ServiceAccountRequestActor = {
|
|
|
443
445
|
delegatedUser: User | null;
|
|
444
446
|
scopes: string[];
|
|
445
447
|
credentialId?: string | null;
|
|
448
|
+
/** Public OAuth client identifier, never the bearer token. */
|
|
449
|
+
oauthClientId?: string | null;
|
|
446
450
|
credentialExpiresAt?: string | null;
|
|
447
451
|
delegation?: InvocationProvenance;
|
|
448
452
|
};
|
|
@@ -12,6 +12,8 @@ export const UserNotificationPreferenceSchema = z.object({
|
|
|
12
12
|
description: z.string(),
|
|
13
13
|
recommendedChannels: z.array(z.string()),
|
|
14
14
|
requiredChannels: z.array(z.string()),
|
|
15
|
+
excludedChannels: z.array(z.string()),
|
|
16
|
+
excludedReason: z.string().nullable(),
|
|
15
17
|
selectedChannels: z.array(z.string()),
|
|
16
18
|
effectiveChannels: z.array(z.string()),
|
|
17
19
|
customized: z.boolean(),
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import type { Topic, TopicConfig } from "@k2b/sync";
|
|
2
|
+
import { sql } from "bun";
|
|
3
|
+
import { assertAppEventSize } from "../_internal/app-event-schemas";
|
|
4
|
+
import { lazySync } from "../_internal/process-sync";
|
|
5
|
+
import { type AppEventEnvelope, AppEventEnvelopeSchema } from "../contracts/app-events";
|
|
6
|
+
import { logger } from "../services/logging";
|
|
7
|
+
import { createPgOutbox } from "../services/outbox";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Frozen shared stream. It only buffers events until Core has stored them, and
|
|
11
|
+
* JetStream reserves `maxBytes` on every replica: 256 MiB hold 52,428 envelopes
|
|
12
|
+
* at the 4096-byte limit plus a 1024-byte transport allowance. The shared
|
|
13
|
+
* producer budget and thin-envelope outage scenarios are in operations/app-events.
|
|
14
|
+
* Retention passing unseen events becomes a visible gap.
|
|
15
|
+
*/
|
|
16
|
+
export const appEventTopicConfig = () =>
|
|
17
|
+
({
|
|
18
|
+
id: "cloud:app-events",
|
|
19
|
+
owner: "cloud",
|
|
20
|
+
retention: { maxAgeMs: 7 * 24 * 3_600_000, maxBytes: 256 * 1024 ** 2 },
|
|
21
|
+
maxPayloadBytes: 8 * 1024,
|
|
22
|
+
deadLetterRetention: { maxBytes: 1024 ** 2 },
|
|
23
|
+
}) satisfies TopicConfig;
|
|
24
|
+
|
|
25
|
+
const topic = lazySync((sync) => sync.topic<AppEventEnvelope>(appEventTopicConfig()));
|
|
26
|
+
export const appEventTopic = (): Topic<AppEventEnvelope> => topic();
|
|
27
|
+
|
|
28
|
+
export type AppEventOutboxRow = {
|
|
29
|
+
id: string;
|
|
30
|
+
app_id: string;
|
|
31
|
+
created_at: Date | string;
|
|
32
|
+
attempts: number;
|
|
33
|
+
ordering_key: string;
|
|
34
|
+
payload: AppEventEnvelope;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/** The committed row owns identity, source and timestamp, including for SQL writers. */
|
|
38
|
+
export const appEventFromOutbox = (row: AppEventOutboxRow): AppEventEnvelope => {
|
|
39
|
+
const envelope = AppEventEnvelopeSchema.parse({
|
|
40
|
+
...row.payload,
|
|
41
|
+
id: row.id,
|
|
42
|
+
source: row.app_id,
|
|
43
|
+
time: new Date(row.created_at).toISOString(),
|
|
44
|
+
});
|
|
45
|
+
assertAppEventSize(envelope);
|
|
46
|
+
return envelope;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export const appEventOutbox = (appId: string, publish: (row: AppEventOutboxRow) => Promise<unknown>) =>
|
|
50
|
+
createPgOutbox<AppEventOutboxRow>({
|
|
51
|
+
table: "events.outbox",
|
|
52
|
+
name: `events:app:${appId}`,
|
|
53
|
+
where: { kind: "event", app_id: appId },
|
|
54
|
+
orderBy: "ordering_key",
|
|
55
|
+
sequence: "seq",
|
|
56
|
+
reconcileIntervalMs: 1_000,
|
|
57
|
+
publish,
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const dispatchers = new Map<string, (() => void) | null>();
|
|
61
|
+
export const registerAppEventPublisher = (appId: string): void => {
|
|
62
|
+
if (!dispatchers.has(appId)) dispatchers.set(appId, null);
|
|
63
|
+
};
|
|
64
|
+
export const wakeAppEventDelivery = (appId: string): void => dispatchers.get(appId)?.();
|
|
65
|
+
|
|
66
|
+
export const startAppEventDelivery = async (appId: string): Promise<(() => Promise<void>) | null> => {
|
|
67
|
+
if (!dispatchers.has(appId)) return null;
|
|
68
|
+
const [installed] = await sql<{ ready: boolean }[]>`
|
|
69
|
+
SELECT to_regprocedure('events.enqueue(uuid,text,text,text,jsonb,text)') IS NOT NULL AS ready
|
|
70
|
+
`;
|
|
71
|
+
if (!installed?.ready) throw new Error(`"${appId}" publishes app events: update Cloud Core first to install events.outbox`);
|
|
72
|
+
const destination = appEventTopic();
|
|
73
|
+
const outbox = appEventOutbox(appId, (row) =>
|
|
74
|
+
destination.publish({
|
|
75
|
+
data: appEventFromOutbox(row),
|
|
76
|
+
orderingKey: row.ordering_key,
|
|
77
|
+
idempotencyKey: row.id,
|
|
78
|
+
}),
|
|
79
|
+
);
|
|
80
|
+
outbox.start();
|
|
81
|
+
dispatchers.set(appId, () => void outbox.notify());
|
|
82
|
+
const log = logger("events:app");
|
|
83
|
+
const staleCheck = setInterval(() => {
|
|
84
|
+
sql<
|
|
85
|
+
{ count: number }[]
|
|
86
|
+
>`SELECT COUNT(*)::int AS count FROM events.outbox WHERE kind = 'event' AND app_id = ${appId} AND created_at < now() - interval '60 seconds'`
|
|
87
|
+
.then(([row]) => {
|
|
88
|
+
if (row && row.count > 0) log.warn("App events wait to be published", { appId, count: row.count });
|
|
89
|
+
})
|
|
90
|
+
.catch((error: unknown) =>
|
|
91
|
+
log.warn("App event outbox check failed", { appId, error: error instanceof Error ? error.message : String(error) }),
|
|
92
|
+
);
|
|
93
|
+
}, 60_000);
|
|
94
|
+
staleCheck.unref();
|
|
95
|
+
return async () => {
|
|
96
|
+
clearInterval(staleCheck);
|
|
97
|
+
dispatchers.set(appId, null);
|
|
98
|
+
await outbox.stop();
|
|
99
|
+
};
|
|
100
|
+
};
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type AppEventDeliveryContext,
|
|
3
|
+
type AppEventEnvelope,
|
|
4
|
+
AppEventEnvelopeSchema,
|
|
5
|
+
appEventSubscriptionId,
|
|
6
|
+
} from "../contracts/app-events";
|
|
7
|
+
import type { CapabilityDefinitions } from "../contracts/capabilities";
|
|
8
|
+
import {
|
|
9
|
+
type AppEventPublishInput,
|
|
10
|
+
appEventBinding,
|
|
11
|
+
bindAppEvents,
|
|
12
|
+
buildAppEventEnvelope,
|
|
13
|
+
deliverAppEvent as deliverCompiledAppEvent,
|
|
14
|
+
} from "./app-events";
|
|
15
|
+
|
|
16
|
+
type EventKey<T extends CapabilityDefinitions> = Extract<keyof NonNullable<T["events"]>, string>;
|
|
17
|
+
|
|
18
|
+
/** Producer fixture, validated exactly like publish. A standalone test may supply its app ID explicitly. */
|
|
19
|
+
export const appEventFixture = <const T extends CapabilityDefinitions, Key extends EventKey<T>>(
|
|
20
|
+
capabilities: T,
|
|
21
|
+
localName: Key,
|
|
22
|
+
input: AppEventPublishInput<T, Key>,
|
|
23
|
+
options: { id?: string; time?: string; appId?: string } = {},
|
|
24
|
+
): AppEventEnvelope => {
|
|
25
|
+
if (options.appId !== undefined) bindAppEvents(options.appId, capabilities);
|
|
26
|
+
return buildAppEventEnvelope(capabilities, localName, input, {
|
|
27
|
+
id: options.id ?? "00000000-0000-4000-8000-000000000001",
|
|
28
|
+
time: options.time ?? "2026-01-01T00:00:00.000Z",
|
|
29
|
+
});
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/** Exercises the framework's real delivery validation and the declared subscriber. */
|
|
33
|
+
export const deliverAppEvent = async (
|
|
34
|
+
capabilities: CapabilityDefinitions,
|
|
35
|
+
eventType: string,
|
|
36
|
+
envelope: unknown,
|
|
37
|
+
context: Partial<AppEventDeliveryContext> & { appId?: string } = {},
|
|
38
|
+
): Promise<void> => {
|
|
39
|
+
if (context.appId !== undefined) bindAppEvents(context.appId, capabilities);
|
|
40
|
+
const { appId, compiled } = appEventBinding(capabilities);
|
|
41
|
+
const parsed = AppEventEnvelopeSchema.parse(envelope);
|
|
42
|
+
if (parsed.type !== eventType) throw new Error("App event type does not match the tested subscription");
|
|
43
|
+
const subscriptionId = appEventSubscriptionId(appId, eventType, parsed.version);
|
|
44
|
+
await deliverCompiledAppEvent({
|
|
45
|
+
compiled,
|
|
46
|
+
subscriptionId,
|
|
47
|
+
envelope,
|
|
48
|
+
context: {
|
|
49
|
+
subscriptionId: context.subscriptionId ?? subscriptionId,
|
|
50
|
+
eventId: context.eventId ?? parsed.id,
|
|
51
|
+
attempt: context.attempt ?? 1,
|
|
52
|
+
signal: context.signal ?? new AbortController().signal,
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
/** Explicit app ID for standalone declarations that have no defineApp binding. */
|
|
58
|
+
export const createAppEventTestHarness = (appId: string, capabilities: CapabilityDefinitions) => {
|
|
59
|
+
bindAppEvents(appId, capabilities);
|
|
60
|
+
return {
|
|
61
|
+
manifest: appEventBinding(capabilities).compiled.manifest,
|
|
62
|
+
deliver: (envelope: AppEventEnvelope, context: Partial<AppEventDeliveryContext> = {}): Promise<void> =>
|
|
63
|
+
deliverAppEvent(capabilities, envelope.type, envelope, context),
|
|
64
|
+
};
|
|
65
|
+
};
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { isDeepStrictEqual } from "node:util";
|
|
2
|
+
import type { SQL } from "bun";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { assertAppEventSize } from "../_internal/app-event-schemas";
|
|
5
|
+
import { type CompiledCapabilities, compileCapabilities } from "../_internal/capabilities";
|
|
6
|
+
import { capabilityHash } from "../_internal/capability-schemas";
|
|
7
|
+
import {
|
|
8
|
+
type AppEventDeliveryContext,
|
|
9
|
+
type AppEventEnvelope,
|
|
10
|
+
AppEventEnvelopeSchema,
|
|
11
|
+
appEventSubscriptionId,
|
|
12
|
+
} from "../contracts/app-events";
|
|
13
|
+
import { CapabilityAppIdSchema, type CapabilityDefinitions } from "../contracts/capabilities";
|
|
14
|
+
import { registerAppEventPublisher, wakeAppEventDelivery } from "./app-event-delivery";
|
|
15
|
+
|
|
16
|
+
type EventBinding = { appId: string; compiled: CompiledCapabilities };
|
|
17
|
+
const bindings = new WeakMap<CapabilityDefinitions, EventBinding>();
|
|
18
|
+
const bindingsByApp = new Map<string, EventBinding>();
|
|
19
|
+
|
|
20
|
+
/** Framework hook: bind the exact declaration object when defineApp assigns its app ID. */
|
|
21
|
+
export const bindAppEvents = (appId: string, definitions: CapabilityDefinitions, compiledInput?: CompiledCapabilities): void => {
|
|
22
|
+
CapabilityAppIdSchema.parse(appId);
|
|
23
|
+
const prior = bindings.get(definitions);
|
|
24
|
+
if (prior && prior.appId !== appId) throw new Error(`Capabilities are already bound to ${prior.appId}`);
|
|
25
|
+
const compiled = compiledInput ?? prior?.compiled ?? compileCapabilities(appId, definitions);
|
|
26
|
+
const binding = { appId, compiled };
|
|
27
|
+
bindings.set(definitions, binding);
|
|
28
|
+
bindingsByApp.set(appId, binding);
|
|
29
|
+
if (compiled.events.size > 0) registerAppEventPublisher(appId);
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export const appEventBinding = (definitions: CapabilityDefinitions | string) => {
|
|
33
|
+
const binding = typeof definitions === "string" ? bindingsByApp.get(definitions) : bindings.get(definitions);
|
|
34
|
+
if (!binding)
|
|
35
|
+
throw new Error("Bind event capabilities with defineApp before publishing or testing; a test harness accepts an explicit appId");
|
|
36
|
+
return binding;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
type EventDeclarations<T extends CapabilityDefinitions> = NonNullable<T["events"]>;
|
|
40
|
+
export type AppEventPublishInput<T extends CapabilityDefinitions, Key extends keyof EventDeclarations<T>> = {
|
|
41
|
+
subject: string;
|
|
42
|
+
data: z.input<EventDeclarations<T>[Key]["schema"]>;
|
|
43
|
+
requestId?: string;
|
|
44
|
+
} & (EventDeclarations<T>[Key] extends { place: string } ? { place: string } : { place?: never });
|
|
45
|
+
|
|
46
|
+
/** The same declaration, JSON and byte validation for publishers and producer fixtures. */
|
|
47
|
+
export const buildAppEventEnvelope = <const T extends CapabilityDefinitions, Key extends Extract<keyof EventDeclarations<T>, string>>(
|
|
48
|
+
definitions: T | string,
|
|
49
|
+
localId: Key,
|
|
50
|
+
input: AppEventPublishInput<T, Key>,
|
|
51
|
+
identity: { id: string; time: string },
|
|
52
|
+
): AppEventEnvelope => {
|
|
53
|
+
const binding = appEventBinding(definitions);
|
|
54
|
+
const event = binding.compiled.events.get(localId);
|
|
55
|
+
if (!event) throw new Error(`Unknown app event ${localId}`);
|
|
56
|
+
if ((event.manifest.place !== undefined) !== (input.place !== undefined)) {
|
|
57
|
+
throw new Error(`Event ${localId} place does not match its declaration`);
|
|
58
|
+
}
|
|
59
|
+
const data = z.json().parse(input.data);
|
|
60
|
+
if (!isDeepStrictEqual(input.data, JSON.parse(JSON.stringify(data)))) {
|
|
61
|
+
throw new Error("Event data must round-trip through JSON without normalization");
|
|
62
|
+
}
|
|
63
|
+
const parsed = event.definition.schema.parse(data);
|
|
64
|
+
if (capabilityHash(parsed) !== capabilityHash(data)) throw new Error("Event schema must preserve the original JSON data");
|
|
65
|
+
const envelope = AppEventEnvelopeSchema.parse({
|
|
66
|
+
...identity,
|
|
67
|
+
type: `${binding.appId}.${localId}`,
|
|
68
|
+
version: event.manifest.version,
|
|
69
|
+
source: binding.appId,
|
|
70
|
+
subject: { type: `${binding.appId}.${event.manifest.subject}`, id: input.subject },
|
|
71
|
+
...(event.manifest.place !== undefined ? { place: { type: `${binding.appId}.${event.manifest.place}`, id: input.place } } : {}),
|
|
72
|
+
...(input.requestId !== undefined ? { cause: { requestId: input.requestId } } : {}),
|
|
73
|
+
data,
|
|
74
|
+
});
|
|
75
|
+
assertAppEventSize(envelope);
|
|
76
|
+
return envelope;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
/** Writes in the domain transaction; wake only after commit. Delivery is at least once. */
|
|
80
|
+
export const appEvents = <const T extends CapabilityDefinitions>(definitions: T | string) => ({
|
|
81
|
+
publish: async <Key extends Extract<keyof EventDeclarations<T>, string>>(
|
|
82
|
+
tx: SQL,
|
|
83
|
+
localId: Key,
|
|
84
|
+
input: AppEventPublishInput<T, Key>,
|
|
85
|
+
): Promise<AppEventEnvelope> => {
|
|
86
|
+
const envelope = buildAppEventEnvelope<T, Key>(definitions, localId, input, {
|
|
87
|
+
id: crypto.randomUUID(),
|
|
88
|
+
time: new Date().toISOString(),
|
|
89
|
+
});
|
|
90
|
+
// A full valid resource ref can exceed the existing outbox's 600-character key bound.
|
|
91
|
+
const orderingKey = capabilityHash(envelope.subject);
|
|
92
|
+
await tx`SELECT events.enqueue(${envelope.id}::uuid, ${envelope.source}, 'event', ${orderingKey}, ${JSON.stringify(envelope)}::text::jsonb)`;
|
|
93
|
+
return envelope;
|
|
94
|
+
},
|
|
95
|
+
wake: (): void => {
|
|
96
|
+
const binding = typeof definitions === "string" ? bindingsByApp.get(definitions) : bindings.get(definitions);
|
|
97
|
+
if (binding) wakeAppEventDelivery(binding.appId);
|
|
98
|
+
},
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
/** Shared by the framework route and subscription tests; validates before executing application code. */
|
|
102
|
+
export const deliverAppEvent = async (params: {
|
|
103
|
+
compiled: CompiledCapabilities;
|
|
104
|
+
subscriptionId: string;
|
|
105
|
+
envelope: unknown;
|
|
106
|
+
context: AppEventDeliveryContext;
|
|
107
|
+
}): Promise<void> => {
|
|
108
|
+
const envelope = AppEventEnvelopeSchema.parse(params.envelope);
|
|
109
|
+
assertAppEventSize(envelope);
|
|
110
|
+
if (
|
|
111
|
+
!envelope.type.startsWith(`${envelope.source}.`) ||
|
|
112
|
+
!envelope.subject.type.startsWith(`${envelope.source}.`) ||
|
|
113
|
+
(envelope.place && !envelope.place.type.startsWith(`${envelope.source}.`))
|
|
114
|
+
)
|
|
115
|
+
throw new Error("App event source does not match its namespaced types");
|
|
116
|
+
if (envelope.place) throw new Error("resource_events_unsupported");
|
|
117
|
+
const subscription = params.compiled.subscribes.get(envelope.type);
|
|
118
|
+
if (!subscription || subscription.manifest.version !== envelope.version) {
|
|
119
|
+
throw new Error("App event subscription not found for type and version");
|
|
120
|
+
}
|
|
121
|
+
if (params.subscriptionId !== appEventSubscriptionId(params.compiled.manifest.appId, envelope.type, envelope.version)) {
|
|
122
|
+
throw new Error("App event subscription ID mismatch");
|
|
123
|
+
}
|
|
124
|
+
if (
|
|
125
|
+
params.context.subscriptionId !== params.subscriptionId ||
|
|
126
|
+
params.context.eventId !== envelope.id ||
|
|
127
|
+
!Number.isSafeInteger(params.context.attempt) ||
|
|
128
|
+
params.context.attempt < 1
|
|
129
|
+
)
|
|
130
|
+
throw new Error("App event delivery context mismatch");
|
|
131
|
+
params.context.signal.throwIfAborted();
|
|
132
|
+
const data = subscription.definition.schema.parse(envelope.data);
|
|
133
|
+
await subscription.definition.run({ ...envelope, data }, params.context);
|
|
134
|
+
};
|
|
@@ -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,7 +36,7 @@ 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
42
|
export type { AppInstallable } from "./contracts/installable";
|
|
@@ -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
|
@@ -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 {
|