@k2b/cloud 0.41.0 → 0.42.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 +6 -5
- package/src/_internal/bounded-json.ts +14 -2
- package/src/_internal/capabilities.ts +87 -8
- package/src/_internal/capability-reviews.ts +6 -0
- package/src/_internal/capability-timeout.ts +71 -0
- package/src/_internal/define-app.ts +145 -89
- package/src/_internal/http-drain.ts +56 -0
- package/src/_internal/registry-validation.ts +3 -0
- package/src/_internal/runtime-context.ts +1 -0
- package/src/_internal/status-preserving-ssr.ts +2 -0
- package/src/access/PermissionEditor.tsx +16 -17
- package/src/access/PrincipalPicker.tsx +45 -10
- package/src/access/messages.ts +8 -0
- package/src/access/principal-label.ts +18 -0
- package/src/access/ui.ts +1 -0
- package/src/account/EntitySearch.tsx +5 -4
- package/src/ai/action-review.ts +2 -1
- package/src/ai/app-skill-store.ts +1 -1
- package/src/ai/capabilities.ts +21 -3
- package/src/ai/capability-execution.ts +14 -12
- package/src/ai/chat/action-card.ts +7 -1
- package/src/ai/chat/action-messages.ts +14 -0
- package/src/ai/chat/action-question.browser-harness.tsx +12 -0
- package/src/ai/chat/action-question.tsx +202 -22
- package/src/ai/chat/action-text.ts +1 -1
- package/src/ai/chat-tasks.ts +2 -1
- package/src/ai/client/controller.ts +150 -32
- package/src/ai/client/transport.ts +14 -2
- package/src/ai/code-capability-routes.ts +27 -23
- package/src/ai/code-runtime-tools.ts +14 -16
- package/src/ai/code-source-tools.ts +16 -18
- package/src/ai/executor.ts +56 -21
- package/src/ai/fetch-file-tool.ts +67 -76
- package/src/ai/http.ts +1 -0
- package/src/ai/migrate.ts +13 -0
- package/src/ai/open-questions.ts +4 -8
- package/src/ai/projects.ts +1 -2
- package/src/ai/protocol.ts +2 -2
- package/src/ai/recent-resources.ts +12 -5
- package/src/ai/review-content.ts +77 -0
- package/src/ai/routes.ts +69 -31
- package/src/ai/runtime.ts +1 -0
- package/src/ai/skill-tool.ts +17 -6
- package/src/ai/skills-routes.ts +72 -27
- package/src/ai/skills.ts +139 -60
- package/src/ai/store.ts +168 -33
- package/src/ai/types.ts +10 -3
- package/src/ai/usage.ts +2 -2
- package/src/api/admin-core-settings.ts +15 -2
- package/src/api/admin-geo.ts +54 -0
- package/src/api/admin-linux-identities.ts +6 -3
- package/src/api/admin-outgoing-mail.ts +29 -15
- package/src/api/approvals-loader.ts +3 -3
- package/src/api/approvals.ts +51 -0
- package/src/api/apps.ts +4 -2
- package/src/api/capabilities.ts +258 -181
- package/src/api/capability-streams.ts +21 -13
- package/src/api/cli-plugins.ts +4 -1
- package/src/api/geo-glyphs.ts +11 -0
- package/src/api/geo.ts +128 -0
- package/src/api/help.ts +12 -4
- package/src/api/index.ts +5 -0
- package/src/api/mcp.ts +46 -38
- package/src/api/me-panel-consent.ts +8 -5
- package/src/api/search.ts +21 -14
- package/src/api/sync-ops.ts +19 -13
- package/src/api/widgets.ts +42 -18
- package/src/approvals/Approvals.tsx +5 -3
- package/src/browser/CloudResourceSearch.browser-harness.tsx +53 -1
- package/src/browser/CloudResourceSearch.tsx +3 -13
- package/src/browser/PanelLeaveNotice.tsx +18 -0
- package/src/browser/navigation-search.ts +78 -25
- package/src/browser/panel-leave.ts +69 -0
- package/src/browser/panel-messages.ts +12 -0
- package/src/browser/panel-slot.ts +173 -24
- package/src/browser/panel.ts +3 -0
- package/src/browser/resource-search-messages.ts +2 -0
- package/src/capabilities/client.ts +28 -1
- package/src/capabilities/executions.ts +22 -3
- package/src/capabilities/geo-client.ts +57 -0
- package/src/capabilities/response.ts +2 -1
- package/src/capabilities/review-storage.ts +24 -0
- package/src/capabilities/reviews.ts +17 -3
- package/src/capabilities/server.ts +36 -16
- package/src/cli/access.ts +15 -23
- package/src/cli/admin/linux.ts +13 -2
- package/src/cli/admin/outgoing-mail.ts +26 -4
- package/src/cli/capabilities.ts +19 -0
- package/src/config/env.ts +0 -6
- package/src/contracts/app-access.ts +48 -0
- package/src/contracts/app.ts +4 -1
- package/src/contracts/approvals.ts +2 -2
- package/src/contracts/avatar.ts +186 -0
- package/src/contracts/capabilities.ts +148 -54
- package/src/contracts/geo-admin.ts +127 -0
- package/src/contracts/geo-map.ts +19 -0
- package/src/contracts/geo-route.ts +168 -0
- package/src/contracts/geo.ts +248 -0
- package/src/contracts/index.ts +4 -0
- package/src/contracts/outgoing-mail.ts +17 -4
- package/src/contracts/registry.ts +2 -0
- package/src/events/app-access.ts +68 -0
- package/src/events/live-delivery.ts +128 -0
- package/src/events/live.ts +14 -130
- package/src/geo/CloudMap.tsx +95 -0
- package/src/geo/map-messages.ts +17 -0
- package/src/geo/ui.ts +1 -0
- package/src/index.ts +2 -0
- package/src/server/client-ip.ts +36 -0
- package/src/server/index.ts +3 -4
- package/src/server/middleware/app-access.ts +312 -0
- package/src/server/middleware/auth.ts +1 -1
- package/src/server/middleware/invocation.ts +11 -0
- package/src/server/middleware/rate-limit.ts +4 -4
- package/src/server/services/access.ts +13 -9
- package/src/server/services/index.ts +0 -2
- package/src/server/services/services.ts +0 -2
- package/src/services/accounts/posix.ts +21 -7
- package/src/services/allowed-network-fetch.ts +16 -10
- package/src/services/app-access-metrics.ts +24 -0
- package/src/services/app-access.ts +123 -0
- package/src/services/app-discovery.ts +27 -0
- package/src/services/approvals/content.ts +40 -0
- package/src/services/approvals/index.ts +14 -6
- package/src/services/approvals/list.ts +6 -11
- package/src/services/approvals/question.ts +11 -7
- package/src/services/downstream-budget.ts +87 -0
- package/src/services/geo/admin.ts +262 -0
- package/src/services/geo/bucket.ts +67 -0
- package/src/services/geo/cache.ts +57 -0
- package/src/services/geo/costs.ts +5 -0
- package/src/services/geo/errors.ts +41 -0
- package/src/services/geo/fixtures/README.md +17 -0
- package/src/services/geo/fixtures/along.json +355 -0
- package/src/services/geo/fixtures/autocomplete.json +218 -0
- package/src/services/geo/fixtures/geocode.json +129 -0
- package/src/services/geo/fixtures/places.json +355 -0
- package/src/services/geo/fixtures/reverse.json +61 -0
- package/src/services/geo/fixtures/routing.json +57 -0
- package/src/services/geo/geoapify-route.ts +93 -0
- package/src/services/geo/geoapify.ts +281 -0
- package/src/services/geo/ledger.ts +122 -0
- package/src/services/geo/map.ts +160 -0
- package/src/services/geo/migrate.ts +25 -0
- package/src/services/geo/polyline.ts +184 -0
- package/src/services/geo/route-chain.ts +145 -0
- package/src/services/geo/route-costs.ts +9 -0
- package/src/services/geo/route-geometry.ts +39 -0
- package/src/services/geo/route-plan.ts +172 -0
- package/src/services/geo/route-service.ts +433 -0
- package/src/services/geo/service.ts +300 -0
- package/src/services/geo/settings.ts +49 -0
- package/src/services/help/index.ts +16 -7
- package/src/services/help/types.ts +6 -1
- package/src/services/identity/invocation-actor.ts +20 -2
- package/src/services/identity/issue-invocation.ts +67 -0
- package/src/services/identity/key-ring.ts +5 -3
- package/src/services/identity/runtime-config.ts +23 -0
- package/src/services/index.ts +9 -0
- package/src/services/mandates/index.ts +40 -3
- package/src/services/network-security.ts +6 -3
- package/src/services/network-transport.ts +99 -0
- package/src/services/notifications/catalog.ts +3 -2
- package/src/services/notifications/runtime.ts +1 -0
- package/src/services/notifications/web-push-transport.ts +45 -38
- package/src/services/outgoing-mail/dispatcher.ts +6 -1
- package/src/services/outgoing-mail/index.ts +55 -1
- package/src/services/outgoing-mail/messages.ts +82 -3
- package/src/services/outgoing-mail/resolve-host.ts +2 -2
- package/src/services/outgoing-mail/retention.ts +13 -1
- package/src/services/outgoing-mail/store.ts +36 -6
- package/src/services/outgoing-mail/test-send.ts +34 -4
- package/src/services/outgoing-mail/transport.ts +1 -1
- package/src/services/pdf/gotenberg.ts +11 -38
- package/src/services/pdf/template-preview.ts +1 -1
- package/src/services/public-http.ts +28 -40
- package/src/services/settings/app.ts +4 -1
- package/src/services/settings/core-settings.ts +88 -0
- package/src/services/settings/defaults.ts +8 -2
- package/src/services/settings/errors.ts +11 -0
- package/src/services/settings/geo-budget.ts +40 -0
- package/src/services/settings/index.ts +4 -3
- package/src/services/settings/store.ts +25 -17
- package/src/services/weather/geo.ts +29 -111
- package/src/services/weather/index.ts +4 -3
- package/src/services/weather/locations.ts +8 -5
- package/src/services/weather/migrate.ts +1 -0
- package/src/services/weather/settings.ts +0 -16
- package/src/shared/__snapshots__/avatar-art.test.ts.snap +5 -0
- package/src/shared/avatar-art.ts +400 -0
- package/src/shared/index.ts +2 -0
- package/src/shared/network-address.ts +22 -3
- package/src/shared/template-rendering.ts +36 -4
- package/src/shared-text/documents.ts +1 -1
- package/src/shared-text/index.ts +2 -0
- package/src/shared-text/log.ts +51 -3
- package/src/shared-text/snapshots.ts +340 -0
- package/src/ssr/AdminLayout.tsx +4 -2
- package/src/ssr/CloudShell.island.tsx +6 -0
- package/src/ssr/Layout.tsx +65 -15
- package/src/ssr/LayoutHelp.browser-harness.tsx +1 -1
- package/src/ssr/LayoutHelp.tsx +3 -2
- package/src/ssr/PanelRuntime.island.tsx +11 -7
- package/src/ssr/PwaLayout.tsx +2 -2
- package/src/ssr/RailApps.tsx +6 -0
- package/src/ssr/ShellRuntime.island.tsx +23 -7
- package/src/ssr/__snapshots__/Layout.unchanged.render.test.tsx.snap +151 -0
- package/src/ssr/__snapshots__/LayoutHelp.ssr.render.test.tsx.snap +6 -6
- package/src/ssr/admin-navigation.ts +1 -0
- package/src/ssr/app-badges.ts +6 -1
- package/src/ssr/compact-shell.browser-harness.ts +44 -5
- package/src/ssr/help.ts +3 -2
- package/src/ssr/index.ts +1 -1
- package/src/ssr/layout-context.ts +2 -0
- package/src/ssr/mobile-menu-history.ts +4 -0
- package/src/ssr/page-swap-fixture/Column.island.tsx +15 -0
- package/src/ssr/page-swap-fixture/ProviderProbe.island.tsx +31 -0
- package/src/ssr/page-swap.browser-harness.tsx +177 -0
- package/src/ssr/page-swap.ts +28 -0
- package/src/ssr/page-trail-runtime.ts +32 -13
- package/src/ssr/panel-consent.ts +2 -2
- package/src/ssr/panel-slot.browser-harness.tsx +3 -0
- package/src/ssr/platform-messages.ts +8 -0
- package/src/ssr/runtime.ts +17 -0
- package/src/ssr/shell-data.tsx +10 -2
- package/src/styles/global.css +70 -6
- package/src/server/services/geo.ts +0 -215
package/src/events/live.ts
CHANGED
|
@@ -1,72 +1,18 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import { type ServerWebSocket, type SQL, sql } from "bun";
|
|
1
|
+
import type { ServerWebSocket } from "bun";
|
|
3
2
|
import { type Context, Hono } from "hono";
|
|
4
3
|
import { upgradeWebSocket } from "hono/bun";
|
|
5
4
|
import type { z } from "zod";
|
|
6
|
-
import {
|
|
5
|
+
import { guardAppAccessSocket } from "../server/middleware/app-access";
|
|
7
6
|
import { type AuthContext, auth } from "../server/middleware/auth";
|
|
8
7
|
import { rateLimit } from "../server/middleware/rate-limit";
|
|
9
|
-
import { logger } from "../services/logging";
|
|
10
|
-
import { createPgOutbox } from "../services/outbox";
|
|
11
8
|
import * as settings from "../services/settings";
|
|
12
9
|
import { publicCloudOrigin } from "../shared/app-url";
|
|
13
|
-
import {
|
|
14
|
-
|
|
15
|
-
type LiveChannel,
|
|
16
|
-
type LiveConnectionHandle,
|
|
17
|
-
type LiveEngine,
|
|
18
|
-
type LiveEnvelope,
|
|
19
|
-
type LiveViewer,
|
|
20
|
-
} from "./live-engine";
|
|
10
|
+
import { defineLivePublisher, liveTopic, startLiveDelivery } from "./live-delivery";
|
|
11
|
+
import { createLiveEngine, type LiveChannel, type LiveConnectionHandle, type LiveEngine, type LiveViewer } from "./live-engine";
|
|
21
12
|
|
|
13
|
+
export { liveOutbox, liveTopicConfig } from "./live-delivery";
|
|
22
14
|
export type { LiveChannel, LiveViewer } from "./live-engine";
|
|
23
15
|
|
|
24
|
-
const RECONCILE_INTERVAL_MS = 1_000;
|
|
25
|
-
const STALE_CHECK_INTERVAL_MS = 60_000;
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* The live topic of one application. Frozen: Sync rejects a declaration that
|
|
29
|
-
* differs from the existing stream, so two releases with different values
|
|
30
|
-
* would break each other during a rollout. A change ships under a new id.
|
|
31
|
-
*/
|
|
32
|
-
export const liveTopicConfig = (appId: string) =>
|
|
33
|
-
({
|
|
34
|
-
id: `cloud:live:${appId}`,
|
|
35
|
-
owner: "cloud",
|
|
36
|
-
retention: { maxAgeMs: 24 * 3_600_000, maxBytes: 64 * 1024 ** 2 },
|
|
37
|
-
maxPayloadBytes: 40 * 1024,
|
|
38
|
-
deadLetterRetention: { maxBytes: 1024 ** 2 },
|
|
39
|
-
}) satisfies TopicConfig;
|
|
40
|
-
|
|
41
|
-
type LiveOutboxRow = { id: string; attempts: number; ordering_key: string; payload: LiveEnvelope };
|
|
42
|
-
|
|
43
|
-
const log = logger("events:live");
|
|
44
|
-
|
|
45
|
-
const liveTopics = lazySync((sync) => {
|
|
46
|
-
const topics = new Map<string, Topic<LiveEnvelope>>();
|
|
47
|
-
return (appId: string): Topic<LiveEnvelope> => {
|
|
48
|
-
const existing = topics.get(appId);
|
|
49
|
-
if (existing) return existing;
|
|
50
|
-
const topic = sync.topic<LiveEnvelope>(liveTopicConfig(appId));
|
|
51
|
-
topics.set(appId, topic);
|
|
52
|
-
return topic;
|
|
53
|
-
};
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
/** Dispatcher of one application's live rows: ordered per key, deleted once published. */
|
|
57
|
-
export const liveOutbox = (appId: string, publish: (row: LiveOutboxRow) => Promise<unknown>) =>
|
|
58
|
-
createPgOutbox<LiveOutboxRow>({
|
|
59
|
-
table: "events.outbox",
|
|
60
|
-
name: `events:live:${appId}`,
|
|
61
|
-
where: { kind: "live", app_id: appId },
|
|
62
|
-
orderBy: "ordering_key",
|
|
63
|
-
sequence: "seq",
|
|
64
|
-
reconcileIntervalMs: RECONCILE_INTERVAL_MS,
|
|
65
|
-
publish,
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
/** Applications whose live updates this process defines, with the wake of their running dispatcher. */
|
|
69
|
-
const dispatchers = new Map<string, (() => void) | null>();
|
|
70
16
|
/** Applications whose live socket this process mounts; only the started application may be among them. */
|
|
71
17
|
const served = new Set<string>();
|
|
72
18
|
/** Live sockets served by this process; they close when the application stops. */
|
|
@@ -80,23 +26,7 @@ export const stopLiveEngines = () => {
|
|
|
80
26
|
for (const engine of engines) engine.stop();
|
|
81
27
|
};
|
|
82
28
|
|
|
83
|
-
|
|
84
|
-
const [row] = await sql<{ count: number; oldest_seconds: number | null }[]>`
|
|
85
|
-
SELECT COUNT(*)::int AS count, EXTRACT(EPOCH FROM now() - MIN(created_at))::int AS oldest_seconds
|
|
86
|
-
FROM events.outbox
|
|
87
|
-
WHERE kind = 'live' AND app_id = ${appId} AND created_at < now() - interval '60 seconds'
|
|
88
|
-
`;
|
|
89
|
-
if (row && row.count > 0) log.warn("Live updates wait to be published", { appId, count: row.count, oldestSeconds: row.oldest_seconds });
|
|
90
|
-
};
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
* Publishes the rows of the started application's live definitions. `app.start()`
|
|
94
|
-
* calls it with the started application's ID. A definition of another
|
|
95
|
-
* application only reads that application's cursor, and its rows are published
|
|
96
|
-
* by that application. It does nothing without a definition of the started
|
|
97
|
-
* application, and fails when this process mounts the socket of another
|
|
98
|
-
* application or Core has not created the outbox yet.
|
|
99
|
-
*/
|
|
29
|
+
/** Starts delivery only for the application whose live sockets this process serves. */
|
|
100
30
|
export const startLiveOutbox = async (appId: string): Promise<(() => Promise<void>) | null> => {
|
|
101
31
|
const foreign = [...served].filter((other) => other !== appId);
|
|
102
32
|
if (foreign.length > 0) {
|
|
@@ -104,35 +34,16 @@ export const startLiveOutbox = async (appId: string): Promise<(() => Promise<voi
|
|
|
104
34
|
`This process mounts the live socket of "${foreign.join('", "')}", but starts "${appId}". Use the ID from the application's declaration.`,
|
|
105
35
|
);
|
|
106
36
|
}
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
SELECT to_regprocedure('events.enqueue(uuid,text,text,text,jsonb,text)') IS NOT NULL AS ready
|
|
110
|
-
`;
|
|
111
|
-
if (!installed?.ready) {
|
|
112
|
-
throw new Error(
|
|
113
|
-
`"${appId}" writes live updates to events.outbox, which does not exist. Update Cloud Core first: its migration creates the outbox.`,
|
|
114
|
-
);
|
|
115
|
-
}
|
|
37
|
+
const stopDelivery = await startLiveDelivery(appId);
|
|
38
|
+
if (!stopDelivery) return null;
|
|
116
39
|
// Delivery starts again: the engines stopped before give way to new ones.
|
|
117
40
|
if (stopped) {
|
|
118
41
|
engines.clear();
|
|
119
42
|
stopped = false;
|
|
120
43
|
}
|
|
121
|
-
const topic = liveTopics()(appId);
|
|
122
|
-
const outbox = liveOutbox(appId, (row) => topic.publish({ data: row.payload, orderingKey: row.ordering_key, idempotencyKey: row.id }));
|
|
123
|
-
outbox.start();
|
|
124
|
-
dispatchers.set(appId, () => void outbox.notify());
|
|
125
|
-
const staleCheck = setInterval(() => {
|
|
126
|
-
logStaleRows(appId).catch((error) =>
|
|
127
|
-
log.warn("Live outbox check failed", { appId, error: error instanceof Error ? error.message : String(error) }),
|
|
128
|
-
);
|
|
129
|
-
}, STALE_CHECK_INTERVAL_MS);
|
|
130
|
-
staleCheck.unref();
|
|
131
44
|
return async () => {
|
|
132
45
|
stopLiveEngines();
|
|
133
|
-
|
|
134
|
-
dispatchers.set(appId, null);
|
|
135
|
-
await outbox.stop();
|
|
46
|
+
await stopDelivery();
|
|
136
47
|
};
|
|
137
48
|
};
|
|
138
49
|
|
|
@@ -217,36 +128,9 @@ const refusalOf = async (c: Context<AuthContext>): Promise<Refusal | null> => {
|
|
|
217
128
|
* write updates; only the application itself publishes and serves them.
|
|
218
129
|
*/
|
|
219
130
|
export const defineLive = <const Event extends z.ZodType>(definition: { appId: string; event: Event }) => {
|
|
220
|
-
const { appId
|
|
221
|
-
if (!dispatchers.has(appId)) dispatchers.set(appId, null);
|
|
131
|
+
const { appId } = definition;
|
|
222
132
|
return {
|
|
223
|
-
|
|
224
|
-
* Writes one update in `tx`, the transaction that makes the change: a
|
|
225
|
-
* rollback writes nothing, a commit publishes it at least once. Data above
|
|
226
|
-
* 32 KiB becomes a reload hint for `key`; `data` must not hold anything a
|
|
227
|
-
* reader of `key` may not see. `access: true` announces that who may read
|
|
228
|
-
* `key` changed, in a row of its own before the data: every replica checks
|
|
229
|
-
* its subscribers of `key` again before it delivers the data, and its
|
|
230
|
-
* collections right after.
|
|
231
|
-
*/
|
|
232
|
-
publish: async (
|
|
233
|
-
tx: SQL,
|
|
234
|
-
input: { key: string; data: z.input<Event>; access?: true } | { key: string; data?: undefined; access: true },
|
|
235
|
-
): Promise<void> => {
|
|
236
|
-
// Store the JSON form of the input, and validate exactly that form here:
|
|
237
|
-
// the browser parses it, and data that does not survive JSON fails this write.
|
|
238
|
-
const data: unknown = input.data === undefined ? undefined : JSON.parse(JSON.stringify(input.data));
|
|
239
|
-
if (data !== undefined) event.parse(data);
|
|
240
|
-
const enqueue = (envelope: LiveEnvelope) =>
|
|
241
|
-
tx`SELECT events.enqueue(${crypto.randomUUID()}::uuid, ${appId}, 'live', ${input.key}, ${JSON.stringify(envelope)}::text::jsonb)`;
|
|
242
|
-
// The access change is a row of its own, so data too large for the outbox cannot drop it.
|
|
243
|
-
if (input.access) await enqueue({ v: 1, k: input.key, a: true });
|
|
244
|
-
if (data !== undefined) await enqueue({ v: 1, k: input.key, d: data });
|
|
245
|
-
},
|
|
246
|
-
/** Publishes committed updates now instead of within the next second. Call it after the commit. */
|
|
247
|
-
wake: (): void => dispatchers.get(appId)?.(),
|
|
248
|
-
/** The topic head. Read it before loading the snapshot it belongs to. */
|
|
249
|
-
cursor: (): Promise<string> => liveTopics()(appId).head(),
|
|
133
|
+
...defineLivePublisher(definition),
|
|
250
134
|
/**
|
|
251
135
|
* The live socket with these channels. Mount it once, at `/api/<app>/live`,
|
|
252
136
|
* in the application's own process: `app.start()` of another application fails.
|
|
@@ -261,7 +145,7 @@ export const defineLive = <const Event extends z.ZodType>(definition: { appId: s
|
|
|
261
145
|
let engine: LiveEngine | null = null;
|
|
262
146
|
const serve = (): LiveEngine => {
|
|
263
147
|
if (engine && engines.has(engine)) return engine;
|
|
264
|
-
engine = createLiveEngine({ appId, topic: () =>
|
|
148
|
+
engine = createLiveEngine({ appId, topic: () => liveTopic(appId), channels });
|
|
265
149
|
engines.add(engine);
|
|
266
150
|
// A socket that opens while the application stops closes with 1012 and starts nothing.
|
|
267
151
|
if (stopped) engine.stop();
|
|
@@ -284,13 +168,13 @@ export const defineLive = <const Event extends z.ZodType>(definition: { appId: s
|
|
|
284
168
|
const viewer = viewerOf(c);
|
|
285
169
|
const revalidate = revalidator(c);
|
|
286
170
|
let connection: LiveConnectionHandle | null = null;
|
|
287
|
-
return {
|
|
171
|
+
return guardAppAccessSocket(c, appId, {
|
|
288
172
|
onOpen: (_event, ws) => {
|
|
289
173
|
connection = serve().open(ws.raw as ServerWebSocket<unknown>, viewer, revalidate);
|
|
290
174
|
},
|
|
291
175
|
onMessage: (message) => connection?.message(message.data),
|
|
292
176
|
onClose: () => connection?.closed(),
|
|
293
|
-
};
|
|
177
|
+
});
|
|
294
178
|
}),
|
|
295
179
|
);
|
|
296
180
|
},
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { MapView, MapViewFrame, type MapViewProps, mapAttribution, useLocale } from "@k2b/ui";
|
|
2
|
+
import { createSignal, For, onCleanup, Show, splitProps } from "solid-js";
|
|
3
|
+
import type { z } from "zod";
|
|
4
|
+
import { invokeCapabilityWithDataSchema } from "../capabilities/client";
|
|
5
|
+
import { type GeoAvailabilityDataSchema, geoContract } from "../contracts/geo";
|
|
6
|
+
import { mapMessages } from "./map-messages";
|
|
7
|
+
|
|
8
|
+
export type CloudMapProps = Omit<MapViewProps, "style" | "attribution"> & {
|
|
9
|
+
/** The authorized SSR snapshot keeps the first browser render identical. */
|
|
10
|
+
availability?: z.infer<typeof GeoAvailabilityDataSchema>;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
/** Cloud's same-origin, budgeted map. Domain lists and recovery actions belong to the host. */
|
|
14
|
+
export function CloudMap(props: CloudMapProps) {
|
|
15
|
+
const [local, map] = splitProps(props, ["availability", "onStateChange", "notice"]);
|
|
16
|
+
const locale = useLocale();
|
|
17
|
+
const [recovery, setRecovery] = createSignal("");
|
|
18
|
+
const controller = new AbortController();
|
|
19
|
+
onCleanup(() => controller.abort());
|
|
20
|
+
let generation = 0;
|
|
21
|
+
const onStateChange: NonNullable<MapViewProps["onStateChange"]> = (state) => {
|
|
22
|
+
local.onStateChange?.(state);
|
|
23
|
+
const current = ++generation;
|
|
24
|
+
if (state !== "error") {
|
|
25
|
+
setRecovery("");
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
void invokeCapabilityWithDataSchema(
|
|
29
|
+
{ kind: "query", appId: "core", capabilityId: geoContract.availability.id, input: {}, signal: controller.signal },
|
|
30
|
+
geoContract.availability.data,
|
|
31
|
+
)
|
|
32
|
+
.then((result) => {
|
|
33
|
+
if (!result.ok || controller.signal.aborted || current !== generation) return;
|
|
34
|
+
const availability = result.data.data;
|
|
35
|
+
const t = mapMessages.resolve([locale()]).t;
|
|
36
|
+
if (!availability.configured) setRecovery(t.notConfigured);
|
|
37
|
+
else if (availability.cloud.exhausted || availability.person.exhausted) {
|
|
38
|
+
const exhausted = availability.cloud.exhausted ? availability.cloud : availability.person;
|
|
39
|
+
const reset = new Intl.DateTimeFormat(locale(), { dateStyle: "short", timeStyle: "short" }).format(new Date(exhausted.resetsAt));
|
|
40
|
+
setRecovery(availability.cloud.exhausted ? t.cloudBudget({ reset }) : t.personBudget({ reset }));
|
|
41
|
+
}
|
|
42
|
+
})
|
|
43
|
+
.catch(() => {
|
|
44
|
+
/* MapView already displays its retry state. */
|
|
45
|
+
});
|
|
46
|
+
};
|
|
47
|
+
const language = () => encodeURIComponent(new Intl.Locale(locale()).language);
|
|
48
|
+
const enabled = () =>
|
|
49
|
+
!local.availability || (local.availability.configured && !local.availability.cloud.exhausted && !local.availability.person.exhausted);
|
|
50
|
+
return (
|
|
51
|
+
<Show
|
|
52
|
+
when={enabled()}
|
|
53
|
+
fallback={
|
|
54
|
+
<div class="relative" style={{ height: `${map.height}px` }}>
|
|
55
|
+
<MapViewFrame {...map} attribution={<CloudMapAttribution />} />
|
|
56
|
+
<Show when={local.notice}>
|
|
57
|
+
<div class="k2b-map-view__messages">
|
|
58
|
+
<div class="k2b-map-view__notice">{local.notice}</div>
|
|
59
|
+
</div>
|
|
60
|
+
</Show>
|
|
61
|
+
</div>
|
|
62
|
+
}
|
|
63
|
+
>
|
|
64
|
+
<MapView
|
|
65
|
+
{...map}
|
|
66
|
+
onStateChange={onStateChange}
|
|
67
|
+
notice={
|
|
68
|
+
<>
|
|
69
|
+
{local.notice}
|
|
70
|
+
<Show when={recovery()}>
|
|
71
|
+
<span role="status">{recovery()}</span>
|
|
72
|
+
</Show>
|
|
73
|
+
</>
|
|
74
|
+
}
|
|
75
|
+
style={{ light: `/api/geo/style?theme=light&locale=${language()}`, dark: `/api/geo/style?theme=dark&locale=${language()}` }}
|
|
76
|
+
attribution={<CloudMapAttribution />}
|
|
77
|
+
/>
|
|
78
|
+
</Show>
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function CloudMapAttribution() {
|
|
83
|
+
return (
|
|
84
|
+
<For each={mapAttribution}>
|
|
85
|
+
{(item, index) => (
|
|
86
|
+
<>
|
|
87
|
+
{index() > 0 && " · "}
|
|
88
|
+
<a href={item.href} target="_blank" rel="noopener noreferrer">
|
|
89
|
+
{item.label}
|
|
90
|
+
</a>
|
|
91
|
+
</>
|
|
92
|
+
)}
|
|
93
|
+
</For>
|
|
94
|
+
);
|
|
95
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { i18n } from "@k2b/stdlib";
|
|
2
|
+
|
|
3
|
+
export const mapMessages = i18n.define({
|
|
4
|
+
baseLocale: "en",
|
|
5
|
+
messages: {
|
|
6
|
+
en: {
|
|
7
|
+
notConfigured: "Maps are not set up. The saved places remain available.",
|
|
8
|
+
cloudBudget: ({ reset }: { reset: string }) => `The Cloud map budget is used up until ${reset}.`,
|
|
9
|
+
personBudget: ({ reset }: { reset: string }) => `Your map budget is used up until ${reset}.`,
|
|
10
|
+
},
|
|
11
|
+
de: {
|
|
12
|
+
notConfigured: "Karten sind nicht eingerichtet. Die gespeicherten Orte bleiben verfügbar.",
|
|
13
|
+
cloudBudget: ({ reset }) => `Das Kartenbudget der Cloud ist bis ${reset} aufgebraucht.`,
|
|
14
|
+
personBudget: ({ reset }) => `Dein Kartenbudget ist bis ${reset} aufgebraucht.`,
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
});
|
package/src/geo/ui.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { CloudMap, CloudMapAttribution, type CloudMapProps } from "./CloudMap";
|
package/src/index.ts
CHANGED
|
@@ -36,6 +36,8 @@ export type { RuntimeCompatibilityIssue } from "./_internal/runtime-compatibilit
|
|
|
36
36
|
export { assessRuntimeCompatibility } from "./_internal/runtime-compatibility";
|
|
37
37
|
export { buildRuntimeFromRegistry } from "./_internal/runtime-context";
|
|
38
38
|
export { type AppSkillDefinition, skill } from "./ai/app-skills";
|
|
39
|
+
export type { AppAccessDeclaration, AppAccessRule, FixedAppAccessRule } from "./contracts/app-access";
|
|
40
|
+
export { appAccess } from "./contracts/app-access";
|
|
39
41
|
export { defineCapabilities } from "./contracts/capabilities";
|
|
40
42
|
export type {
|
|
41
43
|
AnyBoundNotificationDefinition,
|
package/src/server/client-ip.ts
CHANGED
|
@@ -1,5 +1,41 @@
|
|
|
1
|
+
import { isIP } from "node:net";
|
|
1
2
|
import type { Context } from "hono";
|
|
2
3
|
|
|
3
4
|
// The gateway (packages/gateway/src/client-address.ts) resolves this address using GATEWAY_TRUSTED_PROXIES.
|
|
4
5
|
export const clientIp = (c: Context): string =>
|
|
5
6
|
c.req.header("x-forwarded-for")?.split(",")[0]?.trim() ?? c.req.header("x-real-ip") ?? "unknown";
|
|
7
|
+
|
|
8
|
+
/** Stable network key for IP budgets: IPv4 addresses and IPv6 /64 networks. */
|
|
9
|
+
export const clientIpBucket = (ip: string): string => {
|
|
10
|
+
if (isIP(ip) !== 6) return `ip:${ip}`;
|
|
11
|
+
|
|
12
|
+
let address = ip.split("%", 1)[0]!;
|
|
13
|
+
if (address.includes(".")) {
|
|
14
|
+
const tail = address.lastIndexOf(":") + 1;
|
|
15
|
+
const value = address
|
|
16
|
+
.slice(tail)
|
|
17
|
+
.split(".")
|
|
18
|
+
.reduce((value, octet) => value * 256 + Number(octet), 0);
|
|
19
|
+
address = `${address.slice(0, tail)}${(value >>> 16).toString(16)}:${(value & 0xffff).toString(16)}`;
|
|
20
|
+
}
|
|
21
|
+
const [head = "", tail = ""] = address.split("::");
|
|
22
|
+
const left = head ? head.split(":") : [];
|
|
23
|
+
const right = tail ? tail.split(":") : [];
|
|
24
|
+
const groups = (address.includes("::") ? [...left, ...Array<string>(8 - left.length - right.length).fill("0"), ...right] : left).map(
|
|
25
|
+
(group) => Number.parseInt(group, 16),
|
|
26
|
+
);
|
|
27
|
+
const ipv4 = (high: number, low: number) => `ip:${[high >> 8, high & 255, low >> 8, low & 255].join(".")}`;
|
|
28
|
+
const [g0, g1, g2, g3, g4, g5, g6 = 0, g7 = 0] = groups;
|
|
29
|
+
if (g0 === 0 && g1 === 0 && g2 === 0 && g3 === 0 && g4 === 0 && g5 === 0xffff) return ipv4(g6, g7);
|
|
30
|
+
// NAT64 (64:ff9b::/96) and Teredo (2001::/32, inverted) carry the client's IPv4 address in the low 32 bits;
|
|
31
|
+
// their /64 names a translator or relay that unrelated clients share.
|
|
32
|
+
if (g0 === 0x64 && g1 === 0xff9b && g2 === 0 && g3 === 0 && g4 === 0 && g5 === 0) return ipv4(g6, g7);
|
|
33
|
+
if (g0 === 0x2001 && g1 === 0) return ipv4(g6 ^ 0xffff, g7 ^ 0xffff);
|
|
34
|
+
// 6to4 (2002::/16) gives one IPv4 holder a whole /48; key it by that IPv4 address instead of 65,536 /64 budgets.
|
|
35
|
+
if (g0 === 0x2002) return ipv4(g1 ?? 0, g2 ?? 0);
|
|
36
|
+
// One subscriber usually has at least a /64; rotating host addresses must not reset the budget.
|
|
37
|
+
return `ip6:${groups
|
|
38
|
+
.slice(0, 4)
|
|
39
|
+
.map((group) => group.toString(16))
|
|
40
|
+
.join(":")}::/64`;
|
|
41
|
+
};
|
package/src/server/index.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { buildMetadata } from "../_internal/build-metadata";
|
|
2
2
|
export { loadPendingApprovals } from "../api/approvals-loader";
|
|
3
|
+
export { geoClient } from "../capabilities/geo-client";
|
|
3
4
|
export { expectUserBackedActor, getUserBackedActor, isAppSessionActor, userFromActor } from "./actor";
|
|
4
5
|
export type { ApiErrorBody, ApiErrorResponse, ApiErrorStatus } from "./api";
|
|
5
6
|
export { api, respond, respondMessage } from "./api";
|
|
@@ -7,6 +8,7 @@ export type { CreateApiClientConfig } from "./api-client";
|
|
|
7
8
|
export { api as apiClient } from "./api-client";
|
|
8
9
|
export { appAssetPath } from "./app-assets";
|
|
9
10
|
export type { AppContext } from "./app-context";
|
|
11
|
+
export { clientIp, clientIpBucket } from "./client-ip";
|
|
10
12
|
export { defineHelp, defineHelpCollection, type HelpCollection, type HelpDefinition, type HelpDefinitionDocument } from "./help";
|
|
11
13
|
export { DEFAULT_LOCALE, getLocale, LOCALE_COOKIE, LOCALE_HEADER, locale, preferredLocale, resolveLocale } from "./locale";
|
|
12
14
|
export type {
|
|
@@ -39,6 +41,7 @@ export {
|
|
|
39
41
|
v,
|
|
40
42
|
validator,
|
|
41
43
|
} from "./middleware";
|
|
44
|
+
export { guardAppAccessSocket } from "./middleware/app-access";
|
|
42
45
|
export { RateLimitError, type RateLimiter, type RateLimiterConfig, type RateLimitResult, ratelimit } from "./ratelimit";
|
|
43
46
|
export { REQUEST_IDLE_TIMEOUT_MAX_SECONDS, REQUEST_IDLE_TIMEOUT_SECONDS, setRequestTimeout } from "./request-timeout";
|
|
44
47
|
export type {
|
|
@@ -49,8 +52,6 @@ export type {
|
|
|
49
52
|
AccessUser,
|
|
50
53
|
AccessUserSource,
|
|
51
54
|
EffectiveGroup,
|
|
52
|
-
GeoPlace,
|
|
53
|
-
GeoService,
|
|
54
55
|
PageParams,
|
|
55
56
|
Paginated,
|
|
56
57
|
PermissionLevel,
|
|
@@ -72,8 +73,6 @@ export {
|
|
|
72
73
|
fail,
|
|
73
74
|
freeipa,
|
|
74
75
|
generatePassword,
|
|
75
|
-
geo,
|
|
76
|
-
geoService,
|
|
77
76
|
getAccess,
|
|
78
77
|
getEffectiveGroupIds,
|
|
79
78
|
getEffectiveGroups,
|