@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
|
@@ -17,15 +17,16 @@ const isSchemaUnavailable = (error: unknown): boolean => {
|
|
|
17
17
|
const upsertDefinition = async (definition: AnyBoundNotificationDefinition, db: typeof sql = sql): Promise<void> => {
|
|
18
18
|
const recommended = toPgTextArray([...(definition.delivery?.recommended ?? [])]);
|
|
19
19
|
const required = toPgTextArray([...(definition.delivery?.required ?? [])]);
|
|
20
|
+
const excluded = toPgTextArray([...(definition.delivery?.excluded ?? [])]);
|
|
20
21
|
await db`
|
|
21
22
|
INSERT INTO notifications.definitions (
|
|
22
23
|
id, app_id, kind, label, description, presentation, recipient_kind,
|
|
23
|
-
recommended_channels, required_channels, active, last_seen_at, updated_at
|
|
24
|
+
recommended_channels, required_channels, excluded_channels, excluded_reason, active, last_seen_at, updated_at
|
|
24
25
|
) VALUES (
|
|
25
26
|
${definition.id}, ${definition.appId}, ${definition.key}, ${definition.label},
|
|
26
27
|
${definition.description}, (${definition.presentation ? JSON.stringify(definition.presentation) : null}::text)::jsonb,
|
|
27
28
|
${definition.recipient}, ${recommended}::text[],
|
|
28
|
-
${required}::text[], true, now(), now()
|
|
29
|
+
${required}::text[], ${excluded}::text[], ${definition.excludedReason ?? null}, true, now(), now()
|
|
29
30
|
)
|
|
30
31
|
ON CONFLICT (id) DO UPDATE SET
|
|
31
32
|
app_id = EXCLUDED.app_id,
|
|
@@ -36,6 +37,8 @@ const upsertDefinition = async (definition: AnyBoundNotificationDefinition, db:
|
|
|
36
37
|
recipient_kind = EXCLUDED.recipient_kind,
|
|
37
38
|
recommended_channels = EXCLUDED.recommended_channels,
|
|
38
39
|
required_channels = EXCLUDED.required_channels,
|
|
40
|
+
excluded_channels = EXCLUDED.excluded_channels,
|
|
41
|
+
excluded_reason = EXCLUDED.excluded_reason,
|
|
39
42
|
active = true,
|
|
40
43
|
last_seen_at = now(),
|
|
41
44
|
updated_at = now()
|
|
@@ -95,6 +95,9 @@ const validatePresentation = (presentation: NotificationPresentation, appId: str
|
|
|
95
95
|
if (presentation.badge !== undefined && (!Number.isSafeInteger(presentation.badge) || presentation.badge < 0)) {
|
|
96
96
|
throw new Error("Notification badge must be a non-negative safe integer");
|
|
97
97
|
}
|
|
98
|
+
if (presentation.alert !== undefined && typeof presentation.alert !== "boolean") {
|
|
99
|
+
throw new Error("Notification alert must be a boolean");
|
|
100
|
+
}
|
|
98
101
|
return {
|
|
99
102
|
title,
|
|
100
103
|
...(body ? { body } : {}),
|
|
@@ -102,6 +105,7 @@ const validatePresentation = (presentation: NotificationPresentation, appId: str
|
|
|
102
105
|
...(targetHref ? { targetHref } : {}),
|
|
103
106
|
...(presentation.group !== undefined ? { group: presentation.group } : {}),
|
|
104
107
|
...(presentation.badge !== undefined ? { badge: presentation.badge } : {}),
|
|
108
|
+
...(presentation.alert === false ? { alert: false } : {}),
|
|
105
109
|
};
|
|
106
110
|
};
|
|
107
111
|
|
|
@@ -274,10 +278,11 @@ export const sendTypedNotification = async <
|
|
|
274
278
|
const candidateEventId = crypto.randomUUID();
|
|
275
279
|
await ensureNotificationDefinition(definition);
|
|
276
280
|
|
|
277
|
-
const
|
|
281
|
+
const excluded = new Set<string>(definition.delivery?.excluded ?? []);
|
|
282
|
+
const requiredChannels = unique([...(definition.delivery?.required ?? [])]).filter((channel) => !excluded.has(channel));
|
|
278
283
|
const requiredChannelSet = new Set<string>(requiredChannels);
|
|
279
284
|
const preference = await preferredChannels(definition.id, resolved.recipient, definition.delivery?.recommended ?? []);
|
|
280
|
-
const preferred = preference.channels.filter((channel) => !requiredChannelSet.has(channel));
|
|
285
|
+
const preferred = preference.channels.filter((channel) => !requiredChannelSet.has(channel) && !excluded.has(channel));
|
|
281
286
|
|
|
282
287
|
const identity = await sql.begin(async (tx) => {
|
|
283
288
|
const eventRows = await tx<{ id: string }[]>`
|
|
@@ -25,6 +25,8 @@ type PreferenceRow = {
|
|
|
25
25
|
presentation: NotificationDefinitionPresentationCatalog | null;
|
|
26
26
|
recommended_channels: string[];
|
|
27
27
|
required_channels: string[];
|
|
28
|
+
excluded_channels: string[];
|
|
29
|
+
excluded_reason: string | null;
|
|
28
30
|
channels: string[] | null;
|
|
29
31
|
customized: boolean;
|
|
30
32
|
};
|
|
@@ -57,12 +59,14 @@ const definitionPresentation = (
|
|
|
57
59
|
): { label: string; description: string } => resolveNotificationDefinitionPresentation(row, locale);
|
|
58
60
|
|
|
59
61
|
const mapPreference = (row: PreferenceRow, locale?: string): UserNotificationPreference => {
|
|
60
|
-
const
|
|
62
|
+
const excludedChannels = unique(row.excluded_channels);
|
|
63
|
+
const excluded = new Set(excludedChannels);
|
|
64
|
+
const requiredChannels = unique(row.required_channels).filter((channel) => !excluded.has(channel));
|
|
61
65
|
const required = new Set(requiredChannels);
|
|
62
66
|
const selectedChannels = unique(row.customized ? (row.channels ?? []) : row.recommended_channels).filter(
|
|
63
|
-
(channel) => !required.has(channel),
|
|
67
|
+
(channel) => !required.has(channel) && !excluded.has(channel),
|
|
64
68
|
);
|
|
65
|
-
const presentation =
|
|
69
|
+
const presentation = resolveNotificationDefinitionPresentation({ ...row, excludedReason: row.excluded_reason ?? undefined }, locale);
|
|
66
70
|
return {
|
|
67
71
|
id: row.id,
|
|
68
72
|
appId: row.app_id,
|
|
@@ -71,6 +75,8 @@ const mapPreference = (row: PreferenceRow, locale?: string): UserNotificationPre
|
|
|
71
75
|
description: presentation.description,
|
|
72
76
|
recommendedChannels: unique(row.recommended_channels),
|
|
73
77
|
requiredChannels,
|
|
78
|
+
excludedChannels,
|
|
79
|
+
excludedReason: presentation.excludedReason ?? null,
|
|
74
80
|
selectedChannels,
|
|
75
81
|
effectiveChannels: unique([...requiredChannels, ...selectedChannels]),
|
|
76
82
|
customized: row.customized,
|
|
@@ -80,7 +86,7 @@ const mapPreference = (row: PreferenceRow, locale?: string): UserNotificationPre
|
|
|
80
86
|
const listPreferences = async (userId: string, locale?: string): Promise<UserNotificationPreferencesResponse> => {
|
|
81
87
|
const rows = await sql<PreferenceRow[]>`
|
|
82
88
|
SELECT d.id, d.app_id, d.kind, d.label, d.description, d.presentation,
|
|
83
|
-
d.recommended_channels, d.required_channels, p.channels,
|
|
89
|
+
d.recommended_channels, d.required_channels, d.excluded_channels, d.excluded_reason, p.channels,
|
|
84
90
|
(p.user_id IS NOT NULL) AS customized
|
|
85
91
|
FROM notifications.definitions d
|
|
86
92
|
LEFT JOIN notifications.preferences p
|
|
@@ -110,8 +116,8 @@ const setPreference = async (config: {
|
|
|
110
116
|
channels: string[];
|
|
111
117
|
locale?: string;
|
|
112
118
|
}): Promise<MutationResult<UserNotificationPreference>> => {
|
|
113
|
-
const rows = await sql<{ required_channels: string[] }[]>`
|
|
114
|
-
SELECT required_channels
|
|
119
|
+
const rows = await sql<{ required_channels: string[]; excluded_channels: string[] }[]>`
|
|
120
|
+
SELECT required_channels, excluded_channels
|
|
115
121
|
FROM notifications.definitions
|
|
116
122
|
WHERE id = ${config.definitionId} AND active = true AND recipient_kind = 'user'
|
|
117
123
|
LIMIT 1
|
|
@@ -119,8 +125,13 @@ const setPreference = async (config: {
|
|
|
119
125
|
const definition = rows[0];
|
|
120
126
|
if (!definition) return { ok: false, error: "Notification preference not found", status: 404 };
|
|
121
127
|
|
|
128
|
+
const requested = unique(config.channels.map((channel) => channel.trim()).filter(Boolean));
|
|
129
|
+
const excluded = requested.filter((channel) => definition.excluded_channels.includes(channel));
|
|
130
|
+
if (excluded.length > 0) {
|
|
131
|
+
return { ok: false, error: `Excluded notification channel: ${excluded.join(", ")}`, status: 400 };
|
|
132
|
+
}
|
|
122
133
|
const required = new Set(definition.required_channels);
|
|
123
|
-
const channels =
|
|
134
|
+
const channels = requested.filter((channel) => !required.has(channel));
|
|
124
135
|
const available = new Set(listNotificationChannels());
|
|
125
136
|
const unavailable = channels.filter((channel) => !available.has(channel));
|
|
126
137
|
if (unavailable.length > 0) {
|
|
@@ -4,7 +4,7 @@ import type { User } from "../contracts/shared";
|
|
|
4
4
|
import { isAccountCategoryAllowed } from "./account-category-policy";
|
|
5
5
|
import { isAccountExpired } from "./account-model";
|
|
6
6
|
import { IDENTITY_JWKS_MAX_AGE_SECONDS } from "./identity/constants";
|
|
7
|
-
import { getIdentityRuntimeConfig } from "./identity/runtime-config";
|
|
7
|
+
import { getIdentityRuntimeConfig, readIdentityGroupsAdmin } from "./identity/runtime-config";
|
|
8
8
|
import type { ServiceAccount } from "./service-accounts";
|
|
9
9
|
import { buildProjectedUser, userProjectionJoin, userProjectionSql } from "./session/user";
|
|
10
10
|
|
|
@@ -59,6 +59,7 @@ export type AuthenticatedOAuthToken =
|
|
|
59
59
|
kind: "service_account";
|
|
60
60
|
payload: JWTPayload;
|
|
61
61
|
serviceAccount: ServiceAccount;
|
|
62
|
+
oauthClientId: string;
|
|
62
63
|
delegatedUser: User | null;
|
|
63
64
|
scopes: string[];
|
|
64
65
|
};
|
|
@@ -138,7 +139,7 @@ export const resolveOAuthTokenActor = async (
|
|
|
138
139
|
if (serviceAccount.kind === "user_delegated" && !delegatedUser) return null;
|
|
139
140
|
if (delegatedUser && isAccountExpired(delegatedUser.accountExpires)) return null;
|
|
140
141
|
if (delegatedUser && !(await isAccountCategoryAllowed(delegatedUser, query))) return null;
|
|
141
|
-
return { kind: "service_account", payload, serviceAccount, delegatedUser, scopes };
|
|
142
|
+
return { kind: "service_account", payload, serviceAccount, oauthClientId: clientId, delegatedUser, scopes };
|
|
142
143
|
}
|
|
143
144
|
|
|
144
145
|
const rawUserId = getStringClaim(payload, "id");
|
|
@@ -161,6 +162,31 @@ export const resolveOAuthTokenActor = async (
|
|
|
161
162
|
return { kind: "user", payload, user, scopes };
|
|
162
163
|
};
|
|
163
164
|
|
|
165
|
+
/**
|
|
166
|
+
* Revalidate a retained, previously authenticated service-account OAuth reference.
|
|
167
|
+
* Identifiers are not credentials: callers must protect their provenance and keep
|
|
168
|
+
* the captured scopes and expiry as caps, then authorize the domain operation.
|
|
169
|
+
*/
|
|
170
|
+
export const isServiceAccountOAuthAuthorityCurrent = async (
|
|
171
|
+
authority: {
|
|
172
|
+
serviceAccountId: string;
|
|
173
|
+
clientId: string;
|
|
174
|
+
expiresAt: string | null | undefined;
|
|
175
|
+
},
|
|
176
|
+
options: { query?: typeof sql; groupsAdmin?: readonly string[] } = {},
|
|
177
|
+
): Promise<boolean> => {
|
|
178
|
+
const expiry = authority.expiresAt ? Date.parse(authority.expiresAt) : NaN;
|
|
179
|
+
if (!Number.isFinite(expiry) || expiry <= Date.now()) return false;
|
|
180
|
+
const groupsAdmin =
|
|
181
|
+
options.groupsAdmin ?? (options.query ? await readIdentityGroupsAdmin(options.query) : (await getIdentityRuntimeConfig()).groupsAdmin);
|
|
182
|
+
const current = await resolveOAuthTokenActor(
|
|
183
|
+
{ client_id: authority.clientId, service_account_id: authority.serviceAccountId },
|
|
184
|
+
[...groupsAdmin],
|
|
185
|
+
options.query ?? sql,
|
|
186
|
+
);
|
|
187
|
+
return current?.kind === "service_account";
|
|
188
|
+
};
|
|
189
|
+
|
|
164
190
|
export const verifyAccessToken = async (
|
|
165
191
|
token: string,
|
|
166
192
|
expectedAudience: string | string[] = "cloud",
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import type { S3ListObjectsResponse, S3Stats } from "bun";
|
|
2
|
+
import { objectStoreArea } from "../_internal/object-store-area";
|
|
3
|
+
import { objectStoreBackend } from "../_internal/object-store-config";
|
|
4
|
+
import { validateObjectKey as validateKey } from "../_internal/object-store-key";
|
|
5
|
+
import { publicCloudOrigin } from "../shared/app-url";
|
|
6
|
+
import { get } from "./settings";
|
|
7
|
+
|
|
8
|
+
export type ObjectVisibility = "private" | "public";
|
|
9
|
+
export type ObjectWriteData = string | ArrayBuffer | ArrayBufferView | Blob | Request | Response;
|
|
10
|
+
export type ObjectWriteOptions = { type?: string };
|
|
11
|
+
export type ObjectPresignOptions = { method?: "GET" | "HEAD" | "DELETE"; expiresIn?: number };
|
|
12
|
+
export type ObjectListOptions = {
|
|
13
|
+
prefix?: string;
|
|
14
|
+
startAfter?: string;
|
|
15
|
+
continuationToken?: string;
|
|
16
|
+
maxKeys?: number;
|
|
17
|
+
};
|
|
18
|
+
export type ObjectListResult = Pick<S3ListObjectsResponse, "contents" | "isTruncated" | "nextContinuationToken">;
|
|
19
|
+
|
|
20
|
+
export interface ObjectStoreFile {
|
|
21
|
+
readonly key: string;
|
|
22
|
+
readonly size: Promise<number>;
|
|
23
|
+
stat(): Promise<S3Stats>;
|
|
24
|
+
write(data: ObjectWriteData, options?: ObjectWriteOptions): Promise<number>;
|
|
25
|
+
text(): Promise<string>;
|
|
26
|
+
bytes(): Promise<Uint8Array>;
|
|
27
|
+
stream(): ReadableStream<Uint8Array>;
|
|
28
|
+
exists(): Promise<boolean>;
|
|
29
|
+
delete(): Promise<void>;
|
|
30
|
+
copy(destinationKey: string): Promise<number>;
|
|
31
|
+
presign(options?: ObjectPresignOptions): Promise<string>;
|
|
32
|
+
readonly url: Promise<string>;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const assertSize = (size: number, limit: number): void => {
|
|
36
|
+
if (!Number.isSafeInteger(size) || size < 0 || size > limit) throw new RangeError(`Object exceeds the ${limit}-byte limit.`);
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/** Pull one chunk at a time, propagate cancellation, and reject before buffering past the budget. */
|
|
40
|
+
const boundedStream = (source: ReadableStream<Uint8Array>, limit: number): ReadableStream<Uint8Array> => {
|
|
41
|
+
const reader = source.getReader();
|
|
42
|
+
let size = 0;
|
|
43
|
+
return new ReadableStream<Uint8Array>(
|
|
44
|
+
{
|
|
45
|
+
async pull(controller) {
|
|
46
|
+
try {
|
|
47
|
+
const next = await reader.read();
|
|
48
|
+
if (next.done) {
|
|
49
|
+
reader.releaseLock();
|
|
50
|
+
controller.close();
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
size += next.value.byteLength;
|
|
54
|
+
assertSize(size, limit);
|
|
55
|
+
controller.enqueue(next.value);
|
|
56
|
+
} catch (error) {
|
|
57
|
+
await reader.cancel(error).catch(() => {});
|
|
58
|
+
reader.releaseLock();
|
|
59
|
+
controller.error(error);
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
async cancel(reason) {
|
|
63
|
+
try {
|
|
64
|
+
await reader.cancel(reason);
|
|
65
|
+
} finally {
|
|
66
|
+
reader.releaseLock();
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
{ highWaterMark: 0 },
|
|
71
|
+
);
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const uploadData = async (data: ObjectWriteData, limit: number): Promise<string | ArrayBuffer | Uint8Array<ArrayBufferLike>> => {
|
|
75
|
+
if (typeof data === "string") {
|
|
76
|
+
assertSize(Buffer.byteLength(data), limit);
|
|
77
|
+
return data;
|
|
78
|
+
}
|
|
79
|
+
if (data instanceof ArrayBuffer || ArrayBuffer.isView(data)) {
|
|
80
|
+
assertSize(data.byteLength, limit);
|
|
81
|
+
return ArrayBuffer.isView(data) ? new Uint8Array(data.buffer, data.byteOffset, data.byteLength) : data;
|
|
82
|
+
}
|
|
83
|
+
if (data instanceof Blob || data instanceof Request || data instanceof Response) {
|
|
84
|
+
if (data instanceof Blob) assertSize(await data.size, limit);
|
|
85
|
+
const stream = data instanceof Blob ? data.stream() : data.body;
|
|
86
|
+
if (!stream) return new Uint8Array();
|
|
87
|
+
return new Response(boundedStream(stream, limit)).bytes();
|
|
88
|
+
}
|
|
89
|
+
throw new TypeError("Unsupported object data.");
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const file = (key: string, visibility: ObjectVisibility = "private"): ObjectStoreFile => {
|
|
93
|
+
const prefix = objectStoreArea(visibility);
|
|
94
|
+
validateKey(key, prefix);
|
|
95
|
+
const path = prefix + key;
|
|
96
|
+
const stream = () => {
|
|
97
|
+
let reader: Promise<ReadableStreamDefaultReader<Uint8Array>> | undefined;
|
|
98
|
+
const source = () =>
|
|
99
|
+
(reader ??= objectStoreBackend().then(({ client, limit }) => boundedStream(client.file(path).stream(), limit).getReader()));
|
|
100
|
+
return new ReadableStream<Uint8Array>(
|
|
101
|
+
{
|
|
102
|
+
async pull(controller) {
|
|
103
|
+
try {
|
|
104
|
+
const current = await source();
|
|
105
|
+
const next = await current.read();
|
|
106
|
+
if (next.done) {
|
|
107
|
+
current.releaseLock();
|
|
108
|
+
controller.close();
|
|
109
|
+
} else controller.enqueue(next.value);
|
|
110
|
+
} catch (error) {
|
|
111
|
+
void reader?.then(
|
|
112
|
+
(current) => current.releaseLock(),
|
|
113
|
+
() => {},
|
|
114
|
+
);
|
|
115
|
+
controller.error(error);
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
async cancel(reason) {
|
|
119
|
+
if (!reader) return;
|
|
120
|
+
const current = await reader;
|
|
121
|
+
try {
|
|
122
|
+
await current.cancel(reason);
|
|
123
|
+
} finally {
|
|
124
|
+
current.releaseLock();
|
|
125
|
+
}
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
{ highWaterMark: 0 },
|
|
129
|
+
);
|
|
130
|
+
};
|
|
131
|
+
return Object.freeze({
|
|
132
|
+
key,
|
|
133
|
+
get size() {
|
|
134
|
+
return objectStoreBackend().then(({ client }) => client.size(path));
|
|
135
|
+
},
|
|
136
|
+
async stat() {
|
|
137
|
+
return (await objectStoreBackend()).client.stat(path);
|
|
138
|
+
},
|
|
139
|
+
async write(data: ObjectWriteData, options: ObjectWriteOptions = {}) {
|
|
140
|
+
const { client, limit } = await objectStoreBackend();
|
|
141
|
+
const content = await uploadData(data, limit);
|
|
142
|
+
const inheritedType =
|
|
143
|
+
data instanceof Blob
|
|
144
|
+
? data.type
|
|
145
|
+
: data instanceof Request || data instanceof Response
|
|
146
|
+
? data.headers.get("content-type")
|
|
147
|
+
: undefined;
|
|
148
|
+
return client.file(path).write(content, { type: options.type ?? (inheritedType || undefined) });
|
|
149
|
+
},
|
|
150
|
+
text: () => new Response(stream()).text(),
|
|
151
|
+
bytes: () => new Response(stream()).bytes(),
|
|
152
|
+
stream,
|
|
153
|
+
async exists() {
|
|
154
|
+
return (await objectStoreBackend()).client.file(path).exists();
|
|
155
|
+
},
|
|
156
|
+
async delete() {
|
|
157
|
+
return (await objectStoreBackend()).client.file(path).delete();
|
|
158
|
+
},
|
|
159
|
+
async copy(destinationKey: string) {
|
|
160
|
+
validateKey(destinationKey, prefix);
|
|
161
|
+
const { client, limit } = await objectStoreBackend();
|
|
162
|
+
const metadata = await client.stat(path);
|
|
163
|
+
assertSize(metadata.size, limit);
|
|
164
|
+
const content = await new Response(boundedStream(client.file(path).stream(), limit)).bytes();
|
|
165
|
+
return client.file(prefix + destinationKey).write(content, { type: metadata.type });
|
|
166
|
+
},
|
|
167
|
+
presign(options: ObjectPresignOptions = {}) {
|
|
168
|
+
if (visibility !== "private") throw new Error("Use url for public objects.");
|
|
169
|
+
const expiresIn = options.expiresIn ?? 300;
|
|
170
|
+
const method = options.method ?? "GET";
|
|
171
|
+
if (!Number.isInteger(expiresIn) || expiresIn < 1 || expiresIn > 900) throw new RangeError("Presign expiry must be 1–900 seconds.");
|
|
172
|
+
if (!["GET", "HEAD", "DELETE"].includes(method))
|
|
173
|
+
throw new TypeError("Presign supports GET, HEAD, and DELETE; upload through write().");
|
|
174
|
+
return objectStoreBackend().then(({ client }) => client.file(path).presign({ method, expiresIn }));
|
|
175
|
+
},
|
|
176
|
+
get url() {
|
|
177
|
+
if (visibility !== "public") throw new Error("Private objects have no public URL.");
|
|
178
|
+
return get<string>("app.url").then(
|
|
179
|
+
(base) => `${publicCloudOrigin(base)}/public/storage/${path.slice("public/".length).split("/").map(encodeURIComponent).join("/")}`,
|
|
180
|
+
);
|
|
181
|
+
},
|
|
182
|
+
});
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
/** App-bound S3 operations. Identity comes from app.start(), never from an API parameter. */
|
|
186
|
+
const list = async (options: ObjectListOptions = {}, visibility: ObjectVisibility = "private"): Promise<ObjectListResult> => {
|
|
187
|
+
const prefix = objectStoreArea(visibility);
|
|
188
|
+
const relative = validateKey(options.prefix ?? "", prefix, true);
|
|
189
|
+
const startAfter = options.startAfter === undefined ? undefined : prefix + validateKey(options.startAfter, prefix);
|
|
190
|
+
const maxKeys = options.maxKeys ?? 1000;
|
|
191
|
+
if (!Number.isInteger(maxKeys) || maxKeys < 1 || maxKeys > 1000) throw new RangeError("List maxKeys must be 1–1,000.");
|
|
192
|
+
if (
|
|
193
|
+
options.continuationToken !== undefined &&
|
|
194
|
+
(typeof options.continuationToken !== "string" || options.continuationToken.length > 8192)
|
|
195
|
+
) {
|
|
196
|
+
throw new TypeError("Invalid S3 continuation token.");
|
|
197
|
+
}
|
|
198
|
+
const { client } = await objectStoreBackend();
|
|
199
|
+
const result = await client.list({ prefix: prefix + relative, startAfter, maxKeys, continuationToken: options.continuationToken });
|
|
200
|
+
const contents = result.contents?.flatMap((object) => {
|
|
201
|
+
if (!object.key.startsWith(prefix + relative)) throw new Error("S3 returned an object outside the requested area.");
|
|
202
|
+
try {
|
|
203
|
+
return [{ ...object, key: validateKey(object.key.slice(prefix.length), prefix) }];
|
|
204
|
+
} catch {
|
|
205
|
+
// Console folder markers and externally written nonconforming names are
|
|
206
|
+
// inaccessible through this API, but must not break paging for valid keys.
|
|
207
|
+
return [];
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
return { contents, isTruncated: result.isTruncated, nextContinuationToken: result.nextContinuationToken };
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
export const storage = Object.freeze({
|
|
214
|
+
file: (key: string) => file(key),
|
|
215
|
+
list: (options?: ObjectListOptions) => list(options),
|
|
216
|
+
public: Object.freeze({ file: (key: string) => file(key, "public"), list: (options?: ObjectListOptions) => list(options, "public") }),
|
|
217
|
+
});
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { sql } from "bun";
|
|
2
2
|
import type { MutationResult, UserProfile } from "../../../contracts/shared";
|
|
3
|
+
import { coreUserEventPublisher } from "../../../events/core-user-created";
|
|
3
4
|
import { isAccountCategoryAllowed } from "../../account-category-policy";
|
|
4
5
|
import { writeDeletedAccountAudit } from "../../account-lifecycle/audit";
|
|
5
6
|
import { emailAlreadyUsed, findAccountsByEmail, lockAccountEmail, normalizeAccountEmail } from "../../accounts/email-write";
|
|
@@ -96,6 +97,10 @@ export const create = async (params: {
|
|
|
96
97
|
)
|
|
97
98
|
RETURNING id
|
|
98
99
|
`;
|
|
100
|
+
await coreUserEventPublisher().publish(tx, "user.created", {
|
|
101
|
+
subject: rows[0]!.id,
|
|
102
|
+
data: { userId: rows[0]!.id, provider: "local" },
|
|
103
|
+
});
|
|
99
104
|
return { ok: true, data: { id: rows[0]!.id } };
|
|
100
105
|
}, params.actor);
|
|
101
106
|
} catch (error) {
|