@k2b/cloud 0.48.0 → 0.50.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-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/capabilities.ts +17 -2
- package/src/_internal/define-app.ts +2 -0
- 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-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 +23 -1
- 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 +79 -5
- package/src/api/index.ts +2 -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/server-invoke.ts +45 -18
- package/src/capabilities/server.ts +2 -1
- package/src/cli/admin/app-keys.ts +25 -3
- package/src/config/env.ts +4 -4
- package/src/contracts/capabilities.ts +15 -2
- package/src/contracts/outgoing-mail.ts +1 -1
- package/src/events/app-event-delivery.ts +1 -1
- package/src/events/live-delivery.ts +2 -1
- package/src/runtime/bootstrap-protocol.ts +11 -4
- package/src/runtime/clients.ts +6 -2
- package/src/server/index.ts +1 -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 +2 -1
- package/src/services/app-access.ts +2 -1
- package/src/services/app-admin-facts.ts +11 -3
- package/src/services/app-approval.ts +34 -30
- package/src/services/app-bootstrap.ts +104 -21
- package/src/services/app-details.ts +3 -2
- 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 +42 -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/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 +1 -1
- 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/logging/index.ts +6 -3
- package/src/services/logging/trace.ts +1 -1
- 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/index.ts +1 -1
- package/src/services/settings/store.ts +1 -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/shared-text/documents.ts +2 -2
- package/src/ssr/compact-shell.browser-harness.ts +2 -0
- 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/src/api/admin-ai.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { sql } from "bun";
|
|
2
1
|
import { Hono, type MiddlewareHandler } from "hono";
|
|
3
2
|
import { z } from "zod";
|
|
4
3
|
import { setAiCredential } from "../ai/credentials";
|
|
@@ -15,6 +14,7 @@ import {
|
|
|
15
14
|
testAiProvider,
|
|
16
15
|
} from "../ai/provider-connections";
|
|
17
16
|
import { getAiRequestHeaders, setAiRequestHeaders } from "../ai/request-headers";
|
|
17
|
+
import { sql } from "../runtime/clients";
|
|
18
18
|
import { type AuthContext, auth, v } from "../server";
|
|
19
19
|
import { ConfigConflict } from "../services/config-conflict";
|
|
20
20
|
import { ImageMetadataError } from "../services/image-metadata";
|
|
@@ -3,7 +3,7 @@ import { Hono } from "hono";
|
|
|
3
3
|
import { describeRoute } from "hono-openapi";
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
import { validateAppKeyAppId } from "../app-key";
|
|
6
|
-
import { type AuthContext, auth, respond, v } from "../server";
|
|
6
|
+
import { type AuthContext, auth, jsonResponse, respond, v } from "../server";
|
|
7
7
|
import { noTransform } from "../server/middleware/no-transform";
|
|
8
8
|
import { appKeys } from "../services/app-keys";
|
|
9
9
|
|
|
@@ -21,9 +21,49 @@ const pagination = z.object({
|
|
|
21
21
|
perPage: z.coerce.number().int().min(1).max(500).default(100),
|
|
22
22
|
});
|
|
23
23
|
const input = z
|
|
24
|
-
.object({
|
|
24
|
+
.object({
|
|
25
|
+
publicKey: z.string().length(43),
|
|
26
|
+
name: z.string().trim().min(1).max(120).optional(),
|
|
27
|
+
expiresAt: z.iso.datetime().optional(),
|
|
28
|
+
grants: z.array(z.literal("oauth:issue")).max(1).optional(),
|
|
29
|
+
})
|
|
25
30
|
.strict();
|
|
26
31
|
|
|
32
|
+
const keyListResponse = z.object({
|
|
33
|
+
installationId: z.uuid(),
|
|
34
|
+
items: z.array(
|
|
35
|
+
z.object({
|
|
36
|
+
keyId: z.uuid(),
|
|
37
|
+
appId: z.string(),
|
|
38
|
+
serviceAccountId: z.uuid(),
|
|
39
|
+
kind: z.literal("app_key"),
|
|
40
|
+
name: z.string(),
|
|
41
|
+
publicKey: z.string(),
|
|
42
|
+
grants: z.array(z.literal("oauth:issue")),
|
|
43
|
+
fingerprint: z.string(),
|
|
44
|
+
status: z.enum(["active", "expired", "revoked"]),
|
|
45
|
+
accountStatus: z.enum(["active", "disabled"]),
|
|
46
|
+
createdAt: z.iso.datetime(),
|
|
47
|
+
createdBy: z.uuid().nullable(),
|
|
48
|
+
expiresAt: z.iso.datetime().nullable(),
|
|
49
|
+
revokedAt: z.iso.datetime().nullable(),
|
|
50
|
+
revokedBy: z.uuid().nullable(),
|
|
51
|
+
lastUsedAt: z.iso.datetime().nullable(),
|
|
52
|
+
}),
|
|
53
|
+
),
|
|
54
|
+
lastRejectedStart: z
|
|
55
|
+
.object({
|
|
56
|
+
at: z.iso.datetime(),
|
|
57
|
+
reason: z.enum(["revoked", "expired", "disabled", "connections_not_permitted"]),
|
|
58
|
+
keyId: z.uuid().nullable(),
|
|
59
|
+
})
|
|
60
|
+
.nullable(),
|
|
61
|
+
page: z.number().int(),
|
|
62
|
+
perPage: z.number().int(),
|
|
63
|
+
total: z.number().int(),
|
|
64
|
+
hasNext: z.boolean(),
|
|
65
|
+
});
|
|
66
|
+
|
|
27
67
|
export const createAdminAppKeyRoutes = (
|
|
28
68
|
authenticate: MiddlewareHandler<AuthContext> = auth.requireRole("admin"),
|
|
29
69
|
service: typeof appKeys = appKeys,
|
|
@@ -44,7 +84,11 @@ export const createAdminAppKeyRoutes = (
|
|
|
44
84
|
)
|
|
45
85
|
.get(
|
|
46
86
|
"/:appId/keys",
|
|
47
|
-
describeRoute({
|
|
87
|
+
describeRoute({
|
|
88
|
+
tags: ["App keys"],
|
|
89
|
+
summary: "List app public keys, lifecycle metadata and last rejected start",
|
|
90
|
+
responses: { 200: jsonResponse(keyListResponse, "App keys and last rejected start") },
|
|
91
|
+
}),
|
|
48
92
|
v("param", params),
|
|
49
93
|
v("query", pagination),
|
|
50
94
|
(c) => respond(c, () => service.list({ actor: c.get("user"), ...c.req.valid("param"), ...c.req.valid("query") })),
|
|
@@ -6,13 +6,13 @@
|
|
|
6
6
|
* core app mounts it under `/api/admin/core/settings`.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import { sql } from "bun";
|
|
10
9
|
import { Hono } from "hono";
|
|
11
10
|
import { describeRoute } from "hono-openapi";
|
|
12
11
|
import { z } from "zod";
|
|
13
12
|
import { ObjectStoreConfigurationError, ObjectStoreConnectionError, testObjectStoreConnection } from "../_internal/object-store-config";
|
|
14
13
|
import { listApps } from "../_internal/registry";
|
|
15
14
|
import { enrichDirtyAiConversations } from "../ai/enrich";
|
|
15
|
+
import { sql } from "../runtime/clients";
|
|
16
16
|
import { type AuthContext, auth, getLocale, jsonResponse, requiresAdmin, v } from "../server";
|
|
17
17
|
import { settingsDeleteLegacyKeys, settingsListLegacyKeys, settingsService } from "../services";
|
|
18
18
|
import { readAccountCategoryPolicy } from "../services/account-category-policy";
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { sql } from "bun";
|
|
2
1
|
import { type Context, Hono, type MiddlewareHandler } from "hono";
|
|
3
2
|
import { describeRoute, validator } from "hono-openapi";
|
|
4
3
|
import { z } from "zod";
|
|
@@ -13,6 +12,7 @@ import {
|
|
|
13
12
|
MailRetentionSchema,
|
|
14
13
|
MailStatusSchema,
|
|
15
14
|
} from "../contracts/outgoing-mail";
|
|
15
|
+
import { sql } from "../runtime/clients";
|
|
16
16
|
import { type AuthContext, auth, jsonResponse, requiresAdmin } from "../server";
|
|
17
17
|
import { noTransform } from "../server/middleware/no-transform";
|
|
18
18
|
import { audit } from "../services/audit";
|
package/src/api/app-bootstrap.ts
CHANGED
|
@@ -11,8 +11,17 @@ import {
|
|
|
11
11
|
import { connections } from "../runtime/connections";
|
|
12
12
|
import { clientIpBucket } from "../server/client-ip";
|
|
13
13
|
import { type RateLimitResult, ratelimit } from "../server/ratelimit";
|
|
14
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
authenticateBootstrap,
|
|
16
|
+
BootstrapProofError,
|
|
17
|
+
bootstrapRateLimit,
|
|
18
|
+
clearRejectedBootstrapStart,
|
|
19
|
+
consumeBootstrapProof,
|
|
20
|
+
RejectedBootstrapStart,
|
|
21
|
+
recordRejectedBootstrapStart,
|
|
22
|
+
} from "../services/app-bootstrap";
|
|
15
23
|
import { touchAppKey } from "../services/app-keys";
|
|
24
|
+
import { logger } from "../services/logging";
|
|
16
25
|
|
|
17
26
|
type BootstrapContext = { Variables: { bootstrapBody: Uint8Array<ArrayBuffer> } };
|
|
18
27
|
|
|
@@ -110,6 +119,14 @@ export const createAppBootstrapRoutes = (dependencies: Dependencies = {}) => {
|
|
|
110
119
|
try {
|
|
111
120
|
verified = await authenticateBootstrap(header.slice(7), { method: c.req.method, path: url.pathname, body }, dependencies.db);
|
|
112
121
|
} catch (error) {
|
|
122
|
+
if (error instanceof RejectedBootstrapStart) {
|
|
123
|
+
try {
|
|
124
|
+
const rate = dependencies.admit ? await dependencies.admit(error.identity.keyId) : await admit("key", error.identity.keyId);
|
|
125
|
+
if (!rate.limited) await recordRejectedBootstrapStart(error, dependencies.db);
|
|
126
|
+
} catch {
|
|
127
|
+
logger("app-bootstrap").warn("Could not record rejected app start", { appId: error.identity.appId });
|
|
128
|
+
}
|
|
129
|
+
}
|
|
113
130
|
if (error instanceof BootstrapProofError) return c.json({ code: error.code }, error.code === "invalid_proof" ? 401 : 403);
|
|
114
131
|
throw error;
|
|
115
132
|
}
|
|
@@ -124,6 +141,11 @@ export const createAppBootstrapRoutes = (dependencies: Dependencies = {}) => {
|
|
|
124
141
|
await touchAppKey(identity, dependencies.db);
|
|
125
142
|
const response = JSON.stringify(BootstrapResponseSchema.parse({ identity, connections: snapshot }));
|
|
126
143
|
if (Buffer.byteLength(response) > BOOTSTRAP_RESPONSE_BYTES) throw new Error("Bootstrap response exceeds protocol budget");
|
|
144
|
+
try {
|
|
145
|
+
await clearRejectedBootstrapStart(identity.appId, dependencies.db);
|
|
146
|
+
} catch {
|
|
147
|
+
logger("app-bootstrap").warn("Could not clear rejected app start", { appId: identity.appId });
|
|
148
|
+
}
|
|
127
149
|
c.header("Content-Type", "application/json");
|
|
128
150
|
return c.body(response);
|
|
129
151
|
},
|
package/src/api/app-events.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { sql } from "bun";
|
|
2
1
|
import { Hono, type MiddlewareHandler } from "hono";
|
|
3
2
|
import { describeRoute } from "hono-openapi";
|
|
4
3
|
import { z } from "zod";
|
|
5
4
|
import { AppDetailsSchema } from "../contracts/app-details";
|
|
6
5
|
import { AppEventEnvelopeSchema } from "../contracts/app-events";
|
|
7
6
|
import { CapabilityQualifiedIdSchema } from "../contracts/capabilities";
|
|
7
|
+
import { sql } from "../runtime/clients";
|
|
8
8
|
import { type AuthContext, auth, expectUserBackedActor, getLocale, v } from "../server";
|
|
9
9
|
import { AppAccessManagementError } from "../services/app-access-management";
|
|
10
10
|
import { appDetails } from "../services/app-details";
|
package/src/api/auth.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { sql } from "bun";
|
|
2
1
|
import { type Context, Hono } from "hono";
|
|
3
2
|
import { describeRoute } from "hono-openapi";
|
|
4
3
|
import { z } from "zod";
|
|
5
4
|
import { env } from "../config";
|
|
6
5
|
import { ChangeExpiredPasswordSchema } from "../contracts";
|
|
7
6
|
import { coreUserEventPublisher } from "../events/core-user-created";
|
|
7
|
+
import { sql } from "../runtime/clients";
|
|
8
8
|
import { type AuthContext, auth, getLocale, jsonResponse, rateLimit, respond, v } from "../server";
|
|
9
9
|
import { accounts, audit, authFlows, getFreeIpaConfig, logger, webauthn } from "../services";
|
|
10
10
|
import { isAccountCategoryAllowed } from "../services/account-category-policy";
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import type { SQL } from "bun";
|
|
2
|
+
import type { Context } from "hono";
|
|
3
|
+
import { getConnInfo } from "hono/bun";
|
|
4
|
+
import { CAPABILITY_MAX_REQUEST_BYTES } from "../contracts/capabilities";
|
|
5
|
+
import { clientIpBucket } from "../server/client-ip";
|
|
6
|
+
import { type RateLimitResult, ratelimit } from "../server/ratelimit";
|
|
7
|
+
import { REQUEST_IDLE_TIMEOUT_SECONDS } from "../server/request-timeout";
|
|
8
|
+
import {
|
|
9
|
+
authenticateAppProof,
|
|
10
|
+
BootstrapProofError,
|
|
11
|
+
bootstrapRateLimit,
|
|
12
|
+
consumeBootstrapProof,
|
|
13
|
+
inspectAppProof,
|
|
14
|
+
} from "../services/app-bootstrap";
|
|
15
|
+
import { touchAppKey } from "../services/app-keys";
|
|
16
|
+
import type { AuthenticatedWorkload } from "../services/identity/workload-auth";
|
|
17
|
+
|
|
18
|
+
type Dependencies = {
|
|
19
|
+
db?: SQL;
|
|
20
|
+
ingress?: (peer: string) => Promise<RateLimitResult>;
|
|
21
|
+
admit?: (keyId: string) => Promise<RateLimitResult>;
|
|
22
|
+
};
|
|
23
|
+
const admission = async (kind: "ingress" | "key", id: string) =>
|
|
24
|
+
ratelimit({ id: `app-bootstrap-${kind}`, limit: await bootstrapRateLimit() }).check(id);
|
|
25
|
+
|
|
26
|
+
/** Core-only request authentication; the body remains available to the domain handler. */
|
|
27
|
+
export const authenticateSignedAppRequest = async (
|
|
28
|
+
c: Context,
|
|
29
|
+
scope: "identity:invoke" | "oauth:issue",
|
|
30
|
+
dependencies: Dependencies = {},
|
|
31
|
+
): Promise<AuthenticatedWorkload | Response> => {
|
|
32
|
+
c.header("Cache-Control", "no-store");
|
|
33
|
+
const deadline = AbortSignal.timeout(REQUEST_IDLE_TIMEOUT_SECONDS * 1000);
|
|
34
|
+
const signal = AbortSignal.any([c.req.raw.signal, deadline]);
|
|
35
|
+
const reject = (status: 401 | 403 | 413 | 429 | 503 | 504, code: string, message: string) => c.json({ code, message }, status);
|
|
36
|
+
const limited = (rate: RateLimitResult) => {
|
|
37
|
+
c.header("Retry-After", String(Math.max(1, Math.ceil(rate.resetIn / 1000))));
|
|
38
|
+
return reject(429, "RATE_LIMITED", "App request admission limit reached");
|
|
39
|
+
};
|
|
40
|
+
try {
|
|
41
|
+
let peer = "unknown";
|
|
42
|
+
try {
|
|
43
|
+
peer = getConnInfo(c).remote.address ?? peer;
|
|
44
|
+
} catch {
|
|
45
|
+
/* In-process requests have no socket. */
|
|
46
|
+
}
|
|
47
|
+
const ingress = await (dependencies.ingress ?? ((id) => admission("ingress", id)))(clientIpBucket(peer));
|
|
48
|
+
if (ingress.limited) return limited(ingress);
|
|
49
|
+
const header = c.req.header("Authorization") ?? "";
|
|
50
|
+
if (!header.startsWith("Bearer ")) return reject(401, "invalid_proof", "A valid app proof is required");
|
|
51
|
+
inspectAppProof(header.slice(7));
|
|
52
|
+
const tooLarge = () => reject(413, "body_too_large", "App request exceeds the byte limit");
|
|
53
|
+
if (Number(c.req.header("Content-Length")) > CAPABILITY_MAX_REQUEST_BYTES) {
|
|
54
|
+
void c.req.raw.body?.cancel().catch(() => undefined);
|
|
55
|
+
return tooLarge();
|
|
56
|
+
}
|
|
57
|
+
signal.throwIfAborted();
|
|
58
|
+
const copy = c.req.raw.clone();
|
|
59
|
+
const reader = copy.body?.getReader();
|
|
60
|
+
const bytes = new Uint8Array(CAPABILITY_MAX_REQUEST_BYTES);
|
|
61
|
+
let total = 0;
|
|
62
|
+
if (reader) {
|
|
63
|
+
const cancel = () => {
|
|
64
|
+
// A tee cancels its source only after both branches cancel. Never await a stalled source.
|
|
65
|
+
void reader.cancel(signal.reason).catch(() => undefined);
|
|
66
|
+
void c.req.raw.body?.cancel(signal.reason).catch(() => undefined);
|
|
67
|
+
};
|
|
68
|
+
signal.addEventListener("abort", cancel, { once: true });
|
|
69
|
+
try {
|
|
70
|
+
signal.throwIfAborted();
|
|
71
|
+
while (true) {
|
|
72
|
+
const chunk = await reader.read();
|
|
73
|
+
if (chunk.done) break;
|
|
74
|
+
if (total + chunk.value.byteLength > bytes.byteLength) {
|
|
75
|
+
cancel();
|
|
76
|
+
return tooLarge();
|
|
77
|
+
}
|
|
78
|
+
bytes.set(chunk.value, total);
|
|
79
|
+
total += chunk.value.byteLength;
|
|
80
|
+
}
|
|
81
|
+
signal.throwIfAborted();
|
|
82
|
+
} catch (error) {
|
|
83
|
+
cancel();
|
|
84
|
+
if (signal.aborted) throw signal.reason;
|
|
85
|
+
throw error;
|
|
86
|
+
} finally {
|
|
87
|
+
signal.removeEventListener("abort", cancel);
|
|
88
|
+
reader.releaseLock();
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
const url = new URL(c.req.url);
|
|
92
|
+
const verified = await authenticateAppProof(
|
|
93
|
+
header.slice(7),
|
|
94
|
+
{ method: c.req.method, path: url.pathname + url.search, body: bytes.slice(0, total) },
|
|
95
|
+
dependencies.db,
|
|
96
|
+
);
|
|
97
|
+
// Recheck the operator-owned key grant on every transmission.
|
|
98
|
+
if (scope === "oauth:issue" && !verified.key.oauthIssueAllowed)
|
|
99
|
+
return reject(403, "scope_not_permitted", "App is not permitted to issue OAuth credentials");
|
|
100
|
+
const declaredApp = c.req.header("X-Cloud-App-Id");
|
|
101
|
+
if (declaredApp && declaredApp !== verified.identity.appId) return reject(403, "wrong_app", "App identity does not match the proof");
|
|
102
|
+
const rate = await (dependencies.admit ?? ((id) => admission("key", id)))(verified.identity.keyId);
|
|
103
|
+
if (rate.limited) return limited(rate);
|
|
104
|
+
if (!(await consumeBootstrapProof(verified.identity.keyId, verified.jti, verified.exp, dependencies.db)))
|
|
105
|
+
return reject(403, "replayed_proof", "App proof has already been used");
|
|
106
|
+
await touchAppKey(verified.identity, dependencies.db);
|
|
107
|
+
return {
|
|
108
|
+
appId: verified.identity.appId,
|
|
109
|
+
keyId: verified.identity.keyId,
|
|
110
|
+
serviceAccountId: verified.key.serviceAccountId,
|
|
111
|
+
credentialId: verified.identity.keyId,
|
|
112
|
+
scope: "identity:invoke",
|
|
113
|
+
};
|
|
114
|
+
} catch (error) {
|
|
115
|
+
if (signal.aborted && error === signal.reason) {
|
|
116
|
+
void c.req.raw.body?.cancel(signal.reason).catch(() => undefined);
|
|
117
|
+
return c.req.raw.signal.aborted
|
|
118
|
+
? Response.json(
|
|
119
|
+
{ code: "REQUEST_CANCELLED", message: "App request was cancelled" },
|
|
120
|
+
{ status: 499, headers: { "Cache-Control": "no-store" } },
|
|
121
|
+
)
|
|
122
|
+
: reject(504, "DEADLINE_EXCEEDED", "App request body deadline exceeded");
|
|
123
|
+
}
|
|
124
|
+
if (error instanceof BootstrapProofError)
|
|
125
|
+
return reject(error.code === "invalid_proof" ? 401 : 403, error.code, "App proof was rejected");
|
|
126
|
+
c.header("Retry-After", "1");
|
|
127
|
+
return reject(503, "app_proof_unavailable", "App request authentication is temporarily unavailable");
|
|
128
|
+
}
|
|
129
|
+
};
|
package/src/api/capabilities.ts
CHANGED
|
@@ -61,11 +61,12 @@ import { noTransform } from "../server/middleware/no-transform";
|
|
|
61
61
|
import { hasOAuthScope } from "../server/oauth-scope";
|
|
62
62
|
import { ratelimit } from "../server/ratelimit";
|
|
63
63
|
import { appAccessAuthority, canUseApp } from "../services/app-access";
|
|
64
|
+
import { bootstrapRateLimit } from "../services/app-bootstrap";
|
|
64
65
|
import { discoverableApps } from "../services/app-discovery";
|
|
65
66
|
import { authorityFromParentInvocation, invocationAuthorityFromRequest } from "../services/identity/invocation-authority";
|
|
66
67
|
import { capabilityInvocationOperation } from "../services/identity/invocation-operations";
|
|
67
68
|
import type { CloudInvocationClaims, InvocationAuthority } from "../services/identity/invocation-token";
|
|
68
|
-
import { normalizeInvocationRequestId, signAppQueryInvocation } from "../services/identity/invocation-token";
|
|
69
|
+
import { normalizeInvocationRequestId, signAppCapabilityInvocation, signAppQueryInvocation } from "../services/identity/invocation-token";
|
|
69
70
|
import { issueInvocationToken as signInvocationToken } from "../services/identity/issue-invocation";
|
|
70
71
|
import { withActiveIdentitySigner } from "../services/identity/key-ring";
|
|
71
72
|
import {
|
|
@@ -128,6 +129,7 @@ export type CapabilityRouteDependencies = {
|
|
|
128
129
|
withMandateIssueAuthority?: typeof withMandateIssueAuthority;
|
|
129
130
|
queryTimeoutMs?: number;
|
|
130
131
|
actionTimeoutMs?: number;
|
|
132
|
+
checkAppCapabilityRate?: (appId: string) => Promise<{ limited: boolean; resetIn: number }>;
|
|
131
133
|
checkAppQueryRate?: (appId: string) => Promise<{ limited: boolean }>;
|
|
132
134
|
reviewTimeoutMs?: number;
|
|
133
135
|
appUrl?: () => Promise<string>;
|
|
@@ -135,6 +137,7 @@ export type CapabilityRouteDependencies = {
|
|
|
135
137
|
|
|
136
138
|
export type CapabilityDispatchDependencies = Pick<
|
|
137
139
|
CapabilityRouteDependencies,
|
|
140
|
+
| "checkAppCapabilityRate"
|
|
138
141
|
| "checkAppQueryRate"
|
|
139
142
|
| "getApp"
|
|
140
143
|
| "getCapability"
|
|
@@ -293,6 +296,8 @@ export type CapabilityDispatchParams = {
|
|
|
293
296
|
deadline?: number;
|
|
294
297
|
/** Core-only authenticated workload. Never populated from request JSON. */
|
|
295
298
|
workload?: AuthenticatedWorkload;
|
|
299
|
+
/** Verified Ed25519 app identity, separate from person and legacy workload authority. */
|
|
300
|
+
appCaller?: { appId: string; keyId: string };
|
|
296
301
|
mandate?: MandatedCapabilityAuthority;
|
|
297
302
|
callingAppId?: string;
|
|
298
303
|
/** Internal observation only: called immediately before attempting the app request. */
|
|
@@ -363,6 +368,39 @@ const runCapabilityDispatch = async (
|
|
|
363
368
|
}
|
|
364
369
|
if ("appOnly" in operation && operation.appOnly !== undefined && !params.workload)
|
|
365
370
|
return errorResponse("FORBIDDEN", "App authority required for this query", 403);
|
|
371
|
+
if (params.appCaller) {
|
|
372
|
+
if (
|
|
373
|
+
params.authority ||
|
|
374
|
+
params.workload ||
|
|
375
|
+
params.mandate ||
|
|
376
|
+
params.parentInvocation ||
|
|
377
|
+
params.review ||
|
|
378
|
+
!operation.appPermission ||
|
|
379
|
+
operation.stream
|
|
380
|
+
)
|
|
381
|
+
return errorResponse("FORBIDDEN", "This capability does not accept app authority", 403);
|
|
382
|
+
try {
|
|
383
|
+
const app = params.appCaller;
|
|
384
|
+
const caller = await deadline.wait(() => (params.dependencies?.getApp ?? getApp)(app.appId));
|
|
385
|
+
if (!caller) return errorResponse("APP_UNAVAILABLE", "Calling app registry entry unavailable", 503);
|
|
386
|
+
if (!caller.platformPermissions?.includes(operation.appPermission))
|
|
387
|
+
return errorResponse("APP_CAPABILITY_NOT_ADMITTED", `App caller does not declare ${operation.appPermission}`, 403);
|
|
388
|
+
// Same operator-owned per-second budget as signed app ingress; shared by all keys of the app.
|
|
389
|
+
const rate = await deadline.wait(() =>
|
|
390
|
+
(
|
|
391
|
+
params.dependencies?.checkAppCapabilityRate ??
|
|
392
|
+
(async (appId) => ratelimit({ id: "app-capabilities", limit: await bootstrapRateLimit() }).check(appId))
|
|
393
|
+
)(app.appId),
|
|
394
|
+
);
|
|
395
|
+
if (rate.limited)
|
|
396
|
+
return errorResponse("RATE_LIMITED", "App capability rate limit reached", 429, {
|
|
397
|
+
retryAfter: Math.max(1, Math.ceil(rate.resetIn / 1000)),
|
|
398
|
+
});
|
|
399
|
+
} catch {
|
|
400
|
+
deadline.check();
|
|
401
|
+
return errorResponse("APP_UNAVAILABLE", "App capability admission unavailable", 503);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
366
404
|
if (params.workload) {
|
|
367
405
|
if (
|
|
368
406
|
params.authority ||
|
|
@@ -544,6 +582,36 @@ const runCapabilityDispatch = async (
|
|
|
544
582
|
});
|
|
545
583
|
})()
|
|
546
584
|
: await (async () => {
|
|
585
|
+
if (params.appCaller && operation.appPermission) {
|
|
586
|
+
const app = params.appCaller;
|
|
587
|
+
const permission = operation.appPermission;
|
|
588
|
+
return deadline.wait(() =>
|
|
589
|
+
(params.dependencies?.withActiveSigner ?? withActiveIdentitySigner)(
|
|
590
|
+
"invocation",
|
|
591
|
+
async (signer) => {
|
|
592
|
+
deadline.check();
|
|
593
|
+
const signed = await signAppCapabilityInvocation({
|
|
594
|
+
app,
|
|
595
|
+
permission,
|
|
596
|
+
kind: params.kind === "queries" ? "query" : "action",
|
|
597
|
+
targetAppId: params.appId,
|
|
598
|
+
capabilityId: params.capabilityId,
|
|
599
|
+
schemaHash: operation.schemaHash,
|
|
600
|
+
input: input.data,
|
|
601
|
+
requestId,
|
|
602
|
+
signer,
|
|
603
|
+
});
|
|
604
|
+
observed.invocationId = signed.claims.jti;
|
|
605
|
+
return new Headers({
|
|
606
|
+
"content-type": "application/json",
|
|
607
|
+
accept: "application/json",
|
|
608
|
+
authorization: `Bearer ${signed.token}`,
|
|
609
|
+
});
|
|
610
|
+
},
|
|
611
|
+
{ signal, timeoutMs: deadline.remainingMs() },
|
|
612
|
+
),
|
|
613
|
+
);
|
|
614
|
+
}
|
|
547
615
|
if (params.workload) {
|
|
548
616
|
const workload = params.workload;
|
|
549
617
|
return deadline.wait(() =>
|
|
@@ -675,7 +743,11 @@ const runCapabilityDispatch = async (
|
|
|
675
743
|
? {
|
|
676
744
|
appId: params.appId,
|
|
677
745
|
capability: `${params.appId}.${params.capabilityId}`,
|
|
678
|
-
principal:
|
|
746
|
+
principal: params.appCaller
|
|
747
|
+
? `app:${params.appCaller.appId}`
|
|
748
|
+
: claimPrincipal
|
|
749
|
+
? `${claimPrincipal.type}:${claimPrincipal.id}`
|
|
750
|
+
: "anonymous",
|
|
679
751
|
keyHash: capabilityIdempotencyKeyHash(idempotencyKey.data),
|
|
680
752
|
}
|
|
681
753
|
: null;
|
|
@@ -749,7 +821,7 @@ const runCapabilityDispatch = async (
|
|
|
749
821
|
params.onUpstreamAttempt?.();
|
|
750
822
|
const fetchResponse = () =>
|
|
751
823
|
fetchUpstream(
|
|
752
|
-
`${entry.endpoint}/${params.workload ? "app-queries" : params.kind}/${encodeURIComponent(params.capabilityId)}${params.review ? "/review" : ""}`,
|
|
824
|
+
`${entry.endpoint}/${params.appCaller ? `app-capabilities/${params.kind}` : params.workload ? "app-queries" : params.kind}/${encodeURIComponent(params.capabilityId)}${params.review ? "/review" : ""}`,
|
|
753
825
|
{
|
|
754
826
|
method: "POST",
|
|
755
827
|
headers,
|
|
@@ -986,13 +1058,15 @@ export const dispatchCapability = async (params: CapabilityDispatchParams): Prom
|
|
|
986
1058
|
origin: params.origin,
|
|
987
1059
|
invocationId: observed.invocationId,
|
|
988
1060
|
parentInvocationId: params.parentInvocation?.jti,
|
|
989
|
-
viaAppId: params.workload?.appId ?? (params.parentInvocation ? params.callingAppId : undefined),
|
|
1061
|
+
viaAppId: params.appCaller?.appId ?? params.workload?.appId ?? (params.parentInvocation ? params.callingAppId : undefined),
|
|
990
1062
|
appId: params.appId,
|
|
991
1063
|
capability: `${params.appId}.${params.capabilityId}`,
|
|
992
1064
|
kind: params.kind === "queries" ? "query" : "action",
|
|
993
1065
|
destructive: observed.destructive,
|
|
994
|
-
actorKind: params.workload ? "app" : (principal?.type ?? null),
|
|
1066
|
+
actorKind: params.appCaller || params.workload ? "app" : (principal?.type ?? null),
|
|
995
1067
|
actorId: principal?.id ?? null,
|
|
1068
|
+
actorAppId: params.appCaller?.appId ?? params.workload?.appId,
|
|
1069
|
+
actorKeyId: params.appCaller?.keyId,
|
|
996
1070
|
userId: actor?.kind === "user" ? actor.user.id : (actor?.delegatedUser?.id ?? (principal?.type === "user" ? principal.id : null)),
|
|
997
1071
|
accessSubject: subject && subject.id !== principal?.id ? subject : null,
|
|
998
1072
|
status: executionStatus(outcome.status, errorCode),
|
package/src/api/index.ts
CHANGED
|
@@ -142,6 +142,8 @@ export { createAdminAppKeyRoutes } from "./admin-app-keys";
|
|
|
142
142
|
export { createAppAccessRoutes } from "./app-access";
|
|
143
143
|
/** Core-owned internal startup protocol; applications use @k2b/cloud/start. */
|
|
144
144
|
export { createAppBootstrapRoutes } from "./app-bootstrap";
|
|
145
|
+
/** Core-owned app-key request authentication. */
|
|
146
|
+
export { authenticateSignedAppRequest } from "./authenticate-app-request";
|
|
145
147
|
export {
|
|
146
148
|
CLOUD_MCP_PATH,
|
|
147
149
|
CLOUD_MCP_PROTECTED_RESOURCE_PATH,
|
package/src/api/me.ts
CHANGED
|
@@ -5,7 +5,6 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { err, fail, ok, type Result } from "@k2b/stdlib";
|
|
8
|
-
import { sql } from "bun";
|
|
9
8
|
import { type Context, Hono, type MiddlewareHandler } from "hono";
|
|
10
9
|
import { deleteCookie } from "hono/cookie";
|
|
11
10
|
import { createMiddleware } from "hono/factory";
|
|
@@ -42,6 +41,7 @@ import {
|
|
|
42
41
|
WebAuthnPasskeySchema,
|
|
43
42
|
} from "../contracts";
|
|
44
43
|
import { CapabilityAppIdSchema } from "../contracts/capabilities";
|
|
44
|
+
import { sql } from "../runtime/clients";
|
|
45
45
|
import { type AuthContext, auth, getLocale, getTimeZone, jsonResponse, rateLimit, requiresAuth, respond, v } from "../server";
|
|
46
46
|
import { APP_SESSION_FORBIDDEN } from "../server/middleware/auth";
|
|
47
47
|
import { noTransform } from "../server/middleware/no-transform";
|
package/src/api/search.bench.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { ok } from "@k2b/stdlib";
|
|
2
|
-
import { sql } from "bun";
|
|
3
2
|
import type { MiddlewareHandler } from "hono";
|
|
4
3
|
import { generateKeyPair, jwtVerify, SignJWT } from "jose";
|
|
5
4
|
import { compileCapabilities } from "../_internal/capabilities";
|
|
6
5
|
import { defineCapabilities, UniversalSearchDataSchema, UniversalSearchInputSchema } from "../contracts/capabilities";
|
|
7
6
|
import type { CapabilityRegistryEntry } from "../contracts/registry";
|
|
7
|
+
import { sql } from "../runtime/clients";
|
|
8
8
|
import type { AuthContext } from "../server";
|
|
9
9
|
import type { signInvocationToken } from "../services/identity/invocation-token";
|
|
10
10
|
import type { withActiveIdentitySigner } from "../services/identity/key-ring";
|
package/src/app-pwa/service.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { TransactionSQL } from "bun";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import { GeoAutocompleteInputSchema } from "../contracts/geo";
|
|
4
4
|
import { isInstallablePath } from "../contracts/installable";
|
|
5
5
|
import { PWA_LIMITS } from "../contracts/pwa";
|
|
6
|
+
import { sql } from "../runtime/clients";
|
|
6
7
|
import { ratelimit } from "../server/ratelimit";
|
|
7
8
|
import { REQUEST_IDLE_TIMEOUT_SECONDS } from "../server/request-timeout";
|
|
8
9
|
import { getEffectivePermission, hasPermission } from "../server/services/access";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { isPromise } from "node:util/types";
|
|
2
2
|
import type { ReservedSQL, SQL, TransactionSQL } from "bun";
|
|
3
|
-
import
|
|
3
|
+
import { createSql } from "../runtime/clients";
|
|
4
4
|
import { REQUEST_IDLE_TIMEOUT_SECONDS } from "../server/request-timeout";
|
|
5
5
|
import { boundTransaction } from "./authority";
|
|
6
6
|
import { AppPwaError } from "./contracts";
|
|
@@ -22,8 +22,9 @@ const cancellationPool = (db: SQL) => {
|
|
|
22
22
|
const connection: Record<string, string | number> = {};
|
|
23
23
|
for (let index = 0; index + 1 < parts.length; index += 2) connection[parts[index]!] = parts[index + 1]!;
|
|
24
24
|
connection.statement_timeout = requestBudget;
|
|
25
|
-
return
|
|
25
|
+
return createSql({
|
|
26
26
|
...options,
|
|
27
|
+
adapter: "postgres",
|
|
27
28
|
// Bun adds SNI to normalized prefer options. Passing that generated object
|
|
28
29
|
// back as explicit TLS upgrades prefer to require. Recreate its default SNI
|
|
29
30
|
// from hostname instead; explicit verification options remain unchanged.
|
|
@@ -35,6 +36,7 @@ const cancellationPool = (db: SQL) => {
|
|
|
35
36
|
maxLifetime: REQUEST_IDLE_TIMEOUT_SECONDS,
|
|
36
37
|
onconnect: undefined,
|
|
37
38
|
onclose: undefined,
|
|
39
|
+
url: `postgres://localhost?sslmode=${modes[sslMode]}`,
|
|
38
40
|
});
|
|
39
41
|
};
|
|
40
42
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { sql } from "bun";
|
|
2
1
|
import { createCapabilityDeadline } from "../_internal/capability-timeout";
|
|
3
2
|
import type { CapabilityOrigin } from "../contracts/capabilities";
|
|
3
|
+
import { sql } from "../runtime/clients";
|
|
4
4
|
import { REQUEST_IDLE_TIMEOUT_SECONDS } from "../server/request-timeout";
|
|
5
5
|
import { pruneApprovalRequests } from "../services/approvals";
|
|
6
6
|
import { logger } from "../services/logging";
|
|
@@ -65,6 +65,8 @@ export type CapabilityExecutionInput = {
|
|
|
65
65
|
destructive: boolean;
|
|
66
66
|
actorKind?: "user" | "service_account" | "app" | null;
|
|
67
67
|
actorId?: string | null;
|
|
68
|
+
actorAppId?: string | null;
|
|
69
|
+
actorKeyId?: string | null;
|
|
68
70
|
userId?: string | null;
|
|
69
71
|
/** Recorded only when the access subject differs from the acting principal. */
|
|
70
72
|
accessSubject?: { type: "user" | "service_account"; id: string } | null;
|
|
@@ -92,6 +94,8 @@ export type CapabilityExecution = {
|
|
|
92
94
|
destructive: boolean;
|
|
93
95
|
actorKind: "user" | "service_account" | "app" | null;
|
|
94
96
|
actorId: string | null;
|
|
97
|
+
actorAppId?: string | null;
|
|
98
|
+
actorKeyId?: string | null;
|
|
95
99
|
userId: string | null;
|
|
96
100
|
accessSubjectType: "user" | "service_account" | null;
|
|
97
101
|
accessSubjectId: string | null;
|
|
@@ -122,12 +126,12 @@ export const recordCapabilityExecution = async (input: CapabilityExecutionInput)
|
|
|
122
126
|
const [row] = await db<{ id: string }[]>`
|
|
123
127
|
INSERT INTO capabilities.executions (
|
|
124
128
|
request_id, invocation_id, parent_invocation_id, via_app_id, origin, app_id, capability, kind, destructive,
|
|
125
|
-
actor_kind, actor_id, user_id, access_subject_type, access_subject_id,
|
|
129
|
+
actor_kind, actor_id, actor_app_id, actor_key_id, user_id, access_subject_type, access_subject_id,
|
|
126
130
|
status, error_code, input_meta, output_meta, idempotency_key, replayed,
|
|
127
131
|
started_at, completed_at, duration_ms
|
|
128
132
|
) VALUES (
|
|
129
133
|
${input.requestId}, ${input.invocationId ?? null}, ${input.parentInvocationId ?? null}, ${input.viaAppId ?? null}, ${input.origin}, ${input.appId}, ${input.capability}, ${input.kind}, ${input.destructive},
|
|
130
|
-
${input.actorKind ?? null}, ${input.actorId ?? null}, ${input.userId ?? null},
|
|
134
|
+
${input.actorKind ?? null}, ${input.actorId ?? null}, ${input.actorAppId ?? null}, ${input.actorKeyId ?? null}::uuid, ${input.userId ?? null},
|
|
131
135
|
${input.accessSubject?.type ?? null}, ${input.accessSubject?.id ?? null},
|
|
132
136
|
${input.status}, ${input.errorCode ?? null},
|
|
133
137
|
${input.inputMeta ? JSON.stringify(input.inputMeta) : null}::text::jsonb,
|
|
@@ -177,7 +181,7 @@ const columns = () => sql`
|
|
|
177
181
|
execution.id::text AS id, execution.request_id AS "requestId",
|
|
178
182
|
execution.invocation_id::text AS "invocationId", execution.parent_invocation_id::text AS "parentInvocationId", execution.via_app_id AS "viaAppId", execution.origin, execution.app_id AS "appId",
|
|
179
183
|
execution.capability, execution.kind, execution.destructive,
|
|
180
|
-
execution.actor_kind AS "actorKind", execution.actor_id::text AS "actorId", execution.user_id::text AS "userId",
|
|
184
|
+
execution.actor_app_id AS "actorAppId", execution.actor_key_id::text AS "actorKeyId", execution.actor_kind AS "actorKind", execution.actor_id::text AS "actorId", execution.user_id::text AS "userId",
|
|
181
185
|
execution.access_subject_type AS "accessSubjectType", execution.access_subject_id::text AS "accessSubjectId",
|
|
182
186
|
execution.status, execution.error_code AS "errorCode", execution.input_meta AS "inputMeta",
|
|
183
187
|
execution.output_meta AS "outputMeta", execution.idempotency_key AS "idempotencyKey", execution.replayed,
|
|
@@ -4,7 +4,7 @@ import { geoMapContract } from "../contracts/geo-map";
|
|
|
4
4
|
import { geoRouteContract } from "../contracts/geo-route";
|
|
5
5
|
import { type CapabilityCaller, invokeCapabilityWithDataSchema } from "./server";
|
|
6
6
|
|
|
7
|
-
/** Calls Core using the caller's interactive identity, invocation transport, or durable mandate. */
|
|
7
|
+
/** Calls Core using the caller's interactive identity, invocation transport, app authority, or durable mandate. */
|
|
8
8
|
export const geoClient = {
|
|
9
9
|
mapStyle: (input: z.input<typeof geoMapContract.style.input>, caller: CapabilityCaller) =>
|
|
10
10
|
invokeCapabilityWithDataSchema(
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { sql } from "
|
|
1
|
+
import { sql } from "../runtime/clients";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Platform-owned capability execution history.
|
|
@@ -58,6 +58,8 @@ export const migrateCloudCapabilities = async (): Promise<void> => {
|
|
|
58
58
|
await sql`ALTER TABLE capabilities.executions ADD COLUMN IF NOT EXISTS invocation_id UUID`.simple();
|
|
59
59
|
await sql`ALTER TABLE capabilities.executions ADD COLUMN IF NOT EXISTS parent_invocation_id UUID`.simple();
|
|
60
60
|
await sql`ALTER TABLE capabilities.executions ADD COLUMN IF NOT EXISTS via_app_id TEXT`.simple();
|
|
61
|
+
await sql`ALTER TABLE capabilities.executions ADD COLUMN IF NOT EXISTS actor_app_id TEXT`.simple();
|
|
62
|
+
await sql`ALTER TABLE capabilities.executions ADD COLUMN IF NOT EXISTS actor_key_id UUID`.simple();
|
|
61
63
|
|
|
62
64
|
await sql`DO $$ BEGIN
|
|
63
65
|
IF EXISTS (SELECT 1 FROM pg_constraint WHERE conrelid = 'capabilities.executions'::regclass
|
|
@@ -33,7 +33,7 @@ export const readResourcePrincipals = async (options: {
|
|
|
33
33
|
const ref = CloudResourceRefSchema.parse(options.ref);
|
|
34
34
|
// Core issues the credential automatically; an explicit CLOUD_APP_CREDENTIAL override wins.
|
|
35
35
|
const credential = getAppWorkloadCredential();
|
|
36
|
-
if (!credential) return { state: "not_configured", reason: "no_credential" };
|
|
36
|
+
if (!env.CLOUD_APP_KEY && !credential) return { state: "not_configured", reason: "no_credential" };
|
|
37
37
|
if (!env.CLOUD_CORE_INTERNAL_ORIGIN) return { state: "not_configured", reason: "no_internal_origin" };
|
|
38
38
|
const callerAppId = getProcessApplicationId();
|
|
39
39
|
if (!callerAppId) return { state: "not_configured", reason: "no_app_identity" };
|
|
@@ -54,7 +54,7 @@ export const readResourcePrincipals = async (options: {
|
|
|
54
54
|
ResourceReadableDataSchema,
|
|
55
55
|
{
|
|
56
56
|
workload: { appId: callerAppId },
|
|
57
|
-
authorization: `Bearer ${credential}
|
|
57
|
+
...(credential ? { authorization: `Bearer ${credential}` } : {}),
|
|
58
58
|
signal: options.signal,
|
|
59
59
|
locale: options.locale,
|
|
60
60
|
requestId: options.requestId,
|