@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,92 @@
|
|
|
1
|
+
import type { TopicDeadLetter, TopicInventoryEntry } from "@k2b/sync";
|
|
2
|
+
import { type SQL, sql } from "bun";
|
|
3
|
+
import { lazySync } from "../_internal/process-sync";
|
|
4
|
+
import { getCapability, listCapabilities } from "../_internal/registry";
|
|
5
|
+
import type { AppEventEnvelope } from "../contracts/app-events";
|
|
6
|
+
import type { CapabilityRegistryEntry } from "../contracts/registry";
|
|
7
|
+
import { appEventTopic } from "../events/app-event-delivery";
|
|
8
|
+
import { createEventAcceptance } from "./app-events/accept";
|
|
9
|
+
import { createEventAdministration } from "./app-events/admin";
|
|
10
|
+
import { createDeclarationSync } from "./app-events/declarations";
|
|
11
|
+
import { createEventDelivery } from "./app-events/deliver";
|
|
12
|
+
import type { signAppEventInvocation } from "./identity/invocation-token";
|
|
13
|
+
import { logger } from "./logging";
|
|
14
|
+
|
|
15
|
+
export { APP_EVENT_DELIVERY_ATTEMPTS, APP_EVENT_DELIVERY_BACKOFF_MS } from "./app-events/deliver";
|
|
16
|
+
|
|
17
|
+
const log = logger("app-events:broker");
|
|
18
|
+
const topicInventory = lazySync((sync) => () => sync.listTopics({ idPrefix: "cloud:app-events" }));
|
|
19
|
+
type BrokerDependencies = {
|
|
20
|
+
db?: SQL;
|
|
21
|
+
registry?: () => Promise<CapabilityRegistryEntry[]>;
|
|
22
|
+
getApp?: typeof getCapability;
|
|
23
|
+
fetch?: typeof fetch;
|
|
24
|
+
sign?: typeof signAppEventInvocation;
|
|
25
|
+
deadLetters?: (after?: string) => Promise<TopicDeadLetter<AppEventEnvelope>[]>;
|
|
26
|
+
streams?: () => AsyncIterable<Pick<TopicInventoryEntry, "id" | "firstSequence">>;
|
|
27
|
+
};
|
|
28
|
+
const message = (error: unknown): string => (error instanceof Error ? error.message : String(error)).slice(0, 1000);
|
|
29
|
+
|
|
30
|
+
export const createAppEventBroker = (dependencies: BrokerDependencies = {}) => {
|
|
31
|
+
const db = dependencies.db ?? sql;
|
|
32
|
+
const registry = dependencies.registry ?? listCapabilities;
|
|
33
|
+
const declarations = createDeclarationSync(db, registry);
|
|
34
|
+
const admin = createEventAdministration(db, {
|
|
35
|
+
registry,
|
|
36
|
+
deadLetters: dependencies.deadLetters ?? ((after) => appEventTopic().deadLetters.list({ limit: 100, after })),
|
|
37
|
+
streams: dependencies.streams ?? (() => topicInventory()()),
|
|
38
|
+
});
|
|
39
|
+
const acceptance = createEventAcceptance(db, declarations.reconcileSubscriptions);
|
|
40
|
+
const delivery = createEventDelivery(db, dependencies.getApp ?? getCapability, declarations.reconcileSubscriptions, dependencies);
|
|
41
|
+
return { ...declarations, ...acceptance, ...delivery, ...admin };
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export const appEventBroker = createAppEventBroker();
|
|
45
|
+
export const startAppEventBroker = async (): Promise<() => Promise<void>> => {
|
|
46
|
+
const worker = await appEventTopic().process(
|
|
47
|
+
{
|
|
48
|
+
consumer: "core-app-events-v1",
|
|
49
|
+
start: "earliest",
|
|
50
|
+
concurrency: 1,
|
|
51
|
+
recoverDeadLetters: true,
|
|
52
|
+
delivery: { maxAttempts: 8, backoffMs: [1000, 5000, 30000, 120000, 600000, 1800000, 3600000], maxInFlight: 1 },
|
|
53
|
+
},
|
|
54
|
+
async (event) => {
|
|
55
|
+
try {
|
|
56
|
+
await appEventBroker.accept(event.data, event.sequence, event.attempt);
|
|
57
|
+
} catch (error) {
|
|
58
|
+
if (event.attempt >= 8) {
|
|
59
|
+
await appEventBroker.markSeen(event.sequence);
|
|
60
|
+
}
|
|
61
|
+
throw error;
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
);
|
|
65
|
+
let stopping = false;
|
|
66
|
+
let active: Promise<void> | null = null;
|
|
67
|
+
let tickCount = 0;
|
|
68
|
+
const run = () => {
|
|
69
|
+
if (stopping || active) return;
|
|
70
|
+
active = (async () => {
|
|
71
|
+
await appEventBroker.work();
|
|
72
|
+
if (++tickCount % 60 === 0) {
|
|
73
|
+
await appEventBroker.checkGap();
|
|
74
|
+
await appEventBroker.retain();
|
|
75
|
+
}
|
|
76
|
+
})()
|
|
77
|
+
.catch((error) => log.error("App event broker pass failed", { error: message(error) }))
|
|
78
|
+
.finally(() => {
|
|
79
|
+
active = null;
|
|
80
|
+
});
|
|
81
|
+
};
|
|
82
|
+
const timer = setInterval(run, 1000);
|
|
83
|
+
timer.unref();
|
|
84
|
+
run();
|
|
85
|
+
return async () => {
|
|
86
|
+
stopping = true;
|
|
87
|
+
clearInterval(timer);
|
|
88
|
+
worker.stop();
|
|
89
|
+
await worker.drain();
|
|
90
|
+
await active;
|
|
91
|
+
};
|
|
92
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export class ConfigConflict extends Error {
|
|
2
|
+
readonly code = "conflict";
|
|
3
|
+
constructor(
|
|
4
|
+
readonly current: unknown,
|
|
5
|
+
readonly changedBy: { id: string; name: string } | null,
|
|
6
|
+
readonly changedAt: string,
|
|
7
|
+
readonly key?: string,
|
|
8
|
+
) {
|
|
9
|
+
super("This configuration changed. Read the current version before saving.");
|
|
10
|
+
}
|
|
11
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { coreUserEvents, coreUserEventTypes } from "../events/core-user-created";
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { X509Certificate } from "node:crypto";
|
|
2
|
+
import type { SQL } from "bun";
|
|
2
3
|
import { setFreeIpaTlsResolver } from "../server/services/freeipa/tls";
|
|
3
4
|
import { coreSettings } from "./settings/api";
|
|
5
|
+
import { readKeyState, SettingUnreadableError } from "./settings/store";
|
|
4
6
|
|
|
5
7
|
const MAX_CA_CERT_BYTES = 256 * 1024;
|
|
6
8
|
const PEM_CERTIFICATE_PATTERN = /-----BEGIN CERTIFICATE-----[\s\S]*?-----END CERTIFICATE-----/g;
|
|
@@ -82,6 +84,13 @@ const normalizeStringList = (value: unknown, fallback: string[] = []): string[]
|
|
|
82
84
|
return normalized.length > 0 ? normalized : fallback;
|
|
83
85
|
};
|
|
84
86
|
|
|
87
|
+
/** Read the role mapping on the caller's connection during authorization. */
|
|
88
|
+
export const getFreeIpaAdminGroups = async (db: SQL): Promise<string[]> => {
|
|
89
|
+
const state = await readKeyState("freeipa.groups.admin", db);
|
|
90
|
+
if (state.stored === "unreadable") throw new SettingUnreadableError("freeipa.groups.admin");
|
|
91
|
+
return normalizeStringList(state.value, ["admins"]);
|
|
92
|
+
};
|
|
93
|
+
|
|
85
94
|
/**
|
|
86
95
|
* Read the full FreeIPA config snapshot from settings (Redis cache-aside +
|
|
87
96
|
* Postgres fallback). Always returns within Redis-TTL fresh data — no hidden
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type SQL, sql } from "bun";
|
|
2
|
+
import type { AppPwaActor } from "../../app-pwa/contracts";
|
|
2
3
|
import type { AccessSubject, CapabilityExecutionContext } from "../../contracts";
|
|
3
4
|
import { audit } from "../audit";
|
|
4
5
|
import { GeoError } from "./errors";
|
|
@@ -102,7 +103,7 @@ export const createGeoLedger = (db: SQL = sql) => ({
|
|
|
102
103
|
async recordTransition(
|
|
103
104
|
event: "provider_key_rejected" | "monthly_budget_exhausted" | "route_cost_overrun",
|
|
104
105
|
timezone: string,
|
|
105
|
-
context:
|
|
106
|
+
context: { actor: CapabilityExecutionContext["actor"] | AppPwaActor; requestId?: string },
|
|
106
107
|
now: Date,
|
|
107
108
|
metadata: Record<string, number> = {},
|
|
108
109
|
) {
|
|
@@ -114,21 +115,38 @@ export const createGeoLedger = (db: SQL = sql) => ({
|
|
|
114
115
|
: await tx`INSERT INTO geo.audit_transitions(event,period) VALUES(${event},${period}) ON CONFLICT DO NOTHING RETURNING event`;
|
|
115
116
|
if (!inserted.length) return;
|
|
116
117
|
const actor =
|
|
117
|
-
context.actor.kind === "
|
|
118
|
-
?
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
118
|
+
context.actor.kind === "device" || context.actor.kind === "visit"
|
|
119
|
+
? null
|
|
120
|
+
: context.actor.kind === "user"
|
|
121
|
+
? {
|
|
122
|
+
userId: context.actor.user.id,
|
|
123
|
+
uid: context.actor.user.uid,
|
|
124
|
+
provider: context.actor.user.provider,
|
|
125
|
+
roles: context.actor.user.roles,
|
|
126
|
+
}
|
|
127
|
+
: { uid: `service-account:${context.actor.serviceAccount.id}`, provider: "service_account", roles: context.actor.scopes };
|
|
125
128
|
await audit.record(
|
|
126
129
|
{
|
|
127
130
|
action: `core.geo.${event}`,
|
|
128
131
|
outcome: event === "monthly_budget_exhausted" ? "denied" : "failed",
|
|
129
132
|
actor,
|
|
130
133
|
requestId: context.requestId,
|
|
131
|
-
metadata: {
|
|
134
|
+
metadata: {
|
|
135
|
+
period,
|
|
136
|
+
...metadata,
|
|
137
|
+
...(context.actor.kind === "device" || context.actor.kind === "visit"
|
|
138
|
+
? {
|
|
139
|
+
appId: context.actor.appId,
|
|
140
|
+
subject: { type: "user", userId: context.actor.ownerUserId },
|
|
141
|
+
via: {
|
|
142
|
+
kind: context.actor.kind,
|
|
143
|
+
appId: context.actor.appId,
|
|
144
|
+
deviceId: context.actor.deviceId,
|
|
145
|
+
...(context.actor.kind === "visit" ? { visitId: context.actor.visitId } : {}),
|
|
146
|
+
},
|
|
147
|
+
}
|
|
148
|
+
: {}),
|
|
149
|
+
},
|
|
132
150
|
},
|
|
133
151
|
tx,
|
|
134
152
|
);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { z } from "zod";
|
|
2
|
+
import type { AppPwaActor } from "../../app-pwa/contracts";
|
|
2
3
|
import type { AccessSubject, CapabilityExecutionContext, RequestActor } from "../../contracts";
|
|
3
4
|
import {
|
|
4
5
|
GeoAutocompleteDataSchema,
|
|
@@ -26,17 +27,27 @@ import { createGeoRouteService } from "./route-service";
|
|
|
26
27
|
import { type GeoConfig, geoPlan, readGeoConfig } from "./settings";
|
|
27
28
|
|
|
28
29
|
export type GeoContext = Partial<Pick<CapabilityExecutionContext, "actor" | "accessSubject" | "locale" | "signal" | "requestId">>;
|
|
30
|
+
/** Internal, narrow App-PWA autocomplete adapter; never a RequestActor or capability caller. */
|
|
31
|
+
export type AppPwaGeoContext = { actor: AppPwaActor; locale?: string; signal?: AbortSignal; requestId?: string };
|
|
32
|
+
type GeoCallContext = GeoContext | AppPwaGeoContext;
|
|
29
33
|
export const geoIdentity = (context: GeoContext): { actor: RequestActor; accessSubject: AccessSubject } => {
|
|
30
34
|
const { actor, accessSubject } = context;
|
|
31
35
|
if (
|
|
32
36
|
!actor ||
|
|
33
37
|
!accessSubject ||
|
|
34
|
-
!(actor.kind === "user" ? actor.user?.id : actor.serviceAccount?.id) ||
|
|
38
|
+
!(actor.kind === "user" ? actor.user?.id : actor.kind === "service_account" ? actor.serviceAccount?.id : undefined) ||
|
|
35
39
|
!(accessSubject.type === "user" ? accessSubject.userId : accessSubject.serviceAccountId)
|
|
36
40
|
)
|
|
37
41
|
throw new GeoError("GEO_FORBIDDEN");
|
|
38
42
|
return { actor, accessSubject };
|
|
39
43
|
};
|
|
44
|
+
const geoCallIdentity = (context: GeoCallContext): { actor: RequestActor | AppPwaActor; accessSubject: AccessSubject } => {
|
|
45
|
+
if (context.actor?.kind === "device" || context.actor?.kind === "visit") {
|
|
46
|
+
if (!context.actor.ownerUserId) throw new GeoError("GEO_FORBIDDEN");
|
|
47
|
+
return { actor: context.actor, accessSubject: { type: "user", userId: context.actor.ownerUserId } };
|
|
48
|
+
}
|
|
49
|
+
return geoIdentity({ actor: context.actor, accessSubject: "accessSubject" in context ? context.accessSubject : undefined });
|
|
50
|
+
};
|
|
40
51
|
const bound = (name: string, value: number, min: number, max: number, integer = false) => {
|
|
41
52
|
if (!Number.isFinite(value) || value > max || value < min || (integer && !Number.isInteger(value)))
|
|
42
53
|
limitExceeded(name, value < min ? min : max, value);
|
|
@@ -84,10 +95,10 @@ export const createGeoService = (
|
|
|
84
95
|
const recordTransition = async (
|
|
85
96
|
event: "monthly_budget_exhausted" | "provider_key_rejected" | "route_cost_overrun",
|
|
86
97
|
timezone: string,
|
|
87
|
-
context:
|
|
98
|
+
context: GeoCallContext,
|
|
88
99
|
metadata?: Record<string, number>,
|
|
89
100
|
) => {
|
|
90
|
-
const identity =
|
|
101
|
+
const identity = geoCallIdentity(context);
|
|
91
102
|
try {
|
|
92
103
|
await ledger.recordTransition(event, timezone, { ...context, ...identity }, now(), metadata);
|
|
93
104
|
return true;
|
|
@@ -107,7 +118,7 @@ export const createGeoService = (
|
|
|
107
118
|
const paid = async <T>(
|
|
108
119
|
fn: GeoFunction,
|
|
109
120
|
input: unknown,
|
|
110
|
-
context:
|
|
121
|
+
context: GeoCallContext,
|
|
111
122
|
settings: GeoConfig,
|
|
112
123
|
schema: z.ZodType<T>,
|
|
113
124
|
request: (signal: AbortSignal) => Promise<T>,
|
|
@@ -115,7 +126,7 @@ export const createGeoService = (
|
|
|
115
126
|
cacheInput = input,
|
|
116
127
|
empty: (value: T) => boolean = () => false,
|
|
117
128
|
): Promise<T> => {
|
|
118
|
-
const identity =
|
|
129
|
+
const identity = geoCallIdentity(context);
|
|
119
130
|
const locale = context.locale ?? "en";
|
|
120
131
|
const key = geoCacheKey(fn, cacheInput, locale);
|
|
121
132
|
const hit = await cache.get(key, schema);
|
|
@@ -176,29 +187,37 @@ export const createGeoService = (
|
|
|
176
187
|
await release();
|
|
177
188
|
}
|
|
178
189
|
};
|
|
190
|
+
const autocomplete = async (input: GeoAutocompleteInput, context: GeoCallContext) => {
|
|
191
|
+
geoCallIdentity(context);
|
|
192
|
+
const settings = await config();
|
|
193
|
+
if (!settings.apiKey.trim()) throw new GeoError("GEO_NOT_CONFIGURED");
|
|
194
|
+
const value = parse(GeoAutocompleteInputSchema, input);
|
|
195
|
+
bound("text", value.text.length, 3, 300);
|
|
196
|
+
bound("text", value.text.trim().length, 3, 300);
|
|
197
|
+
bound("limit", value.limit ?? 5, 1, 5, true);
|
|
198
|
+
country(value.countryCode);
|
|
199
|
+
area(value.area);
|
|
200
|
+
return paid(
|
|
201
|
+
"autocomplete",
|
|
202
|
+
value,
|
|
203
|
+
context,
|
|
204
|
+
settings,
|
|
205
|
+
GeoAutocompleteDataSchema,
|
|
206
|
+
(signal) => adapter.autocomplete(value, settings.apiKey, context.locale ?? "en", signal),
|
|
207
|
+
value.limit,
|
|
208
|
+
{ ...value, limit: value.limit ?? 5 },
|
|
209
|
+
(result) => !result.suggestions.length,
|
|
210
|
+
);
|
|
211
|
+
};
|
|
179
212
|
const routes = createGeoRouteService({ configured, adapter, cache, bucket, ledger, now, recordTransition });
|
|
180
213
|
return {
|
|
181
214
|
...routes,
|
|
182
|
-
async
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
bound("text", value.text.length, 3, 300);
|
|
186
|
-
bound("text", value.text.trim().length, 3, 300);
|
|
187
|
-
bound("limit", value.limit ?? 5, 1, 5, true);
|
|
188
|
-
country(value.countryCode);
|
|
189
|
-
area(value.area);
|
|
190
|
-
return paid(
|
|
191
|
-
"autocomplete",
|
|
192
|
-
value,
|
|
193
|
-
context,
|
|
194
|
-
settings,
|
|
195
|
-
GeoAutocompleteDataSchema,
|
|
196
|
-
(signal) => adapter.autocomplete(value, settings.apiKey, context.locale ?? "en", signal),
|
|
197
|
-
value.limit,
|
|
198
|
-
{ ...value, limit: value.limit ?? 5 },
|
|
199
|
-
(result) => !result.suggestions.length,
|
|
200
|
-
);
|
|
215
|
+
autocomplete: async (input: GeoAutocompleteInput, context: GeoContext) => {
|
|
216
|
+
geoIdentity(context);
|
|
217
|
+
return autocomplete(input, context);
|
|
201
218
|
},
|
|
219
|
+
// Only the App-PWA service calls this after locking the pairing and optional visit authority.
|
|
220
|
+
autocompleteForAppPwa: (input: GeoAutocompleteInput, context: AppPwaGeoContext) => autocomplete(input, context),
|
|
202
221
|
async geocode(input: GeoGeocodeInput, context: GeoContext) {
|
|
203
222
|
const settings = await configured(context);
|
|
204
223
|
const value = parse(GeoGeocodeInputSchema, input);
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { env } from "../../config/env";
|
|
2
|
+
import { publicCloudOrigin } from "../../shared/app-url";
|
|
3
|
+
import { get } from "../settings";
|
|
4
|
+
|
|
5
|
+
export const coreOrigin = async (internal = false): Promise<string> => {
|
|
6
|
+
const configured = env.CLOUD_CORE_INTERNAL_ORIGIN;
|
|
7
|
+
if (internal && !configured) throw new Error("CLOUD_CORE_INTERNAL_ORIGIN is required for internal Core requests");
|
|
8
|
+
const origin = configured || publicCloudOrigin(await get<string>("app.url"));
|
|
9
|
+
const url = new URL(origin);
|
|
10
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") throw new Error("CLOUD_CORE_INTERNAL_ORIGIN must use http or https");
|
|
11
|
+
return url.origin;
|
|
12
|
+
};
|
|
@@ -3,6 +3,7 @@ import type { RequestActor } from "../../server/middleware/auth";
|
|
|
3
3
|
import type { AccessSubject } from "../../server/services/access";
|
|
4
4
|
import { isAccountCategoryAllowed } from "../account-category-policy";
|
|
5
5
|
import { isAccountExpired } from "../account-model";
|
|
6
|
+
import { isServiceAccountOAuthAuthorityCurrent } from "../oauth-tokens";
|
|
6
7
|
import type { ServiceAccount } from "../service-accounts";
|
|
7
8
|
import { buildProjectedUser, loadCurrentUser, userProjectionJoin, userProjectionSql, withoutAdminRole } from "../session/user";
|
|
8
9
|
import type { CloudInvocationClaims } from "./invocation-token";
|
|
@@ -38,6 +39,8 @@ export type InvocationPrincipalClaims = Pick<
|
|
|
38
39
|
| "session_kind"
|
|
39
40
|
| "scopes"
|
|
40
41
|
| "credential_id"
|
|
42
|
+
| "oauth_client_id"
|
|
43
|
+
| "credential_expires_at"
|
|
41
44
|
| "exp"
|
|
42
45
|
| "act"
|
|
43
46
|
| "credential_kind"
|
|
@@ -155,15 +158,30 @@ export const resolveInvocationAuthority = async (
|
|
|
155
158
|
)
|
|
156
159
|
return null;
|
|
157
160
|
|
|
158
|
-
const actor: RequestActor = {
|
|
161
|
+
const actor: Extract<RequestActor, { kind: "service_account" }> = {
|
|
159
162
|
kind: "service_account",
|
|
160
163
|
serviceAccount,
|
|
161
164
|
delegatedUser,
|
|
162
165
|
scopes: [...claims.scopes],
|
|
163
166
|
credentialId: claims.credential_id ?? null,
|
|
164
|
-
|
|
167
|
+
oauthClientId: claims.oauth_client_id ?? null,
|
|
168
|
+
credentialExpiresAt: new Date(
|
|
169
|
+
Math.min(claims.exp * 1_000, claims.credential_expires_at ? Date.parse(claims.credential_expires_at) : Infinity),
|
|
170
|
+
).toISOString(),
|
|
165
171
|
delegation,
|
|
166
172
|
};
|
|
173
|
+
if (
|
|
174
|
+
claims.oauth_client_id &&
|
|
175
|
+
!(await isServiceAccountOAuthAuthorityCurrent(
|
|
176
|
+
{
|
|
177
|
+
serviceAccountId: serviceAccount.id,
|
|
178
|
+
clientId: claims.oauth_client_id,
|
|
179
|
+
expiresAt: actor.credentialExpiresAt,
|
|
180
|
+
},
|
|
181
|
+
{ query, groupsAdmin },
|
|
182
|
+
))
|
|
183
|
+
)
|
|
184
|
+
return null;
|
|
167
185
|
const accessSubject: AccessSubject = delegatedUser
|
|
168
186
|
? { type: "user", userId: delegatedUser.id, delegatedByServiceAccountId: serviceAccount.id }
|
|
169
187
|
: { type: "service_account", serviceAccountId: serviceAccount.id };
|
|
@@ -35,6 +35,8 @@ export const invocationAuthorityFromRequest = (authority: RequestAuthority): Inv
|
|
|
35
35
|
delegated_user_id: actor.delegatedUser.id,
|
|
36
36
|
credential_kind: authority.credentialKind,
|
|
37
37
|
...(actor.credentialId ? { credential_id: actor.credentialId } : {}),
|
|
38
|
+
...(actor.oauthClientId ? { oauth_client_id: actor.oauthClientId } : {}),
|
|
39
|
+
...(actor.credentialExpiresAt ? { credential_expires_at: actor.credentialExpiresAt } : {}),
|
|
38
40
|
scopes: [...new Set(authority.scopes)].sort(),
|
|
39
41
|
};
|
|
40
42
|
}
|
|
@@ -49,6 +51,8 @@ export const invocationAuthorityFromRequest = (authority: RequestAuthority): Inv
|
|
|
49
51
|
access_subject_id: actor.serviceAccount.id,
|
|
50
52
|
credential_kind: authority.credentialKind,
|
|
51
53
|
...(actor.credentialId ? { credential_id: actor.credentialId } : {}),
|
|
54
|
+
...(actor.oauthClientId ? { oauth_client_id: actor.oauthClientId } : {}),
|
|
55
|
+
...(actor.credentialExpiresAt ? { credential_expires_at: actor.credentialExpiresAt } : {}),
|
|
52
56
|
scopes: [...new Set(authority.scopes)].sort(),
|
|
53
57
|
};
|
|
54
58
|
};
|