@k2b/cloud 0.47.0 → 0.49.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 +3 -3
- package/src/_internal/app-access-manager-sql.ts +10 -0
- package/src/_internal/app-access-resources.ts +2 -1
- package/src/_internal/app-capabilities.ts +73 -0
- package/src/_internal/app-event-catalog.ts +1 -1
- package/src/_internal/app-queries.ts +1 -1
- package/src/_internal/application-package.ts +2 -1
- package/src/_internal/bounded-json.ts +7 -1
- package/src/_internal/capabilities.ts +25 -2
- package/src/_internal/define-app.ts +33 -2
- package/src/_internal/heartbeat.ts +2 -0
- package/src/_internal/nats-connection.ts +10 -9
- package/src/_internal/process-sync.ts +6 -6
- package/src/_internal/public-surface.ts +76 -0
- package/src/_internal/purpose-name.ts +7 -0
- package/src/_internal/registry-validation.ts +1 -0
- package/src/_internal/registry.ts +49 -3
- package/src/_internal/workload-credential.ts +2 -1
- package/src/ai/app-skill-store.ts +2 -1
- package/src/ai/app-skills.ts +2 -1
- package/src/ai/approvals.ts +2 -1
- package/src/ai/chat-tasks.ts +2 -1
- package/src/ai/credentials.ts +2 -1
- package/src/ai/dictation-runtime.ts +1 -1
- package/src/ai/dictations.ts +1 -1
- package/src/ai/files-store.ts +2 -1
- package/src/ai/inference-calls.ts +2 -1
- package/src/ai/map-store.ts +1 -1
- package/src/ai/map-tool.ts +1 -1
- package/src/ai/memories.ts +1 -1
- package/src/ai/memory-learning-runs.ts +1 -1
- package/src/ai/memory-learning.ts +1 -1
- package/src/ai/memory-workflow-evidence.ts +1 -1
- package/src/ai/message-queue.ts +2 -1
- package/src/ai/migrate.ts +1 -1
- package/src/ai/model-access-migrate.ts +1 -1
- package/src/ai/model-access.ts +1 -1
- package/src/ai/model-catalog.ts +1 -1
- package/src/ai/model-delete.ts +1 -1
- package/src/ai/model-usage.ts +2 -1
- package/src/ai/open-questions.ts +1 -1
- package/src/ai/openai-api-slots.ts +2 -1
- package/src/ai/prefs.ts +2 -1
- package/src/ai/projects.ts +2 -1
- package/src/ai/provider-connections.ts +1 -1
- package/src/ai/quota-limits.ts +2 -1
- package/src/ai/quota-provider.ts +2 -1
- package/src/ai/quota-report.ts +2 -1
- package/src/ai/quotas-migrate.ts +2 -1
- package/src/ai/quotas.ts +2 -1
- package/src/ai/request-headers.ts +2 -1
- package/src/ai/review-content.ts +2 -1
- package/src/ai/skill-search.ts +2 -1
- package/src/ai/skills.ts +2 -1
- package/src/ai/store.ts +2 -1
- package/src/ai/structured-runs.ts +2 -1
- package/src/ai/tool-audit.ts +2 -1
- package/src/ai/turn-timing.ts +1 -1
- package/src/ai/usage.ts +2 -1
- package/src/ai/web-provenance.ts +2 -1
- package/src/api/admin-ai-quotas.ts +1 -1
- package/src/api/admin-ai.ts +1 -1
- package/src/api/admin-app-domains.ts +62 -0
- package/src/api/admin-app-keys.ts +47 -3
- package/src/api/admin-core-settings.ts +1 -1
- package/src/api/admin-outgoing-mail.ts +1 -1
- package/src/api/app-bootstrap.ts +153 -0
- package/src/api/app-events.ts +1 -1
- package/src/api/auth.ts +1 -1
- package/src/api/authenticate-app-request.ts +129 -0
- package/src/api/capabilities.ts +87 -6
- package/src/api/index.ts +7 -0
- package/src/api/me.ts +1 -1
- package/src/api/search.bench.ts +1 -1
- package/src/app-pwa/service.ts +2 -1
- package/src/app-pwa/transaction.ts +4 -2
- package/src/capabilities/claims.ts +2 -1
- package/src/capabilities/executions.ts +8 -4
- package/src/capabilities/geo-client.ts +1 -1
- package/src/capabilities/migrate.ts +3 -1
- package/src/capabilities/resource-readers.ts +2 -2
- package/src/capabilities/response.ts +2 -1
- package/src/capabilities/server-invoke.ts +73 -20
- package/src/capabilities/server.ts +2 -1
- package/src/cli/admin/app-domains.ts +140 -0
- package/src/cli/admin/app-keys.ts +25 -3
- package/src/cli/admin/index.ts +107 -88
- package/src/config/env.ts +10 -4
- package/src/contracts/app-domains.ts +91 -0
- package/src/contracts/app-status.ts +104 -0
- package/src/contracts/capabilities.ts +37 -2
- package/src/contracts/index.ts +1 -0
- package/src/contracts/outgoing-mail.ts +1 -1
- package/src/contracts/registry.ts +2 -0
- package/src/events/app-event-delivery.ts +1 -1
- package/src/events/live-delivery.ts +2 -1
- package/src/index.ts +1 -0
- package/src/runtime/bootstrap-client.ts +195 -0
- package/src/runtime/bootstrap-protocol.ts +67 -0
- package/src/runtime/clients.ts +6 -2
- package/src/runtime/connections.ts +9 -0
- package/src/server/app-key.ts +14 -0
- package/src/server/index.ts +2 -0
- package/src/server/ratelimit.ts +1 -1
- package/src/server/services/access.ts +2 -1
- package/src/services/account-category-policy.ts +1 -1
- package/src/services/account-lifecycle/audit.ts +1 -1
- package/src/services/account-lifecycle/index.ts +1 -1
- package/src/services/account-lifecycle/ipa-backfill.ts +2 -1
- package/src/services/accounts/app.ts +1 -1
- package/src/services/accounts/avatar.ts +1 -1
- package/src/services/accounts/base-group.ts +1 -1
- package/src/services/accounts/directory-shares.ts +1 -1
- package/src/services/accounts/duplicate-emails.ts +1 -1
- package/src/services/accounts/entities.ts +2 -1
- package/src/services/accounts/group-sql.ts +2 -1
- package/src/services/accounts/groups.ts +1 -1
- package/src/services/accounts/identities.ts +13 -11
- package/src/services/accounts/ipa-data.ts +1 -1
- package/src/services/accounts/local-groups.ts +2 -1
- package/src/services/accounts/machine-group-management.ts +2 -1
- package/src/services/accounts/posix.ts +13 -12
- package/src/services/accounts/request-policy.ts +2 -1
- package/src/services/accounts/switching.ts +1 -1
- package/src/services/accounts/users.ts +2 -1
- package/src/services/announcements/index.ts +1 -1
- package/src/services/app-access-management.ts +3 -9
- package/src/services/app-access.ts +2 -1
- package/src/services/app-admin-facts.ts +160 -0
- package/src/services/app-approval.ts +34 -30
- package/src/services/app-bootstrap.ts +217 -0
- package/src/services/app-details.ts +3 -2
- package/src/services/app-domain-check.ts +152 -0
- package/src/services/app-domains.ts +172 -0
- package/src/services/app-events/accept.ts +5 -5
- package/src/services/app-events/admin.ts +15 -15
- package/src/services/app-events/declarations.ts +6 -3
- package/src/services/app-events/deliver.ts +6 -6
- package/src/services/app-events.ts +3 -2
- package/src/services/app-keys.ts +45 -11
- package/src/services/approvals/content.ts +1 -1
- package/src/services/approvals/index.ts +2 -1
- package/src/services/approvals/list.ts +1 -1
- package/src/services/approvals/migrate.ts +1 -1
- package/src/services/audit/index.ts +2 -1
- package/src/services/auth-flows/ipa.ts +1 -1
- package/src/services/auth-flows/magic-link.ts +1 -1
- package/src/services/auth-flows/password-reset.ts +1 -1
- package/src/services/auth-flows/proxy-return.ts +1 -1
- package/src/services/downstream-budget.ts +2 -2
- package/src/services/geo/admin.ts +13 -12
- package/src/services/geo/identity.ts +46 -0
- package/src/services/geo/ledger.ts +118 -106
- package/src/services/geo/migrate.ts +2 -1
- package/src/services/geo/route-chain.ts +3 -3
- package/src/services/geo/service.ts +4 -24
- package/src/services/geoip/index.ts +5 -5
- package/src/services/geoip/store.ts +24 -21
- package/src/services/help/index.ts +2 -1
- package/src/services/help/store.ts +2 -1
- package/src/services/identity/app-request.ts +44 -0
- package/src/services/identity/constants.ts +1 -0
- package/src/services/identity/index.ts +1 -0
- package/src/services/identity/invocation-actor.ts +2 -1
- package/src/services/identity/invocation-token.ts +112 -0
- package/src/services/identity/key-ring.ts +2 -2
- package/src/services/identity/resource-readers.ts +1 -1
- package/src/services/identity/routes.ts +11 -3
- package/src/services/identity/runtime-config.ts +14 -3
- package/src/services/identity/workload-auth.ts +2 -0
- package/src/services/identity/workload-runtime.ts +3 -3
- package/src/services/index.ts +4 -0
- package/src/services/ipa/groups.ts +1 -1
- package/src/services/ipa/profile.ts +1 -1
- package/src/services/ipa/search.ts +1 -1
- package/src/services/ipa/sync.ts +1 -1
- package/src/services/ipa/users.ts +1 -1
- package/src/services/known-browsers.ts +2 -1
- package/src/services/legal-consent.ts +1 -1
- package/src/services/links/index.ts +16 -0
- package/src/services/links/runtime.ts +44 -0
- package/src/services/links/store.ts +105 -0
- package/src/services/logging/index.ts +13 -8
- package/src/services/logging/redaction.ts +6 -1
- package/src/services/logging/trace.ts +9 -9
- package/src/services/logging/web-vitals.ts +1 -1
- package/src/services/mandates/index.ts +2 -1
- package/src/services/notifications/batches.ts +1 -1
- package/src/services/notifications/browser.ts +1 -1
- package/src/services/notifications/catalog.ts +1 -1
- package/src/services/notifications/dispatcher.ts +1 -1
- package/src/services/notifications/index.ts +1 -1
- package/src/services/notifications/observability.ts +2 -1
- package/src/services/notifications/platform.ts +1 -1
- package/src/services/notifications/quiet.ts +1 -1
- package/src/services/notifications/user.ts +1 -1
- package/src/services/oauth-tokens.ts +1 -1
- package/src/services/outbox.ts +15 -14
- package/src/services/outgoing-mail/admin.ts +1 -1
- package/src/services/outgoing-mail/dispatcher.ts +1 -1
- package/src/services/outgoing-mail/drain.ts +2 -1
- package/src/services/outgoing-mail/enqueue.ts +2 -1
- package/src/services/outgoing-mail/messages.ts +2 -1
- package/src/services/outgoing-mail/retention.ts +1 -1
- package/src/services/outgoing-mail/send.ts +1 -1
- package/src/services/outgoing-mail/store.ts +2 -1
- package/src/services/panel-context-consent.ts +32 -28
- package/src/services/providers/local/auth.ts +1 -1
- package/src/services/providers/local/users.ts +1 -1
- package/src/services/pwa-devices.ts +2 -1
- package/src/services/rail-preferences.ts +16 -12
- package/src/services/rail-shortcuts.ts +12 -10
- package/src/services/request-cache-redis.ts +3 -3
- package/src/services/service-account-credentials.ts +2 -1
- package/src/services/service-accounts.ts +2 -1
- package/src/services/session/index.ts +2 -1
- package/src/services/session/user.ts +2 -1
- package/src/services/settings/core-settings.ts +20 -0
- package/src/services/settings/crypto.ts +2 -2
- package/src/services/settings/index.ts +1 -1
- package/src/services/settings/store.ts +6 -1
- package/src/services/settings/versions.ts +2 -1
- package/src/services/weather/forecast.ts +2 -1
- package/src/services/weather/locations.ts +1 -1
- package/src/services/weather/migrate.ts +2 -1
- package/src/services/weather/short-id.ts +2 -1
- package/src/services/webauthn.ts +1 -1
- package/src/ssr/admin-navigation.ts +0 -1
- package/src/ssr/compact-shell.browser-harness.ts +2 -0
- package/src/ssr/platform-messages.ts +0 -2
- package/src/start.ts +35 -14
- package/src/workflows/ai/store.ts +2 -1
- package/src/workflows/store/budget.ts +2 -1
- package/src/workflows/store/definitions.ts +2 -1
- package/src/workflows/store/events.ts +2 -1
- package/src/workflows/store/observability.ts +2 -1
- package/src/workflows/store/retention.ts +2 -1
- package/src/workflows/store/runs.ts +2 -1
- package/src/workflows/store/transaction.ts +2 -1
- package/src/workflows/store/worker.ts +2 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@k2b/cloud",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.49.0",
|
|
4
4
|
"description": "Application platform library for independently deployed Hono and SolidJS services behind a dynamic gateway.",
|
|
5
5
|
"license": "AGPL-3.0-or-later",
|
|
6
6
|
"repository": {
|
|
@@ -131,7 +131,7 @@
|
|
|
131
131
|
"@tailwindcss/typography": "0.5.20",
|
|
132
132
|
"@k2b/nessi": "0.18.0",
|
|
133
133
|
"@k2b/ssr": "0.20.2",
|
|
134
|
-
"@k2b/ui": "0.
|
|
134
|
+
"@k2b/ui": "0.38.1",
|
|
135
135
|
"@k2b/stdlib": "0.30.0",
|
|
136
136
|
"@k2b/sync": "7.0.1",
|
|
137
137
|
"@nats-io/jetstream": "3.4.0",
|
|
@@ -163,7 +163,7 @@
|
|
|
163
163
|
"hash-wasm": "4.12.0",
|
|
164
164
|
"@types/sanitize-html": "2.16.1",
|
|
165
165
|
"@types/web-push": "3.6.4",
|
|
166
|
-
"@k2b/editor": "0.
|
|
166
|
+
"@k2b/editor": "0.2.0"
|
|
167
167
|
},
|
|
168
168
|
"devDependencies": {
|
|
169
169
|
"@babel/core": "8.0.6",
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { SQL } from "bun";
|
|
2
|
+
import { recursiveGroupIdsSubquery } from "../services/accounts/group-sql";
|
|
3
|
+
|
|
4
|
+
/** Eligibility of access entry alias `a`, shared by the last-manager guard and administration reads. */
|
|
5
|
+
export const managerEligibility = (db: SQL) => db`
|
|
6
|
+
EXISTS (SELECT 1 FROM auth.users u WHERE (u.account_expires IS NULL OR u.account_expires > now()) AND
|
|
7
|
+
(u.id = a.user_id OR (a.group_id IS NOT NULL AND a.group_id IN (${recursiveGroupIdsSubquery(db`u.id`)}))))
|
|
8
|
+
OR EXISTS (SELECT 1 FROM auth.service_accounts sa WHERE sa.id = a.service_account_id
|
|
9
|
+
AND sa.status = 'active' AND sa.kind IN ('standalone', 'agent'))
|
|
10
|
+
`;
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { isDeepStrictEqual } from "node:util";
|
|
2
|
-
import {
|
|
2
|
+
import type { SQL } from "bun";
|
|
3
3
|
import { waitWithin } from "../api/fanout";
|
|
4
4
|
import type { AppMeta } from "../contracts/app";
|
|
5
5
|
import { type AppAccessDeclaration, type AppAccessRule, AppAccessRuleSchema, type ResourceAppAccessAdapter } from "../contracts/app-access";
|
|
6
|
+
import { sql } from "../runtime/clients";
|
|
6
7
|
import type { RequestAuthority } from "../server/middleware/auth";
|
|
7
8
|
import { coreOrigin } from "../services/identity/core-origin";
|
|
8
9
|
import { invocationAuthorityFromRequest } from "../services/identity/invocation-authority";
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import { CAPABILITY_MAX_REQUEST_BYTES } from "../contracts/capabilities";
|
|
3
|
+
import { resolveLocale } from "../server/locale";
|
|
4
|
+
import { getActiveAppKey } from "../services/app-keys";
|
|
5
|
+
import { verifyAppCapabilityInvocation } from "../services/identity/invocation-token";
|
|
6
|
+
import { get } from "../services/settings";
|
|
7
|
+
import { readBoundedJson } from "./bounded-json";
|
|
8
|
+
import { type CompiledCapabilities, invokeCompiledCapability, serializeCapabilityProviderResult } from "./capabilities";
|
|
9
|
+
import { withCapabilityRequestTimeout } from "./capability-timeout";
|
|
10
|
+
|
|
11
|
+
/** Separate from ordinary auth: application authority never becomes a grant-bearing person. */
|
|
12
|
+
export const createAppCapabilityRoutes = (compiled: CompiledCapabilities) => {
|
|
13
|
+
const router = new Hono();
|
|
14
|
+
for (const kind of ["query", "action"] as const) {
|
|
15
|
+
router.post(`/${kind === "query" ? "queries" : "actions"}/:capabilityId`, (c) =>
|
|
16
|
+
withCapabilityRequestTimeout(c, kind, async () => {
|
|
17
|
+
const operation = (kind === "query" ? compiled.queries : compiled.actions).get(c.req.param("capabilityId"));
|
|
18
|
+
const deny = () => c.json({ code: "FORBIDDEN", message: "A permitted signed app capability invocation is required" }, 403);
|
|
19
|
+
if (!operation?.manifest.appPermission || operation.manifest.stream) return deny();
|
|
20
|
+
const body = await readBoundedJson(c.req.raw, CAPABILITY_MAX_REQUEST_BYTES);
|
|
21
|
+
if (
|
|
22
|
+
!body.ok ||
|
|
23
|
+
!body.data ||
|
|
24
|
+
typeof body.data !== "object" ||
|
|
25
|
+
Array.isArray(body.data) ||
|
|
26
|
+
Object.keys(body.data).length !== 1 ||
|
|
27
|
+
!("input" in body.data)
|
|
28
|
+
)
|
|
29
|
+
return c.json({ code: "VALIDATION_FAILED", message: "Expected a bounded input object" }, 400);
|
|
30
|
+
const token = c.req.header("authorization")?.match(/^Bearer (\S+)$/)?.[1];
|
|
31
|
+
if (!token) return deny();
|
|
32
|
+
try {
|
|
33
|
+
const claims = await verifyAppCapabilityInvocation(token, {
|
|
34
|
+
appId: compiled.manifest.appId,
|
|
35
|
+
kind,
|
|
36
|
+
capabilityId: operation.manifest.localId,
|
|
37
|
+
schemaHash: operation.manifest.schemaHash,
|
|
38
|
+
permission: operation.manifest.appPermission,
|
|
39
|
+
input: body.data.input,
|
|
40
|
+
});
|
|
41
|
+
if (!claims) return deny();
|
|
42
|
+
const locale = resolveLocale(c.req.raw.headers, await get<string>("app.locale"));
|
|
43
|
+
if (!(await getActiveAppKey({ appId: claims.sub, keyId: claims.key_id }))) return deny();
|
|
44
|
+
const result = await invokeCompiledCapability({
|
|
45
|
+
compiled,
|
|
46
|
+
kind,
|
|
47
|
+
localId: operation.manifest.localId,
|
|
48
|
+
input: body.data.input,
|
|
49
|
+
expectedSchemaHash: claims.schema_hash,
|
|
50
|
+
context: {
|
|
51
|
+
actor: { kind: "app", appId: claims.sub, keyId: claims.key_id },
|
|
52
|
+
accessSubject: { type: "none" },
|
|
53
|
+
user: null,
|
|
54
|
+
locale,
|
|
55
|
+
requestId: claims.request_id,
|
|
56
|
+
origin: "app",
|
|
57
|
+
signal: c.req.raw.signal,
|
|
58
|
+
...(c.req.header("idempotency-key") ? { idempotencyKey: c.req.header("idempotency-key") } : {}),
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
const response = serializeCapabilityProviderResult(result);
|
|
62
|
+
return new Response(response.body, {
|
|
63
|
+
status: response.status,
|
|
64
|
+
headers: { "content-type": "application/json", "cache-control": "no-store" },
|
|
65
|
+
});
|
|
66
|
+
} catch {
|
|
67
|
+
return c.json({ code: "APP_UNAVAILABLE", message: "App invocation verification unavailable" }, 503);
|
|
68
|
+
}
|
|
69
|
+
}),
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
return router;
|
|
73
|
+
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { sql } from "bun";
|
|
2
1
|
import type { CapabilityEventCatalogEntry, CapabilityManifest } from "../contracts/capabilities";
|
|
2
|
+
import { sql } from "../runtime/clients";
|
|
3
3
|
|
|
4
4
|
export type CatalogSubscription = CapabilityEventCatalogEntry["subscribers"][number] & { eventType: string; version: number };
|
|
5
5
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { sql } from "bun";
|
|
2
1
|
import { Hono } from "hono";
|
|
3
2
|
import { CAPABILITY_MAX_REQUEST_BYTES } from "../contracts/capabilities";
|
|
3
|
+
import { sql } from "../runtime/clients";
|
|
4
4
|
import { resolveLocale } from "../server/locale";
|
|
5
5
|
import { verifyAppQueryInvocation } from "../services/identity/invocation-token";
|
|
6
6
|
import { get } from "../services/settings";
|
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
export type BoundedJsonResult = { ok: true; data: unknown } | { ok: false; reason: "too_large" | "invalid_json" };
|
|
2
2
|
|
|
3
3
|
/** Reads JSON without allowing a missing or forged Content-Length to bypass the byte cap. */
|
|
4
|
-
export const readBoundedJson = async (
|
|
4
|
+
export const readBoundedJson = async (
|
|
5
|
+
message: Request | Response,
|
|
6
|
+
maxBytes: number,
|
|
7
|
+
signal?: AbortSignal,
|
|
8
|
+
consumeBytes?: (bytes: number) => void,
|
|
9
|
+
): Promise<BoundedJsonResult> => {
|
|
5
10
|
signal?.throwIfAborted();
|
|
6
11
|
const declared = message.headers.get("content-length");
|
|
7
12
|
if (declared !== null) {
|
|
@@ -27,6 +32,7 @@ export const readBoundedJson = async (message: Request | Response, maxBytes: num
|
|
|
27
32
|
while (true) {
|
|
28
33
|
const next = await reader.read();
|
|
29
34
|
if (next.done) break;
|
|
35
|
+
consumeBytes?.(next.value.byteLength);
|
|
30
36
|
total += next.value.byteLength;
|
|
31
37
|
if (total > maxBytes) {
|
|
32
38
|
await reader.cancel().catch(() => undefined);
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { AppEventDefinition, AppEventSubscriptionDefinition } from "../contracts/app-events";
|
|
2
|
+
import { appStatusIssues } from "../contracts/app-status";
|
|
2
3
|
import {
|
|
4
|
+
CapabilityAppStatusManifestSchema,
|
|
3
5
|
type CapabilityEventManifest,
|
|
4
6
|
CapabilityEventManifestSchema,
|
|
5
7
|
CapabilityQualifiedIdSchema,
|
|
@@ -108,6 +110,7 @@ const DECLARATION_FIELDS = {
|
|
|
108
110
|
fileProvider: true,
|
|
109
111
|
calendarSource: true,
|
|
110
112
|
discussions: true,
|
|
113
|
+
appStatus: true,
|
|
111
114
|
events: true,
|
|
112
115
|
subscribes: true,
|
|
113
116
|
} satisfies DefinedFields<CapabilityDefinitions>;
|
|
@@ -130,6 +133,7 @@ const TYPE_FIELDS = {
|
|
|
130
133
|
access: true,
|
|
131
134
|
} satisfies DefinedFields<CapabilityResourceTypeDefinition>;
|
|
132
135
|
const QUERY_FIELDS = {
|
|
136
|
+
app: true,
|
|
133
137
|
appOnly: true,
|
|
134
138
|
title: true,
|
|
135
139
|
description: true,
|
|
@@ -141,6 +145,7 @@ const QUERY_FIELDS = {
|
|
|
141
145
|
run: true,
|
|
142
146
|
} satisfies DefinedFields<CapabilityQueryDefinition>;
|
|
143
147
|
const ACTION_FIELDS = {
|
|
148
|
+
app: true,
|
|
144
149
|
title: true,
|
|
145
150
|
description: true,
|
|
146
151
|
input: true,
|
|
@@ -679,6 +684,11 @@ const compileOperationSchemas = (definition: QueryDefinition | CapabilityActionD
|
|
|
679
684
|
) {
|
|
680
685
|
throw new Error(`${label} input field "idempotencyKey" is reserved for capability transports`);
|
|
681
686
|
}
|
|
687
|
+
if (definition.app !== undefined) {
|
|
688
|
+
assertDefinedFields(definition.app, { permission: true, run: true }, `${label} app`);
|
|
689
|
+
if (typeof definition.app.run !== "function") throw new Error(`${label} app.run must be a function`);
|
|
690
|
+
if (definition.stream) throw new Error(`${label} app calls cannot use streams`);
|
|
691
|
+
}
|
|
682
692
|
const resultZodSchema = capabilityResultSchema(definition.data);
|
|
683
693
|
const dataSchema = projectSchema(definition.data, `${label} data`, "output");
|
|
684
694
|
return {
|
|
@@ -688,6 +698,7 @@ const compileOperationSchemas = (definition: QueryDefinition | CapabilityActionD
|
|
|
688
698
|
schemaHash: capabilityHash({
|
|
689
699
|
inputSchema,
|
|
690
700
|
dataSchema,
|
|
701
|
+
...(definition.app ? { appPermission: definition.app.permission } : {}),
|
|
691
702
|
...("appOnly" in definition && definition.appOnly !== undefined ? { appOnly: definition.appOnly } : {}),
|
|
692
703
|
...(definition.stream ? { stream: { direction: definition.stream.direction, maxBytes: definition.stream.maxBytes } } : {}),
|
|
693
704
|
}),
|
|
@@ -710,6 +721,7 @@ export const compileCapabilities = (
|
|
|
710
721
|
}
|
|
711
722
|
if (definitions.fileProvider) assertDefinedFields(definitions.fileProvider, FILE_PROVIDER_FIELDS, "fileProvider");
|
|
712
723
|
|
|
724
|
+
if (definitions.appStatus !== undefined) CapabilityAppStatusManifestSchema.parse(definitions.appStatus);
|
|
713
725
|
if (definitions.discussions !== undefined) CapabilityDiscussionsManifestSchema.parse(definitions.discussions);
|
|
714
726
|
for (const [localId, type] of Object.entries(definitions.types ?? {})) {
|
|
715
727
|
if (type.access !== undefined) {
|
|
@@ -839,6 +851,7 @@ export const compileCapabilities = (
|
|
|
839
851
|
inputSchema: schemas.inputSchema,
|
|
840
852
|
dataSchema: schemas.dataSchema,
|
|
841
853
|
schemaHash: schemas.schemaHash,
|
|
854
|
+
...(definition.app ? { appPermission: definition.app.permission } : {}),
|
|
842
855
|
...(definition.stream ? { stream: { direction: definition.stream.direction, maxBytes: definition.stream.maxBytes } } : {}),
|
|
843
856
|
openWorld: definition.openWorld,
|
|
844
857
|
universalSearch: normalizeSearchTags(definition, label),
|
|
@@ -923,6 +936,7 @@ export const compileCapabilities = (
|
|
|
923
936
|
inputSchema: schemas.inputSchema,
|
|
924
937
|
dataSchema: schemas.dataSchema,
|
|
925
938
|
schemaHash: schemas.schemaHash,
|
|
939
|
+
...(definition.app ? { appPermission: definition.app.permission } : {}),
|
|
926
940
|
...(definition.stream ? { stream: { direction: definition.stream.direction, maxBytes: definition.stream.maxBytes } } : {}),
|
|
927
941
|
destructive: definition.destructive,
|
|
928
942
|
openWorld: definition.openWorld,
|
|
@@ -1037,6 +1051,7 @@ export const compileCapabilities = (
|
|
|
1037
1051
|
});
|
|
1038
1052
|
const manifestBase = {
|
|
1039
1053
|
...(resourceTypes.length ? { resourceTypes } : {}),
|
|
1054
|
+
...(definitions.appStatus ? { appStatus: definitions.appStatus } : {}),
|
|
1040
1055
|
...(definitions.discussions ? { discussions: definitions.discussions } : {}),
|
|
1041
1056
|
...(resourcePages.length ? { resourcePages } : {}),
|
|
1042
1057
|
protocolVersion: CAPABILITY_PROTOCOL_VERSION,
|
|
@@ -1058,6 +1073,8 @@ export const compileCapabilities = (
|
|
|
1058
1073
|
...manifestBase,
|
|
1059
1074
|
manifestHash: capabilityHash(manifestBase),
|
|
1060
1075
|
});
|
|
1076
|
+
const statusIssues = appStatusIssues(manifest);
|
|
1077
|
+
if (statusIssues.length) throw new Error(`appStatus: ${statusIssues.map((issue) => `${issue.path}: ${issue.message}`).join("; ")}`);
|
|
1061
1078
|
const discussionIssues = discussionsIssues(manifest);
|
|
1062
1079
|
if (discussionIssues.length)
|
|
1063
1080
|
throw new Error(`discussions: ${discussionIssues.map((issue) => `${issue.path}: ${issue.message}`).join("; ")}`);
|
|
@@ -1234,6 +1251,7 @@ export const resolveCapabilityManifestPresentation = (
|
|
|
1234
1251
|
: {}),
|
|
1235
1252
|
...(current.subscribes ? { subscribes: current.subscribes } : {}),
|
|
1236
1253
|
...(current.resourceTypes ? { resourceTypes: current.resourceTypes } : {}),
|
|
1254
|
+
...(current.appStatus ? { appStatus: current.appStatus } : {}),
|
|
1237
1255
|
...(current.discussions ? { discussions: current.discussions } : {}),
|
|
1238
1256
|
...(current.resourcePages ? { resourcePages: current.resourcePages } : {}),
|
|
1239
1257
|
...(current.calendarSource ? { calendarSource: current.calendarSource } : {}),
|
|
@@ -1272,6 +1290,7 @@ const assertUsableOperation = (operation: CapabilityQueryManifest | CapabilityAc
|
|
|
1272
1290
|
const expectedSchemaHash = capabilityHash({
|
|
1273
1291
|
inputSchema: operation.inputSchema,
|
|
1274
1292
|
dataSchema: operation.dataSchema,
|
|
1293
|
+
...(operation.appPermission ? { appPermission: operation.appPermission } : {}),
|
|
1275
1294
|
...("appOnly" in operation && operation.appOnly !== undefined ? { appOnly: operation.appOnly } : {}),
|
|
1276
1295
|
...(operation.stream ? { stream: operation.stream } : {}),
|
|
1277
1296
|
});
|
|
@@ -1774,9 +1793,13 @@ export const invokeCompiledCapability = async (params: {
|
|
|
1774
1793
|
let invoked: CapabilityInvocationResult<unknown>;
|
|
1775
1794
|
if (isAppCapabilityContext(context)) {
|
|
1776
1795
|
const definition = operation.definition;
|
|
1777
|
-
if (
|
|
1796
|
+
if (definition.app && context.actor.keyId) {
|
|
1797
|
+
invoked = await definition.app.run(input.data, context);
|
|
1798
|
+
} else if (params.kind === "query" && "appOnly" in definition && definition.appOnly === true) {
|
|
1799
|
+
invoked = await definition.run(input.data, context);
|
|
1800
|
+
} else {
|
|
1778
1801
|
return { ok: false, error: { code: "FORBIDDEN", message: "App caller is not allowed", status: 403 } };
|
|
1779
|
-
|
|
1802
|
+
}
|
|
1780
1803
|
} else {
|
|
1781
1804
|
if (operation.definition.appOnly) return { ok: false, error: { code: "FORBIDDEN", message: "App authority required", status: 403 } };
|
|
1782
1805
|
invoked = await operation.definition.run(input.data, context);
|
|
@@ -13,6 +13,7 @@ import { registerPageSwap } from "../ssr/page-swap";
|
|
|
13
13
|
import { preloadPanelConsent } from "../ssr/panel-consent";
|
|
14
14
|
import { appAccessManagementPath } from "./app-access-path";
|
|
15
15
|
import { bindResourceAppAccess, RESOURCE_ACCESS_PATH } from "./app-access-resources";
|
|
16
|
+
import { createAppCapabilityRoutes } from "./app-capabilities";
|
|
16
17
|
import { createAppQueryRoutes } from "./app-queries";
|
|
17
18
|
import { registerApplicationPackage, resolveApplicationPackage } from "./application-package";
|
|
18
19
|
import { invokeCapabilityStream } from "./capability-streams";
|
|
@@ -27,7 +28,7 @@ import { bindProcessApplicationId, clearProcessApplicationId } from "./process-i
|
|
|
27
28
|
|
|
28
29
|
import { resolve } from "node:path";
|
|
29
30
|
import { fileURLToPath } from "node:url";
|
|
30
|
-
import type { HtmlFn, SsrConfig } from "@k2b/ssr";
|
|
31
|
+
import type { HtmlFn, SsrConfig, SsrOptions } from "@k2b/ssr";
|
|
31
32
|
import { createConfig as createSsrConfig } from "@k2b/ssr";
|
|
32
33
|
import { routes } from "@k2b/ssr/hono";
|
|
33
34
|
import type { Sync } from "@k2b/sync";
|
|
@@ -48,6 +49,7 @@ import type {
|
|
|
48
49
|
CloudLifecycleContext,
|
|
49
50
|
WidgetEndpoint,
|
|
50
51
|
} from "../contracts/app";
|
|
52
|
+
import { PUBLIC_SURFACE_PREFIX } from "../contracts/app-domains";
|
|
51
53
|
import {
|
|
52
54
|
CAPABILITY_FRAMEWORK_ERROR_CODES,
|
|
53
55
|
CAPABILITY_MAX_REQUEST_BYTES,
|
|
@@ -68,8 +70,10 @@ import { getLocale, resolveLocale } from "../server/locale";
|
|
|
68
70
|
import { requireAppAccess, stopAppAccessConnections } from "../server/middleware/app-access";
|
|
69
71
|
import { type AuthContext, auth } from "../server/middleware/auth";
|
|
70
72
|
import { requireInvocation } from "../server/middleware/invocation";
|
|
73
|
+
import { createPublicSurfaceHandler, validatePublicSurfaceRoutes } from "./public-surface";
|
|
71
74
|
import "../events/app-access";
|
|
72
75
|
import { createNestedCapabilityQuery } from "../capabilities/nested";
|
|
76
|
+
import { connections } from "../runtime/connections";
|
|
73
77
|
import { matchedRouteTemplate, routeTemplate } from "../server/middleware/route-template";
|
|
74
78
|
import { runtime as runtimeMiddleware } from "../server/middleware/runtime";
|
|
75
79
|
import { preloadLayoutAnnouncements, settings as settingsMiddleware } from "../server/middleware/settings";
|
|
@@ -83,6 +87,7 @@ import {
|
|
|
83
87
|
widgetInvocationOperation,
|
|
84
88
|
} from "../services/identity/invocation-operations";
|
|
85
89
|
import { normalizeInvocationRequestId } from "../services/identity/invocation-token";
|
|
90
|
+
import { startLinksRuntime } from "../services/links/runtime";
|
|
86
91
|
import { logger } from "../services/logging";
|
|
87
92
|
import { startNotificationDefinitionRegistration } from "../services/notifications/catalog";
|
|
88
93
|
import { readRailSnapshot } from "../services/rail-snapshot";
|
|
@@ -164,6 +169,8 @@ export type AppOptions<S extends AppSettingsMap = {}, N extends NotificationDefi
|
|
|
164
169
|
/** Shared object-store package. Defaults to the app ID. */
|
|
165
170
|
package?: string;
|
|
166
171
|
access?: AppAccessDeclaration;
|
|
172
|
+
/** Serve a separate public site on domains assigned by an administrator. */
|
|
173
|
+
public?: true;
|
|
167
174
|
name: string;
|
|
168
175
|
icon: string;
|
|
169
176
|
description: string;
|
|
@@ -372,6 +379,10 @@ export type AppDefinition<S extends AppSettingsMap = {}, N extends NotificationD
|
|
|
372
379
|
config: SsrConfig;
|
|
373
380
|
meta: AppMeta;
|
|
374
381
|
baseUrl: string;
|
|
382
|
+
/** Normal Hono router for the public site. Adding routes requires `public: true`. */
|
|
383
|
+
readonly public: Hono;
|
|
384
|
+
/** Public HTML template and island assets, reusing this app's single SSR plugin/build. Call before adding page routes. */
|
|
385
|
+
publicSsr: <T extends object = object>(template: NonNullable<SsrOptions<T>["template"]>) => { html: HtmlFn<T> };
|
|
375
386
|
start: (opts: StartOptions) => Promise<StartResult>;
|
|
376
387
|
/**
|
|
377
388
|
* The process-wide @k2b/sync instance, connected and ready once `start()`
|
|
@@ -478,6 +489,8 @@ export const defineApp = <
|
|
|
478
489
|
>(
|
|
479
490
|
opts: AppOptions<S, N, AppId>,
|
|
480
491
|
): AppDefinition<S, N, AppId> => {
|
|
492
|
+
validatePublicSurfaceRoutes(opts.routes);
|
|
493
|
+
const publicRouter = new Hono();
|
|
481
494
|
const appSkills = opts.skills ? validateAppSkillCatalog(opts.skills) : undefined;
|
|
482
495
|
const skillsManifestHash = appSkills ? appSkillManifestHash(appSkills) : undefined;
|
|
483
496
|
configurePostgresApplicationName(opts.id);
|
|
@@ -692,6 +705,8 @@ export const defineApp = <
|
|
|
692
705
|
|
|
693
706
|
// ── 3. start() — builds and boots the Hono server ────────────────────
|
|
694
707
|
const start = async (startOpts: StartOptions): Promise<StartResult> => {
|
|
708
|
+
if (!opts.public && publicRouter.routes.length)
|
|
709
|
+
throw new Error(`App "${opts.id}" adds app.public routes without declaring public: true`);
|
|
695
710
|
const port = startOpts.port ?? 3000;
|
|
696
711
|
if (env.GOTENBERG_URL)
|
|
697
712
|
console.warn("[deprecation] GOTENBERG_URL only bootstraps gotenberg.url; remove it after cld admin settings status --env succeeds.");
|
|
@@ -705,7 +720,7 @@ export const defineApp = <
|
|
|
705
720
|
// Settings are encrypted at rest, so every container needs the secret. It
|
|
706
721
|
// is only read on the first decrypt, which meant a container without it
|
|
707
722
|
// booted cleanly and failed much later with an unrelated-looking error.
|
|
708
|
-
if (!
|
|
723
|
+
if (!connections().appSecret.current.trim()) {
|
|
709
724
|
throw new Error(
|
|
710
725
|
`APP_SECRET is not set (app "${meta.id}"). Settings are encrypted at rest and every app in the ` +
|
|
711
726
|
"deployment must share the same value; without it the first settings read fails.",
|
|
@@ -751,6 +766,7 @@ export const defineApp = <
|
|
|
751
766
|
const entry: AppRegistryEntry = {
|
|
752
767
|
package: packageName,
|
|
753
768
|
id: meta.id,
|
|
769
|
+
...(opts.public ? { public: true as const } : {}),
|
|
754
770
|
access: meta.access,
|
|
755
771
|
name: meta.name,
|
|
756
772
|
icon: meta.icon,
|
|
@@ -818,6 +834,7 @@ export const defineApp = <
|
|
|
818
834
|
registry: appInstanceRegistry,
|
|
819
835
|
lock: appMembershipLock,
|
|
820
836
|
startedAt: entry.startedAt,
|
|
837
|
+
bootstrap: connections().bootstrap,
|
|
821
838
|
});
|
|
822
839
|
// Heartbeat
|
|
823
840
|
const heartbeat = createHeartbeat(meta.id, entry, {
|
|
@@ -894,7 +911,10 @@ export const defineApp = <
|
|
|
894
911
|
// app middleware cannot report their template. Without this, every hashed
|
|
895
912
|
// island chunk would land in telemetry as its own route.
|
|
896
913
|
const invocationApp = { getApp: async (id: string) => (id === meta.id ? meta : null) };
|
|
914
|
+
const publicSurface = createPublicSurfaceHandler(opts.public ? publicRouter : undefined, isDevelopment);
|
|
897
915
|
const server = new Hono<AuthContext>()
|
|
916
|
+
.all(PUBLIC_SURFACE_PREFIX, publicSurface)
|
|
917
|
+
.all(`${PUBLIC_SURFACE_PREFIX}/*`, publicSurface)
|
|
898
918
|
.use("*", routeTemplate)
|
|
899
919
|
.get(APP_READINESS_PATH, () => appReadinessResponse(meta.id))
|
|
900
920
|
.route(ssrMountPath, routes(config))
|
|
@@ -937,6 +957,7 @@ export const defineApp = <
|
|
|
937
957
|
|
|
938
958
|
if (compiledCapabilities) {
|
|
939
959
|
server.route("/api/_internal/capabilities/v1/app-queries", createAppQueryRoutes(compiledCapabilities));
|
|
960
|
+
server.route("/api/_internal/capabilities/v1/app-capabilities", createAppCapabilityRoutes(compiledCapabilities));
|
|
940
961
|
const invoke = async (c: Context<AuthContext>, kind: "query" | "action" | "review") =>
|
|
941
962
|
withCapabilityRequestTimeout(c, kind, async () => {
|
|
942
963
|
// Framework-owned endpoints run before the app's settings middleware,
|
|
@@ -1179,6 +1200,8 @@ export const defineApp = <
|
|
|
1179
1200
|
// Core creates it in its own setup, so it starts publishing after that.
|
|
1180
1201
|
if (meta.id !== "core") await startOutbox();
|
|
1181
1202
|
|
|
1203
|
+
cleanup.push(await startLinksRuntime());
|
|
1204
|
+
|
|
1182
1205
|
if (!startOpts.skipSetup && startOpts.lifecycle?.setup) {
|
|
1183
1206
|
log.info(`Setup: ${meta.id}`);
|
|
1184
1207
|
const setup = startOpts.lifecycle.setup;
|
|
@@ -1264,6 +1287,14 @@ export const defineApp = <
|
|
|
1264
1287
|
config,
|
|
1265
1288
|
meta,
|
|
1266
1289
|
baseUrl: opts.baseUrl,
|
|
1290
|
+
public: publicRouter,
|
|
1291
|
+
publicSsr: <T extends object = object>(template: NonNullable<SsrOptions<T>["template"]>) => {
|
|
1292
|
+
if (!opts.public) throw new Error(`App "${opts.id}" uses publicSsr without declaring public: true`);
|
|
1293
|
+
const site = createSsrConfig<T>({ dev: config.dev, rootDir: config.rootDir, basePath: "", template });
|
|
1294
|
+
publicRouter.route("/_ssr", routes(site.config));
|
|
1295
|
+
publicRouter.all("/_ssr/*", (c) => c.notFound());
|
|
1296
|
+
return { html: site.html };
|
|
1297
|
+
},
|
|
1267
1298
|
start,
|
|
1268
1299
|
get sync() {
|
|
1269
1300
|
return getProcessSync();
|
|
@@ -48,6 +48,7 @@ export const createRegistryMembership = (
|
|
|
48
48
|
instanceId?: string;
|
|
49
49
|
hostname?: string;
|
|
50
50
|
startedAt?: number;
|
|
51
|
+
bootstrap?: AppInstanceRecord["bootstrap"];
|
|
51
52
|
writeTimeoutMs?: number;
|
|
52
53
|
},
|
|
53
54
|
) => {
|
|
@@ -60,6 +61,7 @@ export const createRegistryMembership = (
|
|
|
60
61
|
hostname: options.hostname ?? hostname(),
|
|
61
62
|
startedAt: options.startedAt ?? Date.now(),
|
|
62
63
|
entries: {},
|
|
64
|
+
...(options.bootstrap ? { bootstrap: structuredClone(options.bootstrap) } : {}),
|
|
63
65
|
};
|
|
64
66
|
const writeTimeoutMs = options.writeTimeoutMs ?? HEARTBEAT_WRITE_TIMEOUT_MS;
|
|
65
67
|
if (!Number.isFinite(writeTimeoutMs) || writeTimeoutMs <= 0) throw new RangeError("Membership write timeout must be positive and finite");
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* NATS connection for @k2b/sync — one connection per app process, created by
|
|
3
|
-
* the app lifecycle. Configuration comes from
|
|
3
|
+
* the app lifecycle. Configuration comes from the platform startup snapshot; sync
|
|
4
4
|
* itself reads no ENV and loads no credentials.
|
|
5
5
|
*/
|
|
6
|
-
import { readFileSync } from "node:fs";
|
|
7
6
|
import { connect, credsAuthenticator, type NatsConnection } from "@nats-io/transport-node";
|
|
8
|
-
import {
|
|
7
|
+
import { connections } from "../runtime/connections";
|
|
9
8
|
|
|
10
9
|
export type ConnectNatsOptions = {
|
|
11
10
|
/** Connection name shown in NATS monitoring (`connz`), e.g. `cloud-<appId>-<instanceId>`. */
|
|
@@ -13,15 +12,17 @@ export type ConnectNatsOptions = {
|
|
|
13
12
|
};
|
|
14
13
|
|
|
15
14
|
/** Dial `NATS_SERVERS` once. `startProcessSync()` requires the list and retries while NATS does not answer. */
|
|
16
|
-
export const connectNats = async ({ name }: ConnectNatsOptions): Promise<NatsConnection> =>
|
|
17
|
-
|
|
18
|
-
|
|
15
|
+
export const connectNats = async ({ name }: ConnectNatsOptions): Promise<NatsConnection> => {
|
|
16
|
+
const nats = connections().nats;
|
|
17
|
+
return connect({
|
|
18
|
+
servers: nats.servers,
|
|
19
19
|
name,
|
|
20
|
-
ignoreClusterUpdates:
|
|
20
|
+
ignoreClusterUpdates: nats.ignoreClusterUpdates,
|
|
21
21
|
// Keep reconnecting for the life of the process: sync work resumes when the cluster is back.
|
|
22
22
|
reconnect: true,
|
|
23
23
|
maxReconnectAttempts: -1,
|
|
24
24
|
reconnectTimeWait: 2_000,
|
|
25
|
-
...(
|
|
26
|
-
...(
|
|
25
|
+
...(nats.creds ? { authenticator: credsAuthenticator(new TextEncoder().encode(nats.creds)) } : {}),
|
|
26
|
+
...(nats.ca ? { tls: { ca: nats.ca } } : {}),
|
|
27
27
|
});
|
|
28
|
+
};
|
|
@@ -11,7 +11,7 @@ import { hostname } from "node:os";
|
|
|
11
11
|
import { createSync, type Sync, SyncError } from "@k2b/sync";
|
|
12
12
|
import { expBackoff } from "@k2b/sync/retry";
|
|
13
13
|
import type { NatsConnection } from "@nats-io/transport-node";
|
|
14
|
-
import {
|
|
14
|
+
import { connections } from "../runtime/connections";
|
|
15
15
|
import { logger } from "../services/logging";
|
|
16
16
|
import { flushSyncTraceEvents, observeSyncEvent } from "../services/logging/trace";
|
|
17
17
|
import { connectNats } from "./nats-connection";
|
|
@@ -135,12 +135,12 @@ const connectReadySync = async (application: string): Promise<{ sync: Sync; conn
|
|
|
135
135
|
sync = withSyncBudgets(
|
|
136
136
|
createSync({
|
|
137
137
|
connection,
|
|
138
|
-
namespace:
|
|
138
|
+
namespace: connections().nats.namespace,
|
|
139
139
|
application,
|
|
140
|
-
defaults: { replicas:
|
|
140
|
+
defaults: { replicas: connections().nats.replicas },
|
|
141
141
|
observe: (event) => observeSyncEvent(event, application),
|
|
142
142
|
}),
|
|
143
|
-
{ connection, namespace:
|
|
143
|
+
{ connection, namespace: connections().nats.namespace },
|
|
144
144
|
);
|
|
145
145
|
bindProcessSync(sync);
|
|
146
146
|
await sync.ready();
|
|
@@ -164,13 +164,13 @@ const connectReadySync = async (application: string): Promise<{ sync: Sync; conn
|
|
|
164
164
|
* serves readiness before this returns.
|
|
165
165
|
*/
|
|
166
166
|
export const startProcessSync = async ({ application }: { application: string }): Promise<ProcessSync> => {
|
|
167
|
-
if (!
|
|
167
|
+
if (!connections().nats.namespace.trim()) {
|
|
168
168
|
throw new Error(
|
|
169
169
|
`SYNC_NAMESPACE is not set (application "${application}"). Every process of one Cloud installation ` +
|
|
170
170
|
"must share the same @k2b/sync namespace.",
|
|
171
171
|
);
|
|
172
172
|
}
|
|
173
|
-
if (
|
|
173
|
+
if (connections().nats.servers.length === 0) {
|
|
174
174
|
throw new Error(
|
|
175
175
|
`NATS_SERVERS is not set (application "${application}"). @k2b/sync needs a comma-separated list of ` +
|
|
176
176
|
"nats://host:port bootstrap servers, e.g. NATS_SERVERS=nats://ipa_nats_1:4222,nats://ipa_nats_2:4222.",
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import type { Server } from "bun";
|
|
2
|
+
import type { Handler, Hono } from "hono";
|
|
3
|
+
import { isCloudCredentialCookie, normalizeAppDomainHost, PUBLIC_HOST_HEADER, PUBLIC_SURFACE_PREFIX } from "../contracts/app-domains";
|
|
4
|
+
import { canonicalRoute } from "./app-path";
|
|
5
|
+
|
|
6
|
+
type RequestServer = Pick<Server<unknown>, "upgrade" | "requestIP" | "timeout">;
|
|
7
|
+
const isRequestServer = (value: unknown): value is RequestServer =>
|
|
8
|
+
typeof value === "object" &&
|
|
9
|
+
value !== null &&
|
|
10
|
+
"upgrade" in value &&
|
|
11
|
+
typeof value.upgrade === "function" &&
|
|
12
|
+
"requestIP" in value &&
|
|
13
|
+
typeof value.requestIP === "function" &&
|
|
14
|
+
"timeout" in value &&
|
|
15
|
+
typeof value.timeout === "function";
|
|
16
|
+
|
|
17
|
+
/** Bun associates transport operations with its native Request, before the public-origin view is built. */
|
|
18
|
+
const publicEnvironment = (environment: object | undefined, request: Request, original: Request): object | undefined => {
|
|
19
|
+
if (!environment) return environment;
|
|
20
|
+
const bindServer = (server: RequestServer) =>
|
|
21
|
+
new Proxy(server, {
|
|
22
|
+
get(target, property) {
|
|
23
|
+
const native = (candidate: Request) => (candidate === request ? original : candidate);
|
|
24
|
+
if (property === "upgrade")
|
|
25
|
+
return (candidate: Request, options: Parameters<RequestServer["upgrade"]>[1]) => target.upgrade(native(candidate), options);
|
|
26
|
+
if (property === "requestIP") return (candidate: Request) => target.requestIP(native(candidate));
|
|
27
|
+
if (property === "timeout") return (candidate: Request, seconds: number) => target.timeout(native(candidate), seconds);
|
|
28
|
+
const value: unknown = Reflect.get(target, property, target);
|
|
29
|
+
return typeof value === "function" ? value.bind(target) : value;
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
if (isRequestServer(environment)) return bindServer(environment);
|
|
33
|
+
if ("server" in environment && isRequestServer(environment.server)) {
|
|
34
|
+
const server = bindServer(environment.server);
|
|
35
|
+
return new Proxy(environment, {
|
|
36
|
+
get: (target, property) => (property === "server" ? server : Reflect.get(target, property, target)),
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
return environment;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/** Framework mounts must never be advertised as application routes. */
|
|
43
|
+
export const validatePublicSurfaceRoutes = (routes: readonly string[]): void => {
|
|
44
|
+
for (const route of routes) {
|
|
45
|
+
const path = new URL(canonicalRoute(decodeURIComponent(route).replaceAll("\\", "/")), "https://cloud.invalid").pathname;
|
|
46
|
+
if (path === PUBLIC_SURFACE_PREFIX || path.startsWith(`${PUBLIC_SURFACE_PREFIX}/`)) {
|
|
47
|
+
throw new Error(`Route "${route}" uses the reserved ${PUBLIC_SURFACE_PREFIX} prefix`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/** Dispatch before Cloud context/authentication, using a fresh public-origin request. */
|
|
53
|
+
export const createPublicSurfaceHandler =
|
|
54
|
+
(router: Pick<Hono, "fetch"> | undefined, development: boolean): Handler =>
|
|
55
|
+
(c) => {
|
|
56
|
+
const authority = c.req.header(PUBLIC_HOST_HEADER);
|
|
57
|
+
const parts = authority?.match(/^([^:]+)(?::([0-9]{1,5}))?$/);
|
|
58
|
+
const host = parts?.[1];
|
|
59
|
+
if (!router || !authority || !host || normalizeAppDomainHost(host) !== host || (parts?.[2] && Number(parts[2]) > 65535))
|
|
60
|
+
return c.notFound();
|
|
61
|
+
const original = new URL(c.req.url);
|
|
62
|
+
const url = new URL(`${development && host.endsWith(".localhost") ? "http" : "https"}://${authority}`);
|
|
63
|
+
url.pathname = original.pathname.slice(PUBLIC_SURFACE_PREFIX.length) || "/";
|
|
64
|
+
url.search = original.search;
|
|
65
|
+
const headers = new Headers(c.req.raw.headers);
|
|
66
|
+
const cookies = headers
|
|
67
|
+
.get("cookie")
|
|
68
|
+
?.split(";")
|
|
69
|
+
.filter((cookie) => !isCloudCredentialCookie(cookie.split("=", 1)[0]!.trim()));
|
|
70
|
+
if (cookies?.some((cookie) => cookie.trim())) headers.set("cookie", cookies.join(";").trim());
|
|
71
|
+
else headers.delete("cookie");
|
|
72
|
+
headers.delete(PUBLIC_HOST_HEADER);
|
|
73
|
+
headers.set("host", authority);
|
|
74
|
+
const request = new Request(url, new Request(c.req.raw, { headers }));
|
|
75
|
+
return router.fetch(request, publicEnvironment(c.env, request, c.req.raw));
|
|
76
|
+
};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
const PURPOSE_NAME = /^[a-z0-9][a-z0-9.-]{0,63}$/;
|
|
2
|
+
|
|
3
|
+
export const isPurposeName = (name: string): boolean => typeof name === "string" && PURPOSE_NAME.test(name);
|
|
4
|
+
|
|
5
|
+
export const validatePurposeName = (name: string): void => {
|
|
6
|
+
if (!isPurposeName(name)) throw new TypeError("Purpose name must match [a-z0-9][a-z0-9.-]{0,63}");
|
|
7
|
+
};
|
|
@@ -20,6 +20,7 @@ export const validateAppRegistryEntry = (value: unknown): string | null => {
|
|
|
20
20
|
for (const field of ["id", "name", "icon", "description", "baseUrl"] as const) {
|
|
21
21
|
if (!isString(value[field]) || value[field].length === 0) return invalid(field, "a non-empty string");
|
|
22
22
|
}
|
|
23
|
+
if (value.public !== undefined && value.public !== true) return invalid("public", "true when present");
|
|
23
24
|
if (value.package !== undefined && (typeof value.package !== "string" || !/^[a-z][a-z0-9-]{0,79}$/.test(value.package))) {
|
|
24
25
|
return invalid("package", "a lowercase package name of 1–80 characters");
|
|
25
26
|
}
|