@k2b/cloud 0.43.0 → 0.45.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 +11 -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/GroupCoverage.tsx +20 -5
- package/src/access/PermissionEditor.tsx +20 -5
- package/src/access/PrincipalAvatar.tsx +30 -21
- 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/access/messages.ts +4 -4
- package/src/account/EntitySearch.tsx +12 -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 +10 -6
- package/src/ai/enrich.ts +2 -2
- package/src/ai/executor.ts +40 -25
- package/src/ai/http.ts +17 -29
- package/src/ai/index.ts +12 -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/project-tool.ts +4 -4
- package/src/ai/projects-authorization.ts +53 -0
- package/src/ai/projects-routes.ts +58 -47
- package/src/ai/projects.ts +110 -88
- 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 +9 -3
- package/src/ai/request-headers.ts +14 -87
- package/src/ai/resource-refs.ts +6 -2
- package/src/ai/routes.ts +47 -48
- package/src/ai/runtime.ts +50 -22
- package/src/ai/settings.ts +93 -580
- package/src/ai/skill-tool.ts +4 -4
- package/src/ai/skills-routes.ts +27 -26
- package/src/ai/skills.ts +120 -81
- 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 +22 -3
- package/src/api/admin-ai-quotas.ts +34 -56
- package/src/api/admin-ai-skills.ts +4 -1
- 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 +35 -5
- 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/browser/session.ts +39 -0
- package/src/browser/shared-text-history-validation.ts +151 -0
- package/src/browser/shared-text-history.ts +464 -0
- package/src/browser/shared-text-offline-authorized.ts +29 -0
- package/src/browser/shared-text-offline.ts +351 -0
- package/src/browser/shared-text-provider.ts +714 -0
- package/src/browser/shared-text-table-contract.ts +150 -0
- package/src/browser/shared-text.ts +19 -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 +6 -1
- 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 +17 -4
- package/src/server/index.ts +2 -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 +31 -10
- package/src/server/services/index.ts +1 -0
- package/src/services/accounts/app.ts +36 -13
- package/src/services/accounts/entities.ts +43 -19
- package/src/services/accounts/group-sql.ts +23 -0
- package/src/services/accounts/groups.ts +19 -14
- package/src/services/accounts/identities.ts +70 -11
- package/src/services/accounts/identity-reconciliation.ts +3 -1
- package/src/services/accounts/local-groups.ts +69 -4
- package/src/services/accounts/users.ts +45 -14
- 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/index.ts +2 -2
- package/src/services/mandates/policy.ts +20 -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 +26 -3
- 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/email-html.ts +1 -0
- package/src/shared/index.ts +2 -0
- package/src/shared/markdown/extensions/katex.ts +8 -1
- package/src/shared/markdown/extensions/tables.ts +14 -1
- package/src/shared/markdown/index.ts +22 -11
- package/src/shared-text/anchors.ts +29 -0
- package/src/shared-text/awareness.ts +87 -0
- package/src/shared-text/changes.ts +27 -0
- package/src/shared-text/documents.ts +433 -65
- package/src/shared-text/events.ts +12 -2
- package/src/shared-text/index.ts +16 -1
- package/src/shared-text/log.ts +24 -5
- package/src/shared-text/snapshots.ts +69 -23
- package/src/shared-text/structural-history.ts +377 -0
- package/src/shared-text/table-actions.ts +432 -0
- package/src/shared-text/text-diff.ts +75 -0
- package/src/shared-text/types.ts +24 -5
- 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 +2 -2
- package/src/ssr/CloudShell.island.tsx +44 -7
- package/src/ssr/Layout.tsx +9 -7
- package/src/ssr/MobileProfileActions.tsx +2 -2
- package/src/ssr/ProfilePreferences.tsx +2 -1
- 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 +26 -31
- package/src/ssr/app-badges.ts +31 -7
- package/src/ssr/app-launchpad.tsx +6 -1
- package/src/ssr/index.ts +8 -0
- package/src/ssr/page-trail-runtime.ts +3 -1
- package/src/ssr/platform-messages.ts +11 -1
- package/src/ssr/profile-actions.ts +3 -6
- 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 +3 -1
- 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/language/expressions.ts +3 -2
- package/src/workflows/runtime/executor.ts +134 -34
- package/src/workflows/runtime/index.ts +2 -0
- package/src/workflows/runtime/ports.ts +5 -1
- package/src/workflows/store/actions.ts +1 -1
- package/src/workflows/store/definitions.ts +5 -0
- package/src/workflows/store/runs.ts +5 -3
- package/src/workflows/store/worker.ts +7 -2
- package/src/ai/model-pricing.ts +0 -43
- package/src/ai/model-request-settings.ts +0 -98
- package/src/ai/settings-messages.ts +0 -129
- package/src/shared/ai-model-request-settings.ts +0 -21
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { PrincipalSchema } from "../contracts/shared";
|
|
3
|
+
import { AiModelPricingSchema } from "./ai-costs";
|
|
4
|
+
import { AiExtraBodySchema, AiReasoningEffortSchema } from "./ai-request-options";
|
|
5
|
+
|
|
6
|
+
export const AI_MODEL_CLASSES = ["fast", "balanced", "thorough", "maximum"] as const;
|
|
7
|
+
export type AiModelClass = (typeof AI_MODEL_CLASSES)[number];
|
|
8
|
+
export const AI_MODEL_SLOTS = [...AI_MODEL_CLASSES, "vision", "audio"] as const;
|
|
9
|
+
export type AiModelSlot = (typeof AI_MODEL_SLOTS)[number];
|
|
10
|
+
export const AiModelRequestSchema = z.union([
|
|
11
|
+
z.object({ class: z.enum(AI_MODEL_CLASSES) }).strict(),
|
|
12
|
+
z.object({ id: z.string().trim().min(1).max(241) }).strict(),
|
|
13
|
+
]);
|
|
14
|
+
export type AiModelRequest = z.infer<typeof AiModelRequestSchema>;
|
|
15
|
+
export const AiResolvedModelRefSchema = z
|
|
16
|
+
.object({
|
|
17
|
+
requestedClass: z.enum(AI_MODEL_CLASSES).optional(),
|
|
18
|
+
class: z.enum(AI_MODEL_CLASSES).optional(),
|
|
19
|
+
id: z.string().min(1).max(241),
|
|
20
|
+
})
|
|
21
|
+
.strict();
|
|
22
|
+
export type AiResolvedModelRef = { requestedClass?: AiModelClass; class?: AiModelClass; id: string };
|
|
23
|
+
/** Only empty assignments fall back; policy and budget failures never do. */
|
|
24
|
+
export const aiModelClassFallbackChain = (modelClass: AiModelClass): AiModelClass[] => {
|
|
25
|
+
const index = AI_MODEL_CLASSES.indexOf(modelClass);
|
|
26
|
+
return [...AI_MODEL_CLASSES.slice(0, index).reverse(), ...AI_MODEL_CLASSES.slice(index + 1)];
|
|
27
|
+
};
|
|
28
|
+
export const parseAiModelChoice = (value: string): AiModelRequest => {
|
|
29
|
+
const modelClass = z.enum(AI_MODEL_CLASSES).safeParse(value);
|
|
30
|
+
return modelClass.success ? { class: modelClass.data } : AiModelRequestSchema.parse({ id: value });
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const slug = z
|
|
34
|
+
.string()
|
|
35
|
+
.trim()
|
|
36
|
+
.regex(/^[a-z0-9][a-z0-9._-]*$/)
|
|
37
|
+
.max(120);
|
|
38
|
+
const image = z
|
|
39
|
+
.string()
|
|
40
|
+
.trim()
|
|
41
|
+
.regex(/^data:image\//)
|
|
42
|
+
.max(28_000);
|
|
43
|
+
const httpUrl = z
|
|
44
|
+
.string()
|
|
45
|
+
.url()
|
|
46
|
+
.refine((value) => ["http:", "https:"].includes(new URL(value).protocol));
|
|
47
|
+
export const AiProviderTypeSchema = z.enum([
|
|
48
|
+
"openai",
|
|
49
|
+
"openrouter",
|
|
50
|
+
"anthropic",
|
|
51
|
+
"mistral",
|
|
52
|
+
"gemini",
|
|
53
|
+
"ollama",
|
|
54
|
+
"vllm",
|
|
55
|
+
"openai-compatible",
|
|
56
|
+
]);
|
|
57
|
+
export type AiProviderType = z.infer<typeof AiProviderTypeSchema>;
|
|
58
|
+
/** Hosted APIs refuse requests without a key; self-hosted endpoints may run without one. */
|
|
59
|
+
export const providerRequiresCredential = (provider: AiProviderType): boolean =>
|
|
60
|
+
["openai", "openrouter", "anthropic", "mistral", "gemini"].includes(provider);
|
|
61
|
+
export const providerSupportsCredential = (provider: AiProviderType): boolean =>
|
|
62
|
+
providerRequiresCredential(provider) || provider === "vllm" || provider === "openai-compatible";
|
|
63
|
+
export const AiProviderStatusSchema = z.enum(["unknown", "ok", "unreachable", "key_rejected", "error"]);
|
|
64
|
+
export const AiProviderInputSchema = z
|
|
65
|
+
.object({
|
|
66
|
+
id: slug,
|
|
67
|
+
type: AiProviderTypeSchema,
|
|
68
|
+
label: z.string().trim().min(1),
|
|
69
|
+
baseUrl: httpUrl.nullable().default(null),
|
|
70
|
+
dataBoundary: z.enum(["hosted", "private"]),
|
|
71
|
+
enabled: z.boolean().default(true),
|
|
72
|
+
logo: image.nullable().default(null),
|
|
73
|
+
})
|
|
74
|
+
.strict();
|
|
75
|
+
export const AiModelIconSchema = z.discriminatedUnion("kind", [
|
|
76
|
+
z.object({ kind: z.literal("provider") }).strict(),
|
|
77
|
+
z.object({ kind: z.literal("emoji"), emoji: z.string().min(1).max(64) }).strict(),
|
|
78
|
+
z.object({ kind: z.literal("image"), dataUrl: image }).strict(),
|
|
79
|
+
]);
|
|
80
|
+
export type AiModelIcon = z.infer<typeof AiModelIconSchema>;
|
|
81
|
+
export const AiModelInputSchema = z
|
|
82
|
+
.object({
|
|
83
|
+
id: slug.refine((value) => !AI_MODEL_CLASSES.some((modelClass) => modelClass === value), "Model IDs cannot be class names."),
|
|
84
|
+
providerModel: z.string().trim().min(1),
|
|
85
|
+
label: z.string().trim().min(1),
|
|
86
|
+
description: z.string().default(""),
|
|
87
|
+
kind: z.enum(["chat", "transcription"]).default("chat"),
|
|
88
|
+
capabilities: z.array(z.enum(["tools", "vision", "transcription"])).default([]),
|
|
89
|
+
contextWindow: z.number().int().positive().nullable().default(null),
|
|
90
|
+
maxOutputTokens: z.number().int().positive().nullable().default(null),
|
|
91
|
+
temperature: z.number().min(0).max(2).nullable().default(null),
|
|
92
|
+
extraBody: AiExtraBodySchema.nullable().default(null),
|
|
93
|
+
maxLoadedTools: z.number().int().nullable().default(null),
|
|
94
|
+
maxToolRounds: z.number().int().nullable().default(null),
|
|
95
|
+
pricing: AiModelPricingSchema.nullable().default(null),
|
|
96
|
+
icon: AiModelIconSchema.default({ kind: "provider" }),
|
|
97
|
+
enabled: z.boolean().default(true),
|
|
98
|
+
})
|
|
99
|
+
.strict();
|
|
100
|
+
export const AiVariantInputSchema = z
|
|
101
|
+
.object({
|
|
102
|
+
slug,
|
|
103
|
+
label: z.string().trim().min(1),
|
|
104
|
+
hint: z.string().default(""),
|
|
105
|
+
reasoningEffort: AiReasoningEffortSchema.nullable().default(null),
|
|
106
|
+
maxOutputTokens: z.number().int().positive().nullable().default(null),
|
|
107
|
+
pricing: AiModelPricingSchema.nullable().default(null),
|
|
108
|
+
extraBody: AiExtraBodySchema.nullable().default(null),
|
|
109
|
+
showInPicker: z.boolean().default(true),
|
|
110
|
+
position: z.number().int().default(0),
|
|
111
|
+
})
|
|
112
|
+
.strict();
|
|
113
|
+
export const AiProviderPatchSchema = AiProviderInputSchema.omit({ id: true })
|
|
114
|
+
.partial()
|
|
115
|
+
.extend({
|
|
116
|
+
baseUrl: AiProviderInputSchema.shape.baseUrl.removeDefault().optional(),
|
|
117
|
+
enabled: AiProviderInputSchema.shape.enabled.removeDefault().optional(),
|
|
118
|
+
logo: AiProviderInputSchema.shape.logo.removeDefault().optional(),
|
|
119
|
+
})
|
|
120
|
+
.strict();
|
|
121
|
+
export const AiModelPatchSchema = AiModelInputSchema.omit({ id: true })
|
|
122
|
+
.partial()
|
|
123
|
+
.extend({
|
|
124
|
+
description: AiModelInputSchema.shape.description.removeDefault().optional(),
|
|
125
|
+
kind: AiModelInputSchema.shape.kind.removeDefault().optional(),
|
|
126
|
+
capabilities: AiModelInputSchema.shape.capabilities.removeDefault().optional(),
|
|
127
|
+
contextWindow: AiModelInputSchema.shape.contextWindow.removeDefault().optional(),
|
|
128
|
+
maxOutputTokens: AiModelInputSchema.shape.maxOutputTokens.removeDefault().optional(),
|
|
129
|
+
temperature: AiModelInputSchema.shape.temperature.removeDefault().optional(),
|
|
130
|
+
extraBody: AiModelInputSchema.shape.extraBody.removeDefault().optional(),
|
|
131
|
+
maxLoadedTools: AiModelInputSchema.shape.maxLoadedTools.removeDefault().optional(),
|
|
132
|
+
maxToolRounds: AiModelInputSchema.shape.maxToolRounds.removeDefault().optional(),
|
|
133
|
+
pricing: AiModelInputSchema.shape.pricing.removeDefault().optional(),
|
|
134
|
+
icon: AiModelInputSchema.shape.icon.removeDefault().optional(),
|
|
135
|
+
enabled: AiModelInputSchema.shape.enabled.removeDefault().optional(),
|
|
136
|
+
})
|
|
137
|
+
.strict();
|
|
138
|
+
export const AiVariantPatchSchema = AiVariantInputSchema.omit({ slug: true })
|
|
139
|
+
.partial()
|
|
140
|
+
.extend({
|
|
141
|
+
hint: AiVariantInputSchema.shape.hint.removeDefault().optional(),
|
|
142
|
+
reasoningEffort: AiVariantInputSchema.shape.reasoningEffort.removeDefault().optional(),
|
|
143
|
+
maxOutputTokens: AiVariantInputSchema.shape.maxOutputTokens.removeDefault().optional(),
|
|
144
|
+
pricing: AiVariantInputSchema.shape.pricing.removeDefault().optional(),
|
|
145
|
+
extraBody: AiVariantInputSchema.shape.extraBody.removeDefault().optional(),
|
|
146
|
+
showInPicker: AiVariantInputSchema.shape.showInPicker.removeDefault().optional(),
|
|
147
|
+
position: AiVariantInputSchema.shape.position.removeDefault().optional(),
|
|
148
|
+
})
|
|
149
|
+
.strict();
|
|
150
|
+
const revisionFields = {
|
|
151
|
+
revision: z.number().int(),
|
|
152
|
+
createdAt: z.string(),
|
|
153
|
+
updatedAt: z.string(),
|
|
154
|
+
updatedBy: z.string().nullable(),
|
|
155
|
+
};
|
|
156
|
+
export const AiProviderSchema = AiProviderInputSchema.extend({
|
|
157
|
+
...revisionFields,
|
|
158
|
+
status: AiProviderStatusSchema,
|
|
159
|
+
statusCode: z.string().nullable(),
|
|
160
|
+
statusMessage: z.string().nullable(),
|
|
161
|
+
statusCheckedAt: z.string().nullable(),
|
|
162
|
+
credentialSet: z.boolean(),
|
|
163
|
+
requestHeaderNames: z.array(z.string()),
|
|
164
|
+
});
|
|
165
|
+
export const AiModelSchema = AiModelInputSchema.extend({ ...revisionFields, providerId: z.string() });
|
|
166
|
+
export const AiVariantSchema = AiVariantInputSchema.extend({ ...revisionFields, id: z.string(), modelId: z.string() });
|
|
167
|
+
export const AiAssignmentSchema = z.object({
|
|
168
|
+
slot: z.enum(AI_MODEL_SLOTS),
|
|
169
|
+
variantId: z.string().nullable(),
|
|
170
|
+
revision: z.number().int(),
|
|
171
|
+
updatedAt: z.string(),
|
|
172
|
+
updatedBy: z.string().nullable(),
|
|
173
|
+
});
|
|
174
|
+
export type AiProvider = z.infer<typeof AiProviderSchema>;
|
|
175
|
+
export type AiModel = z.infer<typeof AiModelSchema>;
|
|
176
|
+
export type AiModelVariant = z.infer<typeof AiVariantSchema>;
|
|
177
|
+
export type AiModelAssignment = z.infer<typeof AiAssignmentSchema>;
|
|
178
|
+
export type AiModelUnavailableReason =
|
|
179
|
+
| { code: "budget_exhausted"; resetsAt: string }
|
|
180
|
+
| { code: "provider_unreachable" | "key_rejected" | "access_revoked" };
|
|
181
|
+
export type AiPeopleModelIcon =
|
|
182
|
+
| { kind: "emoji"; emoji: string }
|
|
183
|
+
| { kind: "image"; src: string }
|
|
184
|
+
| { kind: "mark"; text: string; color: string };
|
|
185
|
+
/** Neutral provider marks: Cloud ships no brand logos, so a provider shows its initials on one of these colours. */
|
|
186
|
+
export const AI_PROVIDER_MARK_COLORS = ["#0f766e", "#6d28d9", "#475569", "#15803d", "#1d4ed8", "#c2410c", "#be123c", "#a16207"] as const;
|
|
187
|
+
/** The initials and colour a provider shows wherever it has no logo, the same in the AI area and in every chooser. */
|
|
188
|
+
export const aiProviderMark = (provider: { id: string; label: string }): { text: string; color: string } => {
|
|
189
|
+
const words = provider.label.split(/\s+/).filter(Boolean);
|
|
190
|
+
const capitals = words[0]?.match(/\p{Lu}/gu) ?? [];
|
|
191
|
+
const text =
|
|
192
|
+
words.length > 1
|
|
193
|
+
? `${words[0]![0]}${words[1]![0]}`
|
|
194
|
+
: capitals.length >= 2
|
|
195
|
+
? capitals.slice(0, 2).join("")
|
|
196
|
+
: (words[0] ?? "").slice(0, 2);
|
|
197
|
+
return {
|
|
198
|
+
text: text.toUpperCase() || "AI",
|
|
199
|
+
color:
|
|
200
|
+
AI_PROVIDER_MARK_COLORS[
|
|
201
|
+
Array.from(provider.id).reduce((sum, letter) => (sum * 31 + letter.charCodeAt(0)) >>> 0, 0) % AI_PROVIDER_MARK_COLORS.length
|
|
202
|
+
]!,
|
|
203
|
+
};
|
|
204
|
+
};
|
|
205
|
+
export type AiPeopleModels = {
|
|
206
|
+
status: "ok";
|
|
207
|
+
selection: AiModelRequest | null;
|
|
208
|
+
effectiveDefault: AiModelRequest;
|
|
209
|
+
classes: { class: AiModelClass; variantId: string; unavailable?: AiModelUnavailableReason }[];
|
|
210
|
+
options: {
|
|
211
|
+
id: string;
|
|
212
|
+
label: string;
|
|
213
|
+
variantLabel?: string;
|
|
214
|
+
description: string;
|
|
215
|
+
icon: AiPeopleModelIcon;
|
|
216
|
+
classes: AiModelClass[];
|
|
217
|
+
unavailable?: AiModelUnavailableReason;
|
|
218
|
+
}[];
|
|
219
|
+
};
|
|
220
|
+
export const AiDeleteModelSchema = z
|
|
221
|
+
.object({
|
|
222
|
+
revision: z.number().int(),
|
|
223
|
+
replacements: z.partialRecord(z.enum(AI_MODEL_SLOTS), z.string().nullable()),
|
|
224
|
+
retarget: z.boolean(),
|
|
225
|
+
retargetTo: z.string().nullable(),
|
|
226
|
+
})
|
|
227
|
+
.strict();
|
|
228
|
+
export type AiDeleteModelInput = z.infer<typeof AiDeleteModelSchema>;
|
|
229
|
+
export type AiModelUsage = {
|
|
230
|
+
assignments: AiModelSlot[];
|
|
231
|
+
projects: { id: string; name: string }[];
|
|
232
|
+
workflows: { id: string; name: string }[];
|
|
233
|
+
scheduledTasks: { id: string; name: string }[];
|
|
234
|
+
chats: number;
|
|
235
|
+
budgetRules: { id: string; label: string }[];
|
|
236
|
+
peopleAtLimit: number;
|
|
237
|
+
stats: { answers14d: number; avgCostPerAnswer: number | null; avgSecondsPerAnswer: number | null };
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
const AiModelGrantSchema = z
|
|
241
|
+
.object({
|
|
242
|
+
principal: PrincipalSchema.refine((principal) => principal.type !== "public", "Assistant models cannot be public."),
|
|
243
|
+
permission: z.literal("read"),
|
|
244
|
+
})
|
|
245
|
+
.strict();
|
|
246
|
+
export const AiModelAccessDraftSchema = z
|
|
247
|
+
.object({
|
|
248
|
+
expectedRevision: z.number().int().positive().max(Number.MAX_SAFE_INTEGER).nullable(),
|
|
249
|
+
entries: z.array(AiModelGrantSchema),
|
|
250
|
+
})
|
|
251
|
+
.strict();
|
|
252
|
+
export type AiModelAccessDraft = z.infer<typeof AiModelAccessDraftSchema>;
|
|
253
|
+
|
|
254
|
+
/** Whether a model supports a catalog assignment; safe to use in browser pickers. */
|
|
255
|
+
export const aiModelSupportsSlot = (slot: AiModelSlot, model: Pick<AiModel, "kind" | "capabilities">): boolean =>
|
|
256
|
+
slot === "audio" ? model.kind === "transcription" : model.kind === "chat" && (slot !== "vision" || model.capabilities.includes("vision"));
|
|
257
|
+
|
|
258
|
+
/** The same replacement requirements apply to deletion previews and committed retargeting. */
|
|
259
|
+
export const aiReplacementProblem = (
|
|
260
|
+
source: Pick<AiProvider, "dataBoundary">,
|
|
261
|
+
target: Pick<AiProvider, "dataBoundary" | "enabled">,
|
|
262
|
+
original: Pick<AiModel, "kind" | "capabilities">,
|
|
263
|
+
replacement: Pick<AiModel, "kind" | "capabilities" | "enabled">,
|
|
264
|
+
): "private_boundary" | "incompatible_model" | null => {
|
|
265
|
+
if (source.dataBoundary === "private" && target.dataBoundary !== "private") return "private_boundary";
|
|
266
|
+
if (
|
|
267
|
+
!target.enabled ||
|
|
268
|
+
!replacement.enabled ||
|
|
269
|
+
original.kind !== replacement.kind ||
|
|
270
|
+
original.capabilities.some((capability) => !replacement.capabilities.includes(capability))
|
|
271
|
+
)
|
|
272
|
+
return "incompatible_model";
|
|
273
|
+
return null;
|
|
274
|
+
};
|
package/src/shared/ai-quotas.ts
CHANGED
|
@@ -143,7 +143,7 @@ export type AiQuotaReport = {
|
|
|
143
143
|
perPage: number;
|
|
144
144
|
};
|
|
145
145
|
export const aiQuotaHref = (query: Partial<AiQuotaReportQuery>) => {
|
|
146
|
-
const params = new URLSearchParams({
|
|
146
|
+
const params = new URLSearchParams({ sec: "budgets", ...(query.view === "rules" ? {} : { rule: "people" }) });
|
|
147
147
|
for (const [key, value] of Object.entries(query)) if (value !== undefined && value !== "") params.set(key, String(value));
|
|
148
|
-
return `/admin/
|
|
148
|
+
return `/admin/ai?${params}`;
|
|
149
149
|
};
|
package/src/shared/ai-usage.ts
CHANGED
|
@@ -8,7 +8,7 @@ export const AiUsageQuerySchema = z.object({
|
|
|
8
8
|
range: z.enum(AI_USAGE_RANGES).default("30d"),
|
|
9
9
|
until: z.iso.datetime({ offset: true }).optional(),
|
|
10
10
|
userId: z.union([z.uuid(), z.literal("unassigned")]).optional(),
|
|
11
|
-
|
|
11
|
+
modelId: text,
|
|
12
12
|
providerModel: text,
|
|
13
13
|
appId: text,
|
|
14
14
|
workflowId: z.uuid().optional(),
|
|
@@ -36,4 +36,4 @@ export const aiUsageSearchParams = (query: Partial<AiUsageQuery>): URLSearchPara
|
|
|
36
36
|
for (const [key, value] of Object.entries(query)) if (value !== undefined && value !== "") params.set(key, String(value));
|
|
37
37
|
return params;
|
|
38
38
|
};
|
|
39
|
-
export const aiUsageHref = (query: Partial<AiUsageQuery>) => `/admin/
|
|
39
|
+
export const aiUsageHref = (query: Partial<AiUsageQuery>) => `/admin/ai?sec=usage&${aiUsageSearchParams(query)}`;
|
package/src/shared/email-html.ts
CHANGED
package/src/shared/index.ts
CHANGED
|
@@ -13,6 +13,8 @@ export {
|
|
|
13
13
|
export * from "./account-display";
|
|
14
14
|
export * from "./account-session";
|
|
15
15
|
export * from "./ai-costs";
|
|
16
|
+
export * from "./ai-models";
|
|
17
|
+
export * from "./ai-models";
|
|
16
18
|
export type { AiPromptContextInput, AiToolPromptHint } from "./ai-platform-prompt";
|
|
17
19
|
export {
|
|
18
20
|
AI_LOCAL_CLIENT_DEFAULT_INSTRUCTIONS,
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
import katex from "katex";
|
|
10
10
|
import type { MarkedExtension, Tokens } from "marked";
|
|
11
|
+
import { escapeHtml } from "../shared";
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
14
|
* Token shape produced by the math tokenizers below. The marked extension API
|
|
@@ -40,7 +41,7 @@ function renderInlineMath(latex: string): string {
|
|
|
40
41
|
}
|
|
41
42
|
}
|
|
42
43
|
|
|
43
|
-
export function katexExtension(): MarkedExtension {
|
|
44
|
+
export function katexExtension(literal = false): MarkedExtension {
|
|
44
45
|
return {
|
|
45
46
|
extensions: [
|
|
46
47
|
// ```math code blocks
|
|
@@ -63,6 +64,10 @@ export function katexExtension(): MarkedExtension {
|
|
|
63
64
|
return undefined;
|
|
64
65
|
},
|
|
65
66
|
renderer(token: Tokens.Generic) {
|
|
67
|
+
if (literal) {
|
|
68
|
+
const source = token.raw.slice("```math\n".length, -"\n```".length);
|
|
69
|
+
return `<pre>${escapeHtml(source)}</pre>\n`;
|
|
70
|
+
}
|
|
66
71
|
const latex = (token as MathToken).latex;
|
|
67
72
|
return renderBlockMath(latex);
|
|
68
73
|
},
|
|
@@ -87,6 +92,7 @@ export function katexExtension(): MarkedExtension {
|
|
|
87
92
|
return undefined;
|
|
88
93
|
},
|
|
89
94
|
renderer(token: Tokens.Generic) {
|
|
95
|
+
if (literal) return `<p>${escapeHtml(token.raw)}</p>\n`;
|
|
90
96
|
const latex = (token as MathToken).latex;
|
|
91
97
|
return renderBlockMath(latex);
|
|
92
98
|
},
|
|
@@ -111,6 +117,7 @@ export function katexExtension(): MarkedExtension {
|
|
|
111
117
|
return undefined;
|
|
112
118
|
},
|
|
113
119
|
renderer(token: Tokens.Generic) {
|
|
120
|
+
if (literal) return escapeHtml(token.raw);
|
|
114
121
|
const latex = (token as MathToken).latex;
|
|
115
122
|
return renderInlineMath(latex);
|
|
116
123
|
},
|
|
@@ -66,11 +66,24 @@ const renderCell = (alignCls: string, originalText: string, ctx: EvalContext, ht
|
|
|
66
66
|
return `<td><span class="md-table-cell md-formula-error${alignCls}" title="${escapeHtml(tooltip)}">⚠ ${escapeHtml(originalText)}</span></td>`;
|
|
67
67
|
};
|
|
68
68
|
|
|
69
|
-
export function tablesExtension(): MarkedExtension {
|
|
69
|
+
export function tablesExtension(plain = false): MarkedExtension {
|
|
70
70
|
return {
|
|
71
71
|
renderer: {
|
|
72
72
|
table(token: Tokens.Table): string {
|
|
73
73
|
const align = token.align ?? [];
|
|
74
|
+
// Email has no Cloud stylesheet or formula widgets. Keep alignment on the
|
|
75
|
+
// structural cells and escape formula source before Markdown can interpret it.
|
|
76
|
+
if (plain) {
|
|
77
|
+
const cellHtml = (cell: Tokens.TableCell, tag: "th" | "td", column: number): string => {
|
|
78
|
+
const alignment = align[column];
|
|
79
|
+
const attribute = alignment ? ` align="${alignment}"` : "";
|
|
80
|
+
const content = isFormula(cell.text) ? escapeHtml(cell.text) : this.parser.parseInline(cell.tokens);
|
|
81
|
+
return `<${tag}${attribute}>${content}</${tag}>`;
|
|
82
|
+
};
|
|
83
|
+
const header = token.header.map((cell, column) => cellHtml(cell, "th", column)).join("");
|
|
84
|
+
const rows = token.rows.map((row) => `<tr>${row.map((cell, column) => cellHtml(cell, "td", column)).join("")}</tr>`).join("");
|
|
85
|
+
return `<table><thead><tr>${header}</tr></thead><tbody>${rows}</tbody></table>`;
|
|
86
|
+
}
|
|
74
87
|
|
|
75
88
|
// Build the EvalContext once per table — formulas reference
|
|
76
89
|
// raw cell text, not other formulas' results, so a single pass
|
|
@@ -25,8 +25,15 @@ import { escapeHtml } from "./shared";
|
|
|
25
25
|
// Create a configured marked instance
|
|
26
26
|
type MarkdownProfile = "content" | "help";
|
|
27
27
|
type LinkStyle = "widget" | "plain";
|
|
28
|
+
type MarkdownOutput = "cloud" | "email";
|
|
28
29
|
|
|
29
|
-
const createMarked = (
|
|
30
|
+
const createMarked = (
|
|
31
|
+
profile: MarkdownProfile,
|
|
32
|
+
links: LinkStyle,
|
|
33
|
+
infoBlocks: MarkedExtension,
|
|
34
|
+
locale?: string,
|
|
35
|
+
output: MarkdownOutput = "cloud",
|
|
36
|
+
) => {
|
|
30
37
|
const marked = new Marked();
|
|
31
38
|
|
|
32
39
|
marked.use({
|
|
@@ -38,12 +45,12 @@ const createMarked = (profile: MarkdownProfile, links: LinkStyle, infoBlocks: Ma
|
|
|
38
45
|
// Note: katexExtension must come before codeExtension to handle ```math blocks
|
|
39
46
|
marked.use(infoBlocks);
|
|
40
47
|
marked.use(taskListExtension());
|
|
41
|
-
marked.use(tablesExtension());
|
|
48
|
+
marked.use(tablesExtension(output === "email"));
|
|
42
49
|
// Plain output keeps marked's own link renderer and renders code blocks as a bare `pre`.
|
|
43
50
|
// Cloud links are calm reference pills and quiet web links from `@k2b/ui`.
|
|
44
51
|
if (links === "widget") marked.use(linksExtension({ internalTarget: profile === "help" ? "_self" : "_blank", locale }));
|
|
45
52
|
marked.use(imagesExtension());
|
|
46
|
-
marked.use(katexExtension());
|
|
53
|
+
marked.use(katexExtension(output === "email"));
|
|
47
54
|
marked.use(links === "widget" ? codeExtension() : plainCodeExtension());
|
|
48
55
|
// Inline-style decorators come last so they run after structural tokenizers.
|
|
49
56
|
marked.use(markExtension());
|
|
@@ -57,12 +64,12 @@ const createMarked = (profile: MarkdownProfile, links: LinkStyle, infoBlocks: Ma
|
|
|
57
64
|
// headers choose the locale, and `markdownInfoBlocks()` returns one extension
|
|
58
65
|
// for every locale that shares a catalog, so the cache stays bounded.
|
|
59
66
|
const instances = new Map<MarkedExtension, Map<string, Marked>>();
|
|
60
|
-
const markedFor = (profile: MarkdownProfile, links: LinkStyle = "widget", locale?: string): Marked => {
|
|
67
|
+
const markedFor = (profile: MarkdownProfile, links: LinkStyle = "widget", locale?: string, output: MarkdownOutput = "cloud"): Marked => {
|
|
61
68
|
const infoBlocks = markdownInfoBlocks({ locale });
|
|
62
69
|
const byStyle = instances.get(infoBlocks) ?? new Map<string, Marked>();
|
|
63
70
|
instances.set(infoBlocks, byStyle);
|
|
64
|
-
const key = `${profile}:${links}`;
|
|
65
|
-
const instance = byStyle.get(key) ?? createMarked(profile, links, infoBlocks, locale);
|
|
71
|
+
const key = `${profile}:${links}:${output}`;
|
|
72
|
+
const instance = byStyle.get(key) ?? createMarked(profile, links, infoBlocks, locale, output);
|
|
66
73
|
byStyle.set(key, instance);
|
|
67
74
|
return instance;
|
|
68
75
|
};
|
|
@@ -70,18 +77,21 @@ const markedFor = (profile: MarkdownProfile, links: LinkStyle = "widget", locale
|
|
|
70
77
|
const marked = markedFor("content");
|
|
71
78
|
|
|
72
79
|
export type MarkdownRenderOptions = {
|
|
80
|
+
/** `"email"` keeps strikethrough and structural table alignment, and renders table formulas and math as literal source. Defaults to Cloud's styled output with formula evaluation. */
|
|
81
|
+
output?: MarkdownOutput;
|
|
73
82
|
/** `"plain"` renders each link as an ordinary anchor around its text, for HTML read outside Cloud such as email. It also renders a code block, fenced or indented and including a Mermaid fence, as its source in a `pre` without the language label. The default renders references to Cloud content as calm pills and web links as quiet text links. */
|
|
74
83
|
links?: LinkStyle;
|
|
75
84
|
/** Request locale for the screen-reader names of untitled info blocks. Defaults to English on the server and the page language in the browser. */
|
|
76
85
|
locale?: string;
|
|
77
86
|
};
|
|
78
87
|
|
|
79
|
-
const sanitizeRenderedHtml = (html: string, profile: MarkdownProfile = "content"): string => {
|
|
88
|
+
const sanitizeRenderedHtml = (html: string, profile: MarkdownProfile = "content", output: MarkdownOutput = "cloud"): string => {
|
|
80
89
|
const callouts = markdownCalloutSanitizeOptions();
|
|
81
90
|
return sanitizeHtml(html, {
|
|
82
91
|
...callouts,
|
|
83
92
|
allowedTags: [
|
|
84
93
|
...sanitizeHtml.defaults.allowedTags,
|
|
94
|
+
...(output === "email" ? ["del"] : []),
|
|
85
95
|
"annotation",
|
|
86
96
|
"aside",
|
|
87
97
|
"br",
|
|
@@ -137,6 +147,7 @@ const sanitizeRenderedHtml = (html: string, profile: MarkdownProfile = "content"
|
|
|
137
147
|
input: ["checked", "class", "disabled", "type"],
|
|
138
148
|
math: ["xmlns"],
|
|
139
149
|
pre: ["class"],
|
|
150
|
+
...(output === "email" ? { th: ["align"], td: ["align"] } : {}),
|
|
140
151
|
...callouts.allowedAttributes,
|
|
141
152
|
span: ["aria-hidden", "class", "style", "title"],
|
|
142
153
|
},
|
|
@@ -192,10 +203,10 @@ const sanitizeRenderedHtml = (html: string, profile: MarkdownProfile = "content"
|
|
|
192
203
|
export function renderMarkdown(content: string, options: MarkdownRenderOptions = {}): string {
|
|
193
204
|
if (!content || typeof content !== "string") return "";
|
|
194
205
|
|
|
195
|
-
const html = markedFor("content", options.links, options.locale).parse(content);
|
|
206
|
+
const html = markedFor("content", options.links, options.locale, options.output).parse(content);
|
|
196
207
|
if (typeof html !== "string") return "";
|
|
197
208
|
|
|
198
|
-
return sanitizeRenderedHtml(html);
|
|
209
|
+
return sanitizeRenderedHtml(html, "content", options.output);
|
|
199
210
|
}
|
|
200
211
|
|
|
201
212
|
/**
|
|
@@ -204,10 +215,10 @@ export function renderMarkdown(content: string, options: MarkdownRenderOptions =
|
|
|
204
215
|
export function renderMarkdownSync(content: string, options: MarkdownRenderOptions = {}): string {
|
|
205
216
|
if (!content || typeof content !== "string") return "";
|
|
206
217
|
|
|
207
|
-
const html = markedFor("content", options.links, options.locale).parse(content);
|
|
218
|
+
const html = markedFor("content", options.links, options.locale, options.output).parse(content);
|
|
208
219
|
if (typeof html !== "string") return "";
|
|
209
220
|
|
|
210
|
-
return sanitizeRenderedHtml(html);
|
|
221
|
+
return sanitizeRenderedHtml(html, "content", options.output);
|
|
211
222
|
}
|
|
212
223
|
|
|
213
224
|
/**
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import * as Y from "yjs";
|
|
2
|
+
|
|
3
|
+
/** Capture an existing character, never a boundary that could drift to a neighbour. */
|
|
4
|
+
export const captureTextAnchor = (text: Y.Text, index: number): Uint8Array => {
|
|
5
|
+
if (!text.doc || !Number.isSafeInteger(index) || index < 0 || index >= text.length)
|
|
6
|
+
throw new RangeError("A text anchor must identify an existing character in an attached Y.Text");
|
|
7
|
+
return Y.encodeRelativePosition(Y.createRelativePositionFromTypeIndex(text, index, 0));
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Resolve only the original live character. Ordinary relative positions also
|
|
12
|
+
* resolve beside deleted characters and follow undo replacements; neither is
|
|
13
|
+
* a safe target for a delayed structural action.
|
|
14
|
+
*/
|
|
15
|
+
export const resolveTextAnchor = (text: Y.Text, bytes: Uint8Array): number | null => {
|
|
16
|
+
if (!text.doc) return null;
|
|
17
|
+
try {
|
|
18
|
+
const anchor = Y.decodeRelativePosition(bytes);
|
|
19
|
+
if (!anchor.item || anchor.assoc !== 0) return null;
|
|
20
|
+
const position = Y.createAbsolutePositionFromRelativePosition(anchor, text.doc, false);
|
|
21
|
+
if (!position || position.type !== text || position.index < 0 || position.index >= text.length) return null;
|
|
22
|
+
// Item encodings omit the root type name. Compare two decoded positions,
|
|
23
|
+
// rather than a decoded anchor with a fresh position containing that name.
|
|
24
|
+
const live = Y.decodeRelativePosition(captureTextAnchor(text, position.index));
|
|
25
|
+
return Y.compareRelativePositions(anchor, live) ? position.index : null;
|
|
26
|
+
} catch {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
};
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import * as decoding from "lib0/decoding";
|
|
2
|
+
import * as encoding from "lib0/encoding";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
|
|
5
|
+
const unsigned = z.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER);
|
|
6
|
+
const positionId = z.strictObject({ client: unsigned, clock: unsigned });
|
|
7
|
+
const relativePosition = z
|
|
8
|
+
.strictObject({
|
|
9
|
+
type: positionId.nullable().optional(),
|
|
10
|
+
tname: z.string().nullable().optional(),
|
|
11
|
+
item: positionId.nullable().optional(),
|
|
12
|
+
assoc: z.number().int().min(Number.MIN_SAFE_INTEGER).max(Number.MAX_SAFE_INTEGER).optional(),
|
|
13
|
+
})
|
|
14
|
+
.refine((position) => position.type != null || position.tname != null);
|
|
15
|
+
const stateSchema = z
|
|
16
|
+
.strictObject({
|
|
17
|
+
// Identity is supplied by the authenticated transport, never copied from here.
|
|
18
|
+
user: z.unknown().optional(),
|
|
19
|
+
cursor: z.strictObject({ anchor: relativePosition, head: relativePosition }).nullable().optional(),
|
|
20
|
+
})
|
|
21
|
+
.nullable();
|
|
22
|
+
|
|
23
|
+
export class InvalidSharedTextAwarenessError extends Error {
|
|
24
|
+
constructor() {
|
|
25
|
+
super("Invalid shared text awareness update");
|
|
26
|
+
this.name = "InvalidSharedTextAwarenessError";
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const encode = (clientId: number, clock: number, state: unknown): Uint8Array => {
|
|
31
|
+
const encoder = encoding.createEncoder();
|
|
32
|
+
encoding.writeVarUint(encoder, 1);
|
|
33
|
+
encoding.writeVarUint(encoder, clientId);
|
|
34
|
+
encoding.writeVarUint(encoder, clock);
|
|
35
|
+
encoding.writeVarString(encoder, JSON.stringify(state));
|
|
36
|
+
return encoding.toUint8Array(encoder);
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/** One authorized connection, with an ID allocated uniquely by its transport. */
|
|
40
|
+
export const createTrustedTextAwareness = (config: {
|
|
41
|
+
clientId: number;
|
|
42
|
+
textName: string;
|
|
43
|
+
user: { userId: string; name: string; color: string };
|
|
44
|
+
}) => {
|
|
45
|
+
if (!Number.isSafeInteger(config.clientId) || config.clientId < 2 ** 32) throw new RangeError("Invalid server awareness ID");
|
|
46
|
+
const user = { userId: config.user.userId, name: config.user.name, color: config.user.color };
|
|
47
|
+
let localClientId: number | undefined;
|
|
48
|
+
let clock = 0;
|
|
49
|
+
let active = false;
|
|
50
|
+
const next = (state: unknown): Uint8Array => {
|
|
51
|
+
if (clock === Number.MAX_SAFE_INTEGER) throw new RangeError("Awareness clock exhausted");
|
|
52
|
+
return encode(config.clientId, ++clock, state);
|
|
53
|
+
};
|
|
54
|
+
return {
|
|
55
|
+
/** Validate one local state and replace its envelope and identity. Throws on invalid input. */
|
|
56
|
+
publish(bytes: Uint8Array): Uint8Array {
|
|
57
|
+
try {
|
|
58
|
+
const decoder = decoding.createDecoder(bytes);
|
|
59
|
+
if (decoding.readVarUint(decoder) !== 1) throw new InvalidSharedTextAwarenessError();
|
|
60
|
+
const clientId = decoding.readVarUint(decoder);
|
|
61
|
+
const clientClock = decoding.readVarUint(decoder);
|
|
62
|
+
if (!Number.isSafeInteger(clientId) || !Number.isSafeInteger(clientClock)) throw new InvalidSharedTextAwarenessError();
|
|
63
|
+
const state = stateSchema.parse(JSON.parse(decoding.readVarString(decoder)));
|
|
64
|
+
if (decoder.pos !== bytes.length || (localClientId !== undefined && localClientId !== clientId))
|
|
65
|
+
throw new InvalidSharedTextAwarenessError();
|
|
66
|
+
if (
|
|
67
|
+
state?.cursor &&
|
|
68
|
+
[state.cursor.anchor, state.cursor.head].some((position) => position.tname !== config.textName || position.type != null)
|
|
69
|
+
)
|
|
70
|
+
throw new InvalidSharedTextAwarenessError();
|
|
71
|
+
const result = next(state === null ? null : { user, writerId: clientId, cursor: state.cursor ?? null });
|
|
72
|
+
localClientId = clientId;
|
|
73
|
+
active = state !== null;
|
|
74
|
+
return result;
|
|
75
|
+
} catch {
|
|
76
|
+
throw new InvalidSharedTextAwarenessError();
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
/** Publish on leave/close, including when the browser cannot send its own null state. */
|
|
80
|
+
remove(): Uint8Array | null {
|
|
81
|
+
if (!active) return null;
|
|
82
|
+
const result = next(null);
|
|
83
|
+
active = false;
|
|
84
|
+
return result;
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { SharedTextChange } from "./types";
|
|
2
|
+
|
|
3
|
+
export const validateChanges = (text: string, changes: readonly SharedTextChange[], maxLength: number): SharedTextChange[] => {
|
|
4
|
+
const sorted = [...changes].sort((a, b) => a.from - b.from || a.to - b.to);
|
|
5
|
+
let length = text.length;
|
|
6
|
+
const splitsCharacter = (at: number) =>
|
|
7
|
+
at > 0 && at < text.length && /[\uD800-\uDBFF]/.test(text[at - 1]!) && /[\uDC00-\uDFFF]/.test(text[at]!);
|
|
8
|
+
for (let index = 0; index < sorted.length; index++) {
|
|
9
|
+
const change = sorted[index]!;
|
|
10
|
+
const previous = sorted[index - 1];
|
|
11
|
+
if (
|
|
12
|
+
!Number.isSafeInteger(change.from) ||
|
|
13
|
+
!Number.isSafeInteger(change.to) ||
|
|
14
|
+
change.from < 0 ||
|
|
15
|
+
change.to < change.from ||
|
|
16
|
+
change.to > text.length ||
|
|
17
|
+
(change.recreate === true && change.to === change.from) ||
|
|
18
|
+
(previous && (change.from < previous.to || change.from === previous.from))
|
|
19
|
+
)
|
|
20
|
+
throw new RangeError("Shared text changes must have distinct, non-overlapping original-source ranges");
|
|
21
|
+
if (splitsCharacter(change.from) || splitsCharacter(change.to) || !change.insert.isWellFormed())
|
|
22
|
+
throw new RangeError("Shared text changes must preserve Unicode characters");
|
|
23
|
+
length += change.insert.length - (change.to - change.from);
|
|
24
|
+
}
|
|
25
|
+
if (length > maxLength) throw new RangeError("Shared text changes exceed maxLength");
|
|
26
|
+
return sorted.filter((change) => change.recreate === true || text.slice(change.from, change.to) !== change.insert);
|
|
27
|
+
};
|