@k2b/cloud 0.43.0 → 0.44.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 +7 -2
- package/src/_internal/app-access-request.ts +13 -0
- package/src/_internal/app-access-resources.ts +141 -0
- package/src/_internal/app-event-catalog.ts +40 -0
- package/src/_internal/app-event-schemas.ts +127 -0
- package/src/_internal/application-package.ts +40 -0
- package/src/_internal/built-in-app-ids.json +27 -0
- package/src/_internal/capabilities.ts +158 -12
- package/src/_internal/capability-origin.ts +4 -0
- package/src/_internal/capability-streams.ts +7 -5
- package/src/_internal/define-app.ts +57 -6
- package/src/_internal/object-store-area.ts +9 -0
- package/src/_internal/object-store-config.ts +101 -0
- package/src/_internal/object-store-key.ts +14 -0
- package/src/_internal/process-identity.ts +7 -2
- package/src/_internal/registry-validation.ts +11 -1
- package/src/_internal/registry.ts +8 -1
- package/src/_internal/runtime-context.ts +8 -1
- package/src/access/AppAccessEditor.tsx +2 -0
- package/src/access/PermissionEditor.tsx +9 -3
- package/src/access/PrincipalPicker.tsx +14 -4
- package/src/access/ResourceApiKeys.tsx +3 -4
- package/src/access/app-access-messages.ts +6 -0
- package/src/account/ProfilePictureDialog.tsx +699 -0
- package/src/account/profile-picture-messages.ts +269 -0
- package/src/account/ui.ts +7 -0
- package/src/ai/admin.ts +3 -0
- package/src/ai/app-frame.ts +12 -0
- package/src/ai/application-tasks.ts +101 -0
- package/src/ai/assistant-models.ts +126 -64
- package/src/ai/capabilities.ts +19 -4
- package/src/ai/chart-block.ts +63 -30
- package/src/ai/chat/chart-block.tsx +12 -3
- package/src/ai/chat/composer-adapter.ts +8 -7
- package/src/ai/chat/message-actions.tsx +1 -1
- package/src/ai/chat/message-utils.ts +2 -2
- package/src/ai/chat/presentation.tsx +22 -1
- package/src/ai/chat-quotas.ts +5 -4
- package/src/ai/chat-tasks.ts +2 -2
- package/src/ai/client/controller.ts +11 -10
- package/src/ai/client/projection.ts +3 -3
- package/src/ai/compaction.ts +3 -3
- package/src/ai/credentials.ts +13 -114
- package/src/ai/default-tool-contracts.ts +4 -3
- package/src/ai/dictation-runtime.ts +4 -4
- package/src/ai/enrich.ts +2 -2
- package/src/ai/executor.ts +31 -22
- package/src/ai/http.ts +17 -29
- package/src/ai/index.ts +11 -5
- package/src/ai/inference-calls.ts +5 -4
- package/src/ai/maintenance.ts +1 -1
- package/src/ai/memory-learning-runs.ts +5 -5
- package/src/ai/memory-learning.ts +3 -3
- package/src/ai/message-queue.ts +1 -2
- package/src/ai/migrate.ts +75 -42
- package/src/ai/model-access-migrate.ts +3 -17
- package/src/ai/model-access.ts +44 -85
- package/src/ai/model-card.tsx +134 -0
- package/src/ai/model-catalog.ts +359 -0
- package/src/ai/model-chooser-messages.ts +88 -0
- package/src/ai/model-chooser.tsx +577 -0
- package/src/ai/model-delete.ts +234 -0
- package/src/ai/model-fixture.ts +39 -0
- package/src/ai/model-icon.tsx +36 -0
- package/src/ai/model-overview.ts +53 -0
- package/src/ai/model-usage.ts +45 -0
- package/src/ai/openai-api-messages.ts +0 -4
- package/src/ai/openai-api.ts +26 -9
- package/src/ai/personal-agent.ts +1 -1
- package/src/ai/prefs.ts +12 -10
- package/src/ai/projects-routes.ts +2 -1
- package/src/ai/projects.ts +13 -12
- package/src/ai/protocol.ts +2 -2
- package/src/ai/provider-connections.ts +161 -0
- package/src/ai/provider-errors.ts +55 -6
- package/src/ai/provider-fetch.ts +4 -0
- package/src/ai/provider.ts +1 -1
- package/src/ai/quota-limits.ts +36 -0
- package/src/ai/quota-provider.ts +4 -2
- package/src/ai/quota-report.ts +7 -4
- package/src/ai/quotas.ts +19 -8
- package/src/ai/recent-resources.ts +6 -0
- package/src/ai/request-headers.ts +14 -87
- package/src/ai/resource-refs.ts +6 -2
- package/src/ai/routes.ts +17 -18
- package/src/ai/runtime.ts +50 -22
- package/src/ai/settings.ts +93 -580
- package/src/ai/store.ts +45 -23
- package/src/ai/stream.ts +1 -1
- package/src/ai/structured-runs.ts +2 -2
- package/src/ai/structured.ts +21 -44
- package/src/ai/task-contracts.ts +25 -5
- package/src/ai/task-execution.ts +15 -6
- package/src/ai/task-model.ts +9 -0
- package/src/ai/transcription.ts +27 -22
- package/src/ai/types.ts +50 -62
- package/src/ai/ui.tsx +13 -0
- package/src/ai/usage.ts +8 -8
- package/src/ai/validate.ts +8 -7
- package/src/ai/view-block.ts +2 -2
- package/src/api/accounts-entities.ts +16 -0
- package/src/api/admin-ai-quotas.ts +34 -56
- package/src/api/admin-ai-usage.ts +1 -1
- package/src/api/admin-ai.ts +173 -0
- package/src/api/admin-core-settings.ts +68 -244
- package/src/api/app-approval.ts +3 -1
- package/src/api/app-event-delivery.ts +51 -0
- package/src/api/app-events.ts +123 -0
- package/src/api/app-resource-access.ts +48 -0
- package/src/api/auth/account-mismatch.ts +24 -0
- package/src/api/auth.ts +26 -3
- package/src/api/capabilities.ts +28 -4
- package/src/api/index.ts +7 -1
- package/src/api/mcp.ts +175 -2
- package/src/api/me.ts +6 -4
- package/src/api/pwa-phone.ts +75 -26
- package/src/api/pwa.ts +34 -8
- package/src/api/resource-links.ts +83 -6
- package/src/api/resource-resolve.ts +50 -0
- package/src/api/search/schemas.ts +3 -1
- package/src/api/search.ts +9 -7
- package/src/app-pwa/contracts.ts +4 -0
- package/src/app-pwa/core-query-error.ts +20 -0
- package/src/app-pwa/index.ts +1 -0
- package/src/app-pwa/service.ts +91 -0
- package/src/app-pwa/transport.ts +22 -0
- package/src/approvals/messages.ts +2 -2
- package/src/browser/cloud-id.ts +86 -0
- package/src/browser/resource-clipboard.ts +4 -0
- package/src/browser/resource-links.ts +52 -0
- package/src/capabilities/server-invoke.ts +2 -11
- package/src/capabilities/testing.ts +5 -2
- package/src/cli/access.ts +6 -5
- package/src/cli/admin/ai-models.ts +199 -0
- package/src/cli/admin/ai-quotas.ts +6 -102
- package/src/cli/admin/ai-usage.ts +4 -4
- package/src/cli/admin/app-credentials.ts +5 -3
- package/src/cli/admin/index.ts +5 -2
- package/src/cli/apps.ts +89 -26
- package/src/cli/capabilities.ts +22 -1
- package/src/cli/events.ts +320 -0
- package/src/config/env.ts +40 -0
- package/src/contracts/app-access.ts +26 -2
- package/src/contracts/app-details.ts +89 -0
- package/src/contracts/app-events.ts +51 -0
- package/src/contracts/app.ts +21 -5
- package/src/contracts/capabilities.ts +143 -3
- package/src/contracts/index.ts +2 -0
- package/src/contracts/notification-types.ts +36 -9
- package/src/contracts/pwa.ts +17 -1
- package/src/contracts/registry.ts +6 -0
- package/src/contracts/resource-clipboard.ts +35 -0
- package/src/contracts/resource-pages.ts +140 -0
- package/src/contracts/resource-resolution.ts +13 -0
- package/src/contracts/shared.ts +4 -0
- package/src/contracts/user-notifications.ts +2 -0
- package/src/events/app-event-delivery.ts +100 -0
- package/src/events/app-events.testing.ts +65 -0
- package/src/events/app-events.ts +134 -0
- package/src/events/core-user-created.ts +24 -0
- package/src/events/index.ts +9 -0
- package/src/events/testing.ts +2 -0
- package/src/index.ts +1 -1
- package/src/server/api/respond.ts +8 -3
- package/src/server/index.ts +1 -0
- package/src/server/middleware/app-access.ts +1 -1
- package/src/server/middleware/auth.ts +21 -7
- package/src/server/middleware/index.ts +1 -0
- package/src/server/middleware/invocation.ts +2 -0
- package/src/server/public-storage.ts +106 -0
- package/src/server/services/access.ts +8 -0
- package/src/services/accounts/app.ts +6 -0
- package/src/services/accounts/entities.ts +14 -3
- package/src/services/accounts/groups.ts +3 -1
- package/src/services/accounts/identities.ts +62 -10
- package/src/services/accounts/users.ts +7 -6
- package/src/services/app-access.ts +25 -7
- package/src/services/app-details.ts +224 -0
- package/src/services/app-discovery.ts +2 -1
- package/src/services/app-event-shapes.ts +139 -0
- package/src/services/app-events/accept.ts +116 -0
- package/src/services/app-events/admin.ts +250 -0
- package/src/services/app-events/declarations.ts +187 -0
- package/src/services/app-events/deliver.ts +218 -0
- package/src/services/app-events.ts +92 -0
- package/src/services/config-conflict.ts +11 -0
- package/src/services/core-user-events.ts +1 -0
- package/src/services/freeipa-config.ts +9 -0
- package/src/services/geo/ledger.ts +28 -10
- package/src/services/geo/service.ts +43 -24
- package/src/services/identity/core-origin.ts +12 -0
- package/src/services/identity/invocation-actor.ts +20 -2
- package/src/services/identity/invocation-authority.ts +4 -0
- package/src/services/identity/invocation-token.ts +238 -56
- package/src/services/identity/issue-invocation.ts +6 -1
- package/src/services/image-metadata.ts +65 -1
- package/src/services/index.ts +13 -1
- package/src/services/ipa/sync.ts +6 -2
- package/src/services/ipa/users.ts +4 -1
- package/src/services/mandates/policy.ts +19 -12
- package/src/services/notifications/browser.ts +4 -0
- package/src/services/notifications/catalog.ts +5 -2
- package/src/services/notifications/platform.ts +7 -2
- package/src/services/notifications/user.ts +18 -7
- package/src/services/oauth-tokens.ts +28 -2
- package/src/services/object-store.ts +217 -0
- package/src/services/providers/local/users.ts +5 -0
- package/src/services/pwa-devices.ts +147 -37
- package/src/services/session/index.ts +14 -0
- package/src/services/settings/core-settings.ts +71 -43
- package/src/services/settings/index.ts +8 -0
- package/src/services/settings/versions.ts +26 -0
- package/src/shared/ai-models.ts +274 -0
- package/src/shared/ai-quotas.ts +2 -2
- package/src/shared/ai-usage.ts +2 -2
- package/src/shared/index.ts +2 -0
- package/src/shared-text/documents.ts +25 -8
- package/src/shared-text/log.ts +1 -1
- package/src/shared-text/text-diff.ts +75 -0
- package/src/ssr/AdminArea.tsx +131 -0
- package/src/ssr/AdminLayout.tsx +9 -3
- package/src/ssr/AdminSidebar.tsx +7 -7
- package/src/ssr/AppAccessDenied.tsx +2 -0
- package/src/ssr/AppBadge.tsx +14 -0
- package/src/ssr/AppLaunchpadPanel.tsx +1 -1
- package/src/ssr/CloudShell.island.tsx +25 -4
- package/src/ssr/Layout.tsx +8 -6
- package/src/ssr/PwaLayout.tsx +16 -1
- package/src/ssr/PwaPartCount.island.tsx +23 -0
- package/src/ssr/SettingsSaveOutcome.tsx +58 -0
- package/src/ssr/SettingsSaveStatus.island.tsx +16 -0
- package/src/ssr/__snapshots__/Layout.unchanged.render.test.tsx.snap +3 -3
- package/src/ssr/admin-active-link.ts +16 -0
- package/src/ssr/admin-navigation.ts +25 -20
- package/src/ssr/app-badges.ts +31 -7
- package/src/ssr/index.ts +8 -0
- package/src/ssr/page-trail-runtime.ts +3 -1
- package/src/ssr/platform-messages.ts +9 -1
- package/src/ssr/pwa-messages.ts +2 -2
- package/src/ssr/rail-navigation.ts +5 -1
- package/src/ssr/settings-draft.ts +153 -0
- package/src/ssr/shell-data.tsx +2 -0
- package/src/styles/global.css +1 -0
- package/src/styles/profile-picture.css +270 -0
- package/src/styles/utilities-navigation.css +6 -0
- package/src/workflows/ai/policy.ts +49 -0
- package/src/workflows/ai/runtime.ts +131 -21
- package/src/workflows/ai/store.ts +283 -35
- package/src/workflows/ai/types.ts +8 -2
- package/src/workflows/ai-actions.ts +148 -120
- package/src/workflows/authoring.ts +31 -0
- package/src/workflows/contracts.ts +1 -0
- package/src/workflows/definition.ts +2 -0
- package/src/workflows/editor.ts +15 -0
- package/src/workflows/index.ts +2 -1
- package/src/workflows/language/compiler.ts +34 -3
- package/src/workflows/runtime/executor.ts +132 -33
- package/src/workflows/runtime/index.ts +2 -0
- package/src/workflows/runtime/ports.ts +5 -1
- package/src/workflows/store/actions.ts +1 -1
- package/src/workflows/store/definitions.ts +5 -0
- package/src/workflows/store/runs.ts +5 -3
- package/src/workflows/store/worker.ts +7 -2
- package/src/ai/model-pricing.ts +0 -43
- package/src/ai/model-request-settings.ts +0 -98
- package/src/ai/settings-messages.ts +0 -129
- package/src/shared/ai-model-request-settings.ts +0 -21
|
@@ -2,6 +2,7 @@ import type { Context, Env, MiddlewareHandler } from "hono";
|
|
|
2
2
|
import { createMiddleware } from "hono/factory";
|
|
3
3
|
import { matchedRoutes } from "hono/route";
|
|
4
4
|
import { findTargetHandler } from "hono/utils/handler";
|
|
5
|
+
import { bindAppAccessRequest } from "../../_internal/app-access-request";
|
|
5
6
|
import { measureServerPhase } from "../../_internal/server-timing";
|
|
6
7
|
import {
|
|
7
8
|
AI_INFERENCE_SCOPE,
|
|
@@ -58,6 +59,8 @@ export type ServiceAccountRequestActor =
|
|
|
58
59
|
delegatedUser: User;
|
|
59
60
|
scopes: string[];
|
|
60
61
|
credentialId?: string | null;
|
|
62
|
+
/** Public OAuth client identifier; safe to retain, never the bearer token. */
|
|
63
|
+
oauthClientId?: string | null;
|
|
61
64
|
credentialExpiresAt?: string | null;
|
|
62
65
|
delegation?: InvocationProvenance;
|
|
63
66
|
}
|
|
@@ -67,6 +70,8 @@ export type ServiceAccountRequestActor =
|
|
|
67
70
|
delegatedUser: null;
|
|
68
71
|
scopes: string[];
|
|
69
72
|
credentialId?: string | null;
|
|
73
|
+
/** Public OAuth client identifier; safe to retain, never the bearer token. */
|
|
74
|
+
oauthClientId?: string | null;
|
|
70
75
|
credentialExpiresAt?: string | null;
|
|
71
76
|
delegation?: InvocationProvenance;
|
|
72
77
|
};
|
|
@@ -248,6 +253,7 @@ const loadAuthenticatedActorUncached = async (
|
|
|
248
253
|
delegatedUser: authResult.delegatedUser,
|
|
249
254
|
scopes: authResult.scopes,
|
|
250
255
|
credentialId: null,
|
|
256
|
+
oauthClientId: authResult.oauthClientId,
|
|
251
257
|
credentialExpiresAt: typeof authResult.payload.exp === "number" ? new Date(authResult.payload.exp * 1_000).toISOString() : null,
|
|
252
258
|
};
|
|
253
259
|
c.set("actor", actor);
|
|
@@ -482,13 +488,21 @@ const getAuthority = <E extends AuthContext>(c: Context<E>): RequestAuthority =>
|
|
|
482
488
|
const accessSubject = c.get("accessSubject");
|
|
483
489
|
const credentialKind = c.get("credentialKind");
|
|
484
490
|
if (!actor || !accessSubject || !credentialKind) throw new Error("Request authority has not been resolved");
|
|
485
|
-
return
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
491
|
+
return bindAppAccessRequest(
|
|
492
|
+
{
|
|
493
|
+
actor,
|
|
494
|
+
accessSubject,
|
|
495
|
+
credentialKind,
|
|
496
|
+
scopes: [...(c.get("credentialScopes") ?? [])],
|
|
497
|
+
...(c.get("credentialProof") ? { credentialProof: c.get("credentialProof") } : {}),
|
|
498
|
+
},
|
|
499
|
+
c.req.raw,
|
|
500
|
+
credentialKind === "session"
|
|
501
|
+
? c.get("sessionToken")
|
|
502
|
+
: credentialKind !== "invocation"
|
|
503
|
+
? (session.getBearerToken(c) ?? undefined)
|
|
504
|
+
: undefined,
|
|
505
|
+
);
|
|
492
506
|
};
|
|
493
507
|
|
|
494
508
|
/**
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Context, MiddlewareHandler } from "hono";
|
|
2
2
|
import { createMiddleware } from "hono/factory";
|
|
3
|
+
import { bindAppAccessRequest } from "../../_internal/app-access-request";
|
|
3
4
|
import { getApp } from "../../_internal/registry";
|
|
4
5
|
import type { AppMeta } from "../../contracts/app";
|
|
5
6
|
import { canUseApp } from "../../services/app-access";
|
|
@@ -53,6 +54,7 @@ export const requireInvocation = (
|
|
|
53
54
|
if (!claims) return reject(c);
|
|
54
55
|
const authority = await (dependencies.resolve ?? resolveInvocationAuthority)(claims);
|
|
55
56
|
if (!authority) return reject(c);
|
|
57
|
+
bindAppAccessRequest(authority, c.req.raw);
|
|
56
58
|
const app = await (dependencies.getApp ?? getApp)(expectation.targetAppId).catch(() => null);
|
|
57
59
|
if (!app || !(await canUseApp(authority, app))) {
|
|
58
60
|
if (app) recordAppAccessDenied(app.id);
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { objectStoreBackend } from "../_internal/object-store-config";
|
|
2
|
+
import { validateObjectKey } from "../_internal/object-store-key";
|
|
3
|
+
import { get } from "../services/settings";
|
|
4
|
+
import { clientIpBucket } from "./client-ip";
|
|
5
|
+
import { ratelimit } from "./ratelimit";
|
|
6
|
+
|
|
7
|
+
const notFound = () => new Response(null, { status: 404, headers: { "Cache-Control": "no-store" } });
|
|
8
|
+
|
|
9
|
+
/** Platform-owned anonymous delivery. Never accepts a bucket, private prefix or listing. */
|
|
10
|
+
export const servePublicStorage = async (request: Request, clientAddress = "unknown"): Promise<Response> => {
|
|
11
|
+
let path: string;
|
|
12
|
+
try {
|
|
13
|
+
// Validate before application decoding; Bun has already resolved transport
|
|
14
|
+
// dot segments. The S3 key below always starts in the public area.
|
|
15
|
+
const raw = request.url.replace(/^[a-z]+:\/\/[^/]+/i, "").split(/[?#]/, 1)[0] ?? "";
|
|
16
|
+
if (/[\\]|%(?:2e|2f|5c)/i.test(raw)) return notFound();
|
|
17
|
+
const match = /^\/public\/storage\/([a-z][a-z0-9-]{0,79})\/(.+)$/.exec(raw);
|
|
18
|
+
if (!match || match[1] === "storage") return notFound();
|
|
19
|
+
const area = `public/${match[1]}/`;
|
|
20
|
+
path = area + validateObjectKey(decodeURIComponent(match[2] ?? ""), area);
|
|
21
|
+
} catch {
|
|
22
|
+
return notFound();
|
|
23
|
+
}
|
|
24
|
+
if (request.method !== "GET" && request.method !== "HEAD") return notFound();
|
|
25
|
+
try {
|
|
26
|
+
const perSecond = Math.max(1, Math.floor(await get<number>("security.rate_limit_per_second")));
|
|
27
|
+
const budget = await ratelimit({ id: "public-storage", limit: perSecond }).check(clientIpBucket(clientAddress));
|
|
28
|
+
if (budget.limited)
|
|
29
|
+
return new Response("Rate limit exceeded.", {
|
|
30
|
+
status: 429,
|
|
31
|
+
headers: { "Cache-Control": "no-store", "Retry-After": String(Math.max(1, Math.ceil(budget.resetIn / 1000))) },
|
|
32
|
+
});
|
|
33
|
+
const { client, limit } = await objectStoreBackend();
|
|
34
|
+
const stat = await client.stat(path);
|
|
35
|
+
if (stat.size > limit) return notFound();
|
|
36
|
+
const headers = new Headers({
|
|
37
|
+
"Content-Type": stat.type || "application/octet-stream",
|
|
38
|
+
"Content-Length": String(stat.size),
|
|
39
|
+
ETag: stat.etag,
|
|
40
|
+
"Last-Modified": stat.lastModified.toUTCString(),
|
|
41
|
+
"Cache-Control": "public, max-age=300",
|
|
42
|
+
"X-Content-Type-Options": "nosniff",
|
|
43
|
+
// Apply to every type, including mislabelled or future active formats.
|
|
44
|
+
"Content-Security-Policy": "sandbox; default-src 'none'; frame-ancestors 'none'",
|
|
45
|
+
});
|
|
46
|
+
const essence = stat.type.split(";", 1)[0]?.trim().toLowerCase() ?? "";
|
|
47
|
+
const inline =
|
|
48
|
+
/^(?:image\/(?:png|jpeg|gif|webp|avif|bmp|x-icon)|audio\/(?:mpeg|ogg|wav|webm|mp4)|video\/(?:mp4|webm|ogg)|text\/plain)$/;
|
|
49
|
+
if (!inline.test(essence)) headers.set("Content-Disposition", "attachment");
|
|
50
|
+
const ifNoneMatch = request.headers.get("if-none-match");
|
|
51
|
+
const modifiedSince = request.headers.get("if-modified-since");
|
|
52
|
+
if (
|
|
53
|
+
ifNoneMatch
|
|
54
|
+
? ifNoneMatch.split(",").some((value) => value.trim() === stat.etag || value.trim() === "*")
|
|
55
|
+
: modifiedSince && stat.lastModified.getTime() <= Date.parse(modifiedSince)
|
|
56
|
+
) {
|
|
57
|
+
headers.delete("Content-Length");
|
|
58
|
+
return new Response(null, { status: 304, headers });
|
|
59
|
+
}
|
|
60
|
+
if (request.method === "HEAD") return new Response(null, { headers });
|
|
61
|
+
const reader = client.file(path).stream().getReader();
|
|
62
|
+
let size = 0;
|
|
63
|
+
const abort = () => {
|
|
64
|
+
void reader.cancel(request.signal.reason).catch(() => {});
|
|
65
|
+
};
|
|
66
|
+
request.signal.addEventListener("abort", abort, { once: true });
|
|
67
|
+
const release = () => {
|
|
68
|
+
request.signal.removeEventListener("abort", abort);
|
|
69
|
+
reader.releaseLock();
|
|
70
|
+
};
|
|
71
|
+
if (request.signal.aborted) abort();
|
|
72
|
+
const stream = new ReadableStream<Uint8Array>(
|
|
73
|
+
{
|
|
74
|
+
async pull(controller) {
|
|
75
|
+
try {
|
|
76
|
+
const next = await reader.read();
|
|
77
|
+
if (next.done) {
|
|
78
|
+
release();
|
|
79
|
+
controller.close();
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
size += next.value.byteLength;
|
|
83
|
+
if (size > limit) throw new RangeError("Public object exceeds the configured size limit.");
|
|
84
|
+
controller.enqueue(next.value);
|
|
85
|
+
} catch (error) {
|
|
86
|
+
await reader.cancel(error).catch(() => {});
|
|
87
|
+
release();
|
|
88
|
+
controller.error(error);
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
async cancel(reason) {
|
|
92
|
+
try {
|
|
93
|
+
await reader.cancel(reason);
|
|
94
|
+
} finally {
|
|
95
|
+
release();
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
{ highWaterMark: 0 },
|
|
100
|
+
);
|
|
101
|
+
return new Response(stream, { headers });
|
|
102
|
+
} catch (error) {
|
|
103
|
+
if (error instanceof Error && "code" in error && ["NoSuchKey", "NotFound", "ENOENT"].includes(String(error.code))) return notFound();
|
|
104
|
+
return new Response("Storage is unavailable.", { status: 503, headers: { "Cache-Control": "no-store" } });
|
|
105
|
+
}
|
|
106
|
+
};
|
|
@@ -5,6 +5,7 @@ import { accessMessages } from "../../access/messages";
|
|
|
5
5
|
import { recursiveGroupIdsSubquery } from "../../services/accounts/group-sql";
|
|
6
6
|
import { toPgUuidArray } from "../../services/postgres";
|
|
7
7
|
import type { ServiceAccountKind } from "../../services/service-accounts";
|
|
8
|
+
import type { RequestAuthority } from "../middleware/auth";
|
|
8
9
|
|
|
9
10
|
export type AccessDb = typeof sql;
|
|
10
11
|
|
|
@@ -305,6 +306,13 @@ export const deleteAccess = async (params: { id: string }, db: AccessDb = sql):
|
|
|
305
306
|
* Each app implements this to connect their resources to auth.access.
|
|
306
307
|
*/
|
|
307
308
|
export type ResourceAccessAdapter<TResourceId = string> = {
|
|
309
|
+
/**
|
|
310
|
+
* Whether any resource in this adapter has an effective read/write/admin right.
|
|
311
|
+
* Use an existence query, never enumerate resources. Preserve domain eligibility,
|
|
312
|
+
* principal semantics, resource binding and credential ceilings. Use db and signal.
|
|
313
|
+
* Optional for existing adapters; required when selected by appAccess.resources().
|
|
314
|
+
*/
|
|
315
|
+
hasAnyRight?: (subject: AccessSubject, context: { authority: RequestAuthority; db: AccessDb; signal: AbortSignal }) => Promise<boolean>;
|
|
308
316
|
/** Get all access entries for a resource */
|
|
309
317
|
list: (resourceId: TResourceId) => Promise<AccessEntry[]>;
|
|
310
318
|
/** Add an access entry to a resource */
|
|
@@ -8,6 +8,8 @@ import type {
|
|
|
8
8
|
EntityListItem,
|
|
9
9
|
GroupMember,
|
|
10
10
|
MutationResult,
|
|
11
|
+
ServiceAccountKind,
|
|
12
|
+
ServiceAccountStatus,
|
|
11
13
|
User,
|
|
12
14
|
UserProfile,
|
|
13
15
|
UserProvider,
|
|
@@ -1253,6 +1255,8 @@ export const accountsAppService = {
|
|
|
1253
1255
|
profile?: UserProfile;
|
|
1254
1256
|
userIds?: string[];
|
|
1255
1257
|
groupIds?: string[];
|
|
1258
|
+
serviceAccountKinds?: ServiceAccountKind[];
|
|
1259
|
+
serviceAccountStatus?: ServiceAccountStatus;
|
|
1256
1260
|
excludeUserIds?: string[];
|
|
1257
1261
|
excludeGroupIds?: string[];
|
|
1258
1262
|
excludeServiceAccountIds?: string[];
|
|
@@ -1284,6 +1288,8 @@ export const accountsAppService = {
|
|
|
1284
1288
|
profile: config.profile,
|
|
1285
1289
|
userIds: config.userIds,
|
|
1286
1290
|
groupIds: config.groupIds,
|
|
1291
|
+
serviceAccountKinds: config.serviceAccountKinds,
|
|
1292
|
+
serviceAccountStatus: config.serviceAccountStatus,
|
|
1287
1293
|
excludeUserIds: config.excludeUserIds,
|
|
1288
1294
|
excludeGroupIds: config.excludeGroupIds,
|
|
1289
1295
|
excludeServiceAccountIds: config.excludeServiceAccountIds,
|
|
@@ -2,7 +2,7 @@ import { sql } from "bun";
|
|
|
2
2
|
import type { EntityKind, EntityListItem, UserProfile, UserProvider } from "../../contracts/shared";
|
|
3
3
|
import { getFreeIpaConfig } from "../freeipa-config";
|
|
4
4
|
import { escapeLikePattern, toPgTextArray, toPgUuidArray } from "../postgres";
|
|
5
|
-
import type { ServiceAccountKind } from "../service-accounts";
|
|
5
|
+
import type { ServiceAccountKind, ServiceAccountStatus } from "../service-accounts";
|
|
6
6
|
import { buildBaseGroup, personalOwnerJoin } from "./base-group";
|
|
7
7
|
import { buildBaseUser } from "./base-user";
|
|
8
8
|
import { buildManagedGroupScopeCondition, recursiveGroupIdsSubquery } from "./group-sql";
|
|
@@ -21,6 +21,8 @@ export type EntityListParams = {
|
|
|
21
21
|
profile?: UserProfile;
|
|
22
22
|
userIds?: string[];
|
|
23
23
|
groupIds?: string[];
|
|
24
|
+
serviceAccountKinds?: ServiceAccountKind[];
|
|
25
|
+
serviceAccountStatus?: ServiceAccountStatus;
|
|
24
26
|
excludeUserIds?: string[];
|
|
25
27
|
excludeGroupIds?: string[];
|
|
26
28
|
excludeServiceAccountIds?: string[];
|
|
@@ -472,6 +474,13 @@ export const list = async (
|
|
|
472
474
|
(params.excludeServiceAccountIds?.length ?? 0) === 0
|
|
473
475
|
? sql`TRUE`
|
|
474
476
|
: sql`(kind <> 'service_account' OR id <> ALL(${toPgUuidArray(params.excludeServiceAccountIds ?? [])}::uuid[]))`;
|
|
477
|
+
const serviceAccountKindsCondition =
|
|
478
|
+
(params.serviceAccountKinds?.length ?? 0) === 0
|
|
479
|
+
? sql`TRUE`
|
|
480
|
+
: sql`(kind <> 'service_account' OR service_account_kind = ANY(${toPgTextArray(params.serviceAccountKinds ?? [])}::text[]))`;
|
|
481
|
+
const serviceAccountStatusCondition = params.serviceAccountStatus
|
|
482
|
+
? sql`(kind <> 'service_account' OR status = ${params.serviceAccountStatus})`
|
|
483
|
+
: sql`TRUE`;
|
|
475
484
|
const userMemberOfGroupCondition =
|
|
476
485
|
(params.userMemberOfGroupIds?.length ?? 0) === 0
|
|
477
486
|
? sql`TRUE`
|
|
@@ -491,6 +500,8 @@ export const list = async (
|
|
|
491
500
|
AND ${excludeUserCondition}
|
|
492
501
|
AND ${excludeGroupCondition}
|
|
493
502
|
AND ${excludeServiceAccountCondition}
|
|
503
|
+
AND ${serviceAccountKindsCondition}
|
|
504
|
+
AND ${serviceAccountStatusCondition}
|
|
494
505
|
AND ${userMemberOfGroupCondition}
|
|
495
506
|
AND ${personalCondition}
|
|
496
507
|
AND (
|
|
@@ -649,9 +660,9 @@ export const list = async (
|
|
|
649
660
|
OFFSET ${offset}
|
|
650
661
|
`;
|
|
651
662
|
|
|
663
|
+
// A page past the end has no rows to carry the window count; the total still counts every match.
|
|
652
664
|
let total = rows.length > 0 ? Number(rows[0]!.total) : 0;
|
|
653
|
-
if (
|
|
654
|
-
total = (await list({ ...params, page: 1, perPage: 1 })).total;
|
|
665
|
+
if (!rows.length && page > 1) total = (await list({ ...params, page: 1, perPage: 1 })).total;
|
|
655
666
|
return {
|
|
656
667
|
items: rows.map((row) => {
|
|
657
668
|
const entity = mapEntityRow(row);
|
|
@@ -86,7 +86,9 @@ const listCanonical = async (params: {
|
|
|
86
86
|
OFFSET ${offset}
|
|
87
87
|
`;
|
|
88
88
|
|
|
89
|
-
|
|
89
|
+
// A page past the end has no rows to carry the window count; the total still counts every match.
|
|
90
|
+
let total = rows.length > 0 ? Number((rows[0] as Record<string, unknown>).total) : 0;
|
|
91
|
+
if (!rows.length && page > 1) total = (await listCanonical({ ...params, page: 1, perPage: 1 })).total;
|
|
90
92
|
return {
|
|
91
93
|
groups: rows.map(buildBaseGroup),
|
|
92
94
|
total,
|
|
@@ -207,22 +207,74 @@ export const createAccountIdentityService = (db: typeof sql = sql, upstreamIdent
|
|
|
207
207
|
if ((input.kind !== "users" && input.kind !== "groups") || typeof input.name !== "string" || !input.name || input.name.includes("\0"))
|
|
208
208
|
throw new AccountIdentityError("invalid_identity_lookup", 400);
|
|
209
209
|
};
|
|
210
|
+
const reconcileIdentity = async (
|
|
211
|
+
input: { kind: "users" | "groups"; provider: UserProvider; name: string; identityId?: string; signal?: AbortSignal },
|
|
212
|
+
state: AccountIdentityAvailability,
|
|
213
|
+
): Promise<AccountIdentityReconciliation> => {
|
|
214
|
+
validateLookup(input);
|
|
215
|
+
if (input.provider !== "local" && input.provider !== "ipa") throw new AccountIdentityError("invalid_identity_lookup", 400);
|
|
216
|
+
if (input.identityId !== undefined) cursor(input.identityId);
|
|
217
|
+
if (input.signal?.aborted) return { state: "unknown", reason: "provider_unavailable" };
|
|
218
|
+
if (input.provider === "ipa") {
|
|
219
|
+
if (!state.freeipaEnabled) return { state: "unknown", reason: "provider_disabled" };
|
|
220
|
+
return upstreamIdentity(input);
|
|
221
|
+
}
|
|
222
|
+
const result = await readLocalIdentity(input);
|
|
223
|
+
return input.signal?.aborted ? { state: "unknown", reason: "provider_unavailable" } : result;
|
|
224
|
+
};
|
|
210
225
|
return {
|
|
226
|
+
/** Trusted application provisioning only; no profile, grants, credentials, or administrator impersonation. */
|
|
227
|
+
async provisioningUsers(input: { provider?: UserProvider; id?: string; after?: string }) {
|
|
228
|
+
if (input.provider !== undefined && input.provider !== "local" && input.provider !== "ipa")
|
|
229
|
+
throw new AccountIdentityError("invalid_provider", 400);
|
|
230
|
+
const id = cursor(input.id);
|
|
231
|
+
const after = cursor(input.after);
|
|
232
|
+
const state = await availability();
|
|
233
|
+
const [localAllowed, ipaAllowed] = await Promise.all([
|
|
234
|
+
isAccountCategoryAllowed({ provider: "local", profile: "user" }, db),
|
|
235
|
+
isAccountCategoryAllowed({ provider: "ipa", profile: "user" }, db),
|
|
236
|
+
]);
|
|
237
|
+
const rows = await db<
|
|
238
|
+
{
|
|
239
|
+
id: string;
|
|
240
|
+
provider: UserProvider;
|
|
241
|
+
uid: string;
|
|
242
|
+
managed_by: UserProvider | null;
|
|
243
|
+
uid_number: number | null;
|
|
244
|
+
primary_gid_number: number | null;
|
|
245
|
+
}[]
|
|
246
|
+
>`SELECT u.id,u.provider,u.uid,p.managed_by,p.uid_number,p.primary_gid_number
|
|
247
|
+
FROM auth.users u LEFT JOIN auth.user_posix p ON p.user_id=u.id
|
|
248
|
+
WHERE u.provider IN ('local','ipa') AND (${input.provider ?? null}::text IS NULL OR u.provider=${input.provider ?? null})
|
|
249
|
+
AND u.profile='user'
|
|
250
|
+
AND (u.account_expires IS NULL OR u.account_expires>now())
|
|
251
|
+
AND (${id}::uuid IS NULL OR u.id=${id}::uuid)
|
|
252
|
+
AND (${after}::uuid IS NULL OR u.id>${after}::uuid)
|
|
253
|
+
ORDER BY u.id LIMIT ${PAGE_SIZE + 1}`;
|
|
254
|
+
const source = page(rows);
|
|
255
|
+
const items = source.items
|
|
256
|
+
.filter((row) => (row.provider === "local" ? localAllowed : ipaAllowed && state.freeipaEnabled))
|
|
257
|
+
.map((row) => ({
|
|
258
|
+
id: row.id,
|
|
259
|
+
provider: row.provider,
|
|
260
|
+
username: row.uid,
|
|
261
|
+
posix:
|
|
262
|
+
row.managed_by === row.provider && positiveId(row.uid_number) && positiveId(row.primary_gid_number)
|
|
263
|
+
? { uidNumber: row.uid_number, primaryGidNumber: row.primary_gid_number }
|
|
264
|
+
: null,
|
|
265
|
+
}));
|
|
266
|
+
return { items, nextCursor: source.nextCursor, availability: state };
|
|
267
|
+
},
|
|
211
268
|
async reconcile(
|
|
212
269
|
actor: RequestActor,
|
|
213
270
|
input: { kind: "users" | "groups"; provider: UserProvider; name: string; identityId?: string; signal?: AbortSignal },
|
|
214
271
|
): Promise<AccountIdentityReconciliation> {
|
|
215
272
|
const { state } = await requireAdmin(actor);
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
if (!state.freeipaEnabled) return { state: "unknown", reason: "provider_disabled" };
|
|
222
|
-
return upstreamIdentity(input);
|
|
223
|
-
}
|
|
224
|
-
const result = await readLocalIdentity(input);
|
|
225
|
-
return input.signal?.aborted ? { state: "unknown", reason: "provider_unavailable" } : result;
|
|
273
|
+
return reconcileIdentity(input, state);
|
|
274
|
+
},
|
|
275
|
+
/** Trusted provisioning services use the same live identity check as administrator provisioning. */
|
|
276
|
+
async provisioningIdentity(input: { kind: "users"; provider: UserProvider; name: string; identityId: string; signal?: AbortSignal }) {
|
|
277
|
+
return reconcileIdentity(input, await availability());
|
|
226
278
|
},
|
|
227
279
|
/** Trusted application jobs only; this read does not authorize an HTTP caller or a filesystem mutation. */
|
|
228
280
|
async localLifecycle(input: { kind: "users" | "groups"; identityId: string; name: string }) {
|
|
@@ -4,7 +4,7 @@ import { freeipa } from "../../server/services";
|
|
|
4
4
|
import { createAuthLoginUrl } from "../../shared/redirect";
|
|
5
5
|
import { isAccountCategoryAllowed } from "../account-category-policy";
|
|
6
6
|
import type { AuditActor } from "../audit";
|
|
7
|
-
import { getFreeIpaConfig } from "../freeipa-config";
|
|
7
|
+
import { getFreeIpaAdminGroups, getFreeIpaConfig } from "../freeipa-config";
|
|
8
8
|
import { getServiceIpaSession } from "../ipa/service-account";
|
|
9
9
|
import { toPgTextArray, toPgUuidArray } from "../postgres";
|
|
10
10
|
import { providers } from "../providers";
|
|
@@ -168,11 +168,12 @@ const buildUser = (row: DbRow, groupsAdmin: string[]): User => {
|
|
|
168
168
|
};
|
|
169
169
|
};
|
|
170
170
|
|
|
171
|
-
export const get = async (params: { id: string } | { uid: string }): Promise<User | null> => {
|
|
172
|
-
const
|
|
173
|
-
const
|
|
174
|
-
const
|
|
175
|
-
const
|
|
171
|
+
export const get = async (params: { id: string } | { uid: string }, options: { db?: SQL } = {}): Promise<User | null> => {
|
|
172
|
+
const db = options.db ?? sql;
|
|
173
|
+
const whereClause = "id" in params ? db`u.id = ${params.id}` : db`u.uid = ${params.uid}`;
|
|
174
|
+
const userIdExpr = "id" in params ? db`${params.id}` : db`u.id`;
|
|
175
|
+
const groupsAdmin = options.db ? await getFreeIpaAdminGroups(db) : (await getFreeIpaConfig()).groupsAdmin;
|
|
176
|
+
const rows = await db<DbRow[]>`
|
|
176
177
|
SELECT u.*,
|
|
177
178
|
${userIpaDataColumns()},
|
|
178
179
|
CASE
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { type SQL, sql } from "bun";
|
|
2
2
|
import type { Context } from "hono";
|
|
3
|
+
import { RESOURCE_ACCESS_TIMEOUT_MS, resourceAppAccess } from "../_internal/app-access-resources";
|
|
3
4
|
import type { AppMeta } from "../contracts/app";
|
|
4
5
|
import { AppAccessRuleSchema, type FixedAppAccessRule } from "../contracts/app-access";
|
|
5
6
|
import { userFromActor } from "../server/actor";
|
|
6
|
-
import type
|
|
7
|
+
import { type AuthContext, auth, type RequestAuthority } from "../server/middleware/auth";
|
|
7
8
|
import { buildAccessPrincipalCondition } from "../server/services/access";
|
|
8
9
|
import { logger } from "./logging";
|
|
9
10
|
import { toPgTextArray } from "./postgres";
|
|
@@ -17,7 +18,7 @@ export const appAccessAuthority = <E extends AuthContext>(c: Context<E>): Reques
|
|
|
17
18
|
const accessSubject = c.get("accessSubject");
|
|
18
19
|
const credentialKind = c.get("credentialKind");
|
|
19
20
|
if (!actor || !accessSubject || !credentialKind) return null;
|
|
20
|
-
return
|
|
21
|
+
return auth.getAuthority(c);
|
|
21
22
|
};
|
|
22
23
|
|
|
23
24
|
const credentialAllows = (authority: RequestAuthority, appId: string): boolean => {
|
|
@@ -65,6 +66,7 @@ export const evaluateAppAccess = (authority: RequestAuthority | null, app: Acces
|
|
|
65
66
|
const parsed = AppAccessRuleSchema.safeParse(app.access);
|
|
66
67
|
if (!parsed.success) return false;
|
|
67
68
|
const rule = parsed.data;
|
|
69
|
+
if (rule.kind === "resources") return false; // Asynchronous adapter decision only.
|
|
68
70
|
if (rule.kind !== "grants") return fixedAllows(authority, rule);
|
|
69
71
|
if (!state?.configured) return !!rule.until && fixedAllows(authority, rule.until);
|
|
70
72
|
return state.permission === "read" || state.permission === "admin";
|
|
@@ -74,21 +76,34 @@ let lastFailureLog = -Infinity;
|
|
|
74
76
|
const reportFailure = () => {
|
|
75
77
|
if (Date.now() - lastFailureLog < 60_000) return;
|
|
76
78
|
lastFailureLog = Date.now();
|
|
77
|
-
logger("app-access").error("Application access could not be resolved;
|
|
79
|
+
logger("app-access").error("Application access could not be resolved; affected applications are unavailable");
|
|
78
80
|
};
|
|
79
81
|
|
|
80
82
|
/**
|
|
81
|
-
* One authoritative SQL read for all configurable
|
|
83
|
+
* One authoritative SQL read for all configurable lists; resource rules query the owning adapters.
|
|
84
|
+
* No cross-request grant cache: revocation applies on the next request.
|
|
82
85
|
* Pass the reserved transaction as `db` when checking access inside a guarded operation.
|
|
86
|
+
* A supplied `signal` replaces the default resource budget; the caller must bound it.
|
|
83
87
|
*/
|
|
84
88
|
export const usableAppIds = async (
|
|
85
89
|
authority: RequestAuthority | null,
|
|
86
90
|
apps: readonly AccessApp[],
|
|
87
|
-
options: { db?: SQL } = {},
|
|
91
|
+
options: { db?: SQL; signal?: AbortSignal } = {},
|
|
88
92
|
): Promise<Set<string>> => {
|
|
89
93
|
const db = options.db ?? sql;
|
|
90
94
|
const allowed = new Set(apps.filter((app) => app.access?.kind !== "grants" && evaluateAppAccess(authority, app)).map((app) => app.id));
|
|
91
95
|
if (!authority) return allowed;
|
|
96
|
+
let resourceSignal = options.signal;
|
|
97
|
+
// Sequential: one existence probe in flight per decision, no resource enumeration.
|
|
98
|
+
for (const app of apps) {
|
|
99
|
+
if (app.access?.kind !== "resources" || !credentialAllows(authority, app.id)) continue;
|
|
100
|
+
resourceSignal ??= AbortSignal.timeout(RESOURCE_ACCESS_TIMEOUT_MS);
|
|
101
|
+
try {
|
|
102
|
+
if (await resourceAppAccess(authority, app, { ...options, signal: resourceSignal })) allowed.add(app.id);
|
|
103
|
+
} catch {
|
|
104
|
+
if (!options.signal?.aborted) reportFailure();
|
|
105
|
+
}
|
|
106
|
+
}
|
|
92
107
|
const configurable = apps.filter((app) => app.access?.kind === "grants" && credentialAllows(authority, app.id));
|
|
93
108
|
if (configurable.length === 0) return allowed;
|
|
94
109
|
try {
|
|
@@ -119,5 +134,8 @@ export const usableAppIds = async (
|
|
|
119
134
|
return allowed;
|
|
120
135
|
};
|
|
121
136
|
|
|
122
|
-
export const canUseApp = async (
|
|
123
|
-
|
|
137
|
+
export const canUseApp = async (
|
|
138
|
+
authority: RequestAuthority | null,
|
|
139
|
+
app: AccessApp,
|
|
140
|
+
options: { db?: SQL; signal?: AbortSignal } = {},
|
|
141
|
+
): Promise<boolean> => (await usableAppIds(authority, [app], options)).has(app.id);
|